Skip to content
Source
Sign inSign up

safeLog.ts

109 lines3.2 KB

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;
13​
14function readSafeLabel(value: unknown): string | undefined {
15 return typeof value === "string" && SAFE_ERROR_LABEL.test(value) ? value : undefined;
17​
18function sanitizeStackUrl(value: string): string {
19 try {
20 const url = new URL(value);
21 // A frame's trailing :line:column parses as part of the query or fragment when one is present.
22 const position = /(?::\d+){1,2}$/.exec(url.hash || url.search)?.[0] ?? "";
23 url.username = "";
24 url.password = "";
25 url.search = "";
26 url.hash = "";
27 return url.toString() + position;
28 } catch {
29 return value;
30 }
32​
33function sanitizeStackFrame(frame: string): string {
34 return frame.replace(/(?:https?|file):\/\/[^\s)]+/g, sanitizeStackUrl);
36​
37function readSafeStack(error: Error): string | undefined {
38 try {
39 const frames = error.stack
40 ?.split(/\r?\n/)
41 .filter((line) => /^\s*at\s+/.test(line) || /^[^@\s]+@(?:https?|file):\/\//.test(line))
42 .slice(0, STACK_FRAME_LIMIT)
43 .map(sanitizeStackFrame);
44 return frames && frames.length > 0 ? frames.join("\n") : undefined;
45 } catch {
46 return undefined;
47 }
49​
50function readErrorTag(error: unknown): string | undefined {
51 try {
52 if (typeof error !== "object" || error === null) {
53 return undefined;
54 }
55 return readSafeLabel((error as { readonly _tag?: unknown })._tag);
56 } catch {
57 return undefined;
58 }
60​
61function readTraceId(error: unknown): string | undefined {
62 try {
63 const seen = new Set<object>();
64 let current: unknown = error;
65​
66 while (typeof current === "object" && current !== null && !seen.has(current)) {
67 seen.add(current);
68 const record = current as { readonly cause?: unknown; readonly traceId?: unknown };
69 if (typeof record.traceId === "string" && SAFE_TRACE_ID.test(record.traceId)) {
70 return record.traceId;
71 }
72 current = record.cause;
73 }
74​
75 return undefined;
76 } catch {
77 return undefined;
78 }
80​
81export function safeErrorLogAttributes(error: unknown): SafeErrorLogAttributes {
82 const errorTag = readErrorTag(error);
83 const traceId = readTraceId(error);
84​
85 if (error instanceof Error) {
86 const errorName = readSafeLabel(error.name);
87 const stack = readSafeStack(error);
88 return {
89 errorType: "error",
90 ...(errorName !== undefined ? { errorName } : {}),
91 ...(errorTag !== undefined ? { errorTag } : {}),
92 ...(traceId !== undefined ? { traceId } : {}),
93 ...(stack !== undefined ? { stack } : {}),
94 };
95 }
96​
97 return {
98 errorType:
99 error === null
100 ? "null"
101 : Array.isArray(error)
102 ? "array"
103 : typeof error === "object"
104 ? "object"
105 : "primitive",
106 ...(errorTag !== undefined ? { errorTag } : {}),
107 ...(traceId !== undefined ? { traceId } : {}),
108 };