import type { Breadcrumb, ErrorEvent, SpanJSON, TransactionEvent } from "@sentry/core"; import { normalizeHttpRoute } from "@/observability/http-transaction"; function sanitizeUrl(value: string): string { try { const parsed = new URL(value, window.location.origin); return `${parsed.origin}${normalizeHttpRoute(parsed.pathname)}`; } catch { return normalizeHttpRoute(value); } } function keepOpaqueUserId(event: ErrorEvent | TransactionEvent): void { event.user = event.user?.id == null ? undefined : { id: String(event.user.id) }; } function scrubRequest(event: ErrorEvent | TransactionEvent): void { if (!event.request) { return; } event.request = { method: event.request.method, url: typeof event.request.url === "string" ? sanitizeUrl(event.request.url) : undefined, }; } const SAFE_SPAN_DATA_KEYS = new Set([ "actor.type", "code.function", "db.operation.name", "db.system", "http.method", "http.request.method", "http.response.status_code", "http.route", "http.status_code", "operation.type", "sentry.op", "sentry.origin", "server.address", ]); function safeSpanData(data: SpanJSON["data"] | undefined): SpanJSON["data"] { return Object.fromEntries( Object.entries(data ?? {}).filter( ([key, value]) => SAFE_SPAN_DATA_KEYS.has(key) && ["string", "number", "boolean"].includes(typeof value), ), ); } function valueAsString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } function sanitizeHttpDescription(description: string | undefined): string | undefined { if (!description) { return undefined; } const match = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(.+)$/i.exec(description); return match ? `${match[1]!.toUpperCase()} ${sanitizeUrl(match[2]!)}` : undefined; } function scrubTransactionSpans(event: TransactionEvent): void { event.spans = event.spans?.map((span) => ({ ...span, data: safeSpanData(span.data), description: span.op?.includes("http") ? sanitizeHttpDescription(span.description) : undefined, })); const trace = event.contexts?.trace; if (trace?.data) { trace.data = safeSpanData(trace.data); } } function setStringTag( tags: NonNullable, key: string, value: unknown, ): void { const stringValue = valueAsString(value); if (stringValue) { tags[key] = stringValue; } } function transactionFunction(event: ErrorEvent | TransactionEvent): string | undefined { const traceFunction = valueAsString(event.contexts?.trace?.data?.["code.function"]); return traceFunction ?? (event.type === "transaction" ? event.transaction : undefined); } function actorType(event: ErrorEvent | TransactionEvent): string { return ( valueAsString(event.tags?.["actor.type"]) ?? valueAsString(event.contexts?.trace?.data?.["actor.type"]) ?? (event.user?.id == null ? "anonymous" : "authenticated") ); } function attachTraceIdentifiers(event: ErrorEvent | TransactionEvent): void { const trace = event.contexts?.trace; const tags = { ...event.tags }; setStringTag(tags, "trace_id", trace?.trace_id); setStringTag(tags, "transaction_id", trace?.span_id); setStringTag(tags, "operation.type", valueAsString(trace?.data?.["operation.type"]) ?? trace?.op); setStringTag(tags, "code.function", transactionFunction(event)); tags["actor.type"] = actorType(event); event.tags = tags; } export function scrubErrorEvent(event: ErrorEvent): ErrorEvent { keepOpaqueUserId(event); scrubRequest(event); attachTraceIdentifiers(event); return event; } export function scrubTransactionEvent(event: TransactionEvent): TransactionEvent { keepOpaqueUserId(event); scrubRequest(event); attachTraceIdentifiers(event); scrubTransactionSpans(event); return event; } export function scrubBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb { const safe: Breadcrumb = { timestamp: breadcrumb.timestamp, type: breadcrumb.type, category: breadcrumb.category, level: breadcrumb.level, }; if (breadcrumb.category === "fetch" || breadcrumb.category === "xhr") { const method = breadcrumb.data?.method; const statusCode = breadcrumb.data?.status_code; const url = breadcrumb.data?.url; safe.data = { ...(typeof method === "string" ? { method } : {}), ...(typeof statusCode === "number" ? { status_code: statusCode } : {}), ...(typeof url === "string" ? { url: sanitizeUrl(url) } : {}), }; } return safe; }