import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { __resetSentryClientForTests, initSentryClient, parseSampleRate, readClientSentryConfig, scrubSensitiveQueryParams } from "./sentry.client"; vi.mock("@sentry/react-router", () => { const initMock = vi.fn(); const reactRouterTracingIntegration = vi.fn(() => ({ name: "react-router-tracing" })); const replayIntegration = vi.fn((opts: Record) => ({ name: "replay", options: opts })); return { __esModule: true, init: initMock, reactRouterTracingIntegration, replayIntegration, }; }); import * as Sentry from "@sentry/react-router"; const mockedInit = Sentry.init as unknown as ReturnType; const mockedReplay = Sentry.replayIntegration as unknown as ReturnType; describe("sentry.client/parseSampleRate", () => { it("returns the fallback for missing or empty values", () => { expect(parseSampleRate(undefined, 0.5)).toBe(0.5); expect(parseSampleRate("", 0.25)).toBe(0.25); }); it("returns the fallback for non-numeric values", () => { expect(parseSampleRate("abc", 0.7)).toBe(0.7); }); it("clamps values to the [0, 1] range", () => { expect(parseSampleRate("-0.5", 0.1)).toBe(0); expect(parseSampleRate("1.5", 0.1)).toBe(1); }); it("parses valid numeric strings", () => { expect(parseSampleRate("0.25", 0.1)).toBe(0.25); expect(parseSampleRate("1.0", 0.1)).toBe(1); expect(parseSampleRate("0", 0.1)).toBe(0); }); }); describe("sentry.client/readClientSentryConfig", () => { it("falls back to defaults when env is empty", () => { const config = readClientSentryConfig({}); expect(config.dsn).toBeUndefined(); expect(config.tracesSampleRate).toBe(0.1); expect(config.replaysSessionSampleRate).toBe(0.1); expect(config.replaysOnErrorSampleRate).toBe(1.0); }); it("reads VITE_SENTRY_* env vars", () => { const config = readClientSentryConfig({ VITE_SENTRY_DSN_WEB: "https://public@sentry.example/1", VITE_SENTRY_RELEASE: "abc123", VITE_SENTRY_TRACES_SAMPLE_RATE: "0.5", VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: "0.2", VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: "1", MODE: "production", }); expect(config.dsn).toBe("https://public@sentry.example/1"); expect(config.release).toBe("abc123"); expect(config.tracesSampleRate).toBe(0.5); expect(config.replaysSessionSampleRate).toBe(0.2); expect(config.replaysOnErrorSampleRate).toBe(1); expect(config.environment).toBe("production"); }); }); describe("sentry.client/initSentryClient", () => { beforeEach(() => { mockedInit.mockClear(); mockedReplay.mockClear(); __resetSentryClientForTests(); }); afterEach(() => { __resetSentryClientForTests(); }); it("is a no-op when DSN is missing", () => { const initialized = initSentryClient({ dsn: undefined, environment: "test", release: undefined, tracesSampleRate: 0.1, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1, }); expect(initialized).toBe(false); expect(mockedInit).not.toHaveBeenCalled(); }); it("calls Sentry.init with the provided sample rates when DSN is set", () => { const initialized = initSentryClient({ dsn: "https://public@sentry.example/1", environment: "staging", release: "release-1", tracesSampleRate: 0.42, replaysSessionSampleRate: 0.12, replaysOnErrorSampleRate: 0.99, }); expect(initialized).toBe(true); expect(mockedInit).toHaveBeenCalledTimes(1); const initArgs = mockedInit.mock.calls[0][0] as Record; expect(initArgs.dsn).toBe("https://public@sentry.example/1"); expect(initArgs.environment).toBe("staging"); expect(initArgs.release).toBe("release-1"); expect(initArgs.tracesSampleRate).toBe(0.42); expect(initArgs.replaysSessionSampleRate).toBe(0.12); expect(initArgs.replaysOnErrorSampleRate).toBe(0.99); expect(initArgs.sendDefaultPii).toBe(false); }); it("configures the replay integration with PII-safe defaults", () => { initSentryClient({ dsn: "https://public@sentry.example/1", environment: undefined, release: undefined, tracesSampleRate: 0.1, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1, }); expect(mockedReplay).toHaveBeenCalledTimes(1); const replayOptions = mockedReplay.mock.calls[0][0] as Record; expect(replayOptions.maskAllInputs).toBe(true); expect(replayOptions.maskAllText).toBe(false); expect(replayOptions.blockAllMedia).toBe(false); }); it("does not initialize twice", () => { const config = { dsn: "https://public@sentry.example/1", environment: undefined, release: undefined, tracesSampleRate: 0.1, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1, }; expect(initSentryClient(config)).toBe(true); expect(initSentryClient(config)).toBe(true); expect(mockedInit).toHaveBeenCalledTimes(1); }); }); describe("sentry.client/scrubSensitiveQueryParams", () => { it("redacts sensitive query parameters in full URLs", () => { expect(scrubSensitiveQueryParams("https://example.com/auth?token=abc&keep=1")).toBe("https://example.com/auth?token=%5BRedacted%5D&keep=1"); }); it("redacts sensitive query parameters in relative URLs", () => { expect(scrubSensitiveQueryParams("/reset?password=abc&id=2")).toBe("/reset?password=%5BRedacted%5D&id=2"); }); it("returns the input untouched when no sensitive keys are present", () => { expect(scrubSensitiveQueryParams("https://example.com/x?keep=1")).toBe("https://example.com/x?keep=1"); }); it("returns the input untouched for non-URL strings", () => { expect(scrubSensitiveQueryParams("not a url")).toBe("not a url"); }); });