1const SAFE_ERROR_LABEL =
2 /^(?:Error|EvalError|RangeError|ReferenceError|SyntaxError|TypeError|URIError|AggregateError|DOMException|[A-Za-z][A-Za-z0-9]*(?:Error|Failure))$/;
3const SAFE_TRACE_ID = /^[A-Za-z0-9._:-]{1,128}$/;
4const STACK_FRAME_LIMIT = 32;
5
6export interface SafeErrorLogAttributes {
7 readonly errorType: "error" | "array" | "null" | "object" | "primitive";
8 readonly errorName?: string;
9 readonly errorTag?: string;
10 readonly traceId?: string;
11 readonly stack?: string;
12}
13
14function readSafeLabel(value: unknown): string | undefined {
15 return typeof value === "string" && SAFE_ERROR_LABEL.test(value) ? value : undefined;
16}
17
18function sanitizeStackUrl(value: string): string {
19 try {
20 const url = new URL(value);
21 url.username = "";
22 url.password = "";
23 url.search = "";
24 url.hash = "";
25 return url.toString();
26 } catch {
27 return value;
28 }
29}
30
31function sanitizeStackFrame(frame: string): string {
32 return frame.replace(/(?:https?|file):\/\/[^\s)]+/g, sanitizeStackUrl);
33}
34
35function readSafeStack(error: Error): string | undefined {
36 try {
37 const frames = error.stack
38 ?.split(/\r?\n/)
39 .filter((line) => /^\s*at\s+/.test(line) || /^[^@\s]+@(?:https?|file):\/\//.test(line))
40 .slice(0, STACK_FRAME_LIMIT)
41 .map(sanitizeStackFrame);
42 return frames && frames.length > 0 ? frames.join("\n") : undefined;
43 } catch {
44 return undefined;
45 }
46}
47
48function readErrorTag(error: unknown): string | undefined {
49 try {
50 if (typeof error !== "object" || error === null) {
51 return undefined;
52 }
53 return readSafeLabel((error as { readonly _tag?: unknown })._tag);
54 } catch {
55 return undefined;
56 }
57}
58
59function readTraceId(error: unknown): string | undefined {
60 try {
61 const seen = new Set<object>();
62 let current: unknown = error;
63
64 while (typeof current === "object" && current !== null && !seen.has(current)) {
65 seen.add(current);
66 const record = current as { readonly cause?: unknown; readonly traceId?: unknown };
67 if (typeof record.traceId === "string" && SAFE_TRACE_ID.test(record.traceId)) {
68 return record.traceId;
69 }
70 current = record.cause;
71 }
72
73 return undefined;
74 } catch {
75 return undefined;
76 }
77}
78
79export function safeErrorLogAttributes(error: unknown): SafeErrorLogAttributes {
80 const errorTag = readErrorTag(error);
81 const traceId = readTraceId(error);
82
83 if (error instanceof Error) {
84 const errorName = readSafeLabel(error.name);
85 const stack = readSafeStack(error);
86 return {
87 errorType: "error",
88 ...(errorName !== undefined ? { errorName } : {}),
89 ...(errorTag !== undefined ? { errorTag } : {}),
90 ...(traceId !== undefined ? { traceId } : {}),
91 ...(stack !== undefined ? { stack } : {}),
92 };
93 }
94
95 return {
96 errorType:
97 error === null
98 ? "null"
99 : Array.isArray(error)
100 ? "array"
101 : typeof error === "object"
102 ? "object"
103 : "primitive",
104 ...(errorTag !== undefined ? { errorTag } : {}),
105 ...(traceId !== undefined ? { traceId } : {}),
106 };
107}