refactor: strip Phase/Plan/R-number references from source comments

This commit is contained in:
2026-05-13 09:51:45 +02:00
parent 075b729266
commit 17ae157365
66 changed files with 980 additions and 647 deletions

View File

@@ -29,7 +29,7 @@ export type AuditFrom = {
*/
export type AuditEntry = {
// WHO
/** User id, or "system"/"service-{name}" for non-user actors. NEVER email or name (R36). */
/** User id, or "system"/"service-{name}" for non-user actors. NEVER email or name. */
actorId: string;
actorType: "user" | "system" | "service";
/** Snapshot of actor's roles AT TIME OF ACTION — preserves historical state. */

View File

@@ -5,7 +5,10 @@ import { UndiciInstrumentation } from "@opentelemetry/instrumentation-undici";
import { PgInstrumentation } from "@opentelemetry/instrumentation-pg";
import { buildResource } from "./resource";
import { createSentryOtelBridge } from "./sentry-bridge";
import { PiiScrubSpanProcessor, PiiScrubLogRecordProcessor } from "./pii-scrub-processor";
import {
PiiScrubSpanProcessor,
PiiScrubLogRecordProcessor,
} from "./pii-scrub-processor";
const { BatchSpanProcessor } = tracing;
@@ -47,7 +50,7 @@ export function initOtelServerNode(opts: InitOtelServerNodeOpts): NodeSDK {
// `as never` works around a TypeScript version conflict: `core-shared`'s direct
// dep on `@opentelemetry/sdk-trace-base@1.30.1` has subtly incompatible types
// vs the 1.28.0 bundled by `sdk-node@0.55.0`. The runtime objects are compatible;
// the structural mismatch is type-only. Phase 1 implementer chose this rather than
// the structural mismatch is type-only chosen rather than
// constraining sdk-trace-base to 1.28.x to avoid losing future bug fixes.
new BatchSpanProcessor(bridge.spanProcessor as never),
]

View File

@@ -1,6 +1,6 @@
// packages/core-shared/src/instrumentation/otel/pii-fields.ts
// R32 — substring match on event keys (case-insensitive).
// Substring match on event keys (case-insensitive).
// IP address attribute KEYS from OTel HttpInstrumentation (semconv 1.20 and 1.27+)
// are listed here so they are key-redacted in addition to the value-level regex
// scrubbing in pii-scrub-processor.ts.
@@ -28,7 +28,7 @@ export const PII_KEY_SUBSTRINGS = [
"host.ip",
] as const;
// R33 — substring match on URL query-param keys (case-insensitive)
// Substring match on URL query-param keys (case-insensitive)
export const PII_QUERY_PARAM_SUBSTRINGS = [
"token",
"email",

View File

@@ -10,11 +10,17 @@ import {
SimpleLogRecordProcessor,
} from "@opentelemetry/sdk-logs";
import { SeverityNumber } from "@opentelemetry/api-logs";
import { PiiScrubSpanProcessor, PiiScrubLogRecordProcessor } from "./pii-scrub-processor";
import {
PiiScrubSpanProcessor,
PiiScrubLogRecordProcessor,
} from "./pii-scrub-processor";
const spanExporter = new InMemorySpanExporter();
const tracerProvider = new BasicTracerProvider({
spanProcessors: [new PiiScrubSpanProcessor(), new SimpleSpanProcessor(spanExporter)],
spanProcessors: [
new PiiScrubSpanProcessor(),
new SimpleSpanProcessor(spanExporter),
],
});
// Use addLogRecordProcessor to chain processors in the right order.
@@ -43,7 +49,7 @@ describe("PiiScrubSpanProcessor", () => {
const exported = spanExporter.getFinishedSpans();
expect(exported[0]!.attributes["user.email"]).toBe("[redacted]");
expect(exported[0]!.attributes["auth.token"]).toBe("[redacted]");
expect(exported[0]!.attributes["user.id"]).toBe("u_123"); // id is fine per R36
expect(exported[0]!.attributes["user.id"]).toBe("u_123"); // id is fine
expect(exported[0]!.attributes["request.path"]).toBe("/api/users");
});
@@ -119,7 +125,7 @@ describe("PiiScrubLogRecordProcessor", () => {
expect(records[0]!.body).toBe("user signed in successfully");
});
it("scrubs IPv4 in log record body (C2 / R32)", () => {
it("scrubs IPv4 in log record body", () => {
const logger = logProvider.getLogger("test");
logger.emit({
severityNumber: SeverityNumber.INFO,
@@ -132,7 +138,7 @@ describe("PiiScrubLogRecordProcessor", () => {
});
});
describe("PiiScrubSpanProcessor — IP address scrubbing (C2 / R32)", () => {
describe("PiiScrubSpanProcessor — IP address scrubbing", () => {
it("scrubs IPv4 addresses in attribute values", () => {
const tracer = tracerProvider.getTracer("test");
const span = tracer.startSpan("test-span", {
@@ -140,7 +146,9 @@ describe("PiiScrubSpanProcessor — IP address scrubbing (C2 / R32)", () => {
});
span.end();
const exported = spanExporter.getFinishedSpans();
expect(exported[0]!.attributes["request.note"]).toBe("request from [redacted-ip]");
expect(exported[0]!.attributes["request.note"]).toBe(
"request from [redacted-ip]",
);
});
it("scrubs IPv6 addresses in attribute values", () => {

View File

@@ -3,14 +3,23 @@
// PII scrub processors for OTel spans and log records.
// These run FIRST in their respective processor chains so downstream exporters
// (including the Sentry exporter) see scrubbed data. This replaces the old
// Sentry beforeSend / beforeSendTransaction hooks (R32, R33) — scrubbing now
// Sentry beforeSend / beforeSendTransaction hooks — scrubbing now
// happens at the OTel layer, vendor-agnostic.
import type { ReadableSpan, SpanProcessor } from "@opentelemetry/sdk-trace-base";
import type {
ReadableSpan,
SpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import type { Span } from "@opentelemetry/api";
import type { Context } from "@opentelemetry/api";
import type { LogRecord, LogRecordProcessor } from "@opentelemetry/sdk-logs";
import { PII_KEY_SUBSTRINGS, REDACTED_VALUE, IPV4_REGEX, IPV6_REGEX, REDACTED_IP } from "./pii-fields";
import {
PII_KEY_SUBSTRINGS,
REDACTED_VALUE,
IPV4_REGEX,
IPV6_REGEX,
REDACTED_IP,
} from "./pii-fields";
function isPiiKey(key: string): boolean {
const lower = key.toLowerCase();
@@ -27,7 +36,7 @@ function containsPiiSubstring(s: string): boolean {
* Called for attribute values whose KEYS did not match a PII substring — the
* old Sentry beforeSend hook performed this kind of value-level scrubbing; we
* replicate it here so IP addresses embedded in non-IP-keyed attributes
* (e.g. "request.note": "from 10.0.0.1") are still redacted (C2 fix / R32).
* (e.g. "request.note": "from 10.0.0.1") are still redacted.
*/
function scrubValue(value: unknown): unknown {
if (typeof value !== "string") return value;
@@ -39,7 +48,9 @@ function scrubValue(value: unknown): unknown {
return scrubbed;
}
function scrubAttributes(attrs: Record<string, unknown>): Record<string, unknown> {
function scrubAttributes(
attrs: Record<string, unknown>,
): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(attrs)) {
if (isPiiKey(key)) {
@@ -54,7 +65,7 @@ function scrubAttributes(attrs: Record<string, unknown>): Record<string, unknown
/**
* Runs FIRST in the span processor chain so downstream exporters see scrubbed attributes.
* Redacts any span attribute whose key contains a PII substring (case-insensitive).
* R32 — attribute-key-based PII redaction.
* Attribute-key-based PII redaction.
*/
export class PiiScrubSpanProcessor implements SpanProcessor {
forceFlush(): Promise<void> {
@@ -70,7 +81,9 @@ export class PiiScrubSpanProcessor implements SpanProcessor {
}
onEnd(span: ReadableSpan): void {
const scrubbed = scrubAttributes(span.attributes as Record<string, unknown>);
const scrubbed = scrubAttributes(
span.attributes as Record<string, unknown>,
);
Object.assign(span.attributes, scrubbed);
}
}
@@ -80,7 +93,7 @@ export class PiiScrubSpanProcessor implements SpanProcessor {
* - Strips PII from attributes (key-based substring match, case-insensitive).
* - Strips PII from the log body string (substring match — if any PII substring
* appears in the body, the entire body is redacted to avoid partial leakage).
* R32 — attribute-key-based PII redaction; R33 — body-level redaction.
* Attribute-key-based PII redaction; body-level redaction.
*/
export class PiiScrubLogRecordProcessor implements LogRecordProcessor {
forceFlush(): Promise<void> {
@@ -93,7 +106,9 @@ export class PiiScrubLogRecordProcessor implements LogRecordProcessor {
onEmit(record: LogRecord): void {
if (record.attributes) {
const scrubbed = scrubAttributes(record.attributes as Record<string, unknown>);
const scrubbed = scrubAttributes(
record.attributes as Record<string, unknown>,
);
Object.assign(record.attributes, scrubbed);
}
if (typeof record.body === "string") {
@@ -103,7 +118,7 @@ export class PiiScrubLogRecordProcessor implements LogRecordProcessor {
record.body = REDACTED_VALUE;
} else {
// No PII keyword, but may still contain IP addresses embedded in text.
// Apply value-level regex scrubbing (C2 fix / R32).
// Apply value-level regex scrubbing.
record.body = scrubValue(record.body) as string;
}
}

View File

@@ -31,7 +31,7 @@ describe("createSentryOtelBridge", () => {
const { createSentryOtelBridge } = await import("./sentry-bridge");
const bridge = createSentryOtelBridge({ dsn: "https://test@sentry.io/1" });
expect(bridge.spanProcessor).toBeDefined();
// Phase 3: logRecordProcessor is now wired (SentryLogRecordForwarder)
// logRecordProcessor is wired (SentryLogRecordForwarder)
expect(bridge.logRecordProcessor).toBeDefined();
expect(bridge.logRecordProcessor).not.toBeNull();
});
@@ -50,7 +50,10 @@ describe("SentryLogRecordForwarder", () => {
const captureException = vi.fn();
const captureMessage = vi.fn();
const forwarder = new SentryLogRecordForwarder({ captureException, captureMessage });
const forwarder = new SentryLogRecordForwarder({
captureException,
captureMessage,
});
const record = {
severityNumber: SEVERITY_ERROR,
@@ -84,7 +87,10 @@ describe("SentryLogRecordForwarder", () => {
const captureException = vi.fn();
const captureMessage = vi.fn();
const forwarder = new SentryLogRecordForwarder({ captureException, captureMessage });
const forwarder = new SentryLogRecordForwarder({
captureException,
captureMessage,
});
const record = {
severityNumber: SEVERITY_ERROR,
@@ -110,7 +116,10 @@ describe("SentryLogRecordForwarder", () => {
const captureException = vi.fn();
const captureMessage = vi.fn();
const forwarder = new SentryLogRecordForwarder({ captureException, captureMessage });
const forwarder = new SentryLogRecordForwarder({
captureException,
captureMessage,
});
const record = {
severityNumber,

View File

@@ -1,4 +1,7 @@
import type { tracing as sdkTracing, logs as sdkLogs } from "@opentelemetry/sdk-node";
import type {
tracing as sdkTracing,
logs as sdkLogs,
} from "@opentelemetry/sdk-node";
import { SeverityNumber } from "@opentelemetry/api-logs";
import type { LogRecord } from "@opentelemetry/sdk-logs";
@@ -64,7 +67,8 @@ export class SentryLogRecordForwarder implements LogRecordProcessor {
if (severityNumber >= SeverityNumber.ERROR) {
// Reconstruct the error from OTel semantic convention attributes
const message =
(attrs["exception.message"] as string | undefined) ?? String(record.body ?? "");
(attrs["exception.message"] as string | undefined) ??
String(record.body ?? "");
const err = new Error(message);
if (attrs["exception.type"]) {
err.name = attrs["exception.type"] as string;
@@ -77,7 +81,11 @@ export class SentryLogRecordForwarder implements LogRecordProcessor {
? (attrs["sentry.fingerprint"] as string).split("|")
: undefined;
Sentry.captureException(err, { tags, extra, ...(fingerprint ? { fingerprint } : {}) });
Sentry.captureException(err, {
tags,
extra,
...(fingerprint ? { fingerprint } : {}),
});
} else {
// Map severity to Sentry level
const level = severityNumber >= SeverityNumber.WARN ? "warning" : "info";
@@ -99,9 +107,11 @@ export class SentryLogRecordForwarder implements LogRecordProcessor {
* Creates Sentry-as-OTel-exporter processors. The OTel SDK uses these to
* forward spans and log records to Sentry. This is the ONLY file in
* core-shared that imports from `@sentry/opentelemetry` — all other Sentry
* coupling is excluded by the R40/R52 ESLint allowlist.
* coupling is excluded by the ESLint allowlist.
*/
export function createSentryOtelBridge(opts: SentryOtelBridgeOpts): SentryOtelBridge {
export function createSentryOtelBridge(
opts: SentryOtelBridgeOpts,
): SentryOtelBridge {
if (!opts.dsn) {
return { spanProcessor: null, logRecordProcessor: null };
}
@@ -125,7 +135,9 @@ function extractTags(attrs: Record<string, unknown>): Record<string, string> {
return tags;
}
function extractExtras(attrs: Record<string, unknown>): Record<string, unknown> {
function extractExtras(
attrs: Record<string, unknown>,
): Record<string, unknown> {
const extras: Record<string, unknown> = {};
for (const [k, v] of Object.entries(attrs)) {
if (k.startsWith("extra.")) {

View File

@@ -2,7 +2,10 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
const { replayIntegration } = vi.hoisted(() => {
const replayIntegration = vi.fn((opts: unknown) => ({ name: "Replay", _opts: opts }));
const replayIntegration = vi.fn((opts: unknown) => ({
name: "Replay",
_opts: opts,
}));
return { replayIntegration };
});
@@ -19,43 +22,35 @@ describe("initSentryClientReact", () => {
vi.clearAllMocks();
});
it("calls SentryReact.init with sendDefaultPii: false (R31)", () => {
it("calls SentryReact.init with sendDefaultPii: false", () => {
initSentryClientReact({ dsn: "https://x@y/1", app: "web-tanstack" });
const call = (SentryReact.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
string,
unknown
>;
const call = (SentryReact.init as ReturnType<typeof vi.fn>).mock
.calls[0]![0] as Record<string, unknown>;
expect(call["sendDefaultPii"]).toBe(false);
});
it("attaches replay integration with mask flags (R34, R35)", () => {
it("attaches replay integration with mask flags", () => {
initSentryClientReact({ dsn: "https://x@y/1", app: "web-tanstack" });
expect(replayIntegration).toHaveBeenCalledTimes(1);
const replayOpts = (replayIntegration as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
string,
unknown
>;
const replayOpts = (replayIntegration as ReturnType<typeof vi.fn>).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 replay sample rates per R37", () => {
it("defaults replay sample rates", () => {
initSentryClientReact({ dsn: "https://x@y/1", app: "web-tanstack" });
const call = (SentryReact.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
string,
unknown
>;
const call = (SentryReact.init as ReturnType<typeof vi.fn>).mock
.calls[0]![0] as Record<string, unknown>;
expect(call["replaysSessionSampleRate"]).toBe(0.0);
expect(call["replaysOnErrorSampleRate"]).toBe(1.0);
});
it("attaches beforeSend + beforeSendTransaction scrubbers", () => {
initSentryClientReact({ dsn: "https://x@y/1", app: "web-tanstack" });
const call = (SentryReact.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
string,
unknown
>;
const call = (SentryReact.init as ReturnType<typeof vi.fn>).mock
.calls[0]![0] as Record<string, unknown>;
expect(typeof call["beforeSend"]).toBe("function");
expect(typeof call["beforeSendTransaction"]).toBe("function");
});

View File

@@ -13,7 +13,7 @@ import {
IPV6_REGEX,
} from "../otel/pii-fields";
// R32 — inline scrub helpers for browser-side Sentry (server uses OTel processors instead).
// 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));
@@ -33,7 +33,9 @@ function redactString(s: string): string {
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);
return parentKey && keyContainsPii(parentKey)
? REDACTED_VALUE
: redactString(value);
}
if (typeof value === "number" || typeof value === "boolean") {
return parentKey && keyContainsPii(parentKey) ? REDACTED_VALUE : value;
@@ -63,8 +65,8 @@ function scrubUrl(url: string): string {
/**
* 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.
* Mirrors init-client.ts but uses @sentry/react directly. Same PII,
* replay, and scrubbing requirements apply.
*/
export function initSentryClientReact(opts: InitClientOpts): void {
if (!opts.dsn) return;
@@ -78,31 +80,48 @@ export function initSentryClientReact(opts: InitClientOpts): void {
: 1.0;
const environment =
process.env["SENTRY_ENVIRONMENT"] ?? process.env["NODE_ENV"] ?? "development";
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 };
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
sendDefaultPii: false,
beforeSend: ((event: SentryEvent) =>
deepScrub(event)) as unknown as NonNullable<InitOpts>["beforeSend"],
beforeSendTransaction: ((event: SentryEvent) => {
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("="))) {
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
replaysSessionSampleRate: 0.0,
replaysOnErrorSampleRate: 1.0,
integrations: [
// R34, R35 — mandatory mask flags; allowlist starts empty
// mandatory mask flags; allowlist starts empty
SentryReact.replayIntegration({
maskAllText: true,
maskAllInputs: true,

View File

@@ -2,7 +2,10 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
const { replayIntegration } = vi.hoisted(() => {
const replayIntegration = vi.fn((opts: unknown) => ({ name: "Replay", _opts: opts }));
const replayIntegration = vi.fn((opts: unknown) => ({
name: "Replay",
_opts: opts,
}));
return { replayIntegration };
});
@@ -19,51 +22,41 @@ describe("initSentryClient", () => {
vi.clearAllMocks();
});
it("calls Sentry.init with sendDefaultPii: false (R31)", () => {
it("calls Sentry.init with sendDefaultPii: false", () => {
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
string,
unknown
>;
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock
.calls[0]![0] as Record<string, unknown>;
expect(call["sendDefaultPii"]).toBe(false);
});
it("attaches replay integration with maskAllText/maskAllInputs/blockAllMedia: true (R34, R35)", () => {
it("attaches replay integration with maskAllText/maskAllInputs/blockAllMedia: true", () => {
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
expect(replayIntegration).toHaveBeenCalledTimes(1);
const replayOpts = (replayIntegration as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
string,
unknown
>;
const replayOpts = (replayIntegration as ReturnType<typeof vi.fn>).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)", () => {
it("defaults replaysSessionSampleRate to 0.0", () => {
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
string,
unknown
>;
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock
.calls[0]![0] as Record<string, unknown>;
expect(call["replaysSessionSampleRate"]).toBe(0.0);
});
it("defaults replaysOnErrorSampleRate to 1.0 (R37)", () => {
it("defaults replaysOnErrorSampleRate to 1.0", () => {
initSentryClient({ dsn: "https://x@y/1", app: "web-next" });
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock.calls[0]![0] as Record<
string,
unknown
>;
const call = (Sentry.init as ReturnType<typeof vi.fn>).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<typeof vi.fn>).mock.calls[0]![0] as Record<
string,
unknown
>;
const call = (Sentry.init as ReturnType<typeof vi.fn>).mock
.calls[0]![0] as Record<string, unknown>;
expect(typeof call["beforeSend"]).toBe("function");
expect(typeof call["beforeSendTransaction"]).toBe("function");
});

View File

@@ -18,7 +18,7 @@ export type InitClientOpts = {
release?: string;
};
// R32 — inline scrub helpers for browser-side Sentry (server uses OTel processors instead).
// 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));
@@ -38,7 +38,9 @@ function redactString(s: string): string {
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);
return parentKey && keyContainsPii(parentKey)
? REDACTED_VALUE
: redactString(value);
}
if (typeof value === "number" || typeof value === "boolean") {
return parentKey && keyContainsPii(parentKey) ? REDACTED_VALUE : value;
@@ -78,31 +80,48 @@ export function initSentryClient(opts: InitClientOpts): void {
: 1.0;
const environment =
process.env["SENTRY_ENVIRONMENT"] ?? process.env["NODE_ENV"] ?? "development";
process.env["SENTRY_ENVIRONMENT"] ??
process.env["NODE_ENV"] ??
"development";
const release = opts.release ?? "unknown";
type InitOpts = Parameters<typeof Sentry.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 };
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;
};
Sentry.init({
dsn: opts.dsn,
environment,
release,
tracesSampleRate,
sendDefaultPii: false, // R31
beforeSend: ((event: SentryEvent) => deepScrub(event)) as unknown as InitOpts["beforeSend"], // R32
beforeSendTransaction: ((event: SentryEvent) => { // R33
sendDefaultPii: false,
beforeSend: ((event: SentryEvent) =>
deepScrub(event)) as unknown as InitOpts["beforeSend"],
beforeSendTransaction: ((event: SentryEvent) => {
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("="))) {
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 InitOpts["beforeSendTransaction"],
replaysSessionSampleRate: 0.0, // R37 — privacy default
replaysOnErrorSampleRate: 1.0, // R37
replaysSessionSampleRate: 0.0, // privacy default
replaysOnErrorSampleRate: 1.0,
integrations: [
// R34, R35 — mandatory mask flags; allowlist starts empty
// mandatory mask flags; allowlist starts empty
Sentry.replayIntegration({
maskAllText: true,
maskAllInputs: true,