@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.
Files changed (68) hide show
  1. package/README.md +28 -9
  2. package/{src/annotation.ts → dist/annotation.d.ts} +36 -47
  3. package/dist/anonymous-id.d.ts +23 -0
  4. package/dist/anonymous-id.js +39 -0
  5. package/dist/anonymous-id.js.map +1 -0
  6. package/dist/auto-capture.d.ts +129 -0
  7. package/dist/auto-capture.js +210 -0
  8. package/dist/auto-capture.js.map +1 -0
  9. package/{src/backoff.ts → dist/backoff.d.ts} +17 -52
  10. package/dist/boot.d.ts +148 -0
  11. package/dist/boot.js +113 -0
  12. package/dist/boot.js.map +1 -0
  13. package/dist/breadcrumb.d.ts +133 -0
  14. package/dist/connect.d.ts +110 -0
  15. package/dist/connect.js +147 -0
  16. package/dist/connect.js.map +1 -0
  17. package/dist/env.d.ts +37 -0
  18. package/dist/env.js +83 -0
  19. package/dist/env.js.map +1 -0
  20. package/dist/fingerprint.d.ts +39 -0
  21. package/dist/fingerprint.js +10 -15
  22. package/dist/fingerprint.js.map +1 -1
  23. package/dist/http.d.ts +51 -0
  24. package/dist/http.js +42 -0
  25. package/dist/http.js.map +1 -0
  26. package/dist/index.d.ts +28 -0
  27. package/dist/index.js +115 -3
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +1057 -0
  30. package/dist/index.mjs.map +7 -0
  31. package/dist/options.d.ts +82 -0
  32. package/dist/options.js +22 -0
  33. package/dist/options.js.map +1 -0
  34. package/dist/release.d.ts +19 -0
  35. package/dist/release.js +36 -0
  36. package/dist/release.js.map +1 -0
  37. package/dist/scrub.d.ts +62 -0
  38. package/dist/stack.d.ts +51 -0
  39. package/dist/stack.js +97 -0
  40. package/dist/stack.js.map +1 -0
  41. package/dist/trace/buffer.d.ts +121 -0
  42. package/dist/trace/buffer.js +230 -0
  43. package/dist/trace/buffer.js.map +1 -0
  44. package/dist/trace/console-args.d.ts +60 -0
  45. package/dist/trace/console-args.js +189 -0
  46. package/dist/trace/console-args.js.map +1 -0
  47. package/dist/trace/console.d.ts +42 -0
  48. package/dist/trace/console.js +71 -0
  49. package/dist/trace/console.js.map +1 -0
  50. package/dist/trace/crumbs.d.ts +88 -0
  51. package/dist/trace/crumbs.js +164 -0
  52. package/dist/trace/crumbs.js.map +1 -0
  53. package/dist/trace/source.d.ts +30 -0
  54. package/dist/trace/source.js +59 -0
  55. package/dist/trace/source.js.map +1 -0
  56. package/dist/version.d.ts +13 -0
  57. package/dist/version.js +17 -0
  58. package/dist/version.js.map +1 -0
  59. package/dist/wire.d.ts +101 -0
  60. package/package.json +12 -8
  61. package/src/backoff.test.ts +0 -94
  62. package/src/breadcrumb.ts +0 -169
  63. package/src/fingerprint.test.ts +0 -96
  64. package/src/fingerprint.ts +0 -112
  65. package/src/index.ts +0 -63
  66. package/src/scrub.test.ts +0 -215
  67. package/src/scrub.ts +0 -226
  68. package/src/wire.ts +0 -116
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * The console crumb's **call site** (spec #122 §C) — the best-effort `file:line` a
4
+ * `console.*` call was made from, shared runtime-agnostically (ADR-0028).
5
+ *
6
+ * Frames are tokenized by the core's single stack parser (`stack.ts`), so a stack
7
+ * that yields a fingerprint frame yields a source location too — the browser's V8
8
+ * and Firefox/Safari dialects and React Native's Hermes/JSC ones alike. A query
9
+ * string on the asset URL is dropped, both for parse safety and privacy (it may be
10
+ * a cache-buster, or defensively a token).
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.CONSOLE_SOURCE_SKIP_FRAMES = void 0;
14
+ exports.parseStackFrame = parseStackFrame;
15
+ exports.sourceFromStack = sourceFromStack;
16
+ const stack_1 = require("../stack");
17
+ /**
18
+ * Parse a single **trimmed** stack line into a `{ file, line }` location, or
19
+ * `undefined` when the line names no locatable file:line.
20
+ */
21
+ function parseStackFrame(line) {
22
+ const tokens = (0, stack_1.parseStackLine)(line);
23
+ if (!tokens || !tokens.location)
24
+ return undefined;
25
+ const loc = tokens.location.replace(/\?[^:]*/, "");
26
+ const m = loc.match(/^(.*):(\d+):\d+$/) ?? loc.match(/^(.*):(\d+)$/);
27
+ if (!m)
28
+ return undefined;
29
+ const file = m[1] ?? "";
30
+ const lineNo = Number(m[2]);
31
+ if (!file || !Number.isFinite(lineNo))
32
+ return undefined;
33
+ return { file, line: lineNo };
34
+ }
35
+ /**
36
+ * The best-effort `file:line` a console call was made from (spec #122 §C). Parses the
37
+ * frames of a stack, skipping `skipFrames` leading (SDK-internal) frames so the source
38
+ * points at the host code that called `console.*`. Returns `undefined` when no frame
39
+ * yields a usable location — the crumb then simply omits `source`.
40
+ */
41
+ function sourceFromStack(stack, skipFrames = 0) {
42
+ if (typeof stack !== "string" || stack.length === 0)
43
+ return undefined;
44
+ const frames = [];
45
+ for (const raw of stack.split("\n")) {
46
+ const frame = parseStackFrame(raw.trim());
47
+ if (frame)
48
+ frames.push(frame);
49
+ }
50
+ return frames[skipFrames];
51
+ }
52
+ /**
53
+ * Frames between `new Error()` (created inside the console wrapper) and the host
54
+ * caller: just the wrapper frame itself. Skipping it points {@link sourceFromStack}
55
+ * at the app code. The wrapper is called through the console object's property, so
56
+ * it is not a direct-call inlining candidate — keeping this skip count stable.
57
+ */
58
+ exports.CONSOLE_SOURCE_SKIP_FRAMES = 1;
59
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/trace/source.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AASH,0CAUC;AAQD,0CAWC;AApCD,oCAA0C;AAG1C;;;GAGG;AACH,SAAgB,eAAe,CAAC,IAAY;IAC1C,MAAM,MAAM,GAAG,IAAA,sBAAc,EAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAC;IAClD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACnD,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IACrE,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzB,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACxB,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IACxD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAChC,CAAC;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAC7B,KAAyB,EACzB,UAAU,GAAG,CAAC;IAEd,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACtE,MAAM,MAAM,GAAqB,EAAE,CAAC;IACpC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1C,IAAI,KAAK;YAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IACD,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;GAKG;AACU,QAAA,0BAA0B,GAAG,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@fixback/sdk-core`'s own version string, kept as an inlined constant rather
3
+ * than a runtime `package.json` import so the published build stays
4
+ * self-contained (the same posture as `packages/sdk/src/version.ts`,
5
+ * `packages/expo/src/version.ts`, and `packages/node/src/version.ts`).
6
+ *
7
+ * It must equal `package.json`'s `version`. Two things keep it there: the
8
+ * Changesets `version` step syncs it automatically (`scripts/sync-sdk-version.mjs`,
9
+ * wired into `version-packages`), and `version.test.ts` fails the build if the two
10
+ * ever drift. Do not hand-edit this line to a value other than `package.json`'s
11
+ * version.
12
+ */
13
+ export declare const SDK_CORE_VERSION = "0.3.0";
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_CORE_VERSION = void 0;
4
+ /**
5
+ * `@fixback/sdk-core`'s own version string, kept as an inlined constant rather
6
+ * than a runtime `package.json` import so the published build stays
7
+ * self-contained (the same posture as `packages/sdk/src/version.ts`,
8
+ * `packages/expo/src/version.ts`, and `packages/node/src/version.ts`).
9
+ *
10
+ * It must equal `package.json`'s `version`. Two things keep it there: the
11
+ * Changesets `version` step syncs it automatically (`scripts/sync-sdk-version.mjs`,
12
+ * wired into `version-packages`), and `version.test.ts` fails the build if the two
13
+ * ever drift. Do not hand-edit this line to a value other than `package.json`'s
14
+ * version.
15
+ */
16
+ exports.SDK_CORE_VERSION = "0.3.0";
17
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACU,QAAA,gBAAgB,GAAG,OAAO,CAAC"}
package/dist/wire.d.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The shared **ingest wire shapes** (ADR-0028) — the value contract a captured
3
+ * report carries to `POST /api/ingest/feedback`, shared by every Fixback capture
4
+ * SDK so they all speak the same shape.
5
+ *
6
+ * Like the rest of the core these are runtime-agnostic value types: the browser
7
+ * and (future) backend SDKs both assemble a {@link ReportContent} from what they
8
+ * captured. Keep them in lock-step with the server: the JSON `payload` part the
9
+ * ingest controller parses (`apps/api/src/ingest/ingest.controller.ts`) and the
10
+ * `SubmissionContent` it maps to (`apps/api/src/ingest/reporter-identity.ts`).
11
+ * Each SDK keeps vendoring its own transport envelope (ticket #47) — the core
12
+ * carries the pure content shape, not the multipart envelope.
13
+ */
14
+ import type { Annotation } from "./annotation";
15
+ import type { Breadcrumb } from "./breadcrumb";
16
+ /**
17
+ * Where a Feedback came from — a human in the overlay (`reporter`, the default) or
18
+ * a runtime error the SDK captured (`error`, ADR-0025: covers both an uncaught
19
+ * crash and a manual capture, on any runtime). Mirrors the server's
20
+ * `FEEDBACK_SOURCES`; the server derives trust independently and ignores anything
21
+ * else the client claims.
22
+ */
23
+ export type FeedbackSource = "reporter" | "error";
24
+ /**
25
+ * The **Platform** a Feedback was captured on (ADR-0025): `browser` (the web SDK),
26
+ * `node` (the backend SDK), or `expo` (the mobile SDK). Additive — future runtimes
27
+ * become new values. Mirrors the server's `PLATFORMS` (`@fixback/shared`) by value;
28
+ * the server stamps it from the ingest route, so it is **not** carried on
29
+ * {@link ReportContent} (the client never declares its own platform).
30
+ */
31
+ export type Platform = "browser" | "node" | "expo";
32
+ /** The capture environment recorded alongside a report. */
33
+ export interface CaptureEnvironment {
34
+ readonly viewportWidth?: number;
35
+ readonly viewportHeight?: number;
36
+ readonly browser?: string;
37
+ readonly sdkVersion?: string;
38
+ /**
39
+ * The host app's **Release** — the build identifier the builder configured at
40
+ * `init` (#117, ADR-0024). Uploaded sourcemaps are keyed by it, so the server
41
+ * can symbolicate this session's stack traces against the exact build that
42
+ * produced them. Omitted when the Project doesn't set one.
43
+ */
44
+ readonly release?: string;
45
+ }
46
+ /**
47
+ * One structured frame of a captured stack trace (#117, ADR-0024), top of stack
48
+ * first — the server's `errorFrames` wire shape. `file` is the scrubbed script
49
+ * URL; `line`/`column` are 1-based as browsers report them.
50
+ */
51
+ export interface CapturedFrame {
52
+ readonly file: string;
53
+ readonly line: number;
54
+ readonly column: number | null;
55
+ readonly function: string | null;
56
+ }
57
+ /**
58
+ * The JSON content of a feedback submission — the object serialised into the
59
+ * multipart `payload` part next to the `key` and identity evidence. Every field
60
+ * is optional: none of it feeds the server's trust decision, so a submission may
61
+ * carry any subset. `annotation` is the structured `{ element?, region?, marks? }`
62
+ * (spec §D); the screenshot is a separate binary part, never part of this JSON.
63
+ */
64
+ export interface ReportContent {
65
+ readonly comment?: string;
66
+ readonly url?: string;
67
+ readonly environment?: CaptureEnvironment;
68
+ readonly annotation?: Annotation;
69
+ /** The masked breadcrumb trace buffer that rode on this report (spec §C). */
70
+ readonly trace?: readonly Breadcrumb[];
71
+ /**
72
+ * Provenance (spec §D/§E). Omitted for a manual report — the transport stamps the
73
+ * `reporter` default on the wire; set to `error` by the SDK's error capture.
74
+ */
75
+ readonly source?: FeedbackSource;
76
+ /**
77
+ * For `source: error` only — whether the error was **handled** (ADR-0025):
78
+ * `false` = an uncaught crash, `true` = a manual capture. Set by the backend SDK
79
+ * (`@fixback/node`), which distinguishes the two; the browser SDK captures only
80
+ * uncaught errors and leaves it unset.
81
+ */
82
+ readonly handled?: boolean;
83
+ /**
84
+ * The deploy **Environment** the SDK was configured with (ADR-0025) —
85
+ * `production` / `staging` / … — distinct from {@link CaptureEnvironment.release}
86
+ * (a build version). Named apart from `environment` (the capture bag above) to
87
+ * avoid the collision. Set by the backend SDK; unset by the browser SDK for now.
88
+ */
89
+ readonly deployEnvironment?: string;
90
+ /** For `source: error` only — the SDK's per-session error fingerprint (spec §E). */
91
+ readonly errorSignature?: string;
92
+ /** For `source: error` only — the running occurrence count within the session (spec §E). */
93
+ readonly occurrences?: number;
94
+ /**
95
+ * For `source: error` only — the captured error's parsed stack frames (#117,
96
+ * ADR-0024), top of stack first, capped client-side. The analysis worker
97
+ * matches them against the release's uploaded sourcemaps to write the Issue's
98
+ * Code-area pointer.
99
+ */
100
+ readonly errorFrames?: readonly CapturedFrame[];
101
+ }
package/package.json CHANGED
@@ -1,23 +1,22 @@
1
1
  {
2
2
  "name": "@fixback/sdk-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Runtime-agnostic core shared by the Fixback capture SDKs — fingerprinting, scrubbing, backoff, and the shared wire shapes.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
8
8
  "main": "./dist/index.js",
9
- "types": "./src/index.ts",
9
+ "types": "./dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "types": "./src/index.ts",
13
- "import": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.mjs",
14
14
  "require": "./dist/index.js"
15
15
  },
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
- "src",
21
20
  "README.md",
22
21
  "LICENSE"
23
22
  ],
@@ -32,17 +31,22 @@
32
31
  "url": "git+https://github.com/wemuda/fixback.git",
33
32
  "directory": "packages/sdk-core"
34
33
  },
35
- "homepage": "https://github.com/wemuda/fixback/tree/master/packages/sdk-core#readme",
36
- "bugs": "https://github.com/wemuda/fixback/issues",
34
+ "homepage": "https://docs.fixback.dev",
35
+ "bugs": "https://docs.fixback.dev",
37
36
  "publishConfig": {
38
37
  "access": "public"
39
38
  },
40
39
  "devDependencies": {
40
+ "esbuild": "^0.25.0",
41
41
  "typescript": "5.9.3",
42
42
  "vitest": "^4.1.10"
43
43
  },
44
+ "engines": {
45
+ "node": ">=20.12"
46
+ },
47
+ "module": "./dist/index.mjs",
44
48
  "scripts": {
45
- "build": "tsc -p tsconfig.build.json && node scripts/finalize-cjs.mjs",
49
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/finalize-cjs.mjs dist && esbuild src/index.ts --bundle --format=esm --platform=neutral --target=es2020 --sourcemap --outfile=dist/index.mjs",
46
50
  "lint": "eslint .",
47
51
  "typecheck": "tsc --noEmit",
48
52
  "test": "vitest run"
@@ -1,94 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import {
4
- AutoReportBackoff,
5
- DEFAULT_RETRY_AFTER_SECONDS,
6
- parseRetryAfter,
7
- } from "./backoff";
8
-
9
- describe("parseRetryAfter", () => {
10
- const NOW = 1_000_000;
11
-
12
- it("reads the delta-seconds form", () => {
13
- expect(parseRetryAfter("30", NOW)).toBe(30);
14
- expect(parseRetryAfter(" 0 ", NOW)).toBe(0);
15
- });
16
-
17
- it("reads the HTTP-date form as whole seconds from now (rounded up)", () => {
18
- // toUTCString carries whole seconds only, so the header parses to NOW + 45_000.
19
- const when = new Date(NOW + 45_000).toUTCString();
20
- expect(parseRetryAfter(when, NOW)).toBe(45);
21
- // A sub-second remainder rounds up: from NOW - 500 the gap is 45_500 ms.
22
- expect(parseRetryAfter(when, NOW - 500)).toBe(46);
23
- });
24
-
25
- it("clamps a past HTTP-date to zero", () => {
26
- const when = new Date(NOW - 10_000).toUTCString();
27
- expect(parseRetryAfter(when, NOW)).toBe(0);
28
- });
29
-
30
- it("falls back to the default when the header is absent, blank, or unparseable", () => {
31
- expect(parseRetryAfter(null, NOW)).toBe(DEFAULT_RETRY_AFTER_SECONDS);
32
- expect(parseRetryAfter(undefined, NOW)).toBe(DEFAULT_RETRY_AFTER_SECONDS);
33
- expect(parseRetryAfter("", NOW)).toBe(DEFAULT_RETRY_AFTER_SECONDS);
34
- expect(parseRetryAfter(" ", NOW)).toBe(DEFAULT_RETRY_AFTER_SECONDS);
35
- expect(parseRetryAfter("soon", NOW)).toBe(DEFAULT_RETRY_AFTER_SECONDS);
36
- });
37
-
38
- it("defaults to 60 seconds", () => {
39
- expect(DEFAULT_RETRY_AFTER_SECONDS).toBe(60);
40
- });
41
- });
42
-
43
- describe("AutoReportBackoff", () => {
44
- function at(t: number): { clock: { t: number }; backoff: AutoReportBackoff } {
45
- const clock = { t };
46
- return { clock, backoff: new AutoReportBackoff(() => clock.t) };
47
- }
48
-
49
- it("starts clear", () => {
50
- const { backoff } = at(1_000);
51
- expect(backoff.isPaused()).toBe(false);
52
- expect(backoff.retryAfterSeconds()).toBe(0);
53
- });
54
-
55
- it("opens a window from a Retry-After value and clears when it elapses", () => {
56
- const { clock, backoff } = at(1_000);
57
-
58
- expect(backoff.hold("30")).toBe(30);
59
- expect(backoff.isPaused()).toBe(true);
60
- expect(backoff.retryAfterSeconds()).toBe(30);
61
-
62
- clock.t = 1_000 + 29_000;
63
- expect(backoff.isPaused()).toBe(true);
64
-
65
- clock.t = 1_000 + 30_000;
66
- expect(backoff.isPaused()).toBe(false);
67
- expect(backoff.retryAfterSeconds()).toBe(0);
68
- });
69
-
70
- it("defaults to 60 seconds when the header is absent", () => {
71
- const { clock, backoff } = at(1_000);
72
-
73
- expect(backoff.hold(null)).toBe(60);
74
- clock.t = 1_000 + 59_000;
75
- expect(backoff.isPaused()).toBe(true);
76
- clock.t = 1_000 + 60_000;
77
- expect(backoff.isPaused()).toBe(false);
78
- });
79
-
80
- it("extends the window but never shrinks it", () => {
81
- const { clock, backoff } = at(1_000);
82
-
83
- backoff.hold("60"); // until 61_000
84
- clock.t = 1_000 + 10_000; // t = 11_000
85
- backoff.hold("5"); // would end at 16_000 — earlier than 61_000, so ignored
86
- clock.t = 1_000 + 20_000; // t = 21_000, still inside the original window
87
- expect(backoff.isPaused()).toBe(true);
88
-
89
- clock.t = 1_000 + 10_000; // back inside; extend past the original
90
- backoff.hold("120"); // until 71_000
91
- clock.t = 1_000 + 65_000; // t = 66_000 — past the original 61_000
92
- expect(backoff.isPaused()).toBe(true);
93
- });
94
- });
package/src/breadcrumb.ts DELETED
@@ -1,169 +0,0 @@
1
- /**
2
- * The trace **breadcrumb wire types** (spec 0003 §C, spec #122) — the shared,
3
- * runtime-agnostic shapes for one entry of a captured trace stream (ADR-0028).
4
- *
5
- * A breadcrumb records recent activity — console, navigation, network metadata,
6
- * masked user actions, and the failing error — that rides on a report as
7
- * Evidence. This module carries only the value types; the SDK's ring buffer and
8
- * DOM/Node instrumentation that produce them live in the consuming SDK.
9
- * Everything private is kept out **at the source** by that instrumentation:
10
- * `ui.input` records that an input changed, never its value; network crumbs carry
11
- * method + URL + status only, never bodies.
12
- */
13
-
14
- /** Console-style severity a `console` crumb records. */
15
- export type BreadcrumbLevel =
16
- | "log"
17
- | "info"
18
- | "warn"
19
- | "error"
20
- | "assert"
21
- | "debug";
22
-
23
- /** The kind of activity a crumb records. */
24
- export type BreadcrumbCategory =
25
- | "console"
26
- | "navigation"
27
- | "fetch"
28
- | "xhr"
29
- | "beacon"
30
- | "ui.click"
31
- | "ui.input"
32
- | "error";
33
-
34
- /**
35
- * Which browser API issued a captured network request (spec #122 §D, ticket #139).
36
- * Every network crumb's `category` is one of these too, so a stream router and the
37
- * read model agree on what is a network entry.
38
- */
39
- export type NetworkApi = "fetch" | "xhr" | "beacon";
40
-
41
- /**
42
- * A network request's failure classification (spec #122 §D, ticket #139). `ok` and
43
- * the HTTP status classes come from a settled response; `network-error` / `timeout`
44
- * / `aborted` from how a request failed; `opaque-cors` from a cross-origin response
45
- * whose status is unreadable. The Network tab flags every non-`ok` outcome.
46
- */
47
- export type NetworkOutcome =
48
- | "ok"
49
- | "http-4xx"
50
- | "http-5xx"
51
- | "network-error"
52
- | "timeout"
53
- | "aborted"
54
- | "opaque-cors";
55
-
56
- /**
57
- * A crumb's structured detail. Deliberately narrow: there is **no** field for a
58
- * request/response body or an input value, so those can never be recorded.
59
- */
60
- export interface BreadcrumbData {
61
- readonly url?: string;
62
- readonly method?: string;
63
- readonly status?: number;
64
- /** A masked CSS selector for a `ui.*` target — never its text or value. */
65
- readonly target?: string;
66
- readonly from?: string;
67
- readonly to?: string;
68
- readonly errorType?: string;
69
- }
70
-
71
- /**
72
- * The type tag on a structured console argument (spec #122 §C, decision D7). A
73
- * console call's arguments are preserved **type-tagged** rather than flattened to a
74
- * string, so an object/array argument can be inspected in the Console tab rather
75
- * than read as `[object Object]`: `string`/`number`/`bool`/`null` carry the value
76
- * directly, `json` a depth-/byte-capped JSON-safe clone, and `error` an Error's
77
- * `{ name, message, stack }`.
78
- */
79
- export type ConsoleArgType =
80
- | "string"
81
- | "number"
82
- | "bool"
83
- | "null"
84
- | "json"
85
- | "error";
86
-
87
- /**
88
- * One structured console argument (spec #122 §C): a type tag plus a JSON-safe value.
89
- * `v` is always serializable — a `json` arg is depth-, breadth-, and string-capped at
90
- * assembly, and exotic values (bigint, symbol, function, circular refs) are rendered
91
- * to safe text — so a console crumb can never carry an unserializable or unbounded
92
- * value onto the wire.
93
- */
94
- export interface ConsoleArg {
95
- readonly t: ConsoleArgType;
96
- readonly v: unknown;
97
- }
98
-
99
- /** A `file:line` source location (spec #122 §C) — where a console call was made. */
100
- export interface SourceLocation {
101
- readonly file: string;
102
- readonly line: number;
103
- }
104
-
105
- /**
106
- * One entry in a trace stream. Alongside its semantic fields every entry carries a
107
- * stable {@link id} and a high-res monotonic {@link mono} timestamp — both stamped
108
- * by the buffer on `add` — so entries from the three independent streams order and
109
- * cross-link exactly (spec #122 §B, decision D9). `timestamp` stays epoch ms.
110
- */
111
- export interface Breadcrumb {
112
- readonly category: BreadcrumbCategory;
113
- readonly message?: string;
114
- readonly level?: BreadcrumbLevel;
115
- /** Epoch milliseconds when the crumb was recorded. */
116
- readonly timestamp: number;
117
- /** A stable id, unique within the buffer — assigned on `add` when not already set. */
118
- readonly id?: string;
119
- /** A high-res monotonic timestamp (`performance.now()`) — assigned on `add`. */
120
- readonly mono?: number;
121
- readonly data?: BreadcrumbData;
122
- /**
123
- * For a `console` crumb (spec #122 §C): the call's arguments preserved as
124
- * structured, type-tagged values (see {@link ConsoleArg}), so the Console tab can
125
- * render each argument expandably instead of a flattened string. `message` stays
126
- * the one-line preview. Absent on non-console crumbs.
127
- */
128
- readonly args?: readonly ConsoleArg[];
129
- /**
130
- * For a `console` crumb (spec #122 §C): the `file:line` the call was made from,
131
- * parsed best-effort from the call stack. Captured only for the levels that keep a
132
- * source — `warn`/`error`/`assert` — and only when the stack yields a usable
133
- * location; a chatty app's `log`/`info`/`debug` omit it. Absent on non-console crumbs.
134
- */
135
- readonly source?: SourceLocation;
136
- /**
137
- * For an auto-captured `error` crumb only (spec #122 §F): the ids of the entries
138
- * immediately preceding the throw — a causal pointer into the same trace, so a
139
- * machine-filed crash names its lead-up. Absent on every other crumb.
140
- */
141
- readonly causedBy?: readonly string[];
142
- /**
143
- * For a network crumb (`fetch` / `xhr` / `beacon`) — spec #122 §D, ticket #139.
144
- * The rich request metadata the Network tab renders, lifted to the top level (the
145
- * console-enrichment precedent) so the read model shapes each into a
146
- * `NetworkTraceEntry`. **No field carries a request/response body or an arbitrary
147
- * header** — `respSize` derives from the `content-length` response header only and
148
- * `contentType` from `content-type`; nothing else is read. Absent on every other crumb.
149
- */
150
- readonly api?: NetworkApi;
151
- /** The request method (network crumb) — e.g. `GET`, `POST`. */
152
- readonly method?: string;
153
- /** The scrubbed request URL (network crumb) — query dropped, path PII redacted. */
154
- readonly url?: string;
155
- /** The final HTTP status (network crumb), when one was known; absent on a network error/beacon. */
156
- readonly status?: number;
157
- /** The HTTP status text (network crumb), when the response carried one. */
158
- readonly statusText?: string;
159
- /** Wall-clock duration of the request in ms (network crumb) — a `performance.now()` delta. */
160
- readonly durationMs?: number;
161
- /** Request body size in bytes (network crumb) — only when trivially known (string/Blob/ArrayBuffer), never by reading a stream. */
162
- readonly reqSize?: number;
163
- /** Response body size in bytes (network crumb) — from the `content-length` response header only. */
164
- readonly respSize?: number;
165
- /** Response content type (network crumb) — the `content-type` header's media type. */
166
- readonly contentType?: string;
167
- /** The request's failure classification (network crumb) — see {@link NetworkOutcome}. */
168
- readonly outcome?: NetworkOutcome;
169
- }
@@ -1,96 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { computeFingerprint, extractTopFrames, hashString, normalize } from "./fingerprint";
4
-
5
- describe("normalize — collapsing the volatile parts of an error message", () => {
6
- it("replaces UUIDs, URLs, hex, and long digit runs with stable placeholders", () => {
7
- expect(normalize("failed for 550e8400-e29b-41d4-a716-446655440000")).toBe(
8
- "failed for <uuid>",
9
- );
10
- expect(
11
- normalize("GET https://api.example.com/users/42?token=abc failed"),
12
- ).toContain("<url>");
13
- expect(normalize("bad pointer 0xdeadbeef")).toBe("bad pointer <hex>");
14
- expect(normalize("chunk 3f9a1c2e4b7d8091 missing")).toBe("chunk <hex> missing");
15
- expect(normalize("order 1234567 not found")).toBe("order <n> not found");
16
- });
17
-
18
- it("keeps short numbers and stable text so distinct bugs stay distinct", () => {
19
- expect(normalize("code 42 raised")).toBe("code 42 raised");
20
- expect(normalize("Cannot read properties of undefined")).toBe(
21
- "Cannot read properties of undefined",
22
- );
23
- });
24
-
25
- it("returns an empty string for a non-string or empty input", () => {
26
- expect(normalize("")).toBe("");
27
- expect(normalize(undefined as unknown as string)).toBe("");
28
- });
29
- });
30
-
31
- describe("extractTopFrames — a compact top-of-stack signature", () => {
32
- it("parses V8 frames to `function@basename:line:col`, origin stripped", () => {
33
- const stack = [
34
- "Error: boom",
35
- " at pay (https://acme.app/assets/checkout.abc123.js:10:5)",
36
- " at onClick (https://acme.app/assets/checkout.abc123.js:20:9)",
37
- ].join("\n");
38
- expect(extractTopFrames(stack)).toBe(
39
- "pay@checkout.abc123.js:10:5 < onClick@checkout.abc123.js:20:9",
40
- );
41
- });
42
-
43
- it("parses Firefox/Safari `fn@loc` frames", () => {
44
- const stack = "pay@https://acme.app/checkout.js:10:5\n@https://acme.app/x.js:1:1";
45
- expect(extractTopFrames(stack)).toBe("pay@checkout.js:10:5 < @x.js:1:1");
46
- });
47
-
48
- it("caps at the frame limit and returns '' for no usable stack", () => {
49
- const many = Array.from(
50
- { length: 10 },
51
- (_v, i) => ` at fn${i} (https://acme.app/a.js:${i}:1)`,
52
- ).join("\n");
53
- expect(extractTopFrames(many).split(" < ")).toHaveLength(5);
54
- expect(extractTopFrames(undefined)).toBe("");
55
- expect(extractTopFrames("")).toBe("");
56
- });
57
- });
58
-
59
- describe("hashString — dependency-free FNV-1a", () => {
60
- it("is deterministic and distinguishes distinct inputs", () => {
61
- expect(hashString("a|b|c")).toBe(hashString("a|b|c"));
62
- expect(hashString("a|b|c")).not.toBe(hashString("a|b|d"));
63
- });
64
- });
65
-
66
- describe("computeFingerprint — the cross-surface grouping key", () => {
67
- it("is stable for the same logical error", () => {
68
- const stack = " at pay (https://acme.app/checkout.js:10:5)";
69
- expect(computeFingerprint("TypeError", "boom", stack)).toBe(
70
- computeFingerprint("TypeError", "boom", stack),
71
- );
72
- });
73
-
74
- it("ignores the volatile parts of the message (same bug, changed ids)", () => {
75
- const stack = " at pay (https://acme.app/checkout.js:10:5)";
76
- expect(computeFingerprint("Error", "order 111111 failed", stack)).toBe(
77
- computeFingerprint("Error", "order 999999 failed", stack),
78
- );
79
- });
80
-
81
- it("separates errors with different types or frames", () => {
82
- const stack = " at pay (https://acme.app/checkout.js:10:5)";
83
- expect(computeFingerprint("TypeError", "boom", stack)).not.toBe(
84
- computeFingerprint("RangeError", "boom", stack),
85
- );
86
- expect(computeFingerprint("Error", "boom", stack)).not.toBe(
87
- computeFingerprint("Error", "boom", " at other (https://acme.app/x.js:1:1)"),
88
- );
89
- });
90
-
91
- it("falls back to type + normalised message when there is no stack", () => {
92
- expect(computeFingerprint("Error", "order 5 failed")).toBe(
93
- computeFingerprint("Error", "order 5 failed", undefined),
94
- );
95
- });
96
- });