@fixback/expo 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +28 -78
  20. package/dist/error-capture.js +76 -294
  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 +21 -12
  45. package/dist/submit.js +24 -41
  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
@@ -1,608 +1,46 @@
1
+ "use strict";
1
2
  /**
2
- * The trace **breadcrumb ring buffer** and its React Native capture
3
- * instrumentation — a port of `packages/sdk/src/breadcrumbs.ts` kept
4
- * file-parallel on purpose (ADR-0021): the entry shapes, stream budgets, caps,
5
- * and eviction rules are the web SDK's exactly, so the server and dashboard
6
- * cannot tell a mobile trace from a web one.
3
+ * The Expo SDK's **React Native capture instrumentation** the runtime seams, and
4
+ * nothing else.
7
5
  *
8
- * Mobile mappings (spec 0004 §D): `console` wrapping is unchanged; network
9
- * capture patches `XMLHttpRequest` only React Native's `fetch` is a polyfill
10
- * over XHR, so wrapping both would double-record; navigation crumbs come from
11
- * the host app's `trackScreen` calls (there is no `history` to patch); the
12
- * masked `ui.*` streams have no mobile source yet and simply never occur.
6
+ * The trace buffer, the crumb builders, the caps and eviction rules, and the
7
+ * `console` wrapper are all shared with the browser SDK through
8
+ * `@fixback/sdk-core` (ADR-0028, replacing the file-parallel port ADR-0021
9
+ * described), so the server and dashboard cannot tell a mobile trace from a web one.
10
+ * What is left here is genuinely mobile: how to find React Native's globals, and
11
+ * the `XMLHttpRequest` patch.
13
12
  *
14
- * Everything private is kept out **at the source**: network crumbs carry method
15
- * + URL + status metadata only, **never** bodies; URLs are scrubbed as the
16
- * crumb is built. The `beforeSend` choke point (`scrub.ts`) is the final gate.
17
- * The SDK must never throw into the host app, so every instrumentation hook is
18
- * wrapped: a capture failure is swallowed and the original behaviour always runs.
19
- */
20
- import { scrubUrl } from "./scrub";
21
- /** Per-stream ring budgets the web SDK's numbers, unchanged. */
22
- export const DEFAULT_STREAM_BUDGETS = {
23
- network: 100,
24
- console: 80,
25
- breadcrumbs: 40,
26
- };
27
- /** The shared age cap: entries older than this are pruned from every stream. */
28
- export const DEFAULT_MAX_AGE_MS = 3 * 60 * 1000;
29
- /** Console levels captured by default: **all** of them. */
30
- export const DEFAULT_CONSOLE_LEVELS = [
31
- "log",
32
- "info",
33
- "warn",
34
- "error",
35
- "assert",
36
- "debug",
37
- ];
38
- /**
39
- * The console levels **pinned** against eviction: when the console stream
40
- * overflows, `warn`/`error`/`assert` are retained ahead of `log`/`info`/`debug`.
41
- */
42
- export const PINNED_CONSOLE_LEVELS = [
43
- "warn",
44
- "error",
45
- "assert",
46
- ];
47
- function isPinnedConsoleLevel(level) {
48
- return level !== undefined && PINNED_CONSOLE_LEVELS.includes(level);
49
- }
50
- /**
51
- * The console levels for which `source` (`file:line`) is captured — reading a
52
- * call site constructs a `new Error()` on every call, so only the levels that
53
- * matter for debugging pay that cost. Mirrors {@link PINNED_CONSOLE_LEVELS}.
54
- */
55
- export const SOURCE_CAPTURE_LEVELS = PINNED_CONSOLE_LEVELS;
56
- function capturesSource(level) {
57
- return SOURCE_CAPTURE_LEVELS.includes(level);
58
- }
59
- /** Which stream a crumb's category belongs to. */
60
- export function streamOf(category) {
61
- if (category === "console")
62
- return "console";
63
- if (category === "fetch" || category === "xhr" || category === "beacon") {
64
- return "network";
65
- }
66
- return "breadcrumbs";
67
- }
68
- function normalizeBudget(value, fallback) {
69
- if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
70
- return fallback;
71
- }
72
- return Math.floor(value);
73
- }
74
- /** The default high-res clock: `performance.now()` where available (React Native provides it), else `Date.now`. */
75
- function defaultMono() {
76
- const perf = globalThis.performance;
77
- return perf && typeof perf.now === "function" ? perf.now() : Date.now();
78
- }
79
- /** A per-buffer id factory: a short random salt + a base-36 sequence. */
80
- function createIdFactory() {
81
- const salt = Math.random().toString(36).slice(2, 8);
82
- let seq = 0;
83
- return () => `${salt}${(seq++).toString(36)}`;
84
- }
85
- /** The streams, in a fixed order for a deterministic merge. */
86
- const STREAMS = ["network", "console", "breadcrumbs"];
87
- /**
88
- * Trim the **console** stream to its budget with eviction priority:
89
- * `warn`/`error`/`assert` are pinned, so the oldest `log`/`info`/`debug`
90
- * entries are dropped first; a pinned entry is evicted only when dropping every
91
- * low-priority one still leaves the stream over budget (oldest pinned first).
92
- */
93
- function trimConsoleStream(held, budget) {
94
- const over = held.length - budget;
95
- if (over <= 0)
96
- return held;
97
- const drop = new Set();
98
- for (let i = 0; i < held.length && drop.size < over; i += 1) {
99
- if (!isPinnedConsoleLevel(held[i].crumb.level))
100
- drop.add(i);
101
- }
102
- for (let i = 0; i < held.length && drop.size < over; i += 1) {
103
- if (!drop.has(i))
104
- drop.add(i);
105
- }
106
- return held.filter((_, i) => !drop.has(i));
107
- }
108
- /**
109
- * Create the per-stream trace buffer: **three independent FIFO rings** —
110
- * `network`, `console`, `breadcrumbs` — each trimmed to its own budget so a
111
- * chatty stream never evicts another's lead-up, plus a shared age cap.
112
- * `snapshot` merges the three streams into one `mono`-ordered array for
113
- * transport — the wire keeps its single `trace` field.
114
- */
115
- export function createBreadcrumbBuffer(config = {}) {
116
- const budgets = {
117
- network: normalizeBudget(config.budgets?.network, DEFAULT_STREAM_BUDGETS.network),
118
- console: normalizeBudget(config.budgets?.console, DEFAULT_STREAM_BUDGETS.console),
119
- breadcrumbs: normalizeBudget(config.budgets?.breadcrumbs, DEFAULT_STREAM_BUDGETS.breadcrumbs),
120
- };
121
- const maxAgeMs = config.maxAgeMs === undefined ? DEFAULT_MAX_AGE_MS : config.maxAgeMs;
122
- const beforeBreadcrumb = config.beforeBreadcrumb;
123
- const now = config.now ?? Date.now;
124
- const mono = config.mono ?? defaultMono;
125
- const nextId = config.nextId ?? createIdFactory();
126
- const streams = {
127
- network: [],
128
- console: [],
129
- breadcrumbs: [],
130
- };
131
- let seq = 0;
132
- function pruneByAge() {
133
- if (typeof maxAgeMs !== "number" || maxAgeMs <= 0)
134
- return;
135
- const cutoff = now() - maxAgeMs;
136
- for (const stream of STREAMS) {
137
- const held = streams[stream];
138
- if (held.length > 0) {
139
- streams[stream] = held.filter((h) => h.crumb.timestamp >= cutoff);
140
- }
141
- }
142
- }
143
- return {
144
- add(crumb) {
145
- let entry = crumb;
146
- if (beforeBreadcrumb) {
147
- try {
148
- entry = beforeBreadcrumb(crumb);
149
- }
150
- catch {
151
- // A throwing filter must never break capture; keep the (already
152
- // masked) crumb rather than silently erasing the trace.
153
- entry = crumb;
154
- }
155
- }
156
- if (!entry)
157
- return;
158
- const stamped = {
159
- ...entry,
160
- id: entry.id ?? nextId(),
161
- timestamp: typeof entry.timestamp === "number" ? entry.timestamp : now(),
162
- mono: typeof entry.mono === "number" ? entry.mono : mono(),
163
- };
164
- const stream = streamOf(stamped.category);
165
- streams[stream].push({ crumb: stamped, seq: seq++ });
166
- pruneByAge();
167
- const held = streams[stream];
168
- const budget = budgets[stream];
169
- if (held.length > budget) {
170
- streams[stream] =
171
- stream === "console"
172
- ? trimConsoleStream(held, budget)
173
- : held.slice(-budget);
174
- }
175
- },
176
- snapshot() {
177
- pruneByAge();
178
- const merged = [];
179
- for (const stream of STREAMS)
180
- merged.push(...streams[stream]);
181
- merged.sort((a, b) => {
182
- const am = typeof a.crumb.mono === "number" ? a.crumb.mono : a.crumb.timestamp;
183
- const bm = typeof b.crumb.mono === "number" ? b.crumb.mono : b.crumb.timestamp;
184
- return am - bm || a.seq - b.seq;
185
- });
186
- return merged.map((h) => h.crumb);
187
- },
188
- clear() {
189
- streams.network = [];
190
- streams.console = [];
191
- streams.breadcrumbs = [];
192
- seq = 0;
193
- },
194
- };
195
- }
196
- // --- Pure crumb builders -----------------------------------------------------
197
- /** Longest crumb message kept; a huge log line is truncated, never dropped. */
198
- const MAX_MESSAGE_LENGTH = 300;
199
- /** Longest scrubbed URL kept on a crumb. */
200
- export const MAX_URL_LENGTH = 2048;
201
- /** Structured console argument caps, all applied at assembly. */
202
- export const MAX_ARG_DEPTH = 4;
203
- export const MAX_ARG_ITEMS = 100;
204
- export const MAX_ARG_STRING_LENGTH = 1024;
205
- export const MAX_CONSOLE_ARGS_BYTES = 4096;
206
- /** Longest Error `stack` kept on a structured `error` arg. */
207
- const MAX_ERROR_STACK_LENGTH = 2048;
208
- function capLength(value, max) {
209
- return value.length > max ? `${value.slice(0, max)}…` : value;
210
- }
211
- function stringifyArg(arg) {
212
- if (typeof arg === "string")
213
- return arg;
214
- if (arg instanceof Error)
215
- return `${arg.name}: ${arg.message}`;
216
- if (arg === null || arg === undefined)
217
- return String(arg);
218
- if (typeof arg === "number" || typeof arg === "boolean")
219
- return String(arg);
220
- try {
221
- return JSON.stringify(arg) ?? String(arg);
222
- }
223
- catch {
224
- return "[object]";
225
- }
226
- }
227
- function joinArgs(args) {
228
- const text = args.map(stringifyArg).join(" ");
229
- return text.length > MAX_MESSAGE_LENGTH
230
- ? `${text.slice(0, MAX_MESSAGE_LENGTH)}…`
231
- : text;
232
- }
233
- /** An Error rendered to a structured, size-capped `{ name, message, stack? }`. */
234
- function describeErrorValue(error) {
235
- const out = {
236
- name: error.name || "Error",
237
- message: capLength(String(error.message ?? ""), MAX_ARG_STRING_LENGTH),
238
- };
239
- if (typeof error.stack === "string" && error.stack.length > 0) {
240
- out.stack = capLength(error.stack, MAX_ERROR_STACK_LENGTH);
241
- }
242
- return out;
243
- }
244
- /**
245
- * Build a JSON-safe, depth-/breadth-/string-capped clone of a value for a
246
- * `json` console argument. Circular references become `"[Circular]"`; exotic
247
- * values are rendered to safe text — always serializable and bounded.
248
- */
249
- function safeCloneValue(value, depth, seen) {
250
- if (value === null)
251
- return null;
252
- const type = typeof value;
253
- if (type === "string")
254
- return capLength(value, MAX_ARG_STRING_LENGTH);
255
- if (type === "number")
256
- return Number.isFinite(value) ? value : String(value);
257
- if (type === "boolean")
258
- return value;
259
- if (type === "bigint")
260
- return `${value.toString()}n`;
261
- if (type === "symbol")
262
- return value.toString();
263
- if (type === "function")
264
- return "[Function]";
265
- if (type === "undefined")
266
- return null;
267
- const obj = value;
268
- if (value instanceof Error) {
269
- const { name, message } = describeErrorValue(value);
270
- return { name, message };
271
- }
272
- if (seen.has(obj))
273
- return "[Circular]";
274
- if (depth <= 0)
275
- return Array.isArray(value) ? "[Array]" : "[Object]";
276
- seen.add(obj);
277
- try {
278
- if (Array.isArray(value)) {
279
- const items = value
280
- .slice(0, MAX_ARG_ITEMS)
281
- .map((item) => safeCloneValue(item, depth - 1, seen));
282
- if (value.length > MAX_ARG_ITEMS) {
283
- items.push(`… ${value.length - MAX_ARG_ITEMS} more`);
284
- }
285
- return items;
286
- }
287
- const source = value;
288
- const keys = Object.keys(source);
289
- const out = {};
290
- for (const key of keys.slice(0, MAX_ARG_ITEMS)) {
291
- const child = source[key];
292
- if (typeof child === "undefined" || typeof child === "function")
293
- continue;
294
- out[key] = safeCloneValue(child, depth - 1, seen);
295
- }
296
- if (keys.length > MAX_ARG_ITEMS)
297
- out["…"] = `${keys.length - MAX_ARG_ITEMS} more`;
298
- return out;
299
- }
300
- finally {
301
- seen.delete(obj);
302
- }
303
- }
304
- /** Classify one console argument into a type-tagged {@link ConsoleArg}. */
305
- export function toConsoleArg(value) {
306
- if (value === null || value === undefined)
307
- return { t: "null", v: null };
308
- const type = typeof value;
309
- if (type === "string") {
310
- return { t: "string", v: capLength(value, MAX_ARG_STRING_LENGTH) };
311
- }
312
- if (type === "number") {
313
- return Number.isFinite(value)
314
- ? { t: "number", v: value }
315
- : { t: "string", v: String(value) };
316
- }
317
- if (type === "boolean")
318
- return { t: "bool", v: value };
319
- if (type === "bigint")
320
- return { t: "string", v: `${value.toString()}n` };
321
- if (type === "symbol")
322
- return { t: "string", v: value.toString() };
323
- if (type === "function")
324
- return { t: "string", v: "[Function]" };
325
- if (value instanceof Error)
326
- return { t: "error", v: describeErrorValue(value) };
327
- return { t: "json", v: safeCloneValue(value, MAX_ARG_DEPTH, new Set()) };
328
- }
329
- /** The serialized UTF-8 byte length of a value; `Infinity` when it cannot serialize. */
330
- function serializedBytes(value) {
331
- try {
332
- const json = JSON.stringify(value) ?? "";
333
- const Encoder = globalThis
334
- .TextEncoder;
335
- return Encoder ? new Encoder().encode(json).length : json.length;
336
- }
337
- catch {
338
- return Number.POSITIVE_INFINITY;
339
- }
340
- }
341
- /**
342
- * Cap an entry's structured args to {@link MAX_CONSOLE_ARGS_BYTES}: drop
343
- * trailing args until the array fits; if even a single arg is over budget, keep
344
- * one honest placeholder rather than an unbounded value.
345
- */
346
- function capConsoleArgs(args) {
347
- if (serializedBytes(args) <= MAX_CONSOLE_ARGS_BYTES)
348
- return args;
349
- let out = args.slice();
350
- while (out.length > 1 && serializedBytes(out) > MAX_CONSOLE_ARGS_BYTES) {
351
- out = out.slice(0, -1);
352
- }
353
- if (out.length === 1 && serializedBytes(out) > MAX_CONSOLE_ARGS_BYTES) {
354
- return [{ t: "string", v: "[trace: console argument omitted (too large)]" }];
355
- }
356
- return out;
357
- }
358
- /**
359
- * A `console` crumb from a captured call's level and arguments. The `message`
360
- * is the one-line preview; `args` preserves each argument as a structured,
361
- * type-tagged, size-capped value; `source` is the best-effort `file:line`.
362
- */
363
- export function consoleCrumb(level, args, timestamp, source) {
364
- const structured = capConsoleArgs(args.map(toConsoleArg));
365
- const crumb = {
366
- category: "console",
367
- level,
368
- message: joinArgs(args),
369
- timestamp,
370
- ...(structured.length > 0 ? { args: structured } : {}),
371
- ...(source ? { source } : {}),
372
- };
373
- return crumb;
374
- }
375
- /**
376
- * A `navigation` crumb; both sides are scrubbed and length-capped as the crumb
377
- * is built. On mobile this is fed by `trackScreen` (spec 0004 §D) — the values
378
- * are screen URLs derived from the configured origin.
379
- */
380
- export function navigationCrumb(from, to, timestamp) {
381
- const fromUrl = capLength(scrubUrl(from), MAX_URL_LENGTH);
382
- const toUrl = capLength(scrubUrl(to), MAX_URL_LENGTH);
383
- return {
384
- category: "navigation",
385
- message: `${fromUrl} → ${toUrl}`,
386
- timestamp,
387
- data: { from: fromUrl, to: toUrl },
388
- };
389
- }
390
- /** Classify an HTTP status into a {@link NetworkOutcome}. */
391
- export function outcomeFromStatus(status) {
392
- if (typeof status !== "number" || status <= 0)
393
- return "network-error";
394
- if (status >= 500)
395
- return "http-5xx";
396
- if (status >= 400)
397
- return "http-4xx";
398
- return "ok";
399
- }
400
- /**
401
- * The trivially-known byte size of a request body — a string's UTF-8 length, a
402
- * `Blob`'s `.size`, or an `ArrayBuffer`/typed-array `.byteLength`. Anything
403
- * that would require **reading** the body returns `undefined`.
404
- */
405
- export function trivialBodySize(body) {
406
- if (typeof body === "string")
407
- return utf8ByteLength(body);
408
- const BlobCtor = globalThis.Blob;
409
- if (BlobCtor && body instanceof BlobCtor)
410
- return body.size;
411
- if (typeof ArrayBuffer !== "undefined") {
412
- if (body instanceof ArrayBuffer)
413
- return body.byteLength;
414
- if (ArrayBuffer.isView(body))
415
- return body.byteLength;
416
- }
417
- return undefined;
418
- }
419
- function utf8ByteLength(value) {
420
- try {
421
- const Encoder = globalThis
422
- .TextEncoder;
423
- return Encoder ? new Encoder().encode(value).length : value.length;
424
- }
425
- catch {
426
- return value.length;
427
- }
428
- }
429
- /** Parse a `content-length` header into a non-negative byte count, or `undefined`. */
430
- export function parseContentLength(value) {
431
- if (typeof value !== "string")
432
- return undefined;
433
- const bytes = Number.parseInt(value, 10);
434
- return Number.isFinite(bytes) && bytes >= 0 ? bytes : undefined;
435
- }
436
- /** The media type from a `content-type` header (before any `;` parameters). */
437
- export function contentTypeOf(value) {
438
- if (typeof value !== "string" || value.length === 0)
439
- return undefined;
440
- const media = value.split(";")[0]?.trim();
441
- return media && media.length > 0 ? media : undefined;
442
- }
443
- /**
444
- * A network crumb with the rich, metadata-only fields the Network tab renders.
445
- * The URL is scrubbed and length-capped at assembly. The shape has **no field
446
- * for a request/response body or an arbitrary header**.
447
- */
448
- export function networkCrumb(input, timestamp) {
449
- const url = capLength(scrubUrl(input.url), MAX_URL_LENGTH);
450
- const hasStatus = typeof input.status === "number" && input.status > 0;
451
- const statusPart = hasStatus ? ` → ${input.status}` : "";
452
- const outcomePart = input.outcome !== "ok" ? ` (${input.outcome})` : "";
453
- return {
454
- category: input.api,
455
- api: input.api,
456
- method: input.method,
457
- url,
458
- message: `${input.method} ${url}${statusPart}${outcomePart}`,
459
- timestamp,
460
- ...(hasStatus ? { status: input.status } : {}),
461
- ...(input.statusText ? { statusText: input.statusText } : {}),
462
- ...(typeof input.durationMs === "number" ? { durationMs: input.durationMs } : {}),
463
- ...(typeof input.reqSize === "number" ? { reqSize: input.reqSize } : {}),
464
- ...(typeof input.respSize === "number" ? { respSize: input.respSize } : {}),
465
- ...(input.contentType ? { contentType: input.contentType } : {}),
466
- outcome: input.outcome,
467
- };
468
- }
469
- /** A thin `xhr` crumb from method + URL + status (outcome derived from the status). */
470
- export function xhrCrumb(method, url, status, timestamp) {
471
- return networkCrumb({ api: "xhr", method, url, status, outcome: outcomeFromStatus(status) }, timestamp);
472
- }
473
- function describeError(error) {
474
- if (error instanceof Error) {
475
- return { name: error.name || "Error", message: error.message };
476
- }
477
- if (typeof error === "string")
478
- return { name: "Error", message: error };
479
- return { name: "Error", message: stringifyArg(error) };
480
- }
481
- /**
482
- * An `error` crumb for the failing exception that ends the trace. When
483
- * `causedBy` is given (an auto-captured error), it rides on the crumb as the
484
- * causal pointer to the ids of the entries immediately preceding the throw.
485
- */
486
- export function errorCrumb(error, timestamp, causedBy) {
487
- const { name, message } = describeError(error);
488
- const crumb = {
489
- category: "error",
490
- level: "error",
491
- message: message ? `${name}: ${message}` : name,
492
- timestamp,
493
- data: { errorType: name },
494
- };
495
- return causedBy && causedBy.length > 0 ? { ...crumb, causedBy } : crumb;
13
+ * Mobile mappings (spec 0004 §D): network capture patches `XMLHttpRequest` **only**
14
+ * React Native's `fetch` is a polyfill over XHR, so wrapping both would
15
+ * double-record; navigation crumbs come from the host app's `trackScreen` calls
16
+ * (there is no `history` to patch); the masked `ui.*` streams have no mobile source
17
+ * yet and simply never occur.
18
+ *
19
+ * Everything private is kept out at the source: network crumbs carry method + URL +
20
+ * status metadata only, **never** bodies, and URLs are scrubbed as the crumb is
21
+ * built. The SDK must never throw into the host app, so every hook is wrapped: a
22
+ * capture failure is swallowed and the original behaviour always runs.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.globalConsole = globalConsole;
26
+ exports.globalXhr = globalXhr;
27
+ exports.instrumentXhr = instrumentXhr;
28
+ exports.instrumentBreadcrumbs = instrumentBreadcrumbs;
29
+ const sdk_core_1 = require("@fixback/sdk-core");
30
+ function noop() {
31
+ /* nothing installed */
496
32
  }
497
33
  /** React Native's global `console`, when present. */
498
- export function globalConsole() {
34
+ function globalConsole() {
499
35
  const g = globalThis;
500
36
  return g.console && typeof g.console === "object" ? g.console : undefined;
501
37
  }
502
- function noop() {
503
- /* nothing installed */
504
- }
505
- /**
506
- * Parse a single stack line into a `{ file, line }` location, or `undefined`.
507
- * Handles V8/Hermes (`at fn (file:line:col)` / `at file:line:col`) and
508
- * JSC (`fn@file:line:col`) frames; a query string on the asset URL is dropped.
509
- */
510
- function parseStackFrame(line) {
511
- let loc;
512
- const v8Named = line.match(/^at\s+.+?\s+\((.+)\)$/);
513
- if (v8Named) {
514
- loc = v8Named[1];
515
- }
516
- else {
517
- const v8Bare = line.match(/^at\s+(.+)$/);
518
- if (v8Bare) {
519
- loc = v8Bare[1];
520
- }
521
- else {
522
- const at = line.indexOf("@");
523
- if (at >= 0)
524
- loc = line.slice(at + 1);
525
- }
526
- }
527
- if (!loc)
528
- return undefined;
529
- loc = loc.replace(/\?[^:]*/, "");
530
- const m = loc.match(/^(.*):(\d+):\d+$/) ?? loc.match(/^(.*):(\d+)$/);
531
- if (!m)
532
- return undefined;
533
- const file = m[1] ?? "";
534
- const lineNo = Number(m[2]);
535
- if (!file || !Number.isFinite(lineNo))
536
- return undefined;
537
- return { file, line: lineNo };
538
- }
539
- /**
540
- * The best-effort `file:line` a console call was made from. Parses the frames
541
- * of a stack, skipping `skipFrames` leading (SDK-internal) frames so the
542
- * source points at the host code that called `console.*`.
543
- */
544
- export function sourceFromStack(stack, skipFrames = 0) {
545
- if (typeof stack !== "string" || stack.length === 0)
546
- return undefined;
547
- const frames = [];
548
- for (const raw of stack.split("\n")) {
549
- const frame = parseStackFrame(raw.trim());
550
- if (frame)
551
- frames.push(frame);
552
- }
553
- return frames[skipFrames];
554
- }
555
- /** Frames between `new Error()` (inside the console wrapper) and the host caller. */
556
- const CONSOLE_SOURCE_SKIP_FRAMES = 1;
557
- /** Wrap `console` methods so calls at the captured levels become crumbs. */
558
- export function instrumentConsole(buffer, consoleObj, levels, now) {
559
- const restores = [];
560
- for (const level of levels) {
561
- const original = consoleObj[level];
562
- if (typeof original !== "function")
563
- continue;
564
- const withSource = capturesSource(level);
565
- const wrapper = (...args) => {
566
- let source;
567
- if (withSource) {
568
- try {
569
- source = sourceFromStack(new Error().stack, CONSOLE_SOURCE_SKIP_FRAMES);
570
- }
571
- catch {
572
- source = undefined;
573
- }
574
- }
575
- try {
576
- if (level === "assert") {
577
- // console.assert records only when the asserted condition is falsy.
578
- if (!args[0]) {
579
- buffer.add(consoleCrumb("assert", args.slice(1), now(), source));
580
- }
581
- }
582
- else {
583
- buffer.add(consoleCrumb(level, args, now(), source));
584
- }
585
- }
586
- catch {
587
- // Capture must never throw into the host app.
588
- }
589
- return original.apply(consoleObj, args);
590
- };
591
- consoleObj[level] = wrapper;
592
- restores.push(() => {
593
- consoleObj[level] = original;
594
- });
595
- }
596
- return () => {
597
- for (const restore of restores)
598
- restore();
599
- };
600
- }
601
38
  /** React Native's global `XMLHttpRequest`, when present. */
602
- export function globalXhr() {
39
+ function globalXhr() {
603
40
  const g = globalThis;
604
41
  return typeof g.XMLHttpRequest === "function" ? g.XMLHttpRequest : undefined;
605
42
  }
43
+ /** Read one XHR response header defensively — `null` when unavailable or on any throw. */
606
44
  function safeXhrHeader(xhr, name) {
607
45
  try {
608
46
  return typeof xhr.getResponseHeader === "function"
@@ -613,6 +51,7 @@ function safeXhrHeader(xhr, name) {
613
51
  return null;
614
52
  }
615
53
  }
54
+ /** Read an XHR's `statusText` defensively — `undefined` when empty or on any throw. */
616
55
  function safeXhrStatusText(xhr) {
617
56
  try {
618
57
  return xhr.statusText || undefined;
@@ -629,7 +68,7 @@ function safeXhrStatusText(xhr) {
629
68
  * every request twice (spec 0004 §D). The `send` body argument is **never read
630
69
  * for content** — only its trivially-known size.
631
70
  */
632
- export function instrumentXhr(buffer, ctor, ignoreUrl, now, mono = defaultMono) {
71
+ function instrumentXhr(buffer, ctor, ignoreUrl, now, mono = sdk_core_1.nowMono) {
633
72
  if (typeof ctor !== "function")
634
73
  return noop;
635
74
  const proto = ctor.prototype;
@@ -643,12 +82,12 @@ export function instrumentXhr(buffer, ctor, ignoreUrl, now, mono = defaultMono)
643
82
  const meta = this.__fixbackMeta;
644
83
  if (meta && !ignoreUrl(meta.url)) {
645
84
  const started = mono();
646
- const reqSize = trivialBodySize(body);
85
+ const reqSize = (0, sdk_core_1.trivialBodySize)(body);
647
86
  // The terminal event names the outcome; `loadend` always follows exactly
648
87
  // one of load/error/timeout/abort, so by record time `settled` is set.
649
88
  let settled;
650
89
  const onLoad = () => {
651
- settled = outcomeFromStatus(this.status || undefined);
90
+ settled = (0, sdk_core_1.outcomeFromStatus)(this.status || undefined);
652
91
  };
653
92
  const onError = () => {
654
93
  settled = "network-error";
@@ -661,7 +100,7 @@ export function instrumentXhr(buffer, ctor, ignoreUrl, now, mono = defaultMono)
661
100
  };
662
101
  const onDone = () => {
663
102
  try {
664
- buffer.add(networkCrumb({
103
+ buffer.add((0, sdk_core_1.networkCrumb)({
665
104
  api: "xhr",
666
105
  method: meta.method,
667
106
  url: meta.url,
@@ -669,9 +108,9 @@ export function instrumentXhr(buffer, ctor, ignoreUrl, now, mono = defaultMono)
669
108
  statusText: safeXhrStatusText(this),
670
109
  durationMs: Math.max(0, mono() - started),
671
110
  reqSize,
672
- respSize: parseContentLength(safeXhrHeader(this, "content-length")),
673
- contentType: contentTypeOf(safeXhrHeader(this, "content-type")),
674
- outcome: settled ?? outcomeFromStatus(this.status || undefined),
111
+ respSize: (0, sdk_core_1.parseContentLength)(safeXhrHeader(this, "content-length")),
112
+ contentType: (0, sdk_core_1.contentTypeOf)(safeXhrHeader(this, "content-type")),
113
+ outcome: settled ?? (0, sdk_core_1.outcomeFromStatus)(this.status || undefined),
675
114
  }, now()));
676
115
  }
677
116
  catch {
@@ -697,23 +136,23 @@ export function instrumentXhr(buffer, ctor, ignoreUrl, now, mono = defaultMono)
697
136
  };
698
137
  }
699
138
  /**
700
- * Install the React Native capture hooks — the console wrapper and the XHR
139
+ * Install the React Native capture hooks — the shared console wrapper and the XHR
701
140
  * patch — and return a single teardown that removes them all. Each hook is
702
141
  * independent and defensive: a failure in one never blocks the other, and none
703
142
  * can throw into the host app. An off stream is never instrumented at all.
704
143
  */
705
- export function instrumentBreadcrumbs(buffer, options = {}) {
144
+ function instrumentBreadcrumbs(buffer, options = {}) {
706
145
  const consoleObj = options.consoleObj === null ? undefined : options.consoleObj ?? globalConsole();
707
146
  const xhr = options.xhr === null ? undefined : options.xhr ?? globalXhr();
708
- const levels = options.consoleLevels ?? DEFAULT_CONSOLE_LEVELS;
147
+ const levels = options.consoleLevels ?? sdk_core_1.DEFAULT_CONSOLE_LEVELS;
709
148
  const ignoreUrl = options.ignoreUrl ?? (() => false);
710
149
  const now = options.now ?? Date.now;
711
- const mono = options.mono ?? defaultMono;
150
+ const mono = options.mono ?? sdk_core_1.nowMono;
712
151
  const captureConsole = options.captureConsole !== false;
713
152
  const captureNetwork = options.captureNetwork !== false;
714
153
  const teardowns = [];
715
154
  if (consoleObj && captureConsole) {
716
- teardowns.push(instrumentConsole(buffer, consoleObj, levels, now));
155
+ teardowns.push((0, sdk_core_1.instrumentConsole)(buffer, consoleObj, levels, now));
717
156
  }
718
157
  if (captureNetwork) {
719
158
  teardowns.push(instrumentXhr(buffer, xhr, ignoreUrl, now, mono));
@@ -729,3 +168,4 @@ export function instrumentBreadcrumbs(buffer, options = {}) {
729
168
  }
730
169
  };
731
170
  }
171
+ //# sourceMappingURL=breadcrumbs.js.map