@fixback/sdk-core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -9
- package/{src/annotation.ts → dist/annotation.d.ts} +36 -47
- package/dist/anonymous-id.d.ts +23 -0
- package/dist/anonymous-id.js +39 -0
- package/dist/anonymous-id.js.map +1 -0
- package/dist/auto-capture.d.ts +129 -0
- package/dist/auto-capture.js +210 -0
- package/dist/auto-capture.js.map +1 -0
- package/{src/backoff.ts → dist/backoff.d.ts} +17 -52
- package/dist/boot.d.ts +148 -0
- package/dist/boot.js +113 -0
- package/dist/boot.js.map +1 -0
- package/dist/breadcrumb.d.ts +133 -0
- package/dist/connect.d.ts +110 -0
- package/dist/connect.js +147 -0
- package/dist/connect.js.map +1 -0
- package/dist/env.d.ts +37 -0
- package/dist/env.js +83 -0
- package/dist/env.js.map +1 -0
- package/dist/fingerprint.d.ts +39 -0
- package/dist/fingerprint.js +10 -15
- package/dist/fingerprint.js.map +1 -1
- package/dist/http.d.ts +51 -0
- package/dist/http.js +42 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +115 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1057 -0
- package/dist/index.mjs.map +7 -0
- package/dist/options.d.ts +82 -0
- package/dist/options.js +22 -0
- package/dist/options.js.map +1 -0
- package/dist/release.d.ts +19 -0
- package/dist/release.js +36 -0
- package/dist/release.js.map +1 -0
- package/dist/scrub.d.ts +62 -0
- package/dist/stack.d.ts +51 -0
- package/dist/stack.js +97 -0
- package/dist/stack.js.map +1 -0
- package/dist/trace/buffer.d.ts +121 -0
- package/dist/trace/buffer.js +230 -0
- package/dist/trace/buffer.js.map +1 -0
- package/dist/trace/console-args.d.ts +60 -0
- package/dist/trace/console-args.js +189 -0
- package/dist/trace/console-args.js.map +1 -0
- package/dist/trace/console.d.ts +42 -0
- package/dist/trace/console.js +71 -0
- package/dist/trace/console.js.map +1 -0
- package/dist/trace/crumbs.d.ts +88 -0
- package/dist/trace/crumbs.js +164 -0
- package/dist/trace/crumbs.js.map +1 -0
- package/dist/trace/source.d.ts +30 -0
- package/dist/trace/source.js +59 -0
- package/dist/trace/source.js.map +1 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +17 -0
- package/dist/version.js.map +1 -0
- package/dist/wire.d.ts +101 -0
- package/package.json +12 -8
- package/src/backoff.test.ts +0 -94
- package/src/breadcrumb.ts +0 -169
- package/src/fingerprint.test.ts +0 -96
- package/src/fingerprint.ts +0 -112
- package/src/index.ts +0 -63
- package/src/scrub.test.ts +0 -215
- package/src/scrub.ts +0 -226
- package/src/wire.ts +0 -116
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/version.ts", "../src/scrub.ts", "../src/stack.ts", "../src/fingerprint.ts", "../src/backoff.ts", "../src/env.ts", "../src/http.ts", "../src/boot.ts", "../src/connect.ts", "../src/anonymous-id.ts", "../src/release.ts", "../src/options.ts", "../src/trace/console-args.ts", "../src/trace/crumbs.ts", "../src/auto-capture.ts", "../src/trace/buffer.ts", "../src/trace/source.ts", "../src/trace/console.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * `@fixback/sdk-core`'s own version string, kept as an inlined constant rather\n * than a runtime `package.json` import so the published build stays\n * self-contained (the same posture as `packages/sdk/src/version.ts`,\n * `packages/expo/src/version.ts`, and `packages/node/src/version.ts`).\n *\n * It must equal `package.json`'s `version`. Two things keep it there: the\n * Changesets `version` step syncs it automatically (`scripts/sync-sdk-version.mjs`,\n * wired into `version-packages`), and `version.test.ts` fails the build if the two\n * ever drift. Do not hand-edit this line to a value other than `package.json`'s\n * version.\n */\nexport const SDK_CORE_VERSION = \"0.3.0\";\n", "/**\n * The single client-side **scrub choke point** every report passes through\n * before transport (spec 0003 \u00A7C; research `sentry-error-capture-findings.md`\n * \u00A77.4) \u2014 the SDK's `beforeSend` equivalent, shared runtime-agnostically across\n * surfaces (ADR-0028).\n *\n * Masking is the SDK's job, done before anything leaves the process. The\n * screenshot is masked at capture, and breadcrumbs never record a value or a\n * body at the source; `runBeforeSend` is the **last** gate over the assembled\n * report. Its default scrubbers are **on**: they strip credentials, query\n * strings, and bearer tokens from URLs, and redact obvious PII (emails, long\n * digit runs, bearer tokens) from crumb and error text. The result is then handed\n * to an optional per-project hook that can mutate it further or drop the whole\n * report by returning `null`.\n *\n * The hook is **synchronous and network-free** by contract, and both manual\n * (overlay) and automatic (error-capture) reports run through the very same\n * choke point. A project relaxes the defaults with `scrub: false`, or reshapes\n * the draft in its own hook \u2014 never a silent raw send.\n */\n\nimport type { Breadcrumb, BreadcrumbData, ConsoleArg } from \"./breadcrumb\";\nimport type { ReportContent } from \"./wire\";\n\n/** The per-project client scrub hook. Return `null` to drop the whole report. */\nexport type BeforeSend = (draft: ReportContent) => ReportContent | null;\n\n/** Options for {@link runBeforeSend}. */\nexport interface BeforeSendOptions {\n /** The per-project hook, run **after** the default scrubbers. */\n readonly hook?: BeforeSend | null;\n /** Run the built-in default scrubbers first. Defaults to `true`. */\n readonly scrub?: boolean;\n}\n\n/** A digit run at least this long is treated as sensitive (phone, card, id). */\nconst MIN_DIGIT_RUN = 7;\nconst EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/gi;\nconst DIGIT_RUN_RE = new RegExp(`\\\\d{${MIN_DIGIT_RUN},}`, \"g\");\nconst BEARER_RE = /\\b(bearer|token)\\s+[\\w.\\-~+/]+=*/gi;\n/** An http(s) URL embedded in free text (e.g. a console log line). */\nconst URL_IN_TEXT_RE = /\\bhttps?:\\/\\/[^\\s\"'<>]+/gi;\n\n/**\n * Redact obvious PII from free text: email addresses, `Bearer <token>` /\n * `token <value>` pairs, and long digit runs. Conservative by design \u2014 it keeps\n * the shape of the message readable while removing the sensitive spans.\n */\nexport function redactPii(text: string): string {\n if (typeof text !== \"string\" || text.length === 0) return text;\n return text\n .replace(EMAIL_RE, \"[redacted-email]\")\n .replace(BEARER_RE, (_match, keyword: string) => `${keyword} [redacted]`)\n .replace(DIGIT_RUN_RE, \"[redacted-number]\");\n}\n\n/**\n * Redact PII from the **path** portion of a URL (spec #122 \u00A7E, ticket #139):\n * emails and long digit runs in path (or fragment-route) segments \u2014 a network\n * list shows URLs prominently, so an identifier baked into a path\n * (`/users/jane@acme.app`, `/card/4111111111111111`) must not leak. Deliberately\n * narrower than {@link redactPii}: no bearer/token rule (URL credentials are\n * already stripped by {@link scrubUrl}) and it is only ever applied to the\n * post-authority remainder, so a host or port with digits is never touched.\n */\nfunction redactPathPii(pathAndBeyond: string): string {\n return pathAndBeyond\n .replace(EMAIL_RE, \"[redacted-email]\")\n .replace(DIGIT_RUN_RE, \"[redacted-number]\");\n}\n\n/**\n * Strip the sensitive parts of a URL: userinfo credentials\n * (`scheme://user:pass@host`), the entire query string, a token-bearing\n * fragment (one that carries `key=value`), and PII (emails, long digit runs) in\n * the **path segments** (#139). Plain hash routes (`#/checkout`) are kept. Works\n * on absolute and relative URLs alike, with no dependency and no throw. The host\n * (authority) is never redacted \u2014 only the path and any surviving fragment route.\n */\nexport function scrubUrl(url: string): string {\n if (typeof url !== \"string\" || url.length === 0) return url;\n let out = url;\n // Drop userinfo credentials: scheme://user:pass@host \u2192 scheme://host\n out = out.replace(/(^[a-z][a-z0-9+.-]*:\\/\\/)[^/@?#]*@/i, \"$1\");\n // Drop the query string entirely (everything from '?' up to a '#').\n out = out.replace(/\\?[^#]*/, \"\");\n // Drop a token-bearing fragment (it carries '='); keep plain hash routes.\n out = out.replace(/#.*$/, (fragment) =>\n fragment.includes(\"=\") ? \"\" : fragment,\n );\n // Redact PII in the path (and any kept fragment route), never in the authority.\n // Split off `scheme://authority` (or a protocol-relative `//authority`) so a\n // host/port that contains digits is preserved; the remainder is the path onward.\n const absolute = out.match(/^([a-z][a-z0-9+.-]*:\\/\\/[^/?#]*)([\\s\\S]*)$/i);\n if (absolute) return absolute[1] + redactPathPii(absolute[2] ?? \"\");\n const protocolRelative = out.match(/^(\\/\\/[^/?#]*)([\\s\\S]*)$/);\n if (protocolRelative) {\n return protocolRelative[1] + redactPathPii(protocolRelative[2] ?? \"\");\n }\n // A relative URL is all path \u2014 redact the whole thing.\n return redactPathPii(out);\n}\n\ntype MutableBreadcrumbData = { -readonly [K in keyof BreadcrumbData]: BreadcrumbData[K] };\ntype MutableBreadcrumb = { -readonly [K in keyof Breadcrumb]: Breadcrumb[K] };\ntype MutableContent = { -readonly [K in keyof ReportContent]: ReportContent[K] };\n\nfunction scrubCrumbData(data: BreadcrumbData): BreadcrumbData {\n const next: MutableBreadcrumbData = { ...data };\n if (typeof next.url === \"string\") next.url = scrubUrl(next.url);\n if (typeof next.from === \"string\") next.from = scrubUrl(next.from);\n if (typeof next.to === \"string\") next.to = scrubUrl(next.to);\n return next;\n}\n\n/** Scrub a crumb's free-text message: strip URL query strings, then redact PII. */\nfunction scrubMessage(message: string): string {\n return redactPii(message.replace(URL_IN_TEXT_RE, (url) => scrubUrl(url)));\n}\n\n/**\n * Scrub the string leaves of a structured console-arg value (spec #122 \u00A7C/\u00A7E) \u2014 the\n * value a `json` arg carries. Walks arrays and plain objects, applying the same\n * URL-strip + PII redaction as a crumb message to every nested string, so a value\n * logged through `console.*` is swept just like the flattened preview.\n */\nfunction scrubArgValue(value: unknown): unknown {\n if (typeof value === \"string\") return scrubMessage(value);\n if (Array.isArray(value)) return value.map(scrubArgValue);\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [key, v] of Object.entries(value as Record<string, unknown>)) {\n out[key] = scrubArgValue(v);\n }\n return out;\n }\n return value;\n}\n\n/**\n * Scrub one structured console argument (spec #122 \u00A7C): redact PII / strip URL query\n * strings from a `string` value, from every string leaf of a `json` value, and from an\n * `error` value's `message` and `stack`. Other tags (`number`/`bool`/`null`) carry no\n * free text and pass through unchanged.\n */\nfunction scrubConsoleArg(arg: ConsoleArg): ConsoleArg {\n switch (arg.t) {\n case \"string\":\n return typeof arg.v === \"string\" ? { t: \"string\", v: scrubMessage(arg.v) } : arg;\n case \"json\":\n return { t: \"json\", v: scrubArgValue(arg.v) };\n case \"error\": {\n if (!arg.v || typeof arg.v !== \"object\") return arg;\n const v = arg.v as { message?: unknown; stack?: unknown };\n const next: Record<string, unknown> = { ...v };\n if (typeof next.message === \"string\") next.message = scrubMessage(next.message);\n if (typeof next.stack === \"string\") next.stack = scrubMessage(next.stack);\n return { t: \"error\", v: next };\n }\n default:\n return arg;\n }\n}\n\nfunction scrubCrumb(crumb: Breadcrumb): Breadcrumb {\n const next: MutableBreadcrumb = { ...crumb };\n if (typeof next.message === \"string\") next.message = scrubMessage(next.message);\n if (next.data) next.data = scrubCrumbData(next.data);\n // A rich network crumb (#139) carries its scrubbed URL at the top level \u2014 sweep it\n // again here (idempotent) so the choke point holds whether the URL rode in `data`\n // (a legacy thin crumb) or top-level, and path-PII redaction is never skipped.\n if (typeof next.url === \"string\") next.url = scrubUrl(next.url);\n // A console crumb's structured args carry the same free text as its preview \u2014 sweep\n // them too, so PII redaction holds whether a value is read from `message` or `args`.\n if (Array.isArray(next.args)) next.args = next.args.map(scrubConsoleArg);\n return next;\n}\n\n/**\n * Apply the built-in default scrubbers to a report draft: strip the page URL,\n * and scrub every crumb's URLs and redact PII from its text. The Reporter's own\n * `comment` is intentionally left untouched \u2014 it is authored on purpose, not\n * scraped. The screenshot and input values are masked elsewhere (at capture and\n * at crumb creation); this is the final URL/PII sweep.\n */\nexport function applyDefaultScrub(draft: ReportContent): ReportContent {\n const next: MutableContent = { ...draft };\n if (typeof next.url === \"string\") next.url = scrubUrl(next.url);\n if (next.trace && next.trace.length > 0) {\n next.trace = next.trace.map(scrubCrumb);\n }\n // The structured stack frames (#117) carry script URLs \u2014 sweep them again here\n // (idempotent, like the crumb URLs) so the choke point holds regardless of\n // where the frames were parsed.\n if (next.errorFrames && next.errorFrames.length > 0) {\n next.errorFrames = next.errorFrames.map((frame) => ({\n ...frame,\n file: scrubUrl(frame.file),\n }));\n }\n return next;\n}\n\n/**\n * Run the report draft through the client scrub choke point: the default\n * scrubbers first (unless `scrub` is `false`), then the optional per-project\n * hook. Returns the scrubbed (and possibly hook-mutated) draft, or `null` when\n * the hook drops the report. A hook that throws is treated as a no-op \u2014 the\n * already-scrubbed draft is kept, so a buggy hook never breaks the report path\n * nor leaks unscrubbed data.\n */\nexport function runBeforeSend(\n draft: ReportContent,\n options: BeforeSendOptions = {},\n): ReportContent | null {\n const current =\n options.scrub === false ? draft : applyDefaultScrub(draft);\n const hook = options.hook;\n if (!hook) return current;\n try {\n const result = hook(current);\n return result ?? null;\n } catch {\n return current;\n }\n}\n", "/**\n * The **one stack parser** every Fixback SDK shares (ADR-0028): a single frame\n * tokenizer ({@link parseStackLine}) and the structured-frame extraction the wire\n * carries ({@link extractStructuredFrames}).\n *\n * Three consumers read a stack, and each used to carry its own copy of the same\n * regexes: the fingerprint's compact top-frames signature (`fingerprint.ts`), the\n * console crumb's `file:line` source (`trace/source.ts`), and the structured\n * frames server-side symbolication matches against uploaded sourcemaps\n * (`extractStructuredFrames`, #117 / ADR-0024). They now all tokenize a frame the\n * same way, so a stack that parses on one surface parses identically on every\n * other \u2014 the parity guarantee the shared fingerprint depends on.\n *\n * Both engine dialects are handled: V8/Hermes (`at fn (loc)` / `at loc`) and\n * JSC/SpiderMonkey (`fn@loc` / `@loc`).\n */\n\nimport { scrubUrl } from \"./scrub\";\nimport type { CapturedFrame } from \"./wire\";\n\n/** The most structured frames shipped per report (mirrors the server's cap). */\nconst STRUCTURED_FRAME_LIMIT = 30;\n\n/** One tokenized stack frame: the function name (when the frame named one) and its raw location. */\nexport interface StackFrameTokens {\n /** The frame's function name, or `null` for an anonymous / bare frame. */\n readonly fn: string | null;\n /** The raw `file:line:col` location, exactly as the engine wrote it. */\n readonly location: string;\n}\n\n/**\n * Tokenize one **trimmed** stack line into its function name and raw location, or\n * `null` when the line is not a frame at all (the leading `Error: message`, a\n * blank line). The single place the two engine dialects are recognised \u2014 every\n * other stack reader in the core builds on this.\n */\nexport function parseStackLine(line: string): StackFrameTokens | null {\n // V8 / Hermes: \"at fn (loc)\"\n const v8Named = line.match(/^at\\s+(.+?)\\s+\\((.+)\\)$/);\n if (v8Named) return { fn: v8Named[1] ?? null, location: v8Named[2] ?? \"\" };\n // V8 / Hermes: \"at loc\"\n const v8Bare = line.match(/^at\\s+(.+)$/);\n if (v8Bare) return { fn: null, location: v8Bare[1] ?? \"\" };\n // JSC / SpiderMonkey: \"fn@loc\" | \"@loc\"\n const at = line.indexOf(\"@\");\n if (at >= 0) {\n return { fn: at > 0 ? line.slice(0, at) : null, location: line.slice(at + 1) };\n }\n return null;\n}\n\n/**\n * Split a `file:line:col` location into its parts; `null` when it carries no line\n * number or names no locatable file. The file may itself contain colons\n * (`https://\u2026`, `node:internal/\u2026`), so the numeric groups are taken from the right.\n * Unlocatable frames (`native`, `<anonymous>`, eval) are rejected here.\n */\nexport function parseFrameLocation(\n location: string,\n): { file: string; line: number; column: number | null } | null {\n const match = location.match(/^(.*?):(\\d+)(?::(\\d+))?$/);\n if (!match) return null;\n const file = match[1] ?? \"\";\n if (file.length === 0 || file === \"native\" || file.includes(\"<anonymous>\")) {\n return null;\n }\n const line = Number(match[2]);\n if (!Number.isFinite(line)) return null;\n const column = match[3] !== undefined ? Number(match[3]) : null;\n return { file, line, column };\n}\n\n/**\n * Extract **structured** frames from a stack for the wire (#117, ADR-0024), top of\n * stack first \u2014 unlike the fingerprint's compact signature (which drops the origin\n * so a per-deploy asset hash cannot split a bug), these keep the full script path,\n * because server-side symbolication matches it against uploaded sourcemap paths.\n * Each URL is scrubbed (query dropped, path PII redacted) before it can leave the\n * process; unlocatable frames are skipped; the count is capped.\n */\nexport function extractStructuredFrames(\n stack: string | undefined,\n limit = STRUCTURED_FRAME_LIMIT,\n): CapturedFrame[] {\n if (typeof stack !== \"string\" || stack.length === 0) return [];\n const frames: CapturedFrame[] = [];\n for (const raw of stack.split(\"\\n\")) {\n if (frames.length >= limit) break;\n const tokens = parseStackLine(raw.trim());\n if (!tokens || !tokens.location) continue;\n const parsed = parseFrameLocation(tokens.location);\n if (!parsed) continue;\n frames.push({\n file: scrubUrl(parsed.file),\n line: parsed.line,\n column: parsed.column,\n function: tokens.fn && tokens.fn.length > 0 ? tokens.fn : null,\n });\n }\n return frames;\n}\n", "/**\n * The cross-surface error **fingerprint** (research `sentry-error-capture-findings.md`\n * \u00A77.2; ADR-0027/0028) \u2014 the pure, runtime-agnostic key by which the same logical\n * error groups together, computed the **same way** on every SDK.\n *\n * `computeFingerprint(type, value, stack)` hashes the error type, a normalised\n * message, and a compact top-frames signature. It only has to be stable and\n * well-distributed (the client key is a flood guard; the server does canonical\n * cross-session clustering), so a dependency-free FNV-1a hash is right. The\n * `normalize` shape and the frame limit are **starting points** from research\n * (ticket #92), tunable \u2014 never a frozen magic set.\n */\n\nimport { parseStackLine } from \"./stack\";\n\nconst UUID_RE =\n /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;\nconst URL_RE = /\\bhttps?:\\/\\/[^\\s\"')]+/gi;\nconst HEX_0X_RE = /\\b0x[0-9a-f]+\\b/gi;\nconst HEX_RUN_RE = /\\b[0-9a-f]{8,}\\b/gi;\nconst DIGIT_RUN_RE = /\\d{4,}/g;\n\n/** How many top stack frames feed the fingerprint (research \u00A77.2). */\nconst FINGERPRINT_FRAME_LIMIT = 5;\n\n/**\n * Collapse the volatile parts of an error message so a changing string doesn't\n * split one bug: UUIDs, URLs, `0x\u2026` and long hex runs, and long digit runs are\n * replaced with stable placeholders. Short numbers and stable text are kept so\n * genuinely distinct bugs stay distinct. A small, dependency-free regex set \u2014\n * tunable per ticket #92, never a frozen magic set.\n */\nexport function normalize(value: string): string {\n if (typeof value !== \"string\" || value.length === 0) return \"\";\n return value\n .replace(UUID_RE, \"<uuid>\")\n .replace(URL_RE, \"<url>\")\n .replace(HEX_0X_RE, \"<hex>\")\n .replace(HEX_RUN_RE, \"<hex>\")\n .replace(DIGIT_RUN_RE, \"<n>\")\n .trim();\n}\n\n/**\n * A dependency-free FNV-1a hash rendered in base-36. It only has to be stable and\n * well-distributed within one session (the client key is a flood guard; the server\n * does canonical cross-session clustering), so a non-cryptographic hash is right.\n */\nexport function hashString(input: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(36);\n}\n\n/** Reduce a frame location to `basename:line:col`, dropping origin and query. */\nfunction compactLocation(location: string): string {\n const noQuery = location.replace(/\\?[^:]*/, \"\");\n const lastSlash = noQuery.lastIndexOf(\"/\");\n return lastSlash >= 0 ? noQuery.slice(lastSlash + 1) : noQuery;\n}\n\n/**\n * Reduce one stack line to a compact `function@basename:line:col` frame id, using\n * the core's single frame tokenizer (`stack.ts`) so the fingerprint sees exactly\n * the frames every other stack reader does.\n */\nfunction parseFrame(line: string): string | null {\n const tokens = parseStackLine(line);\n if (!tokens) return null;\n return `${tokens.fn ?? \"\"}@${compactLocation(tokens.location)}`;\n}\n\n/**\n * Extract a compact, stable signature of the top in-app frames of a stack: up to\n * {@link FINGERPRINT_FRAME_LIMIT} frames as `function@basename:line:col`, origin\n * and cache-busting query stripped so a per-deploy asset hash doesn't matter within\n * a session. Returns `\"\"` when there is no usable stack (message-only fallback).\n */\nexport function extractTopFrames(\n stack: string | undefined,\n limit = FINGERPRINT_FRAME_LIMIT,\n): string {\n if (typeof stack !== \"string\" || stack.length === 0) return \"\";\n const frames: string[] = [];\n for (const raw of stack.split(\"\\n\")) {\n const frame = parseFrame(raw.trim());\n if (frame) {\n frames.push(frame);\n if (frames.length >= limit) break;\n }\n }\n return frames.join(\" < \");\n}\n\n/**\n * The per-session fingerprint (research \u00A77.2):\n * `hash(errorType + \"|\" + normalize(value) + \"|\" + topFrames)`. Stack frames\n * dominate when present; otherwise it falls back to type + normalized value.\n */\nexport function computeFingerprint(\n type: string,\n value: string,\n stack?: string,\n): string {\n return hashString(`${type}|${normalize(value)}|${extractTopFrames(stack)}`);\n}\n", "/**\n * Client-side backpressure for **automatic** error reports (spec 0003 \u00A7E/\u00A7H,\n * ticket #89) \u2014 the pure, runtime-agnostic backoff every SDK shares (ADR-0028).\n * When ingest sheds `source: error` load it answers `429` with a `Retry-After`;\n * the SDK honours it by holding a pause window during which further automatic\n * reports are dropped without touching the network. **Manual** reports \u2014 a human\n * clicking Send \u2014 never consult this gate.\n *\n * This is the transport's counterpart to the server's per-Project token bucket\n * (`apps/api/src/ingest/auto-report-rate-limiter.ts`): one shared window per page,\n * so a 429 from one auto-report shed applies to the auto-reports that follow it.\n * The clock is injectable so the window is unit-tested deterministically, never on\n * wall time \u2014 mirroring the server limiter's `Clock`.\n */\n\n/** A source of the current time in epoch milliseconds \u2014 injectable for tests. */\nexport type Clock = () => number;\n\n/** The hold window applied when a `429` carries no usable `Retry-After` (spec \u00A7E). */\nexport const DEFAULT_RETRY_AFTER_SECONDS = 60;\n\n/**\n * Parse a `Retry-After` header into whole seconds to hold for. Handles both HTTP\n * forms \u2014 a delta-seconds integer and an HTTP-date (measured from `now`, rounded up\n * and clamped at zero) \u2014 and falls back to {@link DEFAULT_RETRY_AFTER_SECONDS} when\n * the header is absent, blank, or unparseable. Ingest sends the delta-seconds form;\n * the date form is handled for spec-completeness.\n */\nexport function parseRetryAfter(\n header: string | null | undefined,\n now: number,\n): number {\n if (header == null) return DEFAULT_RETRY_AFTER_SECONDS;\n const value = header.trim();\n if (value === \"\") return DEFAULT_RETRY_AFTER_SECONDS;\n\n if (/^\\d+$/.test(value)) {\n return Number(value);\n }\n\n const when = Date.parse(value);\n if (!Number.isNaN(when)) {\n return Math.max(0, Math.ceil((when - now) / 1000));\n }\n\n return DEFAULT_RETRY_AFTER_SECONDS;\n}\n\n/**\n * A single pause window for automatic (`source: error`) reports. `hold` opens (or\n * extends) it from a `429`'s `Retry-After`; `isPaused` reports whether it is still\n * open. The default instance in the SDK's transport is shared across a page's\n * reports so the hold persists across successive automatic submissions.\n */\nexport class AutoReportBackoff {\n /** Epoch ms until which automatic reports are held; `0` when clear. */\n private pausedUntil = 0;\n\n constructor(private readonly now: Clock = Date.now) {}\n\n /** Is the automatic-report pause window currently open? */\n isPaused(): boolean {\n return this.now() < this.pausedUntil;\n }\n\n /** Whole seconds remaining in the pause window (`0` when clear). */\n retryAfterSeconds(): number {\n return Math.max(0, Math.ceil((this.pausedUntil - this.now()) / 1000));\n }\n\n /**\n * Open (or extend) the window from a `429`'s `Retry-After` value, returning the\n * seconds it will hold for. The window only ever grows \u2014 a shorter later hold\n * never clips a longer one already in effect.\n */\n hold(retryAfterHeader: string | null | undefined): number {\n const now = this.now();\n const seconds = parseRetryAfter(retryAfterHeader, now);\n const until = now + seconds * 1000;\n if (until > this.pausedUntil) this.pausedUntil = until;\n return seconds;\n }\n}\n", "/**\n * The **runtime probes** every capture SDK shares (ADR-0028): a high-res clock and\n * the three byte-measuring helpers, written against `globalThis` alone so one\n * implementation serves the browser, React Native, and Node.\n *\n * Each probe reads its global defensively \u2014 `performance`, `TextEncoder`, `Blob`\n * and `ArrayBuffer` are all optional here \u2014 and falls back rather than throwing, so\n * a runtime missing one of them degrades to a coarser measurement instead of\n * breaking capture. Nothing in this module reads a body's *content*: a size is\n * taken only when it is trivially known (spec #122 \u00A7D).\n */\n\n/**\n * A high-res monotonic timestamp: `performance.now()` where the runtime offers it\n * (browsers and React Native both do), else `Date.now()`. Used to order trace\n * entries across streams and to measure request durations as a delta.\n */\nexport function nowMono(): number {\n const perf = (globalThis as { performance?: { now?: () => number } }).performance;\n return perf && typeof perf.now === \"function\" ? perf.now() : Date.now();\n}\n\n/** The `TextEncoder` constructor, when the runtime provides one. */\nfunction textEncoder():\n | (new () => { encode(input: string): { length: number } })\n | undefined {\n return (\n globalThis as {\n TextEncoder?: new () => { encode(input: string): { length: number } };\n }\n ).TextEncoder;\n}\n\n/**\n * A string's UTF-8 byte length, measured with `TextEncoder` when available and\n * falling back to the UTF-16 code-unit count otherwise. Never throws \u2014 the\n * fallback is an approximation, not an error.\n */\nexport function utf8ByteLength(value: string): number {\n try {\n const Encoder = textEncoder();\n return Encoder ? new Encoder().encode(value).length : value.length;\n } catch {\n return value.length;\n }\n}\n\n/**\n * The serialized UTF-8 byte length of a value; `Infinity` when it cannot be\n * serialized at all. The cap helpers use it to bound what a single entry may\n * carry, so an unserializable value is treated as over budget rather than shipped.\n */\nexport function serializedBytes(value: unknown): number {\n try {\n const json = JSON.stringify(value) ?? \"\";\n return utf8ByteLength(json);\n } catch {\n return Number.POSITIVE_INFINITY;\n }\n}\n\n/**\n * The trivially-known byte size of a request body (spec #122 \u00A7D) \u2014 a string's\n * UTF-8 length, a `Blob`'s `.size`, or an `ArrayBuffer`/typed-array `.byteLength`.\n * Anything that would require **reading** the body (a `ReadableStream`, `FormData`,\n * a `Request` with a stream body) returns `undefined` \u2014 the \"never read the body\"\n * line holds.\n */\nexport function trivialBodySize(body: unknown): number | undefined {\n if (typeof body === \"string\") return utf8ByteLength(body);\n // `Blob` is optional here (the core compiles without the DOM lib) and its real\n // constructor takes arguments, so the probe is read through `unknown` and only\n // ever used as an `instanceof` right-hand side.\n const BlobCtor = (\n globalThis as unknown as {\n Blob?: abstract new (...args: never[]) => { size: number };\n }\n ).Blob;\n if (BlobCtor && body instanceof BlobCtor) return body.size;\n if (typeof ArrayBuffer !== \"undefined\") {\n if (body instanceof ArrayBuffer) return body.byteLength;\n if (ArrayBuffer.isView(body)) return (body as ArrayBufferView).byteLength;\n }\n return undefined;\n}\n", "/**\n * The **structural HTTP surface** every Fixback SDK talks to ingest through\n * (ADR-0028) \u2014 one description of the `fetch` slice the transports use, shared by\n * the browser, Expo, and Node SDKs.\n *\n * Described structurally rather than against the DOM lib: the core typechecks with\n * `types: []` and no `lib.dom`, React Native is not a browser, and Node's `fetch`\n * is a global with its own types. Every runtime satisfies these shapes at runtime,\n * and tests inject plain fakes through the same seams. Only what the SDKs actually\n * read is described \u2014 status, `ok`, one header at a time, and an optional JSON body.\n */\n\n/**\n * The response slice the transports read: the outcome, the status, the header bag\n * (for `Retry-After` / `content-length`), and \u2014 for the endpoints that answer with\n * JSON \u2014 the parsed body. `json()` is **optional**: the batched Node transport\n * never reads a body, so a fake need not provide one.\n */\nexport interface FetchResponseLike {\n readonly ok: boolean;\n readonly status: number;\n readonly headers?: { get(name: string): string | null } | null;\n json?(): Promise<unknown>;\n}\n\n/** The request init the SDKs send \u2014 a method, plain headers, and a body. */\nexport interface FetchRequestInit {\n readonly method: string;\n readonly headers?: Record<string, string>;\n /**\n * The request body: a JSON string, or a runtime-specific multipart form (a\n * browser `FormData`, React Native's `FormData`). Deliberately `unknown` \u2014 the\n * core never inspects it, it only hands it to the runtime's `fetch`.\n */\n readonly body?: unknown;\n}\n\n/** A `fetch`-shaped function, injectable for tests. */\nexport type FetchLike = (\n url: string,\n init: FetchRequestInit,\n) => Promise<FetchResponseLike>;\n\n/**\n * The runtime's global `fetch` adapted to {@link FetchLike}, or `undefined` when\n * the runtime has none. Bound to `globalThis` so a `this`-less call never throws\n * an \"illegal invocation\", and adapted through one cast here so no call site has\n * to reconcile the runtime's own `fetch` types with the structural slice.\n */\nexport function resolveFetch(): FetchLike | undefined {\n const g = globalThis as unknown as {\n fetch?: (input: string, init?: unknown) => Promise<unknown>;\n };\n if (typeof g.fetch !== \"function\") return undefined;\n const bound = g.fetch.bind(globalThis);\n return (url, init) => bound(url, init) as Promise<FetchResponseLike>;\n}\n\n/**\n * Read one response header defensively \u2014 `null` when the response carries no\n * header bag, the header is absent, or the getter throws (an opaque cross-origin\n * response). A transport must never fail because a header could not be read.\n */\nexport function safeHeader(response: FetchResponseLike, name: string): string | null {\n try {\n return response.headers?.get?.(name) ?? null;\n } catch {\n return null;\n }\n}\n", "/**\n * The ingest **boot + feedback wire contract** \u2014 the one copy every capture SDK\n * speaks (ADR-0028).\n *\n * The SDKs deliberately do not import `@fixback/shared` \u2014 that package is private\n * and server-shaped (ticket #47) \u2014 so the exact slice they need lives here, in the\n * shared core, rather than being vendored once per SDK. Keep it in lock-step with\n * the server: the request body accepted by `POST /api/ingest/boot`\n * (`apps/api/src/ingest/ingest.controller.ts`) and the `BootAnswer` returned by\n * `evaluateBoot` (`apps/api/src/ingest/reporter-identity.ts`).\n *\n * One runtime difference is carried as an option, not a fork: a browser attaches\n * the `Origin` header itself, while a native HTTP stack attaches none \u2014 so React\n * Native passes its configured `origin` and the server reads it to decide\n * `originAllowed` exactly as it does for a browser request (spec 0004 \u00A7A).\n */\n\nimport { type FetchLike, type FetchResponseLike, resolveFetch } from \"./http\";\n\n/** A Project's Gate \u2014 who may submit. Mirrors the server's `ProjectGate`. */\nexport type ProjectGate = \"open\" | \"invited\" | \"internal\";\n\n/** The trust tier a Reporter holds. Mirrors the server's `ReporterTier`. */\nexport type ReporterTier = \"public\" | \"invited\" | \"internal\";\n\n/**\n * Optional identity evidence an SDK forwards to boot. None of it is a tier: the\n * server re-derives trust from this evidence and never honours a self-declared\n * tier, so no SDK sends one.\n */\nexport interface IdentityInputs {\n /** A Host identity token minted by the customer's server (the escape hatch). */\n readonly hostIdentity?: string;\n /**\n * The **Reporter session** token the SDK holds after a Connect (ADR-0032). The\n * server checks it against the live session row and re-derives the Account's tier;\n * a revoked, expired, or forged token is treated as absent (the anonymous branch),\n * never an error. Replaces the removed device-local `reporterId` branch.\n */\n readonly reporterSession?: string;\n /** The anonymous first-party identifier for a Public Reporter on an Open Project. */\n readonly anonymousId?: string;\n}\n\n/** The JSON body `POST /api/ingest/boot` accepts. */\nexport interface BootRequest extends IdentityInputs {\n readonly key: string;\n}\n\n/**\n * The signed-in Account a valid Reporter session resolves to, as the boot answer and\n * the Connect exchange report it for the identity chip (ADR-0032). Name and email\n * come from the `user`, never from anything the client set. `null`/absent means the\n * caller is anonymous.\n */\nexport interface ConnectedAccount {\n readonly name: string;\n readonly email: string;\n}\n\n/**\n * The Project's effective console/network capture config, served on the boot answer\n * (spec #122 \u00A7L; ticket #138). Each flag is the per-project master toggle ANDed with\n * that stream's own toggle, so an SDK gates instrumentation on one boolean per\n * stream. Optional on the wire: an older server that does not send it (or a\n * malformed value) is treated as **capture on** \u2014 default-on, matching the server\n * default \u2014 and init options override whatever is served.\n */\nexport interface CaptureConfig {\n readonly console: boolean;\n readonly network: boolean;\n /**\n * The Project's session-replay toggle (issue #189, ADR-0024) \u2014 browser only.\n * Optional on the wire: a server predating replay omits it, which \u2014 like the\n * other streams \u2014 means **capture on**.\n */\n readonly replay?: boolean;\n}\n\n/**\n * The boot answer: whether this origin is allowlisted, the Project's Gate, the\n * caller's derived tier (`null` when a presented identity was refused), whether a\n * submission would be accepted right now, and the Project's capture config. An SDK\n * shows its launcher (or arms its gesture) only when `canSubmit` is true.\n */\nexport interface BootAnswer {\n readonly originAllowed: boolean;\n readonly gate: ProjectGate;\n readonly tier: ReporterTier | null;\n readonly canSubmit: boolean;\n /** The Project's console/network capture config; absent \u21D2 default-on (#138). */\n readonly capture?: CaptureConfig;\n /**\n * Where the SDK opens **Connect** (ADR-0032) \u2014 the platform's connect page, built\n * by the server from its configured dashboard origin and carried on every boot so\n * the SDK does not need to know the Fixback dashboard origin itself. Absent from an\n * older server; the SDK then cannot offer Connect.\n */\n readonly connectUrl?: string;\n /**\n * The signed-in Account when a valid Reporter session was presented, else `null`.\n * Lets the identity chip show the person's name on a page reload, when the SDK holds\n * only the stored token.\n */\n readonly account?: ConnectedAccount | null;\n /**\n * A **refreshed** Reporter session token when the presented one was valid (30-day\n * sliding lifetime; ADR-0032). The SDK swaps its stored token for this on every\n * boot, so an actively-used session never lapses. Absent when no valid session was\n * presented.\n */\n readonly reporterSession?: string;\n}\n\n/** Join an API base URL with the boot path, tolerating a trailing slash. */\nexport function bootEndpoint(apiUrl: string): string {\n return `${apiUrl.replace(/\\/+$/, \"\")}/api/ingest/boot`;\n}\n\n/** Join an API base URL with the feedback path, tolerating a trailing slash. */\nexport function feedbackEndpoint(apiUrl: string): string {\n return `${apiUrl.replace(/\\/+$/, \"\")}/api/ingest/feedback`;\n}\n\n/** Narrow an unknown JSON body to a {@link BootAnswer} before an SDK trusts it. */\nexport function isBootAnswer(value: unknown): value is BootAnswer {\n if (typeof value !== \"object\" || value === null) return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.originAllowed === \"boolean\" &&\n typeof v.canSubmit === \"boolean\" &&\n typeof v.gate === \"string\" &&\n (v.tier === null || typeof v.tier === \"string\")\n );\n}\n\n/** Drop `undefined` identity fields so a payload carries only what was given. */\nexport function compactIdentity(identity: IdentityInputs | undefined): IdentityInputs {\n if (!identity) return {};\n const out: {\n hostIdentity?: string;\n reporterSession?: string;\n anonymousId?: string;\n } = {};\n if (identity.hostIdentity) out.hostIdentity = identity.hostIdentity;\n if (identity.reporterSession) out.reporterSession = identity.reporterSession;\n if (identity.anonymousId) out.anonymousId = identity.anonymousId;\n return out;\n}\n\n/**\n * Does `url` address one of the SDK's **own** API endpoints? Every request a capture\n * SDK issues lives under two namespaces beneath the API origin: `/api/ingest/*` \u2014\n * boot and feedback \u2014 and `/api/connect/*` \u2014 the Connect code exchange and sign-out\n * (ADR-0032).\n *\n * The trace's network capture ignores **only** these, never the whole origin. That\n * distinction matters when the instrumented app and the Fixback API share an origin \u2014 a\n * self-hosted deployment, or Fixback running its own SDK on its own dashboard \u2014 where\n * ignoring the origin wholesale would swallow all of the app's own requests and leave the\n * Network tab empty. When the app and the API sit on different origins (the common case)\n * this matches nothing extra, since the app's origin is never `apiUrl`. `url` is the raw\n * request URL \u2014 absolute for every call an SDK makes \u2014 so a prefix test over the\n * normalised origin is exact.\n */\nexport function isFixbackApiRequest(apiUrl: string, url: string): boolean {\n const apiBase = apiUrl.replace(/\\/+$/, \"\");\n return (\n url.startsWith(`${apiBase}/api/ingest/`) ||\n url.startsWith(`${apiBase}/api/connect/`)\n );\n}\n\n/** Injectable collaborators for {@link requestBoot}. */\nexport interface BootDeps {\n /** The `fetch` to call. Defaults to the runtime's global. */\n readonly fetch?: FetchLike;\n /**\n * The origin to declare explicitly, for a runtime that attaches no `Origin`\n * header of its own (React Native). Omit in a browser \u2014 the user agent sets it,\n * and a page may not override it.\n */\n readonly origin?: string;\n}\n\n/**\n * Ask ingest whether a submission would be accepted for this key / origin / Gate.\n * Resolves to the boot answer, or `null` when Fixback could not be reached, the key\n * was refused, or the response was not a boot answer. It never throws: any\n * non-answer is treated by the caller as \"stay dormant\", so a Fixback outage stays\n * invisible to the host app (ticket #47: \"fails quietly\").\n */\nexport async function requestBoot(\n apiUrl: string,\n request: BootRequest,\n deps: BootDeps = {},\n): Promise<BootAnswer | null> {\n const doFetch = deps.fetch ?? resolveFetch();\n if (!doFetch) return null;\n\n let response: FetchResponseLike;\n try {\n response = await doFetch(bootEndpoint(apiUrl), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(deps.origin ? { origin: deps.origin } : {}),\n },\n body: JSON.stringify(request),\n });\n } catch {\n return null; // network error / Fixback unreachable\n }\n\n if (!response.ok) return null; // 401 unknown key, or any other refusal\n\n let body: unknown;\n try {\n body = await response.json?.();\n } catch {\n return null;\n }\n\n return isBootAnswer(body) ? body : null;\n}\n", "/**\n * The **Connect** wire contract (ADR-0032, CONTEXT.md \"Connect\") \u2014 the one copy every\n * capture SDK speaks, kept in the shared core beside the boot contract for the same\n * reason (`@fixback/shared` is server-shaped and private, ticket #47).\n *\n * Connect is the round trip that binds an Account to a site: the SDK opens the\n * platform's connect page ({@link BootAnswer.connectUrl}), the person signs in, the\n * page hands back a **one-time code**, and the SDK exchanges it here for a **Reporter\n * session** \u2014 a compact token it stores per origin and forwards on every boot. Keep\n * this in lock-step with the server: `POST /api/connect/exchange` and\n * `POST /api/connect/signout` (`apps/api/src/connect/*`).\n */\n\nimport type { ConnectedAccount, ReporterTier } from \"./boot\";\nimport { type FetchLike, type FetchResponseLike, resolveFetch } from \"./http\";\n\n/**\n * The `postMessage` a connect page in a popup sends back to the opener. `source`\n * pins it to Fixback so an unrelated message on the window is ignored; `code` is the\n * one-time Connect code to exchange. Read with {@link parseConnectMessage}.\n */\nexport const CONNECT_MESSAGE_SOURCE = \"fixback-connect\";\n\n/**\n * The query parameter a Fixback link \u2014 and the connect page's **redirect fallback** \u2014\n * carries the one-time code back on, when a popup could not be used (ADR-0032,\n * \"arriving with the `fixback` query parameter\"). The SDK detects it on boot, exchanges\n * it, and strips it from the URL.\n */\nexport const CONNECT_RETURN_PARAM = \"fixback\";\n\n/** The shape of the popup-to-opener message; `code` is the one-time Connect code. */\nexport interface ConnectMessage {\n readonly source: typeof CONNECT_MESSAGE_SOURCE;\n readonly code: string;\n}\n\n/**\n * The identity the SDK reports through `Fixback.identity()` and renders in the chip:\n * anonymous, or a connected Account with its live tier. The tier is the server's, from\n * the last boot \u2014 never self-declared.\n */\nexport type ReporterIdentity =\n | { readonly status: \"anonymous\" }\n | {\n readonly status: \"connected\";\n readonly name: string;\n readonly email: string;\n readonly tier: ReporterTier | null;\n };\n\n/** The JSON body `POST /api/connect/exchange` accepts. */\nexport interface ConnectExchangeRequest {\n /** The Project's publishable key \u2014 the exchange must be for the code's Project. */\n readonly key: string;\n /** The one-time Connect code from the popup message or the return query parameter. */\n readonly code: string;\n /**\n * The browser's current anonymous id, if any \u2014 so the server can link the existing\n * anonymous Reporter to the Account (one Reporter, prior Feedback attributed).\n */\n readonly anonymousId?: string;\n /** The Platform the Connect happened on; defaults to `browser` server-side. */\n readonly platform?: \"browser\" | \"expo\";\n}\n\n/** The JSON `POST /api/connect/exchange` returns on success. */\nexport interface ConnectExchangeResponse {\n /** The Reporter session token to store per origin and forward on every boot. */\n readonly reporterSession: string;\n /** The signed-in Account, for the identity chip. */\n readonly account: ConnectedAccount;\n}\n\n/** Join an API base URL with the Connect exchange path, tolerating a trailing slash. */\nexport function connectExchangeEndpoint(apiUrl: string): string {\n return `${apiUrl.replace(/\\/+$/, \"\")}/api/connect/exchange`;\n}\n\n/** Join an API base URL with the Connect sign-out path, tolerating a trailing slash. */\nexport function connectSignOutEndpoint(apiUrl: string): string {\n return `${apiUrl.replace(/\\/+$/, \"\")}/api/connect/signout`;\n}\n\n/**\n * Build the connect-page URL the SDK opens (popup or redirect). `connectUrl` is the\n * base the boot answer carried; `key` identifies the Project and `returnUrl` is where\n * the person is sent back \u2014 validated server-side against the Project's allowed\n * origins, so a hostile page cannot redirect the code anywhere else.\n */\nexport function buildConnectUrl(\n connectUrl: string,\n params: { readonly key: string; readonly returnUrl: string },\n): string {\n // Built with `encodeURIComponent` rather than `URLSearchParams`/`URL` so the\n // runtime-agnostic core needs no DOM lib (it runs on web, Expo, and Node alike).\n const base = connectUrl.split(\"#\")[0] ?? connectUrl;\n const sep = base.includes(\"?\") ? \"&\" : \"?\";\n const query =\n `key=${encodeURIComponent(params.key)}` +\n `&return=${encodeURIComponent(params.returnUrl)}`;\n return `${base}${sep}${query}`;\n}\n\n/**\n * The `localStorage` key a browser SDK stores its Reporter session token under,\n * **scoped by publishable key** so two Fixback Projects on one origin keep separate\n * sessions (ADR-0032). The storage binding is per-runtime; the key format is shared.\n */\nexport function reporterSessionStorageKey(publishableKey: string): string {\n return `fixback.reporterSession.${publishableKey}`;\n}\n\n/**\n * Narrow an unknown `message.data` to the Connect code it carries, or `null`. Guards\n * `source` so an unrelated `postMessage` \u2014 the web is noisy \u2014 is ignored.\n */\nexport function parseConnectMessage(data: unknown): string | null {\n if (typeof data !== \"object\" || data === null) return null;\n const v = data as Record<string, unknown>;\n if (v.source !== CONNECT_MESSAGE_SOURCE) return null;\n return typeof v.code === \"string\" && v.code.length > 0 ? v.code : null;\n}\n\n/** Narrow an unknown JSON body to a {@link ConnectExchangeResponse}. */\nexport function isConnectExchangeResponse(\n value: unknown,\n): value is ConnectExchangeResponse {\n if (typeof value !== \"object\" || value === null) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.reporterSession !== \"string\" || v.reporterSession.length === 0) {\n return false;\n }\n const account = v.account as Record<string, unknown> | undefined;\n return (\n typeof account === \"object\" &&\n account !== null &&\n typeof account.name === \"string\" &&\n typeof account.email === \"string\"\n );\n}\n\n/** Injectable collaborators for the Connect fetch helpers. */\nexport interface ConnectDeps {\n readonly fetch?: FetchLike;\n}\n\n/**\n * Exchange a one-time code for a Reporter session. Resolves to the response, or `null`\n * when Fixback was unreachable, the code was refused, or the body was not a valid\n * response \u2014 the caller stays anonymous rather than surfacing an error into the host\n * page (ticket #47: \"fails quietly\").\n */\nexport async function exchangeConnectCode(\n apiUrl: string,\n request: ConnectExchangeRequest,\n deps: ConnectDeps = {},\n): Promise<ConnectExchangeResponse | null> {\n const doFetch = deps.fetch ?? resolveFetch();\n if (!doFetch) return null;\n\n let response: FetchResponseLike;\n try {\n response = await doFetch(connectExchangeEndpoint(apiUrl), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request),\n });\n } catch {\n return null;\n }\n if (!response.ok) return null;\n\n let body: unknown;\n try {\n body = await response.json?.();\n } catch {\n return null;\n }\n return isConnectExchangeResponse(body) ? body : null;\n}\n\n/**\n * Revoke a Reporter session server-side (\"sign out\"). Best-effort: a network failure\n * still lets the SDK clear its stored token locally, and a revoked-anyway token is\n * refused at the next boot regardless.\n */\nexport async function revokeReporterSession(\n apiUrl: string,\n token: string,\n deps: ConnectDeps = {},\n): Promise<void> {\n const doFetch = deps.fetch ?? resolveFetch();\n if (!doFetch) return;\n try {\n await doFetch(connectSignOutEndpoint(apiUrl), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ reporterSession: token }),\n });\n } catch {\n // Best-effort: the local token is cleared by the caller regardless.\n }\n}\n", "/**\n * The Public Reporter's stable, first-party **anonymous id** (ADR-0028) \u2014 the key\n * and the generator, shared by every capture SDK.\n *\n * When the host app supplies no identity of its own, an SDK establishes a\n * per-device id and persists it, so repeated boots from the same device are one\n * Reporter (the server counts distinct Reporters by it). The *storage binding*\n * stays runtime-specific \u2014 `localStorage` in a browser, AsyncStorage on React\n * Native \u2014 because one is synchronous and the other is not; only the key and the\n * id shape are shared, so the value looks the same across clients.\n *\n * Persistence is best-effort everywhere: when storage is unavailable (private\n * mode, blocked cookies, an absent native module) the SDK falls back to an\n * ephemeral id rather than throwing \u2014 capture must never disturb the host app.\n */\n\n/** The storage key every SDK persists the anonymous id under. */\nexport const ANONYMOUS_ID_STORAGE_KEY = \"fixback.anonymousId\";\n\n/**\n * A random id: a UUID where the platform offers one, else a compact\n * timestamp+random fallback. `crypto.randomUUID` is read off `globalThis` and\n * called defensively \u2014 it throws in some non-secure contexts.\n */\nexport function generateAnonymousId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c && typeof c.randomUUID === \"function\") {\n try {\n return c.randomUUID();\n } catch {\n // Fall through to the compact fallback.\n }\n }\n return `fb-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n", "/**\n * **Release** validation (#117, ADR-0024) \u2014 the one copy, mirrored *by value* from\n * the server's shared rules (`@fixback/shared`'s `isValidRelease`).\n *\n * A Release is the build identifier uploaded sourcemaps are keyed by, so the SDK\n * that stamps it and the CLI/plugin that upload against it must agree exactly:\n * 1\u2013{@link RELEASE_MAX_LENGTH} visible characters, no whitespace, control\n * characters, or path separators, and not a reserved name. `@fixback/shared` is\n * workspace-private and the SDKs publish standalone, so the slice is vendored here\n * once and shared from the core rather than copied per package.\n */\n\n/** The longest release string the server accepts (mirrors the server's `RELEASE_MAX_LENGTH`). */\nexport const RELEASE_MAX_LENGTH = 100;\n\n/** Release names the server reserves (mirrors the shared release rules). */\nconst RESERVED_RELEASES = new Set([\".\", \"..\", \"latest\"]);\n\n/**\n * Trim + validate a builder-supplied release. Returns `undefined` for an unusable\n * value \u2014 the caller drops it (with a warning) rather than shipping something the\n * server would discard.\n */\nexport function normaliseRelease(value: string): string | undefined {\n const trimmed = value.trim();\n if (trimmed.length === 0 || trimmed.length > RELEASE_MAX_LENGTH) return undefined;\n if (RESERVED_RELEASES.has(trimmed)) return undefined;\n // eslint-disable-next-line no-control-regex\n if (/[\\s/\\\\\\u0000-\\u001f\\u007f]/.test(trimmed)) return undefined;\n return trimmed;\n}\n", "/**\n * The **shared init vocabulary** (ADR-0028) \u2014 the option names every Fixback SDK\n * spells the same way, and the hosted API origin they all default to.\n *\n * Each SDK's own `InitOptions` extends {@link CommonInitOptions} and adds only what\n * its runtime genuinely needs (the browser: a mount target, reduced motion, replay;\n * Expo: the declared origin, the shake gesture, screenshots; Node: the secret key\n * and the batch knobs). So `apiUrl`, `release`, `environment`, `enabled`, `scrub`,\n * and `beforeSend` mean exactly one thing across the family, and a builder who\n * learns one SDK already knows the others.\n */\n\nimport type { BeforeBreadcrumb } from \"./trace/buffer\";\nimport type { ReportContent } from \"./wire\";\n\n/**\n * The hosted Fixback API origin every SDK talks to by default. A self-hosted or\n * local deployment overrides it with `apiUrl` (the dashboard's install snippet\n * pre-fills the right value for the Project), and the CLI/bundler plugins read the\n * same constant so an upload and a capture agree on where Fixback lives.\n */\nexport const DEFAULT_API_URL = \"https://api.fixback.dev\";\n\n/**\n * The dev-side override of a Project's server-served capture config (spec #122 \u00A7L;\n * ticket #138). Capture is **default-on** and normally governed per-project from\n * the dashboard, surfaced on the boot answer; set a stream here to override what\n * the server serves for it \u2014 `{ network: false }` turns network capture off even\n * where the Project leaves it on, and a stream left unset follows the served\n * config. Turning a stream off means it is never instrumented, so no entry of that\n * kind ever reaches the wire.\n */\nexport interface CaptureToggles {\n readonly console?: boolean;\n readonly network?: boolean;\n /** Buffered session replay \u2014 browser only (issue #189, ADR-0024). */\n readonly replay?: boolean;\n}\n\n/**\n * The options every Fixback SDK accepts, with one meaning each.\n *\n * Generic over the draft a `beforeSend` hook sees, because that differs by surface:\n * the capture SDKs hand it a `ReportContent`, `@fixback/node` a `FixbackErrorEvent`.\n * The hook contract is identical either way \u2014 synchronous, network-free, mutate to\n * redact or return `null` to drop \u2014 and it always runs **after** the built-in\n * scrubbers.\n */\nexport interface CommonInitOptions<Draft> {\n /** The Fixback API origin. Defaults to {@link DEFAULT_API_URL}. */\n readonly apiUrl?: string;\n /**\n * The host app's **Release** \u2014 a build identifier (a git SHA, a semver, a\n * `name@version`) stamped on every submission (#117, ADR-0024). Set it to the\n * same value the build uploaded sourcemaps under (`npx fixback sourcemaps upload\n * --release <v>`), and captured errors gain a symbolicated **code-area pointer**.\n * An invalid value (whitespace, slashes, over 100 chars) is ignored with a warning.\n */\n readonly release?: string;\n /** The deploy environment (`production` / `staging` / \u2026) stamped on every capture. */\n readonly environment?: string;\n /**\n * Master gate. Defaults to `true`; set `false` and the SDK stays completely\n * dormant \u2014 nothing is instrumented, no boot call is made, and no report can be\n * sent. The way to keep Fixback out of local dev without branching your code.\n */\n readonly enabled?: boolean;\n /** Run the built-in default scrubbers before `beforeSend`. Defaults to `true` (private-by-default). */\n readonly scrub?: boolean;\n /**\n * The synchronous, network-free client scrub hook every capture passes through\n * before transport (spec 0003 \u00A7C). Mutate the draft to scrub further, or return\n * `null` to drop it. Runs after the default scrubbers.\n */\n readonly beforeSend?: (draft: Draft) => Draft | null;\n /** A per-crumb trace filter: mute a category, edit a crumb, or drop it (`null`). */\n readonly beforeBreadcrumb?: BeforeBreadcrumb;\n /** Per-stream capture overrides; a stream left unset follows the served config. */\n readonly capture?: CaptureToggles;\n}\n\n/**\n * The capture SDKs' common options, with `beforeSend` already bound to the\n * {@link ReportContent} draft they hand it \u2014 the shape `@fixback/sdk` and\n * `@fixback/expo` extend.\n */\nexport type CommonCaptureInitOptions = CommonInitOptions<ReportContent>;\n", "/**\n * Structured **console arguments** (spec #122 \u00A7C, decision D7) \u2014 the shared,\n * runtime-agnostic assembly that turns a `console.*` call's arguments into\n * type-tagged, JSON-safe, size-capped {@link ConsoleArg} values, plus the one-line\n * preview the crumb's `message` carries.\n *\n * Preserving arguments type-tagged rather than flattening them to a string is what\n * lets the Console tab render an object or an Error expandably instead of\n * `[object Object]`. Every cap is applied **at assembly**, so a single console\n * crumb can never bloat a report and an exotic or circular value can never reach\n * the wire unserialized.\n */\n\nimport { serializedBytes } from \"../env\";\nimport type { ConsoleArg } from \"../breadcrumb\";\n\n/** Longest crumb message kept; a huge log line is truncated, never dropped. */\nexport const MAX_MESSAGE_LENGTH = 300;\n\n/**\n * Structured console argument caps (spec #122 \u00A7C/\u00A7F), all applied **at assembly** so a\n * single console crumb can never bloat a report:\n * - {@link MAX_ARG_DEPTH} bounds how deep a `json` argument is cloned (deeper nodes\n * collapse to an `[Object]`/`[Array]` marker);\n * - {@link MAX_ARG_ITEMS} bounds how many keys/elements are kept at each level;\n * - {@link MAX_ARG_STRING_LENGTH} truncates a single over-long string value;\n * - {@link MAX_CONSOLE_ARGS_BYTES} bounds the serialized size of the whole args array\n * (trailing args are dropped to fit), kept well under ingest's per-entry byte cap.\n */\nexport const MAX_ARG_DEPTH = 4;\nexport const MAX_ARG_ITEMS = 100;\nexport const MAX_ARG_STRING_LENGTH = 1024;\nexport const MAX_CONSOLE_ARGS_BYTES = 4096;\n\n/** Longest Error `stack` kept on a structured `error` arg, truncated never dropped. */\nconst MAX_ERROR_STACK_LENGTH = 2048;\n\n/** Truncate a string to `max` with an ellipsis marker; keeps the value, never drops it. */\nexport function capLength(value: string, max: number): string {\n return value.length > max ? `${value.slice(0, max)}\u2026` : value;\n}\n\n/** Render one console argument to the flat text of the crumb's preview line. */\nexport function stringifyArg(arg: unknown): string {\n if (typeof arg === \"string\") return arg;\n if (arg instanceof Error) return `${arg.name}: ${arg.message}`;\n if (arg === null || arg === undefined) return String(arg);\n if (typeof arg === \"number\" || typeof arg === \"boolean\") return String(arg);\n try {\n return JSON.stringify(arg) ?? String(arg);\n } catch {\n return \"[object]\";\n }\n}\n\n/** The one-line preview a console crumb's `message` carries, length-capped. */\nexport function joinArgs(args: readonly unknown[]): string {\n const text = args.map(stringifyArg).join(\" \");\n return text.length > MAX_MESSAGE_LENGTH\n ? `${text.slice(0, MAX_MESSAGE_LENGTH)}\u2026`\n : text;\n}\n\n/** An Error rendered to a structured, size-capped `{ name, message, stack? }`. */\nexport function describeErrorValue(error: Error): {\n name: string;\n message: string;\n stack?: string;\n} {\n const out: { name: string; message: string; stack?: string } = {\n name: error.name || \"Error\",\n message: capLength(String(error.message ?? \"\"), MAX_ARG_STRING_LENGTH),\n };\n if (typeof error.stack === \"string\" && error.stack.length > 0) {\n out.stack = capLength(error.stack, MAX_ERROR_STACK_LENGTH);\n }\n return out;\n}\n\n/**\n * Build a JSON-safe, depth-/breadth-/string-capped clone of a value for a `json`\n * console argument. Beyond {@link MAX_ARG_DEPTH} the node collapses to a marker;\n * at each level at most {@link MAX_ARG_ITEMS} keys/elements are kept; strings are\n * truncated to {@link MAX_ARG_STRING_LENGTH}; circular references become\n * `\"[Circular]\"`; and exotic values (bigint / symbol / function / undefined) are\n * rendered to safe text \u2014 so the result is always serializable and bounded.\n */\nexport function safeCloneValue(\n value: unknown,\n depth: number,\n seen: Set<object>,\n): unknown {\n if (value === null) return null;\n const type = typeof value;\n if (type === \"string\") return capLength(value as string, MAX_ARG_STRING_LENGTH);\n if (type === \"number\") return Number.isFinite(value as number) ? value : String(value);\n if (type === \"boolean\") return value;\n if (type === \"bigint\") return `${(value as bigint).toString()}n`;\n if (type === \"symbol\") return (value as symbol).toString();\n if (type === \"function\") return \"[Function]\";\n if (type === \"undefined\") return null;\n const obj = value as object;\n if (value instanceof Error) {\n const { name, message } = describeErrorValue(value);\n return { name, message };\n }\n if (seen.has(obj)) return \"[Circular]\";\n if (depth <= 0) return Array.isArray(value) ? \"[Array]\" : \"[Object]\";\n seen.add(obj);\n try {\n if (Array.isArray(value)) {\n const items = value\n .slice(0, MAX_ARG_ITEMS)\n .map((item) => safeCloneValue(item, depth - 1, seen));\n if (value.length > MAX_ARG_ITEMS) {\n items.push(`\u2026 ${value.length - MAX_ARG_ITEMS} more`);\n }\n return items;\n }\n const source = value as Record<string, unknown>;\n const keys = Object.keys(source);\n const out: Record<string, unknown> = {};\n for (const key of keys.slice(0, MAX_ARG_ITEMS)) {\n const child = source[key];\n // Undefined / function values JSON drops anyway \u2014 omit them for a clean clone.\n if (typeof child === \"undefined\" || typeof child === \"function\") continue;\n out[key] = safeCloneValue(child, depth - 1, seen);\n }\n if (keys.length > MAX_ARG_ITEMS) out[\"\u2026\"] = `${keys.length - MAX_ARG_ITEMS} more`;\n return out;\n } finally {\n seen.delete(obj);\n }\n}\n\n/** Classify one console argument into a type-tagged {@link ConsoleArg} (spec #122 \u00A7C). */\nexport function toConsoleArg(value: unknown): ConsoleArg {\n if (value === null || value === undefined) return { t: \"null\", v: null };\n const type = typeof value;\n if (type === \"string\") {\n return { t: \"string\", v: capLength(value as string, MAX_ARG_STRING_LENGTH) };\n }\n if (type === \"number\") {\n return Number.isFinite(value as number)\n ? { t: \"number\", v: value }\n : { t: \"string\", v: String(value) };\n }\n if (type === \"boolean\") return { t: \"bool\", v: value };\n if (type === \"bigint\") return { t: \"string\", v: `${(value as bigint).toString()}n` };\n if (type === \"symbol\") return { t: \"string\", v: (value as symbol).toString() };\n if (type === \"function\") return { t: \"string\", v: \"[Function]\" };\n if (value instanceof Error) return { t: \"error\", v: describeErrorValue(value) };\n return { t: \"json\", v: safeCloneValue(value, MAX_ARG_DEPTH, new Set()) };\n}\n\n/**\n * Cap an entry's structured args to {@link MAX_CONSOLE_ARGS_BYTES} (spec #122 \u00A7F):\n * drop trailing args until the array fits, keeping the earliest (usually the format\n * string / main message); if even a single arg is over budget, keep one honest\n * placeholder rather than an unbounded value. Per-arg depth/breadth/string caps bound\n * most cases already \u2014 this is the whole-entry backstop.\n */\nexport function capConsoleArgs(args: ConsoleArg[]): ConsoleArg[] {\n if (serializedBytes(args) <= MAX_CONSOLE_ARGS_BYTES) return args;\n let out = args.slice();\n while (out.length > 1 && serializedBytes(out) > MAX_CONSOLE_ARGS_BYTES) {\n out = out.slice(0, -1);\n }\n if (out.length === 1 && serializedBytes(out) > MAX_CONSOLE_ARGS_BYTES) {\n return [{ t: \"string\", v: \"[trace: console argument omitted (too large)]\" }];\n }\n return out;\n}\n", "/**\n * The pure **crumb builders** (spec #122 \u00A7C/\u00A7D/\u00A7F) \u2014 the shared, runtime-agnostic\n * constructors every capture SDK assembles trace entries with (ADR-0028), so the\n * server and dashboard cannot tell a mobile trace from a web one.\n *\n * Everything private is kept out **at the source**: a network crumb has no field\n * for a request/response body or an arbitrary header, and every URL is scrubbed\n * (query dropped, path PII redacted) and length-capped as the crumb is built. The\n * `beforeSend` choke point (`../scrub`) is the final gate over the whole report.\n *\n * The two DOM-shaped builders \u2014 `ui.click` and `ui.input`, which need an `Element`\n * to derive a masked selector from \u2014 stay in the browser SDK; every crumb kind a\n * non-browser runtime can also produce lives here.\n */\n\nimport type {\n Breadcrumb,\n BreadcrumbLevel,\n NetworkApi,\n NetworkOutcome,\n SourceLocation,\n} from \"../breadcrumb\";\nimport { scrubUrl } from \"../scrub\";\nimport {\n capConsoleArgs,\n capLength,\n joinArgs,\n stringifyArg,\n toConsoleArg,\n} from \"./console-args\";\n\n/**\n * Longest scrubbed URL kept on a crumb (spec #122 \u00A7F): capped at assembly so an\n * over-long URL is truncated, never dropped and never allowed to bloat a report.\n */\nexport const MAX_URL_LENGTH = 2048;\n\n/**\n * A `console` crumb from a captured call's level and arguments (spec #122 \u00A7C). The\n * `message` is the one-line preview (flattened, truncated); `args` preserves each\n * argument as a structured, type-tagged, size-capped value so the Console tab can\n * render objects/errors expandably; `source` is the best-effort `file:line`, attached\n * only when the stack yielded one. Args are omitted entirely for a no-argument call.\n */\nexport function consoleCrumb(\n level: BreadcrumbLevel,\n args: readonly unknown[],\n timestamp: number,\n source?: SourceLocation,\n): Breadcrumb {\n const structured = capConsoleArgs(args.map(toConsoleArg));\n return {\n category: \"console\",\n level,\n message: joinArgs(args),\n timestamp,\n ...(structured.length > 0 ? { args: structured } : {}),\n ...(source ? { source } : {}),\n };\n}\n\n/**\n * A `navigation` crumb; both URLs are scrubbed and length-capped as the crumb is\n * built. In a browser these come from `history`/`hashchange`; on mobile from the\n * host app's `trackScreen` calls (spec 0004 \u00A7D), where the values are screen URLs\n * derived from the configured origin.\n */\nexport function navigationCrumb(\n from: string,\n to: string,\n timestamp: number,\n): Breadcrumb {\n const fromUrl = capLength(scrubUrl(from), MAX_URL_LENGTH);\n const toUrl = capLength(scrubUrl(to), MAX_URL_LENGTH);\n return {\n category: \"navigation\",\n message: `${fromUrl} \u2192 ${toUrl}`,\n timestamp,\n data: { from: fromUrl, to: toUrl },\n };\n}\n\n/**\n * Classify an HTTP status into a {@link NetworkOutcome} (spec #122 \u00A7D). A missing or\n * `0` status is a `network-error` (a request that never got a response); otherwise\n * the 4xx/5xx classes flag failures and everything else is `ok`. Used both as the\n * fallback outcome and by the thin {@link fetchCrumb} / {@link xhrCrumb} builders.\n */\nexport function outcomeFromStatus(status: number | undefined): NetworkOutcome {\n if (typeof status !== \"number\" || status <= 0) return \"network-error\";\n if (status >= 500) return \"http-5xx\";\n if (status >= 400) return \"http-4xx\";\n return \"ok\";\n}\n\n/** Parse a `content-length` header into a non-negative byte count, or `undefined`. */\nexport function parseContentLength(\n value: string | null | undefined,\n): number | undefined {\n if (typeof value !== \"string\") return undefined;\n const bytes = Number.parseInt(value, 10);\n return Number.isFinite(bytes) && bytes >= 0 ? bytes : undefined;\n}\n\n/** The media type from a `content-type` header (the part before any `;` parameters). */\nexport function contentTypeOf(value: string | null | undefined): string | undefined {\n if (typeof value !== \"string\" || value.length === 0) return undefined;\n const media = value.split(\";\")[0]?.trim();\n return media && media.length > 0 ? media : undefined;\n}\n\n/** The rich metadata a network crumb records (spec #122 \u00A7D) \u2014 never a body or header. */\nexport interface NetworkCrumbInput {\n readonly api: NetworkApi;\n readonly method: string;\n /** The raw request URL \u2014 scrubbed (query dropped, path PII redacted) as the crumb is built. */\n readonly url: string;\n readonly status?: number;\n readonly statusText?: string;\n readonly durationMs?: number;\n readonly reqSize?: number;\n readonly respSize?: number;\n readonly contentType?: string;\n readonly outcome: NetworkOutcome;\n}\n\n/**\n * A network crumb (`fetch` / `xhr` / `beacon`) with the rich, metadata-only fields\n * the Network tab renders (spec #122 \u00A7D, ticket #139): api, method, scrubbed URL,\n * status + statusText, duration, request/response sizes, content type, and a failure\n * outcome. The URL is scrubbed and length-capped at assembly. The shape has **no field\n * for a request/response body or an arbitrary header**, so neither can ever be\n * recorded; only a positive status, and fields that were actually supplied, ride along.\n */\nexport function networkCrumb(\n input: NetworkCrumbInput,\n timestamp: number,\n): Breadcrumb {\n const url = capLength(scrubUrl(input.url), MAX_URL_LENGTH);\n const hasStatus = typeof input.status === \"number\" && input.status > 0;\n const statusPart = hasStatus ? ` \u2192 ${input.status}` : \"\";\n const outcomePart = input.outcome !== \"ok\" ? ` (${input.outcome})` : \"\";\n return {\n category: input.api,\n api: input.api,\n method: input.method,\n url,\n message: `${input.method} ${url}${statusPart}${outcomePart}`,\n timestamp,\n ...(hasStatus ? { status: input.status } : {}),\n ...(input.statusText ? { statusText: input.statusText } : {}),\n ...(typeof input.durationMs === \"number\" ? { durationMs: input.durationMs } : {}),\n ...(typeof input.reqSize === \"number\" ? { reqSize: input.reqSize } : {}),\n ...(typeof input.respSize === \"number\" ? { respSize: input.respSize } : {}),\n ...(input.contentType ? { contentType: input.contentType } : {}),\n outcome: input.outcome,\n };\n}\n\n/**\n * A thin `fetch` crumb from method + URL + status (outcome derived from the status).\n * The instrumentation uses {@link networkCrumb} directly to carry the full metadata;\n * this convenience builder seeds a network crumb from a status alone.\n */\nexport function fetchCrumb(\n method: string,\n url: string,\n status: number | undefined,\n timestamp: number,\n): Breadcrumb {\n return networkCrumb(\n { api: \"fetch\", method, url, status, outcome: outcomeFromStatus(status) },\n timestamp,\n );\n}\n\n/** A thin `xhr` crumb from method + URL + status (outcome derived from the status). */\nexport function xhrCrumb(\n method: string,\n url: string,\n status: number | undefined,\n timestamp: number,\n): Breadcrumb {\n return networkCrumb(\n { api: \"xhr\", method, url, status, outcome: outcomeFromStatus(status) },\n timestamp,\n );\n}\n\n/** Distil any thrown value into the `{ name, message }` an error crumb reads. */\nexport function describeError(error: unknown): { name: string; message: string } {\n if (error instanceof Error) {\n return { name: error.name || \"Error\", message: error.message };\n }\n if (typeof error === \"string\") return { name: \"Error\", message: error };\n return { name: \"Error\", message: stringifyArg(error) };\n}\n\n/**\n * An `error` crumb for the failing exception or rejection that ends the trace. When\n * `causedBy` is given (an auto-captured error), it rides on the crumb as the causal\n * pointer to the ids of the entries immediately preceding the throw (spec #122 \u00A7F).\n */\nexport function errorCrumb(\n error: unknown,\n timestamp: number,\n causedBy?: readonly string[],\n): Breadcrumb {\n const { name, message } = describeError(error);\n const crumb: Breadcrumb = {\n category: \"error\",\n level: \"error\",\n message: message ? `${name}: ${message}` : name,\n timestamp,\n data: { errorType: name },\n };\n return causedBy && causedBy.length > 0 ? { ...crumb, causedBy } : crumb;\n}\n", "/**\n * The **automatic error-capture state machine** (spec 0003 \u00A7E, ADR-0011; research\n * `sentry-error-capture-findings.md` \u00A77.1\u20137.3) \u2014 the shared, runtime-agnostic half\n * of \"errors report themselves, no prompt\" (ADR-0028).\n *\n * Every SDK's `installErrorCapture` is now a thin binding over this: it supplies\n * the runtime's error seam (window listeners, React Native's `ErrorUtils`), an\n * {@link AutoCaptureCoreConfig.extract} that distils that runtime's raw event into\n * an {@link ExtractedError}, and an {@link AutoCaptureCoreConfig.submit} that ships\n * one assembled report. Everything between \u2014 the per-session fingerprint dedup, the\n * token-bucket burst limiter, the distinct-error session cap, the causal error\n * crumb, the optimistic `sentCount` with rollback on a failed send, and the\n * occurrence flush \u2014 lives here, once.\n *\n * Per-firing order (research \u00A77.1): **dedup \u2192 rate-limit/cap \u2192 file**. The Gate\n * check sits *outside*: a binding installs capture only after boot returned\n * `canSubmit`, so an auto-error is never filed where a manual report would be\n * refused. The scrub choke point sits *inside* `submit`, which each binding routes\n * through `runBeforeSend` exactly as a manual report does.\n *\n * Nothing here throws: `capture` and `flush` swallow every failure, because an\n * error raised while capturing an error must never surface in the host app.\n */\n\nimport { computeFingerprint } from \"./fingerprint\";\nimport { extractStructuredFrames } from \"./stack\";\nimport type { Breadcrumb } from \"./breadcrumb\";\nimport type { BreadcrumbBuffer } from \"./trace/buffer\";\nimport { errorCrumb } from \"./trace/crumbs\";\nimport type { CapturedFrame } from \"./wire\";\n\n/** Burst limiter capacity \u2014 how many auto-reports may fire back-to-back (\u00A7E). */\nexport const DEFAULT_BURST_CAPACITY = 5;\n/** Burst limiter refill \u2014 one token returns every this-many ms (\u00A7E). */\nexport const DEFAULT_BURST_REFILL_MS = 2_000;\n/** Per-session ceiling on distinct auto-Feedback; beyond it, only a dropped-count (\u00A7E). */\nexport const DEFAULT_MAX_DISTINCT_AUTO = 20;\n\n/** How many preceding entries an auto-error's causal pointer names (spec #122 \u00A7F). */\nexport const CAUSED_BY_LIMIT = 5;\n\n/** Coerce any value to a string without throwing (a hostile `toString` can throw). */\nexport function asString(value: unknown): string {\n if (typeof value === \"string\") return value;\n if (value == null) return \"\";\n try {\n return String(value);\n } catch {\n return \"\";\n }\n}\n\n/**\n * The ids of up to the last {@link CAUSED_BY_LIMIT} entries preceding the throw, in\n * time order \u2014 the causal pointer an auto-captured error carries (spec #122 \u00A7F), so a\n * machine-filed crash names its lead-up. Entries without an id are skipped.\n */\nexport function precedingIds(entries: readonly Breadcrumb[] | undefined): string[] {\n if (!entries || entries.length === 0) return [];\n const ids: string[] = [];\n for (let i = entries.length - 1; i >= 0 && ids.length < CAUSED_BY_LIMIT; i -= 1) {\n const id = entries[i]?.id;\n if (typeof id === \"string\" && id.length > 0) ids.unshift(id);\n }\n return ids;\n}\n\n// --- Burst limiter (token bucket, research \u00A77.3) ------------------------------\n\n/** Configuration for {@link TokenBucket}. */\nexport interface TokenBucketOptions {\n readonly capacity: number;\n readonly refillIntervalMs: number;\n /** Clock source, injectable for tests. Defaults to `Date.now`. */\n readonly now?: () => number;\n}\n\n/**\n * A token bucket: starts full at `capacity`, refills one token every\n * `refillIntervalMs`, and refuses (`take() === false`) when empty. So a fast error\n * loop that dodges dedup with distinct fingerprints still can't machine-gun ingest.\n * The clock is injectable so the window is unit-tested deterministically.\n */\nexport class TokenBucket {\n private tokens: number;\n private last: number;\n private readonly now: () => number;\n\n constructor(private readonly options: TokenBucketOptions) {\n this.now = options.now ?? Date.now;\n this.tokens = Math.max(0, options.capacity);\n this.last = this.now();\n }\n\n /** Consume a token if one is available (refilling first), else refuse. */\n take(): boolean {\n const now = this.now();\n const { capacity, refillIntervalMs } = this.options;\n if (refillIntervalMs > 0 && now > this.last) {\n const refill = Math.floor((now - this.last) / refillIntervalMs);\n if (refill > 0) {\n this.tokens = Math.min(capacity, this.tokens + refill);\n this.last += refill * refillIntervalMs;\n }\n }\n if (this.tokens >= 1) {\n this.tokens -= 1;\n return true;\n }\n return false;\n }\n}\n\n// --- The shared state machine -------------------------------------------------\n\n/** The distilled shape a fingerprint and a report are built from. */\nexport interface ExtractedError {\n readonly type: string;\n readonly value: string;\n readonly stack?: string;\n /** The original throwable (Error, rejection reason, thrown string) for the trace crumb. */\n readonly original: unknown;\n}\n\n/** One report the binding is asked to assemble, scrub, and ship. */\nexport interface AutoReport {\n /** The per-session fingerprint \u2014 the report's `errorSignature`. */\n readonly fingerprint: string;\n /** The occurrence count this send claims. */\n readonly occurrences: number;\n /**\n * A **rich first report** (`true`) versus a light count-update flush (`false`).\n * The trace, the structured frames, and (on the web) the buffered replay window\n * ride only with the rich send; a flush is just the number.\n */\n readonly rich: boolean;\n /**\n * The structured stack frames parsed once from the fingerprint's first firing\n * (#117, ADR-0024). Empty when the stack yielded none.\n */\n readonly frames: readonly CapturedFrame[];\n}\n\n/** Configuration for {@link createAutoCapture}, parameterised over the runtime's raw event. */\nexport interface AutoCaptureCoreConfig<Raw> {\n /**\n * Distil the runtime's raw error event into a fingerprint-able shape, or `null`\n * when it is not an uncaught exception at all (a resource-load `error` event).\n */\n readonly extract: (raw: Raw) => ExtractedError | null;\n /**\n * Assemble, scrub, and ship one report. Resolve `true` when it was accepted (or\n * intentionally dropped at the scrub choke point \u2014 either way there is nothing to\n * retry), `false` when it was held, refused, or unreachable, which rolls the\n * entry's `sentCount` back so a later flush retries the count update.\n */\n readonly submit: (report: AutoReport) => Promise<boolean>;\n /** The shared trace buffer; the failing error is added to it before filing. */\n readonly buffer?: BreadcrumbBuffer | null;\n /** Burst limiter capacity. Defaults to {@link DEFAULT_BURST_CAPACITY}. */\n readonly burstCapacity?: number;\n /** Burst limiter refill interval (ms). Defaults to {@link DEFAULT_BURST_REFILL_MS}. */\n readonly burstRefillMs?: number;\n /** Per-session distinct-Feedback cap. Defaults to {@link DEFAULT_MAX_DISTINCT_AUTO}. */\n readonly maxDistinct?: number;\n /** Clock source, injectable for tests. Defaults to `Date.now`. */\n readonly now?: () => number;\n}\n\n/** The running state machine a binding drives from its runtime's error seam. */\nexport interface AutoCaptureCore<Raw> {\n /** Handle one raw error event: extract \u2192 dedup \u2192 rate-limit/cap \u2192 file. Never throws. */\n capture(raw: Raw): void;\n /** Flush the final occurrence count of every fingerprint that grew since its send. */\n flush(): void;\n /**\n * Distinct auto-Feedback dropped by the burst limiter or the per-session cap \u2014\n * the \"keep only a local dropped-count\" fallback beyond the guardrails (spec \u00A7E).\n */\n droppedCount(): number;\n}\n\n/** A per-session record of one fingerprinted error. */\ninterface AutoEntry {\n count: number;\n firstAt: number;\n lastAt: number;\n /** The occurrence count last shipped to ingest (`0` until the first send). */\n sentCount: number;\n /** The structured frames parsed from the first firing's stack (#117), if any. */\n frames: readonly CapturedFrame[];\n}\n\n/** Create the shared auto-capture state machine. See the module doc. */\nexport function createAutoCapture<Raw>(\n config: AutoCaptureCoreConfig<Raw>,\n): AutoCaptureCore<Raw> {\n const now = config.now ?? Date.now;\n const maxDistinct = config.maxDistinct ?? DEFAULT_MAX_DISTINCT_AUTO;\n const bucket = new TokenBucket({\n capacity: config.burstCapacity ?? DEFAULT_BURST_CAPACITY,\n refillIntervalMs: config.burstRefillMs ?? DEFAULT_BURST_REFILL_MS,\n now,\n });\n\n const seen = new Map<string, AutoEntry>();\n let dropped = 0;\n\n /**\n * Ship one report for a fingerprint at a given count. Sets the entry's `sentCount`\n * optimistically so a concurrent flush never double-sends, and rolls it back on a\n * failed send so a later flush can retry the count update.\n */\n async function file(fingerprint: string, occurrences: number, rich: boolean): Promise<void> {\n const entry = seen.get(fingerprint);\n if (!entry) return;\n const previouslySent = entry.sentCount;\n entry.sentCount = occurrences;\n try {\n const accepted = await config.submit({\n fingerprint,\n occurrences,\n rich,\n frames: entry.frames,\n });\n if (!accepted && entry.sentCount === occurrences) {\n entry.sentCount = previouslySent;\n }\n } catch {\n if (entry.sentCount === occurrences) entry.sentCount = previouslySent;\n // The error path must never throw into the host app.\n }\n }\n\n /** Handle one distilled error: dedup \u2192 burst limiter \u2192 session cap \u2192 file. */\n function handle(extracted: ExtractedError): void {\n const fingerprint = computeFingerprint(\n extracted.type,\n extracted.value,\n extracted.stack,\n );\n const existing = seen.get(fingerprint);\n if (existing) {\n // Dedup: one Feedback per fingerprint per session; count locally, flush later.\n existing.count += 1;\n existing.lastAt = now();\n return;\n }\n if (!bucket.take()) {\n dropped += 1; // burst limiter\n return;\n }\n if (seen.size >= maxDistinct) {\n dropped += 1; // per-session cap \u2014 keep only the local dropped-count\n return;\n }\n const at = now();\n seen.set(fingerprint, {\n count: 1,\n firstAt: at,\n lastAt: at,\n sentCount: 0,\n // Parsed once per fingerprint, from the first firing's stack (#117) \u2014 the\n // wire shape symbolication consumes, scrubbed like every captured URL.\n frames: extractStructuredFrames(extracted.stack),\n });\n // The failing error joins the trace so the lead-up and the failure both show,\n // carrying a causal pointer to the ids of the entries that preceded the throw.\n try {\n const causedBy = precedingIds(config.buffer?.snapshot());\n config.buffer?.add(errorCrumb(extracted.original, at, causedBy));\n } catch {\n /* capture must never throw into the host app */\n }\n void file(fingerprint, 1, true);\n }\n\n return {\n capture(raw: Raw): void {\n try {\n const extracted = config.extract(raw);\n if (extracted) handle(extracted);\n } catch {\n /* never throw into the host app */\n }\n },\n flush(): void {\n try {\n for (const [fingerprint, entry] of seen) {\n if (entry.count > entry.sentCount) {\n void file(fingerprint, entry.count, false);\n }\n }\n } catch {\n /* best-effort on unload / backgrounding */\n }\n },\n droppedCount: () => dropped,\n };\n}\n", "/**\n * The trace **ring buffer** (spec #122 \u00A7A/\u00A7B, decision D6) \u2014 the shared,\n * runtime-agnostic store every Fixback capture SDK records crumbs into (ADR-0028).\n *\n * It holds **three independent FIFO rings** \u2014 `network`, `console`,\n * `breadcrumbs` \u2014 each trimmed to its own budget so a chatty stream never evicts\n * another's lead-up, plus a shared age cap. On `add`, a crumb passes through\n * `beforeBreadcrumb`, is stamped with a stable `id` and a high-res `mono`\n * timestamp (and an epoch `timestamp` if it has none), routed to its stream, then\n * that stream is pruned by age and trimmed to its budget. `snapshot` prunes by age\n * again, then merges the three streams into one `mono`-ordered array for transport\n * \u2014 so the wire keeps its single `trace` field.\n *\n * Nothing here touches a DOM or a Node built-in: the instrumentation that *produces*\n * crumbs lives in the consuming SDK, and the clocks are injectable so the buffer is\n * unit-tested deterministically.\n */\n\nimport type { Breadcrumb, BreadcrumbLevel } from \"../breadcrumb\";\nimport { nowMono } from \"../env\";\n\n/**\n * The three independent trace streams (spec #122 \u00A7A, decision D6). Each is its own\n * FIFO ring with its own size budget, so a chatty stream can't evict another's\n * lead-up. Every crumb category maps to exactly one stream via {@link streamOf}.\n */\nexport type TraceStream = \"network\" | \"console\" | \"breadcrumbs\";\n\n/**\n * Per-stream ring budgets \u2014 starting points from the grill (D6): network is the\n * chattiest, breadcrumbs the sparsest. Per-project tunable, never frozen.\n */\nexport const DEFAULT_STREAM_BUDGETS: Readonly<Record<TraceStream, number>> = {\n network: 100,\n console: 80,\n breadcrumbs: 40,\n};\n\n/**\n * The shared age cap (spec #122 \u00A7A): entries older than this are pruned from every\n * stream, whichever trims first. ~3 minutes and **on** by default, so an idle tab\n * (or a backgrounded app) never ships stale context.\n */\nexport const DEFAULT_MAX_AGE_MS = 3 * 60 * 1000;\n\n/**\n * Console levels captured by default (spec #122 \u00A7C, decision D7): **all** of them, so\n * the Console tab is a real console and not just an error log. Eviction priority (not\n * capture) is what keeps a chatty app's `log`/`info`/`debug` from burying the signal \u2014\n * see {@link PINNED_CONSOLE_LEVELS}.\n */\nexport const DEFAULT_CONSOLE_LEVELS: readonly BreadcrumbLevel[] = [\n \"log\",\n \"info\",\n \"warn\",\n \"error\",\n \"assert\",\n \"debug\",\n];\n\n/**\n * The console levels **pinned** against eviction (spec #122 \u00A7C, decision D7). When the\n * console stream overflows its budget, `warn`/`error`/`assert` are retained ahead of\n * `log`/`info`/`debug`, so an error is never evicted by a burst of chatter \u2014 the\n * low-priority levels fill the remainder and are dropped first.\n */\nexport const PINNED_CONSOLE_LEVELS: readonly BreadcrumbLevel[] = [\n \"warn\",\n \"error\",\n \"assert\",\n];\n\n/** Whether a console level is pinned against eviction (see {@link PINNED_CONSOLE_LEVELS}). */\nexport function isPinnedConsoleLevel(level: BreadcrumbLevel | undefined): boolean {\n return level !== undefined && PINNED_CONSOLE_LEVELS.includes(level);\n}\n\n/**\n * The console levels for which `source` (`file:line`) is captured (ticket #159).\n *\n * Capturing a call site constructs a `new Error()` to read its stack on **every** console\n * call \u2014 a measurable hot-page cost when a chatty app logs in a tight loop. The\n * spec-sanctioned mitigation (decision D7) narrows source capture to the **levels that\n * matter for debugging** \u2014 the same `warn`/`error`/`assert` that are pinned against\n * eviction \u2014 so `log`/`info`/`debug` pay no per-call stack cost and simply omit `source`.\n * Per-project tunability of this set is future work; today it deliberately mirrors\n * {@link PINNED_CONSOLE_LEVELS} so \"what survives eviction\" and \"what carries a call\n * site\" stay one idea.\n */\nexport const SOURCE_CAPTURE_LEVELS: readonly BreadcrumbLevel[] = PINNED_CONSOLE_LEVELS;\n\n/** Whether a console level captures `source` `file:line` (see {@link SOURCE_CAPTURE_LEVELS}). */\nexport function capturesSource(level: BreadcrumbLevel): boolean {\n return SOURCE_CAPTURE_LEVELS.includes(level);\n}\n\n/**\n * Which stream a crumb's category belongs to (spec #122 \u00A7A). Network APIs\n * (`fetch` / `xhr` / `beacon`) form the network stream, `console` its own, and\n * everything else (navigation, masked UI events, the failing error) breadcrumbs.\n */\nexport function streamOf(category: string): TraceStream {\n if (category === \"console\") return \"console\";\n if (category === \"fetch\" || category === \"xhr\" || category === \"beacon\") {\n return \"network\";\n }\n return \"breadcrumbs\";\n}\n\n/** Filters or edits each crumb before it enters the buffer; `null` drops it. */\nexport type BeforeBreadcrumb = (crumb: Breadcrumb) => Breadcrumb | null;\n\n/** Configuration for {@link createBreadcrumbBuffer}. All values are optional. */\nexport interface BreadcrumbBufferConfig {\n /**\n * Per-stream size budgets (oldest drop, independently per stream). Any stream\n * omitted falls back to {@link DEFAULT_STREAM_BUDGETS}.\n */\n readonly budgets?: Partial<Record<TraceStream, number>>;\n /**\n * Shared age cap in ms: entries older than this are dropped from every stream.\n * Defaults to {@link DEFAULT_MAX_AGE_MS} (on); pass `0` (or a non-positive value)\n * to disable age pruning.\n */\n readonly maxAgeMs?: number;\n /** A per-crumb filter (mute a category, edit, or drop by returning `null`). */\n readonly beforeBreadcrumb?: BeforeBreadcrumb | null;\n /** Epoch clock source, injectable for tests. Defaults to `Date.now`. */\n readonly now?: () => number;\n /** High-res monotonic clock, injectable for tests. Defaults to {@link nowMono}. */\n readonly mono?: () => number;\n /** Stable id generator, injectable for tests. Defaults to a per-buffer sequence. */\n readonly nextId?: () => string;\n}\n\n/** A live trace buffer. */\nexport interface BreadcrumbBuffer {\n /** Record a crumb (subject to `beforeBreadcrumb`, id/mono stamping, size, and age trimming). */\n add(crumb: Breadcrumb): void;\n /** The current crumbs, merged across streams and ordered in time \u2014 a fresh array. */\n snapshot(): Breadcrumb[];\n /** Drop every crumb. */\n clear(): void;\n}\n\n/** Coerce a configured budget to a usable positive integer, else the default. */\nexport function normalizeBudget(value: number | undefined, fallback: number): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < 1) {\n return fallback;\n }\n return Math.floor(value);\n}\n\n/** A per-buffer id factory: a short random salt + a base-36 sequence, stable once assigned. */\nexport function createIdFactory(): () => string {\n const salt = Math.random().toString(36).slice(2, 8);\n let seq = 0;\n return () => `${salt}${(seq++).toString(36)}`;\n}\n\n/** The streams, in a fixed order for a deterministic merge. */\nconst STREAMS: readonly TraceStream[] = [\"network\", \"console\", \"breadcrumbs\"];\n\n/** One held entry, tagged with its insertion order for a stable cross-stream sort. */\ninterface HeldCrumb {\n readonly crumb: Breadcrumb;\n readonly seq: number;\n}\n\n/**\n * Trim the **console** stream to its budget with eviction priority (spec #122 \u00A7C,\n * decision D7): `warn`/`error`/`assert` are pinned, so the oldest `log`/`info`/`debug`\n * entries are dropped first; a pinned entry is evicted only when dropping every\n * low-priority one still leaves the stream over budget (oldest pinned first). Survivors\n * keep their insertion order. A plain FIFO `slice(-budget)` handles every other stream.\n */\nexport function trimConsoleStream<T extends { readonly crumb: Breadcrumb }>(\n held: T[],\n budget: number,\n): T[] {\n const over = held.length - budget;\n if (over <= 0) return held;\n const drop = new Set<number>();\n // First: oldest low-priority (not pinned) entries, until enough room is freed.\n for (let i = 0; i < held.length && drop.size < over; i += 1) {\n if (!isPinnedConsoleLevel(held[i]!.crumb.level)) drop.add(i);\n }\n // Then, only if the low-priority ones weren't enough: oldest pinned entries.\n for (let i = 0; i < held.length && drop.size < over; i += 1) {\n if (!drop.has(i)) drop.add(i);\n }\n return held.filter((_, i) => !drop.has(i));\n}\n\n/** Create the per-stream trace buffer. See the module doc. */\nexport function createBreadcrumbBuffer(\n config: BreadcrumbBufferConfig = {},\n): BreadcrumbBuffer {\n const budgets: Record<TraceStream, number> = {\n network: normalizeBudget(config.budgets?.network, DEFAULT_STREAM_BUDGETS.network),\n console: normalizeBudget(config.budgets?.console, DEFAULT_STREAM_BUDGETS.console),\n breadcrumbs: normalizeBudget(\n config.budgets?.breadcrumbs,\n DEFAULT_STREAM_BUDGETS.breadcrumbs,\n ),\n };\n const maxAgeMs =\n config.maxAgeMs === undefined ? DEFAULT_MAX_AGE_MS : config.maxAgeMs;\n const beforeBreadcrumb = config.beforeBreadcrumb;\n const now = config.now ?? Date.now;\n const mono = config.mono ?? nowMono;\n const nextId = config.nextId ?? createIdFactory();\n\n const streams: Record<TraceStream, HeldCrumb[]> = {\n network: [],\n console: [],\n breadcrumbs: [],\n };\n let seq = 0;\n\n function pruneByAge(): void {\n if (typeof maxAgeMs !== \"number\" || maxAgeMs <= 0) return;\n const cutoff = now() - maxAgeMs;\n for (const stream of STREAMS) {\n const held = streams[stream];\n if (held.length > 0) {\n streams[stream] = held.filter((h) => h.crumb.timestamp >= cutoff);\n }\n }\n }\n\n return {\n add(crumb: Breadcrumb): void {\n let entry: Breadcrumb | null = crumb;\n if (beforeBreadcrumb) {\n try {\n entry = beforeBreadcrumb(crumb);\n } catch {\n // A throwing filter must never break capture; keep the (already\n // masked) crumb rather than silently erasing the trace.\n entry = crumb;\n }\n }\n if (!entry) return;\n // Stamp a stable id + high-res mono (and an epoch timestamp if absent) so\n // every entry orders and cross-links exactly, whatever stream it lands in.\n const stamped: Breadcrumb = {\n ...entry,\n id: entry.id ?? nextId(),\n timestamp: typeof entry.timestamp === \"number\" ? entry.timestamp : now(),\n mono: typeof entry.mono === \"number\" ? entry.mono : mono(),\n };\n const stream = streamOf(stamped.category);\n streams[stream].push({ crumb: stamped, seq: seq++ });\n // Age-prune every stream first (it may reassign the array), then trim this\n // stream to its own budget \u2014 a flood here never evicts another stream.\n pruneByAge();\n const held = streams[stream];\n const budget = budgets[stream];\n if (held.length > budget) {\n streams[stream] =\n stream === \"console\"\n ? trimConsoleStream(held, budget)\n : held.slice(-budget);\n }\n },\n snapshot(): Breadcrumb[] {\n pruneByAge();\n const merged: HeldCrumb[] = [];\n for (const stream of STREAMS) merged.push(...streams[stream]);\n // Order in time by the high-res mono clock (every entry has one), with the\n // insertion sequence as a stable tiebreak so equal clocks never scramble.\n merged.sort((a, b) => {\n const am =\n typeof a.crumb.mono === \"number\" ? a.crumb.mono : a.crumb.timestamp;\n const bm =\n typeof b.crumb.mono === \"number\" ? b.crumb.mono : b.crumb.timestamp;\n return am - bm || a.seq - b.seq;\n });\n return merged.map((h) => h.crumb);\n },\n clear(): void {\n streams.network = [];\n streams.console = [];\n streams.breadcrumbs = [];\n seq = 0;\n },\n };\n}\n", "/**\n * The console crumb's **call site** (spec #122 \u00A7C) \u2014 the best-effort `file:line` a\n * `console.*` call was made from, shared runtime-agnostically (ADR-0028).\n *\n * Frames are tokenized by the core's single stack parser (`stack.ts`), so a stack\n * that yields a fingerprint frame yields a source location too \u2014 the browser's V8\n * and Firefox/Safari dialects and React Native's Hermes/JSC ones alike. A query\n * string on the asset URL is dropped, both for parse safety and privacy (it may be\n * a cache-buster, or defensively a token).\n */\n\nimport { parseStackLine } from \"../stack\";\nimport type { SourceLocation } from \"../breadcrumb\";\n\n/**\n * Parse a single **trimmed** stack line into a `{ file, line }` location, or\n * `undefined` when the line names no locatable file:line.\n */\nexport function parseStackFrame(line: string): SourceLocation | undefined {\n const tokens = parseStackLine(line);\n if (!tokens || !tokens.location) return undefined;\n const loc = tokens.location.replace(/\\?[^:]*/, \"\");\n const m = loc.match(/^(.*):(\\d+):\\d+$/) ?? loc.match(/^(.*):(\\d+)$/);\n if (!m) return undefined;\n const file = m[1] ?? \"\";\n const lineNo = Number(m[2]);\n if (!file || !Number.isFinite(lineNo)) return undefined;\n return { file, line: lineNo };\n}\n\n/**\n * The best-effort `file:line` a console call was made from (spec #122 \u00A7C). Parses the\n * frames of a stack, skipping `skipFrames` leading (SDK-internal) frames so the source\n * points at the host code that called `console.*`. Returns `undefined` when no frame\n * yields a usable location \u2014 the crumb then simply omits `source`.\n */\nexport function sourceFromStack(\n stack: string | undefined,\n skipFrames = 0,\n): SourceLocation | undefined {\n if (typeof stack !== \"string\" || stack.length === 0) return undefined;\n const frames: SourceLocation[] = [];\n for (const raw of stack.split(\"\\n\")) {\n const frame = parseStackFrame(raw.trim());\n if (frame) frames.push(frame);\n }\n return frames[skipFrames];\n}\n\n/**\n * Frames between `new Error()` (created inside the console wrapper) and the host\n * caller: just the wrapper frame itself. Skipping it points {@link sourceFromStack}\n * at the app code. The wrapper is called through the console object's property, so\n * it is not a direct-call inlining candidate \u2014 keeping this skip count stable.\n */\nexport const CONSOLE_SOURCE_SKIP_FRAMES = 1;\n", "/**\n * The **console instrumentation** (spec #122 \u00A7C) \u2014 the one wrapper both the browser\n * and React Native SDKs install (ADR-0028).\n *\n * It patches the injected `console` object's level methods so each call becomes a\n * crumb before the original runs. The console surface is a parameter, never read\n * off a global here, so the core stays runtime-agnostic and tests pass a plain\n * object. Capture must never throw into the host app: a failure recording a crumb\n * is swallowed and the original `console` method always runs.\n */\n\nimport type { BreadcrumbLevel } from \"../breadcrumb\";\nimport type { SourceLocation } from \"../breadcrumb\";\nimport { capturesSource, type BreadcrumbBuffer } from \"./buffer\";\nimport { consoleCrumb } from \"./crumbs\";\nimport { CONSOLE_SOURCE_SKIP_FRAMES, sourceFromStack } from \"./source\";\n\n/** Detaches an installed instrumentation, restoring the original behaviour. */\nexport type Teardown = () => void;\n\n/**\n * The `console` surface the wrapper patches \u2014 injected, never read off a global\n * here.\n *\n * Declared as optional **methods** rather than a `Partial<Record<Level, Fn>>` of\n * function-typed properties, deliberately: method signatures are bivariant in\n * their parameters, so a real DOM `Console` \u2014 whose `assert` is overloaded with a\n * leading `condition?: boolean` \u2014 satisfies this directly. That is what lets the\n * browser SDK pass `console` with no cast at all.\n */\nexport interface ConsoleLike {\n log?(...args: unknown[]): unknown;\n info?(...args: unknown[]): unknown;\n warn?(...args: unknown[]): unknown;\n error?(...args: unknown[]): unknown;\n assert?(...args: unknown[]): unknown;\n debug?(...args: unknown[]): unknown;\n}\n\n/**\n * Wrap `console` methods so calls at the captured levels become crumbs, returning a\n * teardown that restores every method it replaced.\n *\n * Whether a level pays the source-capture cost is decided **once per level**, not per\n * call: only `warn`/`error`/`assert` construct a `new Error()` to read the call site,\n * so a tight `log` loop stays cheap (ticket #159; see `SOURCE_CAPTURE_LEVELS`).\n * `console.assert` records only when the asserted condition is falsy.\n */\nexport function instrumentConsole(\n buffer: BreadcrumbBuffer,\n consoleObj: ConsoleLike,\n levels: readonly BreadcrumbLevel[],\n now: () => number,\n): Teardown {\n const restores: Teardown[] = [];\n for (const level of levels) {\n const original = consoleObj[level];\n if (typeof original !== \"function\") continue;\n const withSource = capturesSource(level);\n const wrapper = (...args: unknown[]): unknown => {\n // Capture the call site before anything else so the wrapper is the top frame \u2014\n // but only for the levels that keep a source (the hot-page budget, ticket #159).\n let source: SourceLocation | undefined;\n if (withSource) {\n try {\n source = sourceFromStack(new Error().stack, CONSOLE_SOURCE_SKIP_FRAMES);\n } catch {\n source = undefined;\n }\n }\n try {\n if (level === \"assert\") {\n // console.assert records only when the asserted condition is falsy.\n if (!args[0]) {\n buffer.add(consoleCrumb(\"assert\", args.slice(1), now(), source));\n }\n } else {\n buffer.add(consoleCrumb(level, args, now(), source));\n }\n } catch {\n // Capture must never throw into the host app.\n }\n return original.apply(consoleObj, args);\n };\n consoleObj[level] = wrapper;\n restores.push(() => {\n consoleObj[level] = original;\n });\n }\n return () => {\n for (const restore of restores) restore();\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["DIGIT_RUN_RE"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **shared init vocabulary** (ADR-0028) — the option names every Fixback SDK
|
|
3
|
+
* spells the same way, and the hosted API origin they all default to.
|
|
4
|
+
*
|
|
5
|
+
* Each SDK's own `InitOptions` extends {@link CommonInitOptions} and adds only what
|
|
6
|
+
* its runtime genuinely needs (the browser: a mount target, reduced motion, replay;
|
|
7
|
+
* Expo: the declared origin, the shake gesture, screenshots; Node: the secret key
|
|
8
|
+
* and the batch knobs). So `apiUrl`, `release`, `environment`, `enabled`, `scrub`,
|
|
9
|
+
* and `beforeSend` mean exactly one thing across the family, and a builder who
|
|
10
|
+
* learns one SDK already knows the others.
|
|
11
|
+
*/
|
|
12
|
+
import type { BeforeBreadcrumb } from "./trace/buffer";
|
|
13
|
+
import type { ReportContent } from "./wire";
|
|
14
|
+
/**
|
|
15
|
+
* The hosted Fixback API origin every SDK talks to by default. A self-hosted or
|
|
16
|
+
* local deployment overrides it with `apiUrl` (the dashboard's install snippet
|
|
17
|
+
* pre-fills the right value for the Project), and the CLI/bundler plugins read the
|
|
18
|
+
* same constant so an upload and a capture agree on where Fixback lives.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEFAULT_API_URL = "https://api.fixback.dev";
|
|
21
|
+
/**
|
|
22
|
+
* The dev-side override of a Project's server-served capture config (spec #122 §L;
|
|
23
|
+
* ticket #138). Capture is **default-on** and normally governed per-project from
|
|
24
|
+
* the dashboard, surfaced on the boot answer; set a stream here to override what
|
|
25
|
+
* the server serves for it — `{ network: false }` turns network capture off even
|
|
26
|
+
* where the Project leaves it on, and a stream left unset follows the served
|
|
27
|
+
* config. Turning a stream off means it is never instrumented, so no entry of that
|
|
28
|
+
* kind ever reaches the wire.
|
|
29
|
+
*/
|
|
30
|
+
export interface CaptureToggles {
|
|
31
|
+
readonly console?: boolean;
|
|
32
|
+
readonly network?: boolean;
|
|
33
|
+
/** Buffered session replay — browser only (issue #189, ADR-0024). */
|
|
34
|
+
readonly replay?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The options every Fixback SDK accepts, with one meaning each.
|
|
38
|
+
*
|
|
39
|
+
* Generic over the draft a `beforeSend` hook sees, because that differs by surface:
|
|
40
|
+
* the capture SDKs hand it a `ReportContent`, `@fixback/node` a `FixbackErrorEvent`.
|
|
41
|
+
* The hook contract is identical either way — synchronous, network-free, mutate to
|
|
42
|
+
* redact or return `null` to drop — and it always runs **after** the built-in
|
|
43
|
+
* scrubbers.
|
|
44
|
+
*/
|
|
45
|
+
export interface CommonInitOptions<Draft> {
|
|
46
|
+
/** The Fixback API origin. Defaults to {@link DEFAULT_API_URL}. */
|
|
47
|
+
readonly apiUrl?: string;
|
|
48
|
+
/**
|
|
49
|
+
* The host app's **Release** — a build identifier (a git SHA, a semver, a
|
|
50
|
+
* `name@version`) stamped on every submission (#117, ADR-0024). Set it to the
|
|
51
|
+
* same value the build uploaded sourcemaps under (`npx fixback sourcemaps upload
|
|
52
|
+
* --release <v>`), and captured errors gain a symbolicated **code-area pointer**.
|
|
53
|
+
* An invalid value (whitespace, slashes, over 100 chars) is ignored with a warning.
|
|
54
|
+
*/
|
|
55
|
+
readonly release?: string;
|
|
56
|
+
/** The deploy environment (`production` / `staging` / …) stamped on every capture. */
|
|
57
|
+
readonly environment?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Master gate. Defaults to `true`; set `false` and the SDK stays completely
|
|
60
|
+
* dormant — nothing is instrumented, no boot call is made, and no report can be
|
|
61
|
+
* sent. The way to keep Fixback out of local dev without branching your code.
|
|
62
|
+
*/
|
|
63
|
+
readonly enabled?: boolean;
|
|
64
|
+
/** Run the built-in default scrubbers before `beforeSend`. Defaults to `true` (private-by-default). */
|
|
65
|
+
readonly scrub?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The synchronous, network-free client scrub hook every capture passes through
|
|
68
|
+
* before transport (spec 0003 §C). Mutate the draft to scrub further, or return
|
|
69
|
+
* `null` to drop it. Runs after the default scrubbers.
|
|
70
|
+
*/
|
|
71
|
+
readonly beforeSend?: (draft: Draft) => Draft | null;
|
|
72
|
+
/** A per-crumb trace filter: mute a category, edit a crumb, or drop it (`null`). */
|
|
73
|
+
readonly beforeBreadcrumb?: BeforeBreadcrumb;
|
|
74
|
+
/** Per-stream capture overrides; a stream left unset follows the served config. */
|
|
75
|
+
readonly capture?: CaptureToggles;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The capture SDKs' common options, with `beforeSend` already bound to the
|
|
79
|
+
* {@link ReportContent} draft they hand it — the shape `@fixback/sdk` and
|
|
80
|
+
* `@fixback/expo` extend.
|
|
81
|
+
*/
|
|
82
|
+
export type CommonCaptureInitOptions = CommonInitOptions<ReportContent>;
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The **shared init vocabulary** (ADR-0028) — the option names every Fixback SDK
|
|
4
|
+
* spells the same way, and the hosted API origin they all default to.
|
|
5
|
+
*
|
|
6
|
+
* Each SDK's own `InitOptions` extends {@link CommonInitOptions} and adds only what
|
|
7
|
+
* its runtime genuinely needs (the browser: a mount target, reduced motion, replay;
|
|
8
|
+
* Expo: the declared origin, the shake gesture, screenshots; Node: the secret key
|
|
9
|
+
* and the batch knobs). So `apiUrl`, `release`, `environment`, `enabled`, `scrub`,
|
|
10
|
+
* and `beforeSend` mean exactly one thing across the family, and a builder who
|
|
11
|
+
* learns one SDK already knows the others.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.DEFAULT_API_URL = void 0;
|
|
15
|
+
/**
|
|
16
|
+
* The hosted Fixback API origin every SDK talks to by default. A self-hosted or
|
|
17
|
+
* local deployment overrides it with `apiUrl` (the dashboard's install snippet
|
|
18
|
+
* pre-fills the right value for the Project), and the CLI/bundler plugins read the
|
|
19
|
+
* same constant so an upload and a capture agree on where Fixback lives.
|
|
20
|
+
*/
|
|
21
|
+
exports.DEFAULT_API_URL = "https://api.fixback.dev";
|
|
22
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAKH;;;;;GAKG;AACU,QAAA,eAAe,GAAG,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **Release** validation (#117, ADR-0024) — the one copy, mirrored *by value* from
|
|
3
|
+
* the server's shared rules (`@fixback/shared`'s `isValidRelease`).
|
|
4
|
+
*
|
|
5
|
+
* A Release is the build identifier uploaded sourcemaps are keyed by, so the SDK
|
|
6
|
+
* that stamps it and the CLI/plugin that upload against it must agree exactly:
|
|
7
|
+
* 1–{@link RELEASE_MAX_LENGTH} visible characters, no whitespace, control
|
|
8
|
+
* characters, or path separators, and not a reserved name. `@fixback/shared` is
|
|
9
|
+
* workspace-private and the SDKs publish standalone, so the slice is vendored here
|
|
10
|
+
* once and shared from the core rather than copied per package.
|
|
11
|
+
*/
|
|
12
|
+
/** The longest release string the server accepts (mirrors the server's `RELEASE_MAX_LENGTH`). */
|
|
13
|
+
export declare const RELEASE_MAX_LENGTH = 100;
|
|
14
|
+
/**
|
|
15
|
+
* Trim + validate a builder-supplied release. Returns `undefined` for an unusable
|
|
16
|
+
* value — the caller drops it (with a warning) rather than shipping something the
|
|
17
|
+
* server would discard.
|
|
18
|
+
*/
|
|
19
|
+
export declare function normaliseRelease(value: string): string | undefined;
|
package/dist/release.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* **Release** validation (#117, ADR-0024) — the one copy, mirrored *by value* from
|
|
4
|
+
* the server's shared rules (`@fixback/shared`'s `isValidRelease`).
|
|
5
|
+
*
|
|
6
|
+
* A Release is the build identifier uploaded sourcemaps are keyed by, so the SDK
|
|
7
|
+
* that stamps it and the CLI/plugin that upload against it must agree exactly:
|
|
8
|
+
* 1–{@link RELEASE_MAX_LENGTH} visible characters, no whitespace, control
|
|
9
|
+
* characters, or path separators, and not a reserved name. `@fixback/shared` is
|
|
10
|
+
* workspace-private and the SDKs publish standalone, so the slice is vendored here
|
|
11
|
+
* once and shared from the core rather than copied per package.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.RELEASE_MAX_LENGTH = void 0;
|
|
15
|
+
exports.normaliseRelease = normaliseRelease;
|
|
16
|
+
/** The longest release string the server accepts (mirrors the server's `RELEASE_MAX_LENGTH`). */
|
|
17
|
+
exports.RELEASE_MAX_LENGTH = 100;
|
|
18
|
+
/** Release names the server reserves (mirrors the shared release rules). */
|
|
19
|
+
const RESERVED_RELEASES = new Set([".", "..", "latest"]);
|
|
20
|
+
/**
|
|
21
|
+
* Trim + validate a builder-supplied release. Returns `undefined` for an unusable
|
|
22
|
+
* value — the caller drops it (with a warning) rather than shipping something the
|
|
23
|
+
* server would discard.
|
|
24
|
+
*/
|
|
25
|
+
function normaliseRelease(value) {
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (trimmed.length === 0 || trimmed.length > exports.RELEASE_MAX_LENGTH)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (RESERVED_RELEASES.has(trimmed))
|
|
30
|
+
return undefined;
|
|
31
|
+
// eslint-disable-next-line no-control-regex
|
|
32
|
+
if (/[\s/\\\u0000-\u001f\u007f]/.test(trimmed))
|
|
33
|
+
return undefined;
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=release.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release.js","sourceRoot":"","sources":["../src/release.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAaH,4CAOC;AAlBD,iGAAiG;AACpF,QAAA,kBAAkB,GAAG,GAAG,CAAC;AAEtC,4EAA4E;AAC5E,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEzD;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,KAAa;IAC5C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,0BAAkB;QAAE,OAAO,SAAS,CAAC;IAClF,IAAI,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IACrD,4CAA4C;IAC5C,IAAI,4BAA4B,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IACjE,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/scrub.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single client-side **scrub choke point** every report passes through
|
|
3
|
+
* before transport (spec 0003 §C; research `sentry-error-capture-findings.md`
|
|
4
|
+
* §7.4) — the SDK's `beforeSend` equivalent, shared runtime-agnostically across
|
|
5
|
+
* surfaces (ADR-0028).
|
|
6
|
+
*
|
|
7
|
+
* Masking is the SDK's job, done before anything leaves the process. The
|
|
8
|
+
* screenshot is masked at capture, and breadcrumbs never record a value or a
|
|
9
|
+
* body at the source; `runBeforeSend` is the **last** gate over the assembled
|
|
10
|
+
* report. Its default scrubbers are **on**: they strip credentials, query
|
|
11
|
+
* strings, and bearer tokens from URLs, and redact obvious PII (emails, long
|
|
12
|
+
* digit runs, bearer tokens) from crumb and error text. The result is then handed
|
|
13
|
+
* to an optional per-project hook that can mutate it further or drop the whole
|
|
14
|
+
* report by returning `null`.
|
|
15
|
+
*
|
|
16
|
+
* The hook is **synchronous and network-free** by contract, and both manual
|
|
17
|
+
* (overlay) and automatic (error-capture) reports run through the very same
|
|
18
|
+
* choke point. A project relaxes the defaults with `scrub: false`, or reshapes
|
|
19
|
+
* the draft in its own hook — never a silent raw send.
|
|
20
|
+
*/
|
|
21
|
+
import type { ReportContent } from "./wire";
|
|
22
|
+
/** The per-project client scrub hook. Return `null` to drop the whole report. */
|
|
23
|
+
export type BeforeSend = (draft: ReportContent) => ReportContent | null;
|
|
24
|
+
/** Options for {@link runBeforeSend}. */
|
|
25
|
+
export interface BeforeSendOptions {
|
|
26
|
+
/** The per-project hook, run **after** the default scrubbers. */
|
|
27
|
+
readonly hook?: BeforeSend | null;
|
|
28
|
+
/** Run the built-in default scrubbers first. Defaults to `true`. */
|
|
29
|
+
readonly scrub?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Redact obvious PII from free text: email addresses, `Bearer <token>` /
|
|
33
|
+
* `token <value>` pairs, and long digit runs. Conservative by design — it keeps
|
|
34
|
+
* the shape of the message readable while removing the sensitive spans.
|
|
35
|
+
*/
|
|
36
|
+
export declare function redactPii(text: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Strip the sensitive parts of a URL: userinfo credentials
|
|
39
|
+
* (`scheme://user:pass@host`), the entire query string, a token-bearing
|
|
40
|
+
* fragment (one that carries `key=value`), and PII (emails, long digit runs) in
|
|
41
|
+
* the **path segments** (#139). Plain hash routes (`#/checkout`) are kept. Works
|
|
42
|
+
* on absolute and relative URLs alike, with no dependency and no throw. The host
|
|
43
|
+
* (authority) is never redacted — only the path and any surviving fragment route.
|
|
44
|
+
*/
|
|
45
|
+
export declare function scrubUrl(url: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Apply the built-in default scrubbers to a report draft: strip the page URL,
|
|
48
|
+
* and scrub every crumb's URLs and redact PII from its text. The Reporter's own
|
|
49
|
+
* `comment` is intentionally left untouched — it is authored on purpose, not
|
|
50
|
+
* scraped. The screenshot and input values are masked elsewhere (at capture and
|
|
51
|
+
* at crumb creation); this is the final URL/PII sweep.
|
|
52
|
+
*/
|
|
53
|
+
export declare function applyDefaultScrub(draft: ReportContent): ReportContent;
|
|
54
|
+
/**
|
|
55
|
+
* Run the report draft through the client scrub choke point: the default
|
|
56
|
+
* scrubbers first (unless `scrub` is `false`), then the optional per-project
|
|
57
|
+
* hook. Returns the scrubbed (and possibly hook-mutated) draft, or `null` when
|
|
58
|
+
* the hook drops the report. A hook that throws is treated as a no-op — the
|
|
59
|
+
* already-scrubbed draft is kept, so a buggy hook never breaks the report path
|
|
60
|
+
* nor leaks unscrubbed data.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runBeforeSend(draft: ReportContent, options?: BeforeSendOptions): ReportContent | null;
|
package/dist/stack.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **one stack parser** every Fixback SDK shares (ADR-0028): a single frame
|
|
3
|
+
* tokenizer ({@link parseStackLine}) and the structured-frame extraction the wire
|
|
4
|
+
* carries ({@link extractStructuredFrames}).
|
|
5
|
+
*
|
|
6
|
+
* Three consumers read a stack, and each used to carry its own copy of the same
|
|
7
|
+
* regexes: the fingerprint's compact top-frames signature (`fingerprint.ts`), the
|
|
8
|
+
* console crumb's `file:line` source (`trace/source.ts`), and the structured
|
|
9
|
+
* frames server-side symbolication matches against uploaded sourcemaps
|
|
10
|
+
* (`extractStructuredFrames`, #117 / ADR-0024). They now all tokenize a frame the
|
|
11
|
+
* same way, so a stack that parses on one surface parses identically on every
|
|
12
|
+
* other — the parity guarantee the shared fingerprint depends on.
|
|
13
|
+
*
|
|
14
|
+
* Both engine dialects are handled: V8/Hermes (`at fn (loc)` / `at loc`) and
|
|
15
|
+
* JSC/SpiderMonkey (`fn@loc` / `@loc`).
|
|
16
|
+
*/
|
|
17
|
+
import type { CapturedFrame } from "./wire";
|
|
18
|
+
/** One tokenized stack frame: the function name (when the frame named one) and its raw location. */
|
|
19
|
+
export interface StackFrameTokens {
|
|
20
|
+
/** The frame's function name, or `null` for an anonymous / bare frame. */
|
|
21
|
+
readonly fn: string | null;
|
|
22
|
+
/** The raw `file:line:col` location, exactly as the engine wrote it. */
|
|
23
|
+
readonly location: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Tokenize one **trimmed** stack line into its function name and raw location, or
|
|
27
|
+
* `null` when the line is not a frame at all (the leading `Error: message`, a
|
|
28
|
+
* blank line). The single place the two engine dialects are recognised — every
|
|
29
|
+
* other stack reader in the core builds on this.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseStackLine(line: string): StackFrameTokens | null;
|
|
32
|
+
/**
|
|
33
|
+
* Split a `file:line:col` location into its parts; `null` when it carries no line
|
|
34
|
+
* number or names no locatable file. The file may itself contain colons
|
|
35
|
+
* (`https://…`, `node:internal/…`), so the numeric groups are taken from the right.
|
|
36
|
+
* Unlocatable frames (`native`, `<anonymous>`, eval) are rejected here.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseFrameLocation(location: string): {
|
|
39
|
+
file: string;
|
|
40
|
+
line: number;
|
|
41
|
+
column: number | null;
|
|
42
|
+
} | null;
|
|
43
|
+
/**
|
|
44
|
+
* Extract **structured** frames from a stack for the wire (#117, ADR-0024), top of
|
|
45
|
+
* stack first — unlike the fingerprint's compact signature (which drops the origin
|
|
46
|
+
* so a per-deploy asset hash cannot split a bug), these keep the full script path,
|
|
47
|
+
* because server-side symbolication matches it against uploaded sourcemap paths.
|
|
48
|
+
* Each URL is scrubbed (query dropped, path PII redacted) before it can leave the
|
|
49
|
+
* process; unlocatable frames are skipped; the count is capped.
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractStructuredFrames(stack: string | undefined, limit?: number): CapturedFrame[];
|