shoc-frontend-new/src/observability/sentry-scrubber.ts

152 lines
4.4 KiB
TypeScript
Raw Normal View History

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<TransactionEvent["tags"]>,
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;
}