@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
package/README.md
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
# @fixback/sdk-core
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
so every surface computes it the
|
|
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` /
|
|
8
|
-
helpers, so the same logical error groups identically across
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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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"}
|