// packages/core-shared/src/instrumentation/sentry/init-client-react.ts // Browser-side Sentry init for Vite/React runtimes (TanStack Start). PII scrubbing is // applied via beforeSend/beforeSendTransaction because browser does NOT use the OTel pipeline. // PII field lists imported from otel/pii-fields.ts (vendor-neutral). import * as SentryReact from "@sentry/react"; import type { InitClientOpts } from "./init-client"; import { PII_KEY_SUBSTRINGS, PII_QUERY_PARAM_SUBSTRINGS, REDACTED_VALUE, REDACTED_IP, IPV4_REGEX, IPV6_REGEX, } from "../otel/pii-fields"; // R32 — inline scrub helpers for browser-side Sentry (server uses OTel processors instead). function keyContainsPii(key: string): boolean { const lower = key.toLowerCase(); return PII_KEY_SUBSTRINGS.some((s) => lower.includes(s)); } function queryParamContainsPii(key: string): boolean { const lower = key.toLowerCase(); return PII_QUERY_PARAM_SUBSTRINGS.some((s) => lower.includes(s)); } function redactString(s: string): string { const ipv4 = new RegExp(IPV4_REGEX.source, "g"); const ipv6 = new RegExp(IPV6_REGEX.source, "g"); return s.replace(ipv4, REDACTED_IP).replace(ipv6, REDACTED_IP); } function deepScrub(value: unknown, parentKey = ""): unknown { if (value === null || value === undefined) return value; if (typeof value === "string") { return parentKey && keyContainsPii(parentKey) ? REDACTED_VALUE : redactString(value); } if (typeof value === "number" || typeof value === "boolean") { return parentKey && keyContainsPii(parentKey) ? REDACTED_VALUE : value; } if (Array.isArray(value)) return value.map((v) => deepScrub(v, parentKey)); if (typeof value === "object") { const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { out[k] = keyContainsPii(k) ? REDACTED_VALUE : deepScrub(v, k); } return out; } return value; } function scrubUrl(url: string): string { try { const u = new URL(url, "http://placeholder.local"); for (const [k] of Array.from(u.searchParams.entries())) { if (queryParamContainsPii(k)) u.searchParams.set(k, REDACTED_VALUE); } return url.startsWith("/") ? `${u.pathname}${u.search}` : u.toString(); } catch { return url; } } /** * Client-side init for non-Next.js (Vite/React) runtimes (TanStack Start). * Mirrors init-client.ts but uses @sentry/react directly. R31, R32, R33, * R34, R35, R37 still apply. */ export function initSentryClientReact(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"; type InitOpts = Parameters[0]; type SentryEvent = { extra?: Record | null; contexts?: Record | undefined>; request?: { url?: string; headers?: Record; [key: string]: unknown }; transaction?: string; [key: string]: unknown }; SentryReact.init({ dsn: opts.dsn, environment, release, tracesSampleRate, sendDefaultPii: false, // R31 beforeSend: ((event: SentryEvent) => deepScrub(event)) as unknown as NonNullable["beforeSend"], // R32 beforeSendTransaction: ((event: SentryEvent) => { // R33 const out = { ...event }; if (out.request?.url) out.request = { ...out.request, url: scrubUrl(out.request.url) }; if (out.transaction && (out.transaction.includes("?") || out.transaction.includes("="))) { out.transaction = scrubUrl(out.transaction); } return out; }) as unknown as NonNullable["beforeSendTransaction"], replaysSessionSampleRate: 0.0, // R37 replaysOnErrorSampleRate: 1.0, // R37 integrations: [ // R34, R35 — mandatory mask flags; allowlist starts empty SentryReact.replayIntegration({ maskAllText: true, maskAllInputs: true, blockAllMedia: true, }), ], initialScope: { tags: { app: opts.app } }, }); }