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) => ({ 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; const mockedRequestData = Sentry.requestDataIntegration as unknown as ReturnType; const mockedCaptureException = Sentry.captureException as unknown as ReturnType; const mockedCaptureMessage = Sentry.captureMessage as unknown as ReturnType; 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; 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; const include = rdOpts.include as Record; 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"); }); });