@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
package/README.md CHANGED
@@ -1,19 +1,38 @@
1
1
  # @fixback/sdk-core
2
2
 
3
- The small, runtime-agnostic core shared by the Fixback capture SDKs
4
- (`@fixback/sdk` today; `@fixback/node` next). It holds the **pure** capture logic
5
- so every surface computes it the same way (ADR-0028):
3
+ The runtime-agnostic core shared by every Fixback SDK — `@fixback/sdk`,
4
+ `@fixback/expo`, `@fixback/node`, and the build-time `@fixback/cli`. It holds the
5
+ capture logic none of them should own privately, so every surface computes it the
6
+ same way (ADR-0028):
6
7
 
7
- - **Fingerprinting** — `computeFingerprint` and its `normalize` / `extractTopFrames`
8
- helpers, so the same logical error groups identically across surfaces
9
- (ADR-0027).
8
+ - **Fingerprinting** — `computeFingerprint` and its `normalize` /
9
+ `extractTopFrames` helpers, so the same logical error groups identically across
10
+ surfaces (ADR-0027).
11
+ - **Stack parsing** — one frame tokenizer (`parseStackLine`) behind the
12
+ fingerprint, the console call site, and `extractStructuredFrames` (the frames
13
+ server-side symbolication consumes).
10
14
  - **Scrubbing** — `scrubUrl`, `redactPii`, and the `runBeforeSend` choke point.
15
+ - **The trace buffer** — `createBreadcrumbBuffer`'s three per-stream rings, the
16
+ crumb builders (`consoleCrumb`, `networkCrumb`, `navigationCrumb`,
17
+ `errorCrumb`, …), the structured console-arg caps, and `instrumentConsole`.
18
+ - **Automatic error capture** — `createAutoCapture`: the dedup, token-bucket
19
+ burst limiter, session cap, causal error crumb, and occurrence flush each SDK's
20
+ `installErrorCapture` binds its runtime's error seam to.
11
21
  - **Backoff** — `AutoReportBackoff` + `parseRetryAfter`, honouring ingest's
12
22
  `429` / `Retry-After` backpressure.
23
+ - **The ingest boot contract** — `requestBoot`, `bootEndpoint` /
24
+ `feedbackEndpoint`, `BootAnswer`, and `isFixbackApiRequest`.
25
+ - **The shared init vocabulary** — `DEFAULT_API_URL`, `CommonInitOptions`,
26
+ `normaliseRelease`, and the anonymous-id key + generator.
13
27
  - **Wire shapes** — `ReportContent`, `CapturedFrame`, the `Annotation` /
14
28
  `Breadcrumb` value types, and the provenance vocabulary (`FeedbackSource`,
15
29
  `Platform`).
16
30
 
17
- It is deliberately **runtime-agnostic**: no DOM, no Node built-ins, no rrweb
18
- those live in the consuming SDKs. This is an internal implementation package for
19
- the Fixback SDK family, not a public API.
31
+ It is deliberately **runtime-agnostic**: no DOM, no Node built-ins, no rrweb.
32
+ Every runtime touchpoint is either injected (a `console`, a `fetch`, a storage
33
+ binding) or probed defensively off `globalThis` (`performance`, `TextEncoder`,
34
+ `crypto`). This is an internal implementation package for the Fixback SDK family,
35
+ not a public API.
36
+
37
+ 📖 Documentation for the SDKs that build on this core:
38
+ [docs.fixback.dev](https://docs.fixback.dev).
@@ -13,92 +13,81 @@
13
13
  * consuming SDK. Keep these in lock-step with the server's ingest contract
14
14
  * (spec §D/§H).
15
15
  */
16
-
17
16
  /** A point in screenshot (viewport) coordinate space. */
18
17
  export interface Point {
19
- readonly x: number;
20
- readonly y: number;
18
+ readonly x: number;
19
+ readonly y: number;
21
20
  }
22
-
23
21
  /** A rectangle in screenshot coordinates — the same shape as an element's rect. */
24
22
  export interface Rect {
25
- readonly x: number;
26
- readonly y: number;
27
- readonly width: number;
28
- readonly height: number;
23
+ readonly x: number;
24
+ readonly y: number;
25
+ readonly width: number;
26
+ readonly height: number;
29
27
  }
30
-
31
28
  /** The picked element's viewport rectangle, as the server's annotation `rect`. */
32
29
  export interface ElementRect {
33
- readonly x: number;
34
- readonly y: number;
35
- readonly width: number;
36
- readonly height: number;
30
+ readonly x: number;
31
+ readonly y: number;
32
+ readonly width: number;
33
+ readonly height: number;
37
34
  }
38
-
39
35
  /**
40
36
  * The picked element — the `element` layer of an {@link Annotation}: a stable CSS
41
37
  * selector, a readable DOM path, the tag, and the bounding rect. Exactly the
42
38
  * server's annotation `element` object shape.
43
39
  */
44
40
  export interface SelectedElement {
45
- readonly selector: string;
46
- readonly domPath: string;
47
- readonly tag: string;
48
- readonly rect: ElementRect;
41
+ readonly selector: string;
42
+ readonly domPath: string;
43
+ readonly tag: string;
44
+ readonly rect: ElementRect;
49
45
  }
50
-
51
46
  /** The draw tools the overlay offers, in toolbar order (spec §B, prototype). */
52
47
  export type DrawTool = "arrow" | "box" | "pen" | "text";
53
-
54
48
  /** Fields shared by every vector mark. */
55
49
  interface MarkBase {
56
- /** Stroke/fill colour, as a CSS colour string. */
57
- readonly color: string;
50
+ /** Stroke/fill colour, as a CSS colour string. */
51
+ readonly color: string;
58
52
  }
59
-
60
53
  /** A directional arrow from `(x0,y0)` to its tip at `(x1,y1)`. */
61
54
  export interface ArrowMark extends MarkBase {
62
- readonly type: "arrow";
63
- readonly x0: number;
64
- readonly y0: number;
65
- readonly x1: number;
66
- readonly y1: number;
55
+ readonly type: "arrow";
56
+ readonly x0: number;
57
+ readonly y0: number;
58
+ readonly x1: number;
59
+ readonly y1: number;
67
60
  }
68
-
69
61
  /** A rectangle spanning the drag from `(x0,y0)` to `(x1,y1)`. */
70
62
  export interface BoxMark extends MarkBase {
71
- readonly type: "box";
72
- readonly x0: number;
73
- readonly y0: number;
74
- readonly x1: number;
75
- readonly y1: number;
63
+ readonly type: "box";
64
+ readonly x0: number;
65
+ readonly y0: number;
66
+ readonly x1: number;
67
+ readonly y1: number;
76
68
  }
77
-
78
69
  /** A freehand polyline through `points` (in order). */
79
70
  export interface PenMark extends MarkBase {
80
- readonly type: "pen";
81
- readonly points: ReadonlyArray<Point>;
71
+ readonly type: "pen";
72
+ readonly points: ReadonlyArray<Point>;
82
73
  }
83
-
84
74
  /** A text label anchored at `(x,y)` (its baseline-left, as SVG text). */
85
75
  export interface TextMark extends MarkBase {
86
- readonly type: "text";
87
- readonly x: number;
88
- readonly y: number;
89
- readonly text: string;
76
+ readonly type: "text";
77
+ readonly x: number;
78
+ readonly y: number;
79
+ readonly text: string;
90
80
  }
91
-
92
81
  /** A single vector mark, in screenshot coordinates. */
93
82
  export type Mark = ArrowMark | BoxMark | PenMark | TextMark;
94
-
95
83
  /**
96
84
  * The structured Annotation carried on a report's content (spec §D): the three
97
85
  * optional layers. Every field is optional — a report may carry any subset or
98
86
  * none (a bare comment is a valid Send).
99
87
  */
100
88
  export interface Annotation {
101
- readonly element?: SelectedElement;
102
- readonly region?: Rect;
103
- readonly marks?: ReadonlyArray<Mark>;
89
+ readonly element?: SelectedElement;
90
+ readonly region?: Rect;
91
+ readonly marks?: ReadonlyArray<Mark>;
104
92
  }
93
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The Public Reporter's stable, first-party **anonymous id** (ADR-0028) — the key
3
+ * and the generator, shared by every capture SDK.
4
+ *
5
+ * When the host app supplies no identity of its own, an SDK establishes a
6
+ * per-device id and persists it, so repeated boots from the same device are one
7
+ * Reporter (the server counts distinct Reporters by it). The *storage binding*
8
+ * stays runtime-specific — `localStorage` in a browser, AsyncStorage on React
9
+ * Native — because one is synchronous and the other is not; only the key and the
10
+ * id shape are shared, so the value looks the same across clients.
11
+ *
12
+ * Persistence is best-effort everywhere: when storage is unavailable (private
13
+ * mode, blocked cookies, an absent native module) the SDK falls back to an
14
+ * ephemeral id rather than throwing — capture must never disturb the host app.
15
+ */
16
+ /** The storage key every SDK persists the anonymous id under. */
17
+ export declare const ANONYMOUS_ID_STORAGE_KEY = "fixback.anonymousId";
18
+ /**
19
+ * A random id: a UUID where the platform offers one, else a compact
20
+ * timestamp+random fallback. `crypto.randomUUID` is read off `globalThis` and
21
+ * called defensively — it throws in some non-secure contexts.
22
+ */
23
+ export declare function generateAnonymousId(): string;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ /**
3
+ * The Public Reporter's stable, first-party **anonymous id** (ADR-0028) — the key
4
+ * and the generator, shared by every capture SDK.
5
+ *
6
+ * When the host app supplies no identity of its own, an SDK establishes a
7
+ * per-device id and persists it, so repeated boots from the same device are one
8
+ * Reporter (the server counts distinct Reporters by it). The *storage binding*
9
+ * stays runtime-specific — `localStorage` in a browser, AsyncStorage on React
10
+ * Native — because one is synchronous and the other is not; only the key and the
11
+ * id shape are shared, so the value looks the same across clients.
12
+ *
13
+ * Persistence is best-effort everywhere: when storage is unavailable (private
14
+ * mode, blocked cookies, an absent native module) the SDK falls back to an
15
+ * ephemeral id rather than throwing — capture must never disturb the host app.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ANONYMOUS_ID_STORAGE_KEY = void 0;
19
+ exports.generateAnonymousId = generateAnonymousId;
20
+ /** The storage key every SDK persists the anonymous id under. */
21
+ exports.ANONYMOUS_ID_STORAGE_KEY = "fixback.anonymousId";
22
+ /**
23
+ * A random id: a UUID where the platform offers one, else a compact
24
+ * timestamp+random fallback. `crypto.randomUUID` is read off `globalThis` and
25
+ * called defensively — it throws in some non-secure contexts.
26
+ */
27
+ function generateAnonymousId() {
28
+ const c = globalThis.crypto;
29
+ if (c && typeof c.randomUUID === "function") {
30
+ try {
31
+ return c.randomUUID();
32
+ }
33
+ catch {
34
+ // Fall through to the compact fallback.
35
+ }
36
+ }
37
+ return `fb-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
38
+ }
39
+ //# sourceMappingURL=anonymous-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anonymous-id.js","sourceRoot":"","sources":["../src/anonymous-id.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAUH,kDAUC;AAlBD,iEAAiE;AACpD,QAAA,wBAAwB,GAAG,qBAAqB,CAAC;AAE9D;;;;GAIG;AACH,SAAgB,mBAAmB;IACjC,MAAM,CAAC,GAAI,UAAyD,CAAC,MAAM,CAAC;IAC5E,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC5C,IAAI,CAAC;YACH,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,wCAAwC;QAC1C,CAAC;IACH,CAAC;IACD,OAAO,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACpF,CAAC"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The **automatic error-capture state machine** (spec 0003 §E, ADR-0011; research
3
+ * `sentry-error-capture-findings.md` §7.1–7.3) — the shared, runtime-agnostic half
4
+ * of "errors report themselves, no prompt" (ADR-0028).
5
+ *
6
+ * Every SDK's `installErrorCapture` is now a thin binding over this: it supplies
7
+ * the runtime's error seam (window listeners, React Native's `ErrorUtils`), an
8
+ * {@link AutoCaptureCoreConfig.extract} that distils that runtime's raw event into
9
+ * an {@link ExtractedError}, and an {@link AutoCaptureCoreConfig.submit} that ships
10
+ * one assembled report. Everything between — the per-session fingerprint dedup, the
11
+ * token-bucket burst limiter, the distinct-error session cap, the causal error
12
+ * crumb, the optimistic `sentCount` with rollback on a failed send, and the
13
+ * occurrence flush — lives here, once.
14
+ *
15
+ * Per-firing order (research §7.1): **dedup → rate-limit/cap → file**. The Gate
16
+ * check sits *outside*: a binding installs capture only after boot returned
17
+ * `canSubmit`, so an auto-error is never filed where a manual report would be
18
+ * refused. The scrub choke point sits *inside* `submit`, which each binding routes
19
+ * through `runBeforeSend` exactly as a manual report does.
20
+ *
21
+ * Nothing here throws: `capture` and `flush` swallow every failure, because an
22
+ * error raised while capturing an error must never surface in the host app.
23
+ */
24
+ import type { Breadcrumb } from "./breadcrumb";
25
+ import type { BreadcrumbBuffer } from "./trace/buffer";
26
+ import type { CapturedFrame } from "./wire";
27
+ /** Burst limiter capacity — how many auto-reports may fire back-to-back (§E). */
28
+ export declare const DEFAULT_BURST_CAPACITY = 5;
29
+ /** Burst limiter refill — one token returns every this-many ms (§E). */
30
+ export declare const DEFAULT_BURST_REFILL_MS = 2000;
31
+ /** Per-session ceiling on distinct auto-Feedback; beyond it, only a dropped-count (§E). */
32
+ export declare const DEFAULT_MAX_DISTINCT_AUTO = 20;
33
+ /** How many preceding entries an auto-error's causal pointer names (spec #122 §F). */
34
+ export declare const CAUSED_BY_LIMIT = 5;
35
+ /** Coerce any value to a string without throwing (a hostile `toString` can throw). */
36
+ export declare function asString(value: unknown): string;
37
+ /**
38
+ * The ids of up to the last {@link CAUSED_BY_LIMIT} entries preceding the throw, in
39
+ * time order — the causal pointer an auto-captured error carries (spec #122 §F), so a
40
+ * machine-filed crash names its lead-up. Entries without an id are skipped.
41
+ */
42
+ export declare function precedingIds(entries: readonly Breadcrumb[] | undefined): string[];
43
+ /** Configuration for {@link TokenBucket}. */
44
+ export interface TokenBucketOptions {
45
+ readonly capacity: number;
46
+ readonly refillIntervalMs: number;
47
+ /** Clock source, injectable for tests. Defaults to `Date.now`. */
48
+ readonly now?: () => number;
49
+ }
50
+ /**
51
+ * A token bucket: starts full at `capacity`, refills one token every
52
+ * `refillIntervalMs`, and refuses (`take() === false`) when empty. So a fast error
53
+ * loop that dodges dedup with distinct fingerprints still can't machine-gun ingest.
54
+ * The clock is injectable so the window is unit-tested deterministically.
55
+ */
56
+ export declare class TokenBucket {
57
+ private readonly options;
58
+ private tokens;
59
+ private last;
60
+ private readonly now;
61
+ constructor(options: TokenBucketOptions);
62
+ /** Consume a token if one is available (refilling first), else refuse. */
63
+ take(): boolean;
64
+ }
65
+ /** The distilled shape a fingerprint and a report are built from. */
66
+ export interface ExtractedError {
67
+ readonly type: string;
68
+ readonly value: string;
69
+ readonly stack?: string;
70
+ /** The original throwable (Error, rejection reason, thrown string) for the trace crumb. */
71
+ readonly original: unknown;
72
+ }
73
+ /** One report the binding is asked to assemble, scrub, and ship. */
74
+ export interface AutoReport {
75
+ /** The per-session fingerprint — the report's `errorSignature`. */
76
+ readonly fingerprint: string;
77
+ /** The occurrence count this send claims. */
78
+ readonly occurrences: number;
79
+ /**
80
+ * A **rich first report** (`true`) versus a light count-update flush (`false`).
81
+ * The trace, the structured frames, and (on the web) the buffered replay window
82
+ * ride only with the rich send; a flush is just the number.
83
+ */
84
+ readonly rich: boolean;
85
+ /**
86
+ * The structured stack frames parsed once from the fingerprint's first firing
87
+ * (#117, ADR-0024). Empty when the stack yielded none.
88
+ */
89
+ readonly frames: readonly CapturedFrame[];
90
+ }
91
+ /** Configuration for {@link createAutoCapture}, parameterised over the runtime's raw event. */
92
+ export interface AutoCaptureCoreConfig<Raw> {
93
+ /**
94
+ * Distil the runtime's raw error event into a fingerprint-able shape, or `null`
95
+ * when it is not an uncaught exception at all (a resource-load `error` event).
96
+ */
97
+ readonly extract: (raw: Raw) => ExtractedError | null;
98
+ /**
99
+ * Assemble, scrub, and ship one report. Resolve `true` when it was accepted (or
100
+ * intentionally dropped at the scrub choke point — either way there is nothing to
101
+ * retry), `false` when it was held, refused, or unreachable, which rolls the
102
+ * entry's `sentCount` back so a later flush retries the count update.
103
+ */
104
+ readonly submit: (report: AutoReport) => Promise<boolean>;
105
+ /** The shared trace buffer; the failing error is added to it before filing. */
106
+ readonly buffer?: BreadcrumbBuffer | null;
107
+ /** Burst limiter capacity. Defaults to {@link DEFAULT_BURST_CAPACITY}. */
108
+ readonly burstCapacity?: number;
109
+ /** Burst limiter refill interval (ms). Defaults to {@link DEFAULT_BURST_REFILL_MS}. */
110
+ readonly burstRefillMs?: number;
111
+ /** Per-session distinct-Feedback cap. Defaults to {@link DEFAULT_MAX_DISTINCT_AUTO}. */
112
+ readonly maxDistinct?: number;
113
+ /** Clock source, injectable for tests. Defaults to `Date.now`. */
114
+ readonly now?: () => number;
115
+ }
116
+ /** The running state machine a binding drives from its runtime's error seam. */
117
+ export interface AutoCaptureCore<Raw> {
118
+ /** Handle one raw error event: extract → dedup → rate-limit/cap → file. Never throws. */
119
+ capture(raw: Raw): void;
120
+ /** Flush the final occurrence count of every fingerprint that grew since its send. */
121
+ flush(): void;
122
+ /**
123
+ * Distinct auto-Feedback dropped by the burst limiter or the per-session cap —
124
+ * the "keep only a local dropped-count" fallback beyond the guardrails (spec §E).
125
+ */
126
+ droppedCount(): number;
127
+ }
128
+ /** Create the shared auto-capture state machine. See the module doc. */
129
+ export declare function createAutoCapture<Raw>(config: AutoCaptureCoreConfig<Raw>): AutoCaptureCore<Raw>;
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+ /**
3
+ * The **automatic error-capture state machine** (spec 0003 §E, ADR-0011; research
4
+ * `sentry-error-capture-findings.md` §7.1–7.3) — the shared, runtime-agnostic half
5
+ * of "errors report themselves, no prompt" (ADR-0028).
6
+ *
7
+ * Every SDK's `installErrorCapture` is now a thin binding over this: it supplies
8
+ * the runtime's error seam (window listeners, React Native's `ErrorUtils`), an
9
+ * {@link AutoCaptureCoreConfig.extract} that distils that runtime's raw event into
10
+ * an {@link ExtractedError}, and an {@link AutoCaptureCoreConfig.submit} that ships
11
+ * one assembled report. Everything between — the per-session fingerprint dedup, the
12
+ * token-bucket burst limiter, the distinct-error session cap, the causal error
13
+ * crumb, the optimistic `sentCount` with rollback on a failed send, and the
14
+ * occurrence flush — lives here, once.
15
+ *
16
+ * Per-firing order (research §7.1): **dedup → rate-limit/cap → file**. The Gate
17
+ * check sits *outside*: a binding installs capture only after boot returned
18
+ * `canSubmit`, so an auto-error is never filed where a manual report would be
19
+ * refused. The scrub choke point sits *inside* `submit`, which each binding routes
20
+ * through `runBeforeSend` exactly as a manual report does.
21
+ *
22
+ * Nothing here throws: `capture` and `flush` swallow every failure, because an
23
+ * error raised while capturing an error must never surface in the host app.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.TokenBucket = exports.CAUSED_BY_LIMIT = exports.DEFAULT_MAX_DISTINCT_AUTO = exports.DEFAULT_BURST_REFILL_MS = exports.DEFAULT_BURST_CAPACITY = void 0;
27
+ exports.asString = asString;
28
+ exports.precedingIds = precedingIds;
29
+ exports.createAutoCapture = createAutoCapture;
30
+ const fingerprint_1 = require("./fingerprint");
31
+ const stack_1 = require("./stack");
32
+ const crumbs_1 = require("./trace/crumbs");
33
+ /** Burst limiter capacity — how many auto-reports may fire back-to-back (§E). */
34
+ exports.DEFAULT_BURST_CAPACITY = 5;
35
+ /** Burst limiter refill — one token returns every this-many ms (§E). */
36
+ exports.DEFAULT_BURST_REFILL_MS = 2_000;
37
+ /** Per-session ceiling on distinct auto-Feedback; beyond it, only a dropped-count (§E). */
38
+ exports.DEFAULT_MAX_DISTINCT_AUTO = 20;
39
+ /** How many preceding entries an auto-error's causal pointer names (spec #122 §F). */
40
+ exports.CAUSED_BY_LIMIT = 5;
41
+ /** Coerce any value to a string without throwing (a hostile `toString` can throw). */
42
+ function asString(value) {
43
+ if (typeof value === "string")
44
+ return value;
45
+ if (value == null)
46
+ return "";
47
+ try {
48
+ return String(value);
49
+ }
50
+ catch {
51
+ return "";
52
+ }
53
+ }
54
+ /**
55
+ * The ids of up to the last {@link CAUSED_BY_LIMIT} entries preceding the throw, in
56
+ * time order — the causal pointer an auto-captured error carries (spec #122 §F), so a
57
+ * machine-filed crash names its lead-up. Entries without an id are skipped.
58
+ */
59
+ function precedingIds(entries) {
60
+ if (!entries || entries.length === 0)
61
+ return [];
62
+ const ids = [];
63
+ for (let i = entries.length - 1; i >= 0 && ids.length < exports.CAUSED_BY_LIMIT; i -= 1) {
64
+ const id = entries[i]?.id;
65
+ if (typeof id === "string" && id.length > 0)
66
+ ids.unshift(id);
67
+ }
68
+ return ids;
69
+ }
70
+ /**
71
+ * A token bucket: starts full at `capacity`, refills one token every
72
+ * `refillIntervalMs`, and refuses (`take() === false`) when empty. So a fast error
73
+ * loop that dodges dedup with distinct fingerprints still can't machine-gun ingest.
74
+ * The clock is injectable so the window is unit-tested deterministically.
75
+ */
76
+ class TokenBucket {
77
+ options;
78
+ tokens;
79
+ last;
80
+ now;
81
+ constructor(options) {
82
+ this.options = options;
83
+ this.now = options.now ?? Date.now;
84
+ this.tokens = Math.max(0, options.capacity);
85
+ this.last = this.now();
86
+ }
87
+ /** Consume a token if one is available (refilling first), else refuse. */
88
+ take() {
89
+ const now = this.now();
90
+ const { capacity, refillIntervalMs } = this.options;
91
+ if (refillIntervalMs > 0 && now > this.last) {
92
+ const refill = Math.floor((now - this.last) / refillIntervalMs);
93
+ if (refill > 0) {
94
+ this.tokens = Math.min(capacity, this.tokens + refill);
95
+ this.last += refill * refillIntervalMs;
96
+ }
97
+ }
98
+ if (this.tokens >= 1) {
99
+ this.tokens -= 1;
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+ }
105
+ exports.TokenBucket = TokenBucket;
106
+ /** Create the shared auto-capture state machine. See the module doc. */
107
+ function createAutoCapture(config) {
108
+ const now = config.now ?? Date.now;
109
+ const maxDistinct = config.maxDistinct ?? exports.DEFAULT_MAX_DISTINCT_AUTO;
110
+ const bucket = new TokenBucket({
111
+ capacity: config.burstCapacity ?? exports.DEFAULT_BURST_CAPACITY,
112
+ refillIntervalMs: config.burstRefillMs ?? exports.DEFAULT_BURST_REFILL_MS,
113
+ now,
114
+ });
115
+ const seen = new Map();
116
+ let dropped = 0;
117
+ /**
118
+ * Ship one report for a fingerprint at a given count. Sets the entry's `sentCount`
119
+ * optimistically so a concurrent flush never double-sends, and rolls it back on a
120
+ * failed send so a later flush can retry the count update.
121
+ */
122
+ async function file(fingerprint, occurrences, rich) {
123
+ const entry = seen.get(fingerprint);
124
+ if (!entry)
125
+ return;
126
+ const previouslySent = entry.sentCount;
127
+ entry.sentCount = occurrences;
128
+ try {
129
+ const accepted = await config.submit({
130
+ fingerprint,
131
+ occurrences,
132
+ rich,
133
+ frames: entry.frames,
134
+ });
135
+ if (!accepted && entry.sentCount === occurrences) {
136
+ entry.sentCount = previouslySent;
137
+ }
138
+ }
139
+ catch {
140
+ if (entry.sentCount === occurrences)
141
+ entry.sentCount = previouslySent;
142
+ // The error path must never throw into the host app.
143
+ }
144
+ }
145
+ /** Handle one distilled error: dedup → burst limiter → session cap → file. */
146
+ function handle(extracted) {
147
+ const fingerprint = (0, fingerprint_1.computeFingerprint)(extracted.type, extracted.value, extracted.stack);
148
+ const existing = seen.get(fingerprint);
149
+ if (existing) {
150
+ // Dedup: one Feedback per fingerprint per session; count locally, flush later.
151
+ existing.count += 1;
152
+ existing.lastAt = now();
153
+ return;
154
+ }
155
+ if (!bucket.take()) {
156
+ dropped += 1; // burst limiter
157
+ return;
158
+ }
159
+ if (seen.size >= maxDistinct) {
160
+ dropped += 1; // per-session cap — keep only the local dropped-count
161
+ return;
162
+ }
163
+ const at = now();
164
+ seen.set(fingerprint, {
165
+ count: 1,
166
+ firstAt: at,
167
+ lastAt: at,
168
+ sentCount: 0,
169
+ // Parsed once per fingerprint, from the first firing's stack (#117) — the
170
+ // wire shape symbolication consumes, scrubbed like every captured URL.
171
+ frames: (0, stack_1.extractStructuredFrames)(extracted.stack),
172
+ });
173
+ // The failing error joins the trace so the lead-up and the failure both show,
174
+ // carrying a causal pointer to the ids of the entries that preceded the throw.
175
+ try {
176
+ const causedBy = precedingIds(config.buffer?.snapshot());
177
+ config.buffer?.add((0, crumbs_1.errorCrumb)(extracted.original, at, causedBy));
178
+ }
179
+ catch {
180
+ /* capture must never throw into the host app */
181
+ }
182
+ void file(fingerprint, 1, true);
183
+ }
184
+ return {
185
+ capture(raw) {
186
+ try {
187
+ const extracted = config.extract(raw);
188
+ if (extracted)
189
+ handle(extracted);
190
+ }
191
+ catch {
192
+ /* never throw into the host app */
193
+ }
194
+ },
195
+ flush() {
196
+ try {
197
+ for (const [fingerprint, entry] of seen) {
198
+ if (entry.count > entry.sentCount) {
199
+ void file(fingerprint, entry.count, false);
200
+ }
201
+ }
202
+ }
203
+ catch {
204
+ /* best-effort on unload / backgrounding */
205
+ }
206
+ },
207
+ droppedCount: () => dropped,
208
+ };
209
+ }
210
+ //# sourceMappingURL=auto-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-capture.js","sourceRoot":"","sources":["../src/auto-capture.ts"],"names":[],"mappings":"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"}