159 lines
5.5 KiB
TypeScript
159 lines
5.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@sentry/node", () => {
|
|
const initMock = vi.fn();
|
|
const captureExceptionMock = vi.fn();
|
|
const captureMessageMock = vi.fn();
|
|
const httpIntegration = vi.fn(() => ({ name: "http" }));
|
|
const requestDataIntegration = vi.fn((opts: Record<string, unknown>) => ({ name: "request-data", options: opts }));
|
|
return {
|
|
__esModule: true,
|
|
init: initMock,
|
|
captureException: captureExceptionMock,
|
|
captureMessage: captureMessageMock,
|
|
httpIntegration,
|
|
requestDataIntegration,
|
|
};
|
|
});
|
|
|
|
import * as Sentry from "@sentry/node";
|
|
|
|
import { __resetSentryServerForTests, initSentryServer, parseServerSampleRate, readServerSentryConfig, webErrorReporter } from "./sentry.server";
|
|
|
|
const mockedInit = Sentry.init as unknown as ReturnType<typeof vi.fn>;
|
|
const mockedRequestData = Sentry.requestDataIntegration as unknown as ReturnType<typeof vi.fn>;
|
|
const mockedCaptureException = Sentry.captureException as unknown as ReturnType<typeof vi.fn>;
|
|
const mockedCaptureMessage = Sentry.captureMessage as unknown as ReturnType<typeof vi.fn>;
|
|
|
|
describe("sentry.server/parseServerSampleRate", () => {
|
|
it("clamps to [0, 1] and falls back on bad input", () => {
|
|
expect(parseServerSampleRate(undefined, 0.4)).toBe(0.4);
|
|
expect(parseServerSampleRate("", 0.5)).toBe(0.5);
|
|
expect(parseServerSampleRate("oops", 0.6)).toBe(0.6);
|
|
expect(parseServerSampleRate("-2", 0.1)).toBe(0);
|
|
expect(parseServerSampleRate("9", 0.1)).toBe(1);
|
|
expect(parseServerSampleRate("0.33", 0.1)).toBe(0.33);
|
|
});
|
|
});
|
|
|
|
describe("sentry.server/readServerSentryConfig", () => {
|
|
it("returns undefined dsn for empty env", () => {
|
|
expect(readServerSentryConfig({} as NodeJS.ProcessEnv).dsn).toBeUndefined();
|
|
});
|
|
|
|
it("reads SENTRY_DSN_WEB and friends", () => {
|
|
const config = readServerSentryConfig({
|
|
SENTRY_DSN_WEB: "https://srv@sentry.example/2",
|
|
SENTRY_RELEASE: "v9",
|
|
SENTRY_ENVIRONMENT: "staging",
|
|
SENTRY_TRACES_SAMPLE_RATE: "0.25",
|
|
NODE_ENV: "production",
|
|
} as unknown as NodeJS.ProcessEnv);
|
|
expect(config.dsn).toBe("https://srv@sentry.example/2");
|
|
expect(config.release).toBe("v9");
|
|
expect(config.environment).toBe("staging");
|
|
expect(config.tracesSampleRate).toBe(0.25);
|
|
});
|
|
});
|
|
|
|
describe("sentry.server/initSentryServer", () => {
|
|
beforeEach(() => {
|
|
mockedInit.mockClear();
|
|
mockedRequestData.mockClear();
|
|
__resetSentryServerForTests();
|
|
});
|
|
|
|
afterEach(() => {
|
|
__resetSentryServerForTests();
|
|
});
|
|
|
|
it("is a no-op when DSN is missing", () => {
|
|
expect(
|
|
initSentryServer({
|
|
dsn: undefined,
|
|
environment: "test",
|
|
release: undefined,
|
|
tracesSampleRate: 0.1,
|
|
})
|
|
).toBe(false);
|
|
expect(mockedInit).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("initializes Sentry with PII-safe requestDataIntegration", () => {
|
|
expect(
|
|
initSentryServer({
|
|
dsn: "https://srv@sentry.example/2",
|
|
environment: "prod",
|
|
release: "rel-1",
|
|
tracesSampleRate: 0.2,
|
|
})
|
|
).toBe(true);
|
|
expect(mockedInit).toHaveBeenCalledTimes(1);
|
|
const initArgs = mockedInit.mock.calls[0][0] as Record<string, unknown>;
|
|
expect(initArgs.dsn).toBe("https://srv@sentry.example/2");
|
|
expect(initArgs.environment).toBe("prod");
|
|
expect(initArgs.release).toBe("rel-1");
|
|
expect(initArgs.tracesSampleRate).toBe(0.2);
|
|
expect(initArgs.sendDefaultPii).toBe(false);
|
|
|
|
expect(mockedRequestData).toHaveBeenCalledTimes(1);
|
|
const rdOpts = mockedRequestData.mock.calls[0][0] as Record<string, unknown>;
|
|
const include = rdOpts.include as Record<string, unknown>;
|
|
expect(include.ip).toBe(false);
|
|
expect(include.cookies).toBe(false);
|
|
expect(include.data).toBe(false);
|
|
expect(include.query_string).toBe(false);
|
|
});
|
|
|
|
it("is idempotent", () => {
|
|
const config = {
|
|
dsn: "https://srv@sentry.example/2",
|
|
environment: undefined,
|
|
release: undefined,
|
|
tracesSampleRate: 0.1,
|
|
};
|
|
expect(initSentryServer(config)).toBe(true);
|
|
expect(initSentryServer(config)).toBe(true);
|
|
expect(mockedInit).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("sentry.server/webErrorReporter", () => {
|
|
beforeEach(() => {
|
|
mockedCaptureException.mockClear();
|
|
mockedCaptureMessage.mockClear();
|
|
__resetSentryServerForTests();
|
|
});
|
|
|
|
it("is a no-op before init", async () => {
|
|
await webErrorReporter.report({ component: "test", operation: "noop" });
|
|
expect(mockedCaptureException).not.toHaveBeenCalled();
|
|
expect(mockedCaptureMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("captures exceptions when an error is provided after init", async () => {
|
|
initSentryServer({
|
|
dsn: "https://srv@sentry.example/2",
|
|
environment: undefined,
|
|
release: undefined,
|
|
tracesSampleRate: 0.1,
|
|
});
|
|
const err = new Error("boom");
|
|
await webErrorReporter.report({ component: "loader", operation: "x", error: err });
|
|
expect(mockedCaptureException).toHaveBeenCalledTimes(1);
|
|
expect(mockedCaptureException.mock.calls[0][0]).toBe(err);
|
|
});
|
|
|
|
it("captures messages when no error is provided", async () => {
|
|
initSentryServer({
|
|
dsn: "https://srv@sentry.example/2",
|
|
environment: undefined,
|
|
release: undefined,
|
|
tracesSampleRate: 0.1,
|
|
});
|
|
await webErrorReporter.report({ component: "loader", operation: "x", errorMessage: "explicit-message" });
|
|
expect(mockedCaptureMessage).toHaveBeenCalledTimes(1);
|
|
expect(mockedCaptureMessage.mock.calls[0][0]).toBe("explicit-message");
|
|
});
|
|
});
|