Files
agentic-dev/packages/core-shared/src/instrumentation/sentry/init-client-react.ts

115 lines
4.2 KiB
TypeScript

// 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<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
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<typeof SentryReact.init>[0];
type SentryEvent = { extra?: Record<string, unknown> | null; contexts?: Record<string, Record<string, unknown> | undefined>; request?: { url?: string; headers?: Record<string, string | undefined>; [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<InitOpts>["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<InitOpts>["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 } },
});
}