Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/sentry.client.test.ts
T

188 lines
6.9 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).not.toHaveBeenCalled();
});
it("sanitizes errors, transactions, and breadcrumbs before transport", () => {
initSentryClient({
dsn: "https://public@sentry.example/1",
environment: undefined,
release: undefined,
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1,
});
const initArgs = mockedInit.mock.calls[0][0] as Record<string, (...args: never[]) => unknown>;
const portalToken = "p".repeat(43);
const event = initArgs.beforeSend?.({
request: { url: `https://portal.example.test/acme/login/magic/${portalToken}` },
exception: { values: [{ type: "Error", value: "Bearer raw-secret" }] },
} as never);
const transaction = initArgs.beforeSendTransaction?.({
transaction: `pageload /__portal/acme/reset-password/${portalToken}`,
request: { url: "/search?email=person@example.test" },
} as never);
const breadcrumb = initArgs.beforeBreadcrumb?.({ message: `opened /acme/setup/${portalToken}` } as never);
expect(JSON.stringify(event)).not.toContain(portalToken);
expect(JSON.stringify(event)).not.toContain("raw-secret");
expect(JSON.stringify(transaction)).not.toContain("person@example.test");
expect(JSON.stringify(transaction)).not.toContain(portalToken);
expect(JSON.stringify(breadcrumb)).not.toContain(portalToken);
});
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");
});
});