From 5f74230ad479f66c19c1af435d0dc3a5da1bc5b9 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 6 May 2026 23:48:08 +0200 Subject: [PATCH] feat(core-shared): initSentryClient helper (R34, R35, R37 mandatory replay defaults) --- .../sentry/init-client.test.ts | 75 +++++++++++++++++++ .../src/instrumentation/sentry/init-client.ts | 46 ++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 packages/core-shared/src/instrumentation/sentry/init-client.test.ts create mode 100644 packages/core-shared/src/instrumentation/sentry/init-client.ts diff --git a/packages/core-shared/src/instrumentation/sentry/init-client.test.ts b/packages/core-shared/src/instrumentation/sentry/init-client.test.ts new file mode 100644 index 0000000..7421be3 --- /dev/null +++ b/packages/core-shared/src/instrumentation/sentry/init-client.test.ts @@ -0,0 +1,75 @@ +// packages/core-shared/src/instrumentation/sentry/init-client.test.ts +import { describe, it, expect, vi, beforeEach } from "vitest"; + +const { replayIntegration } = vi.hoisted(() => { + const replayIntegration = vi.fn((opts: unknown) => ({ name: "Replay", _opts: opts })); + return { replayIntegration }; +}); + +vi.mock("@sentry/nextjs", () => ({ + init: vi.fn(), + replayIntegration, +})); + +import * as Sentry from "@sentry/nextjs"; +import { initSentryClient } from "@/instrumentation/sentry/init-client"; + +describe("initSentryClient", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("calls Sentry.init with sendDefaultPii: false (R31)", () => { + initSentryClient({ dsn: "https://x@y/1", app: "web-next" }); + const call = (Sentry.init as ReturnType).mock.calls[0][0] as Record< + string, + unknown + >; + expect(call["sendDefaultPii"]).toBe(false); + }); + + it("attaches replay integration with maskAllText/maskAllInputs/blockAllMedia: true (R34, R35)", () => { + initSentryClient({ dsn: "https://x@y/1", app: "web-next" }); + expect(replayIntegration).toHaveBeenCalledTimes(1); + const replayOpts = (replayIntegration as ReturnType).mock.calls[0][0] as Record< + string, + unknown + >; + expect(replayOpts["maskAllText"]).toBe(true); + expect(replayOpts["maskAllInputs"]).toBe(true); + expect(replayOpts["blockAllMedia"]).toBe(true); + }); + + it("defaults replaysSessionSampleRate to 0.0 (R37)", () => { + initSentryClient({ dsn: "https://x@y/1", app: "web-next" }); + const call = (Sentry.init as ReturnType).mock.calls[0][0] as Record< + string, + unknown + >; + expect(call["replaysSessionSampleRate"]).toBe(0.0); + }); + + it("defaults replaysOnErrorSampleRate to 1.0 (R37)", () => { + initSentryClient({ dsn: "https://x@y/1", app: "web-next" }); + const call = (Sentry.init as ReturnType).mock.calls[0][0] as Record< + string, + unknown + >; + expect(call["replaysOnErrorSampleRate"]).toBe(1.0); + }); + + it("attaches beforeSend + beforeSendTransaction", () => { + initSentryClient({ dsn: "https://x@y/1", app: "web-next" }); + const call = (Sentry.init as ReturnType).mock.calls[0][0] as Record< + string, + unknown + >; + expect(typeof call["beforeSend"]).toBe("function"); + expect(typeof call["beforeSendTransaction"]).toBe("function"); + }); + + it("is a no-op when dsn is empty", () => { + initSentryClient({ dsn: "", app: "web-next" }); + expect(Sentry.init).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core-shared/src/instrumentation/sentry/init-client.ts b/packages/core-shared/src/instrumentation/sentry/init-client.ts new file mode 100644 index 0000000..480f54e --- /dev/null +++ b/packages/core-shared/src/instrumentation/sentry/init-client.ts @@ -0,0 +1,46 @@ +// packages/core-shared/src/instrumentation/sentry/init-client.ts +import * as Sentry from "@sentry/nextjs"; +import { beforeSend, beforeSendTransaction } from "./scrub"; + +export type InitClientOpts = { + dsn: string | undefined; + app: "web-next" | "cms" | "web-tanstack"; + release?: string; +}; + +export function initSentryClient(opts: InitClientOpts): void { + if (!opts.dsn) return; + + const isProd = process.env["NODE_ENV"] === "production"; + const tracesSampleRate = + process.env["SENTRY_TRACES_SAMPLE_RATE"] !== undefined + ? Number(process.env["SENTRY_TRACES_SAMPLE_RATE"]) + : isProd + ? 0.1 + : 1.0; + + const environment = + process.env["SENTRY_ENVIRONMENT"] ?? process.env["NODE_ENV"] ?? "development"; + const release = opts.release ?? "unknown"; + + Sentry.init({ + dsn: opts.dsn, + environment, + release, + tracesSampleRate, + sendDefaultPii: false, // R31 + beforeSend, // R32 + beforeSendTransaction, // R33 + replaysSessionSampleRate: 0.0, // R37 — privacy default + replaysOnErrorSampleRate: 1.0, // R37 + integrations: [ + // R34, R35 — mandatory mask flags; allowlist starts empty + Sentry.replayIntegration({ + maskAllText: true, + maskAllInputs: true, + blockAllMedia: true, + }), + ], + initialScope: { tags: { app: opts.app } }, + }); +}