Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 1 | /** |
| 2 | * Errors grouped: the same exception thrown a thousand times is one group | |
| 3 | * with a count, not a thousand lines. What makes two the same is their | |
| 4 | * name, their message with the parts that change taken out (numbers, ids, | |
| 5 | * quoted values), and where they were thrown (the top frame of the stack, | |
| 6 | * its file and function, not its line, which moves with every change). | |
| 7 | */ | |
| 8 | ||
| 9 | /** The message with what varies between occurrences taken out. */ | |
| 10 | export function messageShape(message: string): string { | |
| 11 | return message | |
| 12 | .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<id>") | |
| 13 | .replace(/\b[0-9a-f]{12,}\b/gi, "<id>") | |
| 14 | .replace(/(["'`])(?:(?!\1).){0,200}\1/g, "$1…$1") | |
| 15 | .replace(/https?:\/\/\S+/g, "<url>") | |
| 16 | .replace(/\d+(\.\d+)?/g, "#") | |
| 17 | .replace(/\s+/g, " ") | |
| 18 | .trim() | |
| 19 | .slice(0, 300); | |
| 20 | } | |
| 21 | ||
| 22 | /** | |
| 23 | * The top frame that is the app's own: its function and file, without | |
| 24 | * line, column or a build's hash. Frames of the runtime and of | |
| 25 | * node_modules are skipped while there is another. | |
| 26 | */ | |
| 27 | export function topFrame(stack: string | null | undefined): string { | |
| 28 | if (!stack) return ""; | |
| 29 | const frames = stack | |
| 30 | .split("\n") | |
| 31 | .map((line) => line.trim()) | |
| 32 | .filter((line) => line.startsWith("at ") || /@.+:\d+/.test(line)); | |
| 33 | const own = frames.find((f) => !/node_modules|node:|internal\/|<anonymous>|native/.test(f)) ?? frames[0] ?? ""; | |
| 34 | return own | |
| 35 | .replace(/:\d+(:\d+)?\)?$/, "") | |
| 36 | .replace(/[.-][0-9a-f]{6,}(?=\.\w+$)/i, "") | |
| 37 | .replace(/\(|\)/g, "") | |
| 38 | .replace(/^at /, "") | |
| 39 | .slice(0, 300); | |
| 40 | } | |
| 41 | ||
| 42 | export async function fingerprint(name: string, message: string, stack: string | null | undefined, service: string): Promise<string> { | |
| 43 | const key = [service, name, messageShape(message), topFrame(stack)].join("\u0000"); | |
| 44 | const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(key)); | |
| 45 | return [...new Uint8Array(digest).slice(0, 8)].map((b) => b.toString(16).padStart(2, "0")).join(""); | |
| 46 | } |