@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.
Files changed (58) hide show
  1. package/README.md +72 -8
  2. package/dist/FeedbackModal.d.ts +10 -2
  3. package/dist/FeedbackModal.js +84 -36
  4. package/dist/FeedbackModal.js.map +1 -0
  5. package/dist/FixbackProvider.d.ts +12 -0
  6. package/dist/FixbackProvider.js +46 -23
  7. package/dist/FixbackProvider.js.map +1 -0
  8. package/dist/adapters.js +73 -23
  9. package/dist/adapters.js.map +1 -0
  10. package/dist/breadcrumbs.d.ts +21 -222
  11. package/dist/breadcrumbs.js +45 -605
  12. package/dist/breadcrumbs.js.map +1 -0
  13. package/dist/client.d.ts +65 -67
  14. package/dist/client.js +288 -151
  15. package/dist/client.js.map +1 -0
  16. package/dist/connect.d.ts +60 -0
  17. package/dist/connect.js +63 -0
  18. package/dist/connect.js.map +1 -0
  19. package/dist/error-capture.d.ts +29 -79
  20. package/dist/error-capture.js +77 -295
  21. package/dist/error-capture.js.map +1 -0
  22. package/dist/http.d.ts +10 -25
  23. package/dist/http.js +15 -12
  24. package/dist/http.js.map +1 -0
  25. package/dist/identity.d.ts +11 -11
  26. package/dist/identity.js +23 -28
  27. package/dist/identity.js.map +1 -0
  28. package/dist/index.d.ts +6 -5
  29. package/dist/index.js +22 -5
  30. package/dist/index.js.map +1 -0
  31. package/dist/origin.d.ts +21 -0
  32. package/dist/origin.js +45 -0
  33. package/dist/origin.js.map +1 -0
  34. package/dist/package.json +3 -0
  35. package/dist/report.d.ts +28 -63
  36. package/dist/report.js +33 -25
  37. package/dist/report.js.map +1 -0
  38. package/dist/reporter-session-store.d.ts +27 -0
  39. package/dist/reporter-session-store.js +53 -0
  40. package/dist/reporter-session-store.js.map +1 -0
  41. package/dist/shake.d.ts +26 -0
  42. package/dist/shake.js +32 -7
  43. package/dist/shake.js.map +1 -0
  44. package/dist/submit.d.ts +24 -15
  45. package/dist/submit.js +30 -47
  46. package/dist/submit.js.map +1 -0
  47. package/dist/tokens.js +5 -1
  48. package/dist/tokens.js.map +1 -0
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +5 -1
  51. package/dist/version.js.map +1 -0
  52. package/package.json +14 -7
  53. package/dist/auto-report-backoff.d.ts +0 -42
  54. package/dist/auto-report-backoff.js +0 -67
  55. package/dist/boot.d.ts +0 -65
  56. package/dist/boot.js +0 -60
  57. package/dist/scrub.d.ts +0 -55
  58. package/dist/scrub.js +0 -184
@@ -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"}
@@ -1,86 +1,35 @@
1
1
  /**
2
- * Automatic error capture on React Native — ADR-0011's model with mobile
3
- * triggers (spec 0004 §E), ported from `packages/sdk/src/error-capture.ts`.
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
- * Native's one global error seam — and become `source: auto`, `Kind = bug`
7
- * Feedback for the current session's Reporter. The **previous handler is always
8
- * chained afterwards**, so dev redboxes and other crash reporters keep working.
9
- * Each firing is deduped by a per-session fingerprint, rate-limited by a
10
- * token-bucket burst limiter and a per-session cap, scrubbed through the same
11
- * `beforeSend` choke point as manual reports, and shipped through the same
12
- * transport (which honours ingest's `429` / `Retry-After` backpressure).
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`. Occurrence-count flushes
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 { IdentityInputs } from "./boot";
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
- /** Burst limiter capacity how many auto-reports may fire back-to-back. */
31
- export declare const DEFAULT_BURST_CAPACITY = 5;
32
- /** Burst limiter refill one token returns every this-many ms. */
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: auto` report on mobile never
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. */
@@ -1,207 +1,57 @@
1
+ "use strict";
1
2
  /**
2
- * Automatic error capture on React Native — ADR-0011's model with mobile
3
- * triggers (spec 0004 §E), ported from `packages/sdk/src/error-capture.ts`.
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
- * Native's one global error seam — and become `source: auto`, `Kind = bug`
7
- * Feedback for the current session's Reporter. The **previous handler is always
8
- * chained afterwards**, so dev redboxes and other crash reporters keep working.
9
- * Each firing is deduped by a per-session fingerprint, rate-limited by a
10
- * token-bucket burst limiter and a per-session cap, scrubbed through the same
11
- * `beforeSend` choke point as manual reports, and shipped through the same
12
- * transport (which honours ingest's `429` / `Retry-After` backpressure).
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`. Occurrence-count flushes
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
- import { errorCrumb, } from "./breadcrumbs";
25
- import { assembleContent, collectEnvironment, } from "./report";
26
- import { runBeforeSend } from "./scrub";
27
- import { submitReport, } from "./submit";
28
- import { EXPO_SDK_VERSION } from "./version";
29
- /** Burst limiter capacity — how many auto-reports may fire back-to-back. */
30
- export const DEFAULT_BURST_CAPACITY = 5;
31
- /** Burst limiter refill one token returns every this-many ms. */
32
- export const DEFAULT_BURST_REFILL_MS = 2_000;
33
- /** Per-session ceiling on distinct auto-Feedback; beyond it, only a dropped-count. */
34
- export const DEFAULT_MAX_DISTINCT_AUTO = 20;
35
- /** How many top stack frames feed the fingerprint. */
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
- export function globalErrorUtils() {
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
- export function installErrorCapture(config) {
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 now = config.now ?? Date.now;
226
- const sdkVersion = config.sdkVersion ?? EXPO_SDK_VERSION;
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
- const bucket = new TokenBucket({
231
- capacity: config.burstCapacity ?? DEFAULT_BURST_CAPACITY,
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: auto`
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
- trace: opts.trace ? config.buffer?.snapshot() : undefined,
253
- reporterName: config.display?.name,
254
- reporterEmail: config.display?.email,
92
+ deployEnvironment: config.deployEnvironment,
93
+ trace: report.rich ? config.buffer?.snapshot() : undefined,
255
94
  });
256
- return { ...base, source: "auto", errorSignature: fp, occurrences };
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
- * Assemble, scrub, and ship one auto-Feedback (never a screenshot — see
260
- * {@link AutoCaptureDeps}). Sets the entry's `sentCount` optimistically so a
261
- * concurrent flush never double-sends, and rolls it back on a failed send so
262
- * the flush can retry. A `beforeSend` that returns `null` drops the report
263
- * without transport.
264
- */
265
- async function file(fp, occurrences, opts) {
266
- const entry = seen.get(fp);
267
- if (!entry)
268
- return;
269
- const previouslySent = entry.sentCount;
270
- entry.sentCount = occurrences;
271
- try {
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
- key: config.key,
281
- origin: config.origin,
282
- identity: config.identity,
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
- try {
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: () => dropped,
174
+ droppedCount: core.droppedCount,
394
175
  };
395
176
  }
177
+ //# sourceMappingURL=error-capture.js.map