164 lines
5.8 KiB
TypeScript
164 lines
5.8 KiB
TypeScript
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<string, unknown>) => ({ 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<typeof vi.fn>;
|
|
const mockedReplay = Sentry.replayIntegration as unknown as ReturnType<typeof vi.fn>;
|
|
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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");
|
|
});
|
|
});
|