UNPKG

autotel

Version:
126 lines (124 loc) 5.49 kB
import { g as nonEmptyString } from "./values-xBtdXtjA.js"; //#region src/exception-fingerprint.ts /** Attribute the fingerprint is written to. */ const EXCEPTION_FINGERPRINT_ATTRIBUTE = "exception.fingerprint"; /** * Frames deep enough to separate two different bugs, shallow enough that the * same bug reached through different callers still groups. */ const DEFAULT_FRAMES = 5; const NODE_FRAME = /^at\s+(.+?)\s+\((.+?):(\d+):\d+\)$/; const ANON_FRAME = /^at\s+(.+?):(\d+):\d+$/; const BROWSER_FRAME = /^(.+?)@(.+?):(\d+):\d+$/; /** * Collapse a path to the part that identifies the code rather than the machine * it ran on: a dependency becomes its package name, and an absolute path * becomes the project-relative one. Without this, the same error fingerprints * differently in CI, in Docker, and on a laptop. */ function normalizeFilePath(filePath) { const nodeModulesMatch = filePath.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/); if (nodeModulesMatch) return `[npm]/${nodeModulesMatch[1]}`; return filePath.replace(/^.*?\/src\//, "src/").replace(/^.*?\/dist\//, "dist/").replace(/^.*?\/lib\//, "lib/").replace(/^file:\/\//, ""); } /** * Strip the parts of a message that change on every occurrence, so * `timeout after 341ms` and `timeout after 78ms` are one issue rather than two. */ function normalizeMessage(message) { return message.replaceAll(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[UUID]").replaceAll(/\b[0-9a-f]{16,}\b/gi, "[ID]").replaceAll(/\d+/g, "[N]").replaceAll(/"[^"]*"/g, "\"[STR]\"").replaceAll(/'[^']*'/g, "'[STR]'").slice(0, 200); } /** * `function@normalized-path` for the top N frames. Line and column are * deliberately dropped: an edit one line above the throw is the same bug. */ function normalizeStackFrames(stack, count) { const frames = []; for (const line of stack.split("\n")) { if (frames.length >= count) break; const trimmed = line.trim(); const node = NODE_FRAME.exec(trimmed); if (node) { frames.push(`${node[1] ?? "anonymous"}@${normalizeFilePath(node[2] ?? "")}`); continue; } const anon = ANON_FRAME.exec(trimmed); if (anon) { frames.push(`anonymous@${normalizeFilePath(anon[1] ?? "")}`); continue; } const browser = BROWSER_FRAME.exec(trimmed); if (browser) frames.push(`${browser[1] ?? "anonymous"}@${normalizeFilePath(browser[2] ?? "")}`); } return frames; } /** 32-bit djb2-style hash, rendered as 8 hex characters. */ function hashFingerprintParts(parts) { const value = parts.join("|"); let hash = 0; for (let index = 0; index < value.length; index++) { hash = (hash << 5) - hash + value.charCodeAt(index); hash = hash & hash; } return Math.abs(hash).toString(16).padStart(8, "0"); } /** * Group key for an exception. Returns `undefined` when there is nothing to * group on, so callers never write an attribute that means "no information". */ function fingerprintException(input, frames = DEFAULT_FRAMES) { const stackFrames = input.stack ? normalizeStackFrames(input.stack, frames) : []; if (stackFrames.length === 0 && !input.message && !input.type) return; const parts = [input.type || "Error"]; if (stackFrames.length > 0) parts.push(...stackFrames); else if (input.message) parts.push(normalizeMessage(input.message)); return hashFingerprintParts(parts); } /** * Read the error off a finished span, wherever it was recorded: the OTel * `exception` event, the `exception.*` attributes autotel-web sets, or the * `error.*` attributes structured errors write instead of an event. */ function readException(span) { const { attributes } = span; const event = span.events?.find((e) => e.name === "exception"); const eventAttributes = event?.attributes ?? {}; const type = nonEmptyString(attributes["exception.type"]) ?? nonEmptyString(attributes["error.type"]) ?? nonEmptyString(eventAttributes["exception.type"]); const stack = nonEmptyString(attributes["exception.stacktrace"]) ?? nonEmptyString(attributes["exception.stack"]) ?? nonEmptyString(attributes["error.stack"]) ?? nonEmptyString(eventAttributes["exception.stacktrace"]) ?? nonEmptyString(eventAttributes["exception.stack"]); if (!type && !stack && !event) return void 0; return { type, message: nonEmptyString(attributes["exception.message"]) ?? nonEmptyString(attributes["error.message"]) ?? nonEmptyString(eventAttributes["exception.message"]) ?? nonEmptyString(span.status.message), stack }; } /** * Span enricher that stamps {@link EXCEPTION_FINGERPRINT_ATTRIBUTE} on every * span that recorded an exception. * * Pass it to `init({ spanEnrichers: [...] })` rather than `spanProcessors`: * enrichers add to the pipeline instead of replacing it, and sit outside the * redaction wrapper so the attribute reaches every exporter. */ function exceptionFingerprint(options = {}) { const frames = options.frames ?? DEFAULT_FRAMES; return { onStart(_span, _context) {}, onEnd(span) { const { attributes } = span; if (attributes["exception.fingerprint"] !== void 0) return; const exception = readException(span); if (!exception) return; const fingerprint = fingerprintException(exception, frames); if (fingerprint) attributes[EXCEPTION_FINGERPRINT_ATTRIBUTE] = fingerprint; }, forceFlush() { return Promise.resolve(); }, shutdown() { return Promise.resolve(); } }; } //#endregion export { EXCEPTION_FINGERPRINT_ATTRIBUTE, exceptionFingerprint, fingerprintException, hashFingerprintParts, normalizeFilePath, normalizeMessage, normalizeStackFrames };