@fixback/expo 0.1.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 +72 -8
- package/dist/FeedbackModal.d.ts +10 -2
- package/dist/FeedbackModal.js +84 -36
- package/dist/FeedbackModal.js.map +1 -0
- package/dist/FixbackProvider.d.ts +12 -0
- package/dist/FixbackProvider.js +46 -23
- package/dist/FixbackProvider.js.map +1 -0
- package/dist/adapters.js +73 -23
- package/dist/adapters.js.map +1 -0
- package/dist/breadcrumbs.d.ts +21 -222
- package/dist/breadcrumbs.js +45 -605
- package/dist/breadcrumbs.js.map +1 -0
- package/dist/client.d.ts +65 -67
- package/dist/client.js +288 -151
- package/dist/client.js.map +1 -0
- package/dist/connect.d.ts +60 -0
- package/dist/connect.js +63 -0
- package/dist/connect.js.map +1 -0
- package/dist/error-capture.d.ts +29 -79
- package/dist/error-capture.js +77 -295
- package/dist/error-capture.js.map +1 -0
- package/dist/http.d.ts +10 -25
- package/dist/http.js +15 -12
- package/dist/http.js.map +1 -0
- package/dist/identity.d.ts +11 -11
- package/dist/identity.js +23 -28
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +22 -5
- package/dist/index.js.map +1 -0
- package/dist/origin.d.ts +21 -0
- package/dist/origin.js +45 -0
- package/dist/origin.js.map +1 -0
- package/dist/package.json +3 -0
- package/dist/report.d.ts +28 -63
- package/dist/report.js +33 -25
- package/dist/report.js.map +1 -0
- package/dist/reporter-session-store.d.ts +27 -0
- package/dist/reporter-session-store.js +53 -0
- package/dist/reporter-session-store.js.map +1 -0
- package/dist/shake.d.ts +26 -0
- package/dist/shake.js +32 -7
- package/dist/shake.js.map +1 -0
- package/dist/submit.d.ts +24 -15
- package/dist/submit.js +30 -47
- package/dist/submit.js.map +1 -0
- package/dist/tokens.js +5 -1
- package/dist/tokens.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +5 -1
- package/dist/version.js.map +1 -0
- package/package.json +14 -7
- package/dist/auto-report-backoff.d.ts +0 -42
- package/dist/auto-report-backoff.js +0 -67
- package/dist/boot.d.ts +0 -65
- package/dist/boot.js +0 -60
- package/dist/scrub.d.ts +0 -55
- package/dist/scrub.js +0 -184
package/dist/connect.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The Expo **Connect** flow (ADR-0032) — the React Native half of the round trip whose
|
|
4
|
+
* pure pieces (URL building, the code exchange, the storage-key format) live in
|
|
5
|
+
* `@fixback/sdk-core`.
|
|
6
|
+
*
|
|
7
|
+
* Where the browser opens the platform's connect page in a popup, Expo opens it in an
|
|
8
|
+
* **in-app auth session** (`WebBrowser.openAuthSessionAsync`, injected here as
|
|
9
|
+
* {@link OpenAuthSession}) — the standard mobile pattern: a secure system browser that
|
|
10
|
+
* shares no cookies with the app, signs the person in, and hands control back when the
|
|
11
|
+
* page redirects to the app's own `returnUrl` (a custom scheme or a universal link the
|
|
12
|
+
* host declares once in `init`). The connect page returns the one-time code on that URL's
|
|
13
|
+
* `?fixback=` query parameter, exactly as the web's redirect fallback does; the SDK reads
|
|
14
|
+
* it here and exchanges it for a Reporter session.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.parseReturnCode = parseReturnCode;
|
|
18
|
+
exports.startExpoConnect = startExpoConnect;
|
|
19
|
+
const sdk_core_1 = require("@fixback/sdk-core");
|
|
20
|
+
/**
|
|
21
|
+
* Read the Connect return code off a returned URL's `?fixback=` parameter, or `null`.
|
|
22
|
+
* Parsed by hand rather than via `URL`/`URLSearchParams` so it works on every React
|
|
23
|
+
* Native runtime (Hermes' URL support is partial), the same DOM-free posture as
|
|
24
|
+
* `@fixback/sdk-core`. Handles the parameter appearing first (`?fixback=`) or later
|
|
25
|
+
* (`&fixback=`), and stops at the next `&` or `#`.
|
|
26
|
+
*/
|
|
27
|
+
function parseReturnCode(url) {
|
|
28
|
+
const match = url.match(new RegExp(`[?&]${sdk_core_1.CONNECT_RETURN_PARAM}=([^&#]*)`));
|
|
29
|
+
if (!match || !match[1])
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const code = decodeURIComponent(match[1]);
|
|
33
|
+
return code.length > 0 ? code : null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Start Connect: open the connect page in an in-app auth session and resolve to the
|
|
41
|
+
* one-time code it returns via `returnUrl`, or `cancelled`. `returnUrl` is where the
|
|
42
|
+
* person is sent back — validated server-side against the Project's allowed origins, so
|
|
43
|
+
* a hostile app cannot redirect the code elsewhere. Never throws: any failure resolves
|
|
44
|
+
* to `cancelled`, so a Connect problem never surfaces on the host app.
|
|
45
|
+
*/
|
|
46
|
+
async function startExpoConnect(openAuthSession, params) {
|
|
47
|
+
const url = (0, sdk_core_1.buildConnectUrl)(params.connectUrl, {
|
|
48
|
+
key: params.key,
|
|
49
|
+
returnUrl: params.returnUrl,
|
|
50
|
+
});
|
|
51
|
+
let result;
|
|
52
|
+
try {
|
|
53
|
+
result = await openAuthSession(url, params.returnUrl);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { kind: "cancelled" };
|
|
57
|
+
}
|
|
58
|
+
if (result.type !== "success" || !result.url)
|
|
59
|
+
return { kind: "cancelled" };
|
|
60
|
+
const code = parseReturnCode(result.url);
|
|
61
|
+
return code ? { kind: "code", code } : { kind: "cancelled" };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;AAwCH,0CAWC;AASD,4CAiBC;AA3ED,gDAA0E;AA+B1E;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,GAAW;IACzC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CACrB,IAAI,MAAM,CAAC,OAAO,+BAAoB,WAAW,CAAC,CACnD,CAAC;IACF,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1C,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,gBAAgB,CACpC,eAAgC,EAChC,MAA8D;IAE9D,MAAM,GAAG,GAAG,IAAA,0BAAe,EAAC,MAAM,CAAC,UAAU,EAAE;QAC7C,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CAAC;IACH,IAAI,MAAyB,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC/B,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,GAAG;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC3E,MAAM,IAAI,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzC,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;AAC/D,CAAC"}
|
package/dist/error-capture.d.ts
CHANGED
|
@@ -1,86 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Automatic error capture on React Native —
|
|
3
|
-
* triggers
|
|
2
|
+
* Automatic error capture on React Native — the **runtime binding** over the shared
|
|
3
|
+
* state machine (ADR-0011's model with mobile triggers, spec 0004 §E).
|
|
4
4
|
*
|
|
5
|
-
* Uncaught JS errors arrive through `ErrorUtils.setGlobalHandler` — React
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* Uncaught JS errors arrive through `ErrorUtils.setGlobalHandler` — React Native's
|
|
6
|
+
* one global error seam — and become `source: error`, `Kind = bug` Feedback for the
|
|
7
|
+
* current session's Reporter. The **previous handler is always chained afterwards**,
|
|
8
|
+
* so dev redboxes and other crash reporters keep working. Occurrence-count flushes
|
|
9
|
+
* ride `AppState` → `background`/`inactive` (the `pagehide` equivalent). Unhandled
|
|
10
|
+
* promise rejections have no stable public Hermes hook and are not captured.
|
|
11
|
+
*
|
|
12
|
+
* Everything between the seam and the wire — the fingerprint dedup, the burst
|
|
13
|
+
* limiter, the session cap, the causal error crumb, the `sentCount` rollback, and
|
|
14
|
+
* the flush — is `@fixback/sdk-core`'s `createAutoCapture` (ADR-0028), identical to
|
|
15
|
+
* the browser SDK's. This module supplies only what is React Native's: how to find
|
|
16
|
+
* and chain `ErrorUtils`, how to distil whatever it hands us, how to assemble the
|
|
17
|
+
* mobile report, and when to flush.
|
|
13
18
|
*
|
|
14
19
|
* Per-firing order: **`canSubmit`/Gate → dedup → rate-limit/cap → `beforeSend`
|
|
15
20
|
* scrub → enqueue Feedback**. The Gate is honoured by construction: the client
|
|
16
|
-
* installs this only when boot returned `canSubmit`.
|
|
17
|
-
* ride `AppState` → `background`/`inactive` (the `pagehide` equivalent).
|
|
18
|
-
* Unhandled promise rejections have no stable public Hermes hook and are not
|
|
19
|
-
* captured (ADR-0021 "Revisit when").
|
|
21
|
+
* installs this only when boot returned `canSubmit`.
|
|
20
22
|
*
|
|
21
23
|
* The whole module is defensive — a Fixback problem (or an error thrown while
|
|
22
24
|
* capturing an error) never surfaces in the host app.
|
|
23
25
|
*/
|
|
24
|
-
import type
|
|
25
|
-
import { type BreadcrumbBuffer, type Teardown } from "./breadcrumbs";
|
|
26
|
+
import { type BeforeSend, type BreadcrumbBuffer, DEFAULT_BURST_CAPACITY, DEFAULT_BURST_REFILL_MS, DEFAULT_MAX_DISTINCT_AUTO, type ExtractedError, type IdentityInputs, type Teardown } from "@fixback/sdk-core";
|
|
26
27
|
import { type EnvironmentInputs } from "./report";
|
|
27
|
-
import { type BeforeSend } from "./scrub";
|
|
28
28
|
import { type SubmitDeps, type SubmitInput, type SubmitResult } from "./submit";
|
|
29
29
|
export type { Teardown };
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
export declare const DEFAULT_BURST_REFILL_MS = 2000;
|
|
34
|
-
/** Per-session ceiling on distinct auto-Feedback; beyond it, only a dropped-count. */
|
|
35
|
-
export declare const DEFAULT_MAX_DISTINCT_AUTO = 20;
|
|
36
|
-
/**
|
|
37
|
-
* Collapse the volatile parts of an error message so a changing string doesn't
|
|
38
|
-
* split one bug: UUIDs, URLs, `0x…` and long hex runs, and long digit runs are
|
|
39
|
-
* replaced with stable placeholders. Short numbers and stable text are kept so
|
|
40
|
-
* genuinely distinct bugs stay distinct.
|
|
41
|
-
*/
|
|
42
|
-
export declare function normalize(value: string): string;
|
|
43
|
-
/**
|
|
44
|
-
* A dependency-free FNV-1a hash rendered in base-36. It only has to be stable
|
|
45
|
-
* and well-distributed within one session (the client key is a flood guard; the
|
|
46
|
-
* server does canonical cross-session clustering).
|
|
47
|
-
*/
|
|
48
|
-
export declare function hashString(input: string): string;
|
|
49
|
-
/**
|
|
50
|
-
* Extract a compact, stable signature of the top frames of a stack: up to
|
|
51
|
-
* {@link FINGERPRINT_FRAME_LIMIT} frames as `function@basename:line:col`,
|
|
52
|
-
* origin and cache-busting query stripped. Returns `""` when there is no
|
|
53
|
-
* usable stack (message-only fallback).
|
|
54
|
-
*/
|
|
55
|
-
export declare function extractTopFrames(stack: string | undefined, limit?: number): string;
|
|
56
|
-
/**
|
|
57
|
-
* The per-session fingerprint:
|
|
58
|
-
* `hash(errorType + "|" + normalize(value) + "|" + topFrames)`. Stack frames
|
|
59
|
-
* dominate when present; otherwise it falls back to type + normalized value.
|
|
60
|
-
*/
|
|
61
|
-
export declare function computeFingerprint(type: string, value: string, stack?: string): string;
|
|
62
|
-
/** Configuration for {@link TokenBucket}. */
|
|
63
|
-
export interface TokenBucketOptions {
|
|
64
|
-
readonly capacity: number;
|
|
65
|
-
readonly refillIntervalMs: number;
|
|
66
|
-
/** Clock source, injectable for tests. Defaults to `Date.now`. */
|
|
67
|
-
readonly now?: () => number;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* A token bucket: starts full at `capacity`, refills one token every
|
|
71
|
-
* `refillIntervalMs`, and refuses (`take() === false`) when empty. So a fast
|
|
72
|
-
* error loop that dodges dedup with distinct fingerprints still can't
|
|
73
|
-
* machine-gun ingest.
|
|
74
|
-
*/
|
|
75
|
-
export declare class TokenBucket {
|
|
76
|
-
private readonly options;
|
|
77
|
-
private tokens;
|
|
78
|
-
private last;
|
|
79
|
-
private readonly now;
|
|
80
|
-
constructor(options: TokenBucketOptions);
|
|
81
|
-
/** Consume a token if one is available (refilling first), else refuse. */
|
|
82
|
-
take(): boolean;
|
|
83
|
-
}
|
|
30
|
+
export { DEFAULT_BURST_CAPACITY, DEFAULT_BURST_REFILL_MS, DEFAULT_MAX_DISTINCT_AUTO };
|
|
31
|
+
/** Distil whatever `ErrorUtils` handed us into a fingerprint-able shape. */
|
|
32
|
+
export declare function extractGlobalError(error: unknown): ExtractedError;
|
|
84
33
|
/** React Native's global error handler signature. */
|
|
85
34
|
export type GlobalErrorHandler = (error: unknown, isFatal?: boolean) => void;
|
|
86
35
|
/** The `ErrorUtils` surface the capture uses — injectable for tests. */
|
|
@@ -101,7 +50,7 @@ export interface AppStateLike {
|
|
|
101
50
|
/**
|
|
102
51
|
* Injectable collaborators, defaulted to the real implementations.
|
|
103
52
|
*
|
|
104
|
-
* Deliberately screenshot-free: a `source:
|
|
53
|
+
* Deliberately screenshot-free: a `source: error` report on mobile never
|
|
105
54
|
* attaches a screenshot. The composer's compensating privacy controls —
|
|
106
55
|
* preview and removal by the Reporter — cannot exist on a machine-filed
|
|
107
56
|
* report, and there is no client-side masking on mobile, so an auto
|
|
@@ -111,11 +60,6 @@ export interface AppStateLike {
|
|
|
111
60
|
export interface AutoCaptureDeps {
|
|
112
61
|
readonly submitReport: (apiUrl: string, input: SubmitInput, deps?: SubmitDeps) => Promise<SubmitResult>;
|
|
113
62
|
}
|
|
114
|
-
/** The Reporter's self-provided display name / email — display only. */
|
|
115
|
-
export interface ReporterDisplay {
|
|
116
|
-
readonly name?: string;
|
|
117
|
-
readonly email?: string;
|
|
118
|
-
}
|
|
119
63
|
/** Configuration for {@link installErrorCapture}. */
|
|
120
64
|
export interface AutoCaptureConfig {
|
|
121
65
|
readonly apiUrl: string;
|
|
@@ -123,9 +67,15 @@ export interface AutoCaptureConfig {
|
|
|
123
67
|
/** The configured origin, forwarded on every submission (spec 0004 §A). */
|
|
124
68
|
readonly origin: string;
|
|
125
69
|
readonly identity?: IdentityInputs;
|
|
126
|
-
/** Display-only name/email carried on auto-captured Feedback too. */
|
|
127
|
-
readonly display?: ReporterDisplay;
|
|
128
70
|
readonly sdkVersion?: string;
|
|
71
|
+
/**
|
|
72
|
+
* The host app's **Release** (#117, ADR-0024) — already validated by the client
|
|
73
|
+
* (`normaliseRelease`). Stamped into every auto report's environment so the
|
|
74
|
+
* server can symbolicate the captured frames against this build's sourcemaps.
|
|
75
|
+
*/
|
|
76
|
+
readonly release?: string;
|
|
77
|
+
/** The deploy environment (`production` / `staging` / …), stamped on every report. */
|
|
78
|
+
readonly deployEnvironment?: string;
|
|
129
79
|
/** The shared trace buffer; the failing error is added to it before filing. */
|
|
130
80
|
readonly buffer?: BreadcrumbBuffer | null;
|
|
131
81
|
/** Per-project client scrub hook, run at the `beforeSend` choke point. */
|
package/dist/error-capture.js
CHANGED
|
@@ -1,207 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
|
-
* Automatic error capture on React Native —
|
|
3
|
-
* triggers
|
|
3
|
+
* Automatic error capture on React Native — the **runtime binding** over the shared
|
|
4
|
+
* state machine (ADR-0011's model with mobile triggers, spec 0004 §E).
|
|
4
5
|
*
|
|
5
|
-
* Uncaught JS errors arrive through `ErrorUtils.setGlobalHandler` — React
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* Uncaught JS errors arrive through `ErrorUtils.setGlobalHandler` — React Native's
|
|
7
|
+
* one global error seam — and become `source: error`, `Kind = bug` Feedback for the
|
|
8
|
+
* current session's Reporter. The **previous handler is always chained afterwards**,
|
|
9
|
+
* so dev redboxes and other crash reporters keep working. Occurrence-count flushes
|
|
10
|
+
* ride `AppState` → `background`/`inactive` (the `pagehide` equivalent). Unhandled
|
|
11
|
+
* promise rejections have no stable public Hermes hook and are not captured.
|
|
12
|
+
*
|
|
13
|
+
* Everything between the seam and the wire — the fingerprint dedup, the burst
|
|
14
|
+
* limiter, the session cap, the causal error crumb, the `sentCount` rollback, and
|
|
15
|
+
* the flush — is `@fixback/sdk-core`'s `createAutoCapture` (ADR-0028), identical to
|
|
16
|
+
* the browser SDK's. This module supplies only what is React Native's: how to find
|
|
17
|
+
* and chain `ErrorUtils`, how to distil whatever it hands us, how to assemble the
|
|
18
|
+
* mobile report, and when to flush.
|
|
13
19
|
*
|
|
14
20
|
* Per-firing order: **`canSubmit`/Gate → dedup → rate-limit/cap → `beforeSend`
|
|
15
21
|
* scrub → enqueue Feedback**. The Gate is honoured by construction: the client
|
|
16
|
-
* installs this only when boot returned `canSubmit`.
|
|
17
|
-
* ride `AppState` → `background`/`inactive` (the `pagehide` equivalent).
|
|
18
|
-
* Unhandled promise rejections have no stable public Hermes hook and are not
|
|
19
|
-
* captured (ADR-0021 "Revisit when").
|
|
22
|
+
* installs this only when boot returned `canSubmit`.
|
|
20
23
|
*
|
|
21
24
|
* The whole module is defensive — a Fixback problem (or an error thrown while
|
|
22
25
|
* capturing an error) never surfaces in the host app.
|
|
23
26
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const FINGERPRINT_FRAME_LIMIT = 5;
|
|
37
|
-
/** How many preceding entries an auto-error's causal pointer names. */
|
|
38
|
-
const CAUSED_BY_LIMIT = 5;
|
|
39
|
-
/**
|
|
40
|
-
* The ids of up to the last {@link CAUSED_BY_LIMIT} entries preceding the
|
|
41
|
-
* throw, in time order — the causal pointer an auto-captured error carries so a
|
|
42
|
-
* machine-filed crash names its lead-up. Entries without an id are skipped.
|
|
43
|
-
*/
|
|
44
|
-
function precedingIds(entries) {
|
|
45
|
-
if (!entries || entries.length === 0)
|
|
46
|
-
return [];
|
|
47
|
-
const ids = [];
|
|
48
|
-
for (let i = entries.length - 1; i >= 0 && ids.length < CAUSED_BY_LIMIT; i -= 1) {
|
|
49
|
-
const id = entries[i]?.id;
|
|
50
|
-
if (typeof id === "string" && id.length > 0)
|
|
51
|
-
ids.unshift(id);
|
|
52
|
-
}
|
|
53
|
-
return ids;
|
|
54
|
-
}
|
|
55
|
-
// --- Fingerprint (per-session flood-guard key) --------------------------------
|
|
56
|
-
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
57
|
-
const URL_RE = /\bhttps?:\/\/[^\s"')]+/gi;
|
|
58
|
-
const HEX_0X_RE = /\b0x[0-9a-f]+\b/gi;
|
|
59
|
-
const HEX_RUN_RE = /\b[0-9a-f]{8,}\b/gi;
|
|
60
|
-
const DIGIT_RUN_RE = /\d{4,}/g;
|
|
61
|
-
/**
|
|
62
|
-
* Collapse the volatile parts of an error message so a changing string doesn't
|
|
63
|
-
* split one bug: UUIDs, URLs, `0x…` and long hex runs, and long digit runs are
|
|
64
|
-
* replaced with stable placeholders. Short numbers and stable text are kept so
|
|
65
|
-
* genuinely distinct bugs stay distinct.
|
|
66
|
-
*/
|
|
67
|
-
export function normalize(value) {
|
|
68
|
-
if (typeof value !== "string" || value.length === 0)
|
|
69
|
-
return "";
|
|
70
|
-
return value
|
|
71
|
-
.replace(UUID_RE, "<uuid>")
|
|
72
|
-
.replace(URL_RE, "<url>")
|
|
73
|
-
.replace(HEX_0X_RE, "<hex>")
|
|
74
|
-
.replace(HEX_RUN_RE, "<hex>")
|
|
75
|
-
.replace(DIGIT_RUN_RE, "<n>")
|
|
76
|
-
.trim();
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* A dependency-free FNV-1a hash rendered in base-36. It only has to be stable
|
|
80
|
-
* and well-distributed within one session (the client key is a flood guard; the
|
|
81
|
-
* server does canonical cross-session clustering).
|
|
82
|
-
*/
|
|
83
|
-
export function hashString(input) {
|
|
84
|
-
let h = 0x811c9dc5;
|
|
85
|
-
for (let i = 0; i < input.length; i++) {
|
|
86
|
-
h ^= input.charCodeAt(i);
|
|
87
|
-
h = Math.imul(h, 0x01000193);
|
|
88
|
-
}
|
|
89
|
-
return (h >>> 0).toString(36);
|
|
90
|
-
}
|
|
91
|
-
/** Reduce a frame location to `basename:line:col`, dropping origin and query. */
|
|
92
|
-
function compactLocation(location) {
|
|
93
|
-
const noQuery = location.replace(/\?[^:]*/, "");
|
|
94
|
-
const lastSlash = noQuery.lastIndexOf("/");
|
|
95
|
-
return lastSlash >= 0 ? noQuery.slice(lastSlash + 1) : noQuery;
|
|
96
|
-
}
|
|
97
|
-
/** Parse one stack line into a compact `function@basename:line:col` frame id. */
|
|
98
|
-
function parseFrame(line) {
|
|
99
|
-
// V8 / Hermes: "at fn (loc)" | "at loc"
|
|
100
|
-
const v8Named = line.match(/^at\s+(.+?)\s+\((.+)\)$/);
|
|
101
|
-
if (v8Named)
|
|
102
|
-
return `${v8Named[1] ?? ""}@${compactLocation(v8Named[2] ?? "")}`;
|
|
103
|
-
const v8Bare = line.match(/^at\s+(.+)$/);
|
|
104
|
-
if (v8Bare)
|
|
105
|
-
return `@${compactLocation(v8Bare[1] ?? "")}`;
|
|
106
|
-
// JSC: "fn@loc" | "@loc"
|
|
107
|
-
const at = line.indexOf("@");
|
|
108
|
-
if (at >= 0) {
|
|
109
|
-
const fn = line.slice(0, at);
|
|
110
|
-
return `${fn}@${compactLocation(line.slice(at + 1))}`;
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Extract a compact, stable signature of the top frames of a stack: up to
|
|
116
|
-
* {@link FINGERPRINT_FRAME_LIMIT} frames as `function@basename:line:col`,
|
|
117
|
-
* origin and cache-busting query stripped. Returns `""` when there is no
|
|
118
|
-
* usable stack (message-only fallback).
|
|
119
|
-
*/
|
|
120
|
-
export function extractTopFrames(stack, limit = FINGERPRINT_FRAME_LIMIT) {
|
|
121
|
-
if (typeof stack !== "string" || stack.length === 0)
|
|
122
|
-
return "";
|
|
123
|
-
const frames = [];
|
|
124
|
-
for (const raw of stack.split("\n")) {
|
|
125
|
-
const frame = parseFrame(raw.trim());
|
|
126
|
-
if (frame) {
|
|
127
|
-
frames.push(frame);
|
|
128
|
-
if (frames.length >= limit)
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return frames.join(" < ");
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* The per-session fingerprint:
|
|
136
|
-
* `hash(errorType + "|" + normalize(value) + "|" + topFrames)`. Stack frames
|
|
137
|
-
* dominate when present; otherwise it falls back to type + normalized value.
|
|
138
|
-
*/
|
|
139
|
-
export function computeFingerprint(type, value, stack) {
|
|
140
|
-
return hashString(`${type}|${normalize(value)}|${extractTopFrames(stack)}`);
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* A token bucket: starts full at `capacity`, refills one token every
|
|
144
|
-
* `refillIntervalMs`, and refuses (`take() === false`) when empty. So a fast
|
|
145
|
-
* error loop that dodges dedup with distinct fingerprints still can't
|
|
146
|
-
* machine-gun ingest.
|
|
147
|
-
*/
|
|
148
|
-
export class TokenBucket {
|
|
149
|
-
options;
|
|
150
|
-
tokens;
|
|
151
|
-
last;
|
|
152
|
-
now;
|
|
153
|
-
constructor(options) {
|
|
154
|
-
this.options = options;
|
|
155
|
-
this.now = options.now ?? Date.now;
|
|
156
|
-
this.tokens = Math.max(0, options.capacity);
|
|
157
|
-
this.last = this.now();
|
|
158
|
-
}
|
|
159
|
-
/** Consume a token if one is available (refilling first), else refuse. */
|
|
160
|
-
take() {
|
|
161
|
-
const now = this.now();
|
|
162
|
-
const { capacity, refillIntervalMs } = this.options;
|
|
163
|
-
if (refillIntervalMs > 0 && now > this.last) {
|
|
164
|
-
const refill = Math.floor((now - this.last) / refillIntervalMs);
|
|
165
|
-
if (refill > 0) {
|
|
166
|
-
this.tokens = Math.min(capacity, this.tokens + refill);
|
|
167
|
-
this.last += refill * refillIntervalMs;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
if (this.tokens >= 1) {
|
|
171
|
-
this.tokens -= 1;
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
function asString(value) {
|
|
178
|
-
if (typeof value === "string")
|
|
179
|
-
return value;
|
|
180
|
-
if (value == null)
|
|
181
|
-
return "";
|
|
182
|
-
try {
|
|
183
|
-
return String(value);
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
return "";
|
|
187
|
-
}
|
|
188
|
-
}
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.DEFAULT_MAX_DISTINCT_AUTO = exports.DEFAULT_BURST_REFILL_MS = exports.DEFAULT_BURST_CAPACITY = void 0;
|
|
29
|
+
exports.extractGlobalError = extractGlobalError;
|
|
30
|
+
exports.globalErrorUtils = globalErrorUtils;
|
|
31
|
+
exports.installErrorCapture = installErrorCapture;
|
|
32
|
+
const sdk_core_1 = require("@fixback/sdk-core");
|
|
33
|
+
Object.defineProperty(exports, "DEFAULT_BURST_CAPACITY", { enumerable: true, get: function () { return sdk_core_1.DEFAULT_BURST_CAPACITY; } });
|
|
34
|
+
Object.defineProperty(exports, "DEFAULT_BURST_REFILL_MS", { enumerable: true, get: function () { return sdk_core_1.DEFAULT_BURST_REFILL_MS; } });
|
|
35
|
+
Object.defineProperty(exports, "DEFAULT_MAX_DISTINCT_AUTO", { enumerable: true, get: function () { return sdk_core_1.DEFAULT_MAX_DISTINCT_AUTO; } });
|
|
36
|
+
const report_1 = require("./report");
|
|
37
|
+
const submit_1 = require("./submit");
|
|
38
|
+
const version_1 = require("./version");
|
|
189
39
|
/** Distil whatever `ErrorUtils` handed us into a fingerprint-able shape. */
|
|
190
40
|
function extractGlobalError(error) {
|
|
191
41
|
if (error && typeof error === "object") {
|
|
192
42
|
const err = error;
|
|
193
43
|
return {
|
|
194
|
-
type: asString(err.name) || "Error",
|
|
195
|
-
value: asString(err.message) || asString(error),
|
|
44
|
+
type: (0, sdk_core_1.asString)(err.name) || "Error",
|
|
45
|
+
value: (0, sdk_core_1.asString)(err.message) || (0, sdk_core_1.asString)(error),
|
|
196
46
|
stack: typeof err.stack === "string" ? err.stack : undefined,
|
|
197
47
|
original: error,
|
|
198
48
|
};
|
|
199
49
|
}
|
|
200
|
-
const value = asString(error);
|
|
50
|
+
const value = (0, sdk_core_1.asString)(error);
|
|
201
51
|
return { type: "Error", value, original: error ?? value };
|
|
202
52
|
}
|
|
203
53
|
/** React Native's global `ErrorUtils`, when present. */
|
|
204
|
-
|
|
54
|
+
function globalErrorUtils() {
|
|
205
55
|
const g = globalThis;
|
|
206
56
|
const utils = g.ErrorUtils;
|
|
207
57
|
return utils && typeof utils.setGlobalHandler === "function" ? utils : undefined;
|
|
@@ -215,151 +65,82 @@ const NOOP_HANDLE = { destroy: () => { }, droppedCount: () => 0 };
|
|
|
215
65
|
* Without a usable `ErrorUtils` (no `getGlobalHandler` to restore from) nothing
|
|
216
66
|
* is installed — the SDK never replaces a handler it could not put back.
|
|
217
67
|
*/
|
|
218
|
-
|
|
68
|
+
function installErrorCapture(config) {
|
|
219
69
|
const errorUtils = config.errorUtils === null ? undefined : config.errorUtils ?? globalErrorUtils();
|
|
220
70
|
if (!errorUtils ||
|
|
221
71
|
typeof errorUtils.setGlobalHandler !== "function" ||
|
|
222
72
|
typeof errorUtils.getGlobalHandler !== "function") {
|
|
223
73
|
return NOOP_HANDLE;
|
|
224
74
|
}
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
const maxDistinct = config.maxDistinct ?? DEFAULT_MAX_DISTINCT_AUTO;
|
|
228
|
-
const submitReportFn = config.deps?.submitReport ?? submitReport;
|
|
75
|
+
const sdkVersion = config.sdkVersion ?? version_1.EXPO_SDK_VERSION;
|
|
76
|
+
const submitReportFn = config.deps?.submitReport ?? submit_1.submitReport;
|
|
229
77
|
const readEnvironment = config.environment ?? (() => ({}));
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
refillIntervalMs: config.burstRefillMs ?? DEFAULT_BURST_REFILL_MS,
|
|
233
|
-
now,
|
|
234
|
-
});
|
|
235
|
-
const seen = new Map();
|
|
236
|
-
/** Distinct auto-Feedback dropped by the burst limiter or the session cap. */
|
|
237
|
-
let dropped = 0;
|
|
238
|
-
/** Build the `source: auto` content for a fingerprint at a given count. */
|
|
239
|
-
function buildContent(fp, occurrences, opts) {
|
|
78
|
+
/** Build the `source: error` content for one report. */
|
|
79
|
+
function buildContent(report) {
|
|
240
80
|
let environment;
|
|
241
81
|
try {
|
|
242
|
-
environment = collectEnvironment(readEnvironment(), sdkVersion);
|
|
82
|
+
environment = (0, report_1.collectEnvironment)(readEnvironment(), sdkVersion, config.release);
|
|
243
83
|
}
|
|
244
84
|
catch {
|
|
245
|
-
environment = collectEnvironment({}, sdkVersion);
|
|
85
|
+
environment = (0, report_1.collectEnvironment)({}, sdkVersion, config.release);
|
|
246
86
|
}
|
|
247
|
-
// No Kind rides along (ADR-0023): the server stamps a `source:
|
|
87
|
+
// No Kind rides along (ADR-0023): the server stamps a `source: error`
|
|
248
88
|
// Issue `bug` deterministically.
|
|
249
|
-
const base = assembleContent({
|
|
89
|
+
const base = (0, report_1.assembleContent)({
|
|
250
90
|
url: config.currentUrl?.(),
|
|
251
91
|
environment,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
reporterEmail: config.display?.email,
|
|
92
|
+
deployEnvironment: config.deployEnvironment,
|
|
93
|
+
trace: report.rich ? config.buffer?.snapshot() : undefined,
|
|
255
94
|
});
|
|
256
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
...base,
|
|
97
|
+
source: "error",
|
|
98
|
+
errorSignature: report.fingerprint,
|
|
99
|
+
occurrences: report.occurrences,
|
|
100
|
+
// The structured frames (#117) ride with the rich first report only — a
|
|
101
|
+
// flush is a light count update, like the trace.
|
|
102
|
+
...(report.rich && report.frames.length > 0 ? { errorFrames: report.frames } : {}),
|
|
103
|
+
};
|
|
257
104
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const draft = buildContent(fp, occurrences, opts);
|
|
273
|
-
const content = runBeforeSend(draft, {
|
|
105
|
+
const core = (0, sdk_core_1.createAutoCapture)({
|
|
106
|
+
extract: extractGlobalError,
|
|
107
|
+
buffer: config.buffer,
|
|
108
|
+
burstCapacity: config.burstCapacity,
|
|
109
|
+
burstRefillMs: config.burstRefillMs,
|
|
110
|
+
maxDistinct: config.maxDistinct,
|
|
111
|
+
now: config.now,
|
|
112
|
+
/**
|
|
113
|
+
* Assemble, scrub, and ship one auto-Feedback (never a screenshot — see
|
|
114
|
+
* {@link AutoCaptureDeps}). A `beforeSend` that returns `null` drops the report
|
|
115
|
+
* without transport, which counts as delivered: there is nothing to retry.
|
|
116
|
+
*/
|
|
117
|
+
submit: async (report) => {
|
|
118
|
+
const content = (0, sdk_core_1.runBeforeSend)(buildContent(report), {
|
|
274
119
|
hook: config.beforeSend,
|
|
275
120
|
scrub: config.scrub,
|
|
276
121
|
});
|
|
277
122
|
if (!content)
|
|
278
|
-
return; // dropped at the client scrub choke point — no transport.
|
|
279
|
-
const result = await submitReportFn(config.apiUrl, {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
content,
|
|
284
|
-
screenshot: null,
|
|
285
|
-
});
|
|
286
|
-
if (!result.ok && entry.sentCount === occurrences) {
|
|
287
|
-
// Held under backpressure, refused, or unreachable — let a flush retry.
|
|
288
|
-
entry.sentCount = previouslySent;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
if (entry.sentCount === occurrences)
|
|
293
|
-
entry.sentCount = previouslySent;
|
|
294
|
-
// The error path must never throw into the host app.
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
/** Handle one distilled error: dedup → burst limiter → session cap → file. */
|
|
298
|
-
function handle(extracted) {
|
|
299
|
-
const fp = computeFingerprint(extracted.type, extracted.value, extracted.stack);
|
|
300
|
-
const existing = seen.get(fp);
|
|
301
|
-
if (existing) {
|
|
302
|
-
// Dedup: one Feedback per fingerprint per session; count locally.
|
|
303
|
-
existing.count += 1;
|
|
304
|
-
existing.lastAt = now();
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (!bucket.take()) {
|
|
308
|
-
dropped += 1; // burst limiter
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
if (seen.size >= maxDistinct) {
|
|
312
|
-
dropped += 1; // per-session cap — keep only the local dropped-count
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
const at = now();
|
|
316
|
-
seen.set(fp, { count: 1, firstAt: at, lastAt: at, sentCount: 0 });
|
|
317
|
-
// The failing error joins the trace so the lead-up and the failure both
|
|
318
|
-
// show, carrying a causal pointer to the entries preceding the throw.
|
|
319
|
-
try {
|
|
320
|
-
const causedBy = precedingIds(config.buffer?.snapshot());
|
|
321
|
-
config.buffer?.add(errorCrumb(extracted.original, at, causedBy));
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
/* capture must never throw into the host app */
|
|
325
|
-
}
|
|
326
|
-
void file(fp, 1, { trace: true });
|
|
327
|
-
}
|
|
123
|
+
return true; // dropped at the client scrub choke point — no transport.
|
|
124
|
+
const result = await submitReportFn(config.apiUrl, { key: config.key, identity: config.identity, content, screenshot: null }, { origin: config.origin });
|
|
125
|
+
return result.ok;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
328
128
|
const previous = errorUtils.getGlobalHandler() ?? null;
|
|
329
129
|
let destroyed = false;
|
|
330
130
|
const wrapper = (error, isFatal) => {
|
|
331
|
-
if (!destroyed)
|
|
332
|
-
|
|
333
|
-
handle(extractGlobalError(error));
|
|
334
|
-
}
|
|
335
|
-
catch {
|
|
336
|
-
/* never throw into the host app */
|
|
337
|
-
}
|
|
338
|
-
}
|
|
131
|
+
if (!destroyed)
|
|
132
|
+
core.capture(error);
|
|
339
133
|
// Always chain the previous handler — redbox in dev, crash reporters, etc.
|
|
340
134
|
if (previous)
|
|
341
135
|
previous(error, isFatal);
|
|
342
136
|
};
|
|
343
137
|
errorUtils.setGlobalHandler(wrapper);
|
|
344
|
-
/** Flush the final occurrence count of every fingerprint that grew. */
|
|
345
|
-
const flush = () => {
|
|
346
|
-
try {
|
|
347
|
-
for (const [fp, entry] of seen) {
|
|
348
|
-
if (entry.count > entry.sentCount) {
|
|
349
|
-
void file(fp, entry.count, { trace: false });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
/* best-effort on backgrounding */
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
138
|
let appStateSub = null;
|
|
358
139
|
if (config.appState) {
|
|
359
140
|
try {
|
|
360
141
|
appStateSub = config.appState.addEventListener("change", (state) => {
|
|
361
142
|
if (state === "background" || state === "inactive")
|
|
362
|
-
flush();
|
|
143
|
+
core.flush();
|
|
363
144
|
});
|
|
364
145
|
}
|
|
365
146
|
catch {
|
|
@@ -390,6 +171,7 @@ export function installErrorCapture(config) {
|
|
|
390
171
|
}
|
|
391
172
|
appStateSub = null;
|
|
392
173
|
},
|
|
393
|
-
droppedCount:
|
|
174
|
+
droppedCount: core.droppedCount,
|
|
394
175
|
};
|
|
395
176
|
}
|
|
177
|
+
//# sourceMappingURL=error-capture.js.map
|