@debugbundle/sdk-react-native 3.0.1 → 3.1.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 (84) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/DebugBundleReactNative.podspec +2 -2
  3. package/MIGRATION-3.1.md +116 -0
  4. package/README.md +37 -1
  5. package/android/build.gradle +1 -1
  6. package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +34 -3
  7. package/app.plugin.js +1 -1
  8. package/dist/NativeDebugBundleReactNative.d.ts +1 -0
  9. package/dist/NativeDebugBundleReactNative.d.ts.map +1 -1
  10. package/dist/before-send.d.ts.map +1 -1
  11. package/dist/before-send.js +9 -131
  12. package/dist/canonical-schema.d.ts +15 -0
  13. package/dist/canonical-schema.d.ts.map +1 -0
  14. package/dist/canonical-schema.js +139 -0
  15. package/dist/client-values.d.ts +17 -0
  16. package/dist/client-values.d.ts.map +1 -0
  17. package/dist/client-values.js +185 -0
  18. package/dist/client.d.ts +7 -0
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +102 -205
  21. package/dist/diagnostic-budget.d.ts +16 -0
  22. package/dist/diagnostic-budget.d.ts.map +1 -0
  23. package/dist/diagnostic-budget.js +120 -0
  24. package/dist/event-schema-data.d.ts +2 -0
  25. package/dist/event-schema-data.d.ts.map +1 -0
  26. package/dist/event-schema-data.js +49 -0
  27. package/dist/evidence-capabilities.d.ts +5 -0
  28. package/dist/evidence-capabilities.d.ts.map +1 -0
  29. package/dist/evidence-capabilities.js +21 -0
  30. package/dist/evidence-controller.d.ts +24 -0
  31. package/dist/evidence-controller.d.ts.map +1 -0
  32. package/dist/evidence-controller.js +113 -0
  33. package/dist/evidence-types.d.ts +33 -0
  34. package/dist/evidence-types.d.ts.map +1 -0
  35. package/dist/evidence-types.js +1 -0
  36. package/dist/evidence-wire.d.ts +7 -0
  37. package/dist/evidence-wire.d.ts.map +1 -0
  38. package/dist/evidence-wire.js +132 -0
  39. package/dist/exception-evidence.d.ts +25 -0
  40. package/dist/exception-evidence.d.ts.map +1 -0
  41. package/dist/exception-evidence.js +175 -0
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -0
  45. package/dist/owned-json.d.ts +11 -0
  46. package/dist/owned-json.d.ts.map +1 -0
  47. package/dist/owned-json.js +85 -0
  48. package/dist/privacy-keys.d.ts +2 -0
  49. package/dist/privacy-keys.d.ts.map +1 -1
  50. package/dist/privacy-keys.js +10 -2
  51. package/dist/privacy-telemetry.js +7 -7
  52. package/dist/privacy-text.d.ts +2 -2
  53. package/dist/privacy-text.d.ts.map +1 -1
  54. package/dist/privacy-text.js +35 -33
  55. package/dist/redaction.d.ts.map +1 -1
  56. package/dist/redaction.js +9 -7
  57. package/dist/safe-error-fields.d.ts +4 -0
  58. package/dist/safe-error-fields.d.ts.map +1 -0
  59. package/dist/safe-error-fields.js +54 -0
  60. package/dist/types.d.ts +21 -6
  61. package/dist/types.d.ts.map +1 -1
  62. package/ios/DebugBundleReactNative.swift +58 -4
  63. package/ios/DebugBundleReactNativeBridge.mm +4 -0
  64. package/package.json +3 -2
  65. package/src/NativeDebugBundleReactNative.ts +1 -0
  66. package/src/before-send.ts +10 -174
  67. package/src/canonical-schema.ts +108 -0
  68. package/src/client-values.ts +196 -0
  69. package/src/client.ts +92 -213
  70. package/src/diagnostic-budget.ts +90 -0
  71. package/src/event-schema-data.ts +49 -0
  72. package/src/evidence-capabilities.ts +19 -0
  73. package/src/evidence-controller.ts +89 -0
  74. package/src/evidence-types.ts +14 -0
  75. package/src/evidence-wire.ts +100 -0
  76. package/src/exception-evidence.ts +112 -0
  77. package/src/index.ts +6 -0
  78. package/src/owned-json.ts +60 -0
  79. package/src/privacy-keys.ts +12 -2
  80. package/src/privacy-telemetry.ts +9 -9
  81. package/src/privacy-text.ts +32 -31
  82. package/src/redaction.ts +8 -7
  83. package/src/safe-error-fields.ts +46 -0
  84. package/src/types.ts +23 -5
package/dist/client.js CHANGED
@@ -1,9 +1,16 @@
1
+ import { utf8Bytes as utf8ByteLength } from "./owned-json.js";
2
+ import { normalizeNativeState, stringValue, asRecord, objectWrap, parseRequestUrl, sanitizeScreenName, generateId, canonicalDevice, toLegacyNativeEvent, correlationFromContext, sameMobileDevice } from "./client-values.js";
3
+ import { MobileEvidenceController, fitMobileEvent } from "./evidence-controller.js";
4
+ import { normalizeCapabilities } from "./evidence-capabilities.js";
5
+ import { canonicalEvent } from "./evidence-wire.js";
6
+ import { ownJSON } from "./owned-json.js";
7
+ import { snapshotError, snapshotExceptionEvidence } from "./exception-evidence.js";
1
8
  import { getNativeModule, degradedNativeState, safeNativeCall } from "./native.js";
2
9
  import { applyBeforeSend } from "./before-send.js";
3
10
  import { defaultRedactFields, DEFAULT_HEADER_ALLOWLIST, sanitizeHeaders, sanitizeValue } from "./redaction.js";
4
11
  import { sanitizeTelemetry } from "./privacy-telemetry.js";
5
12
  const SDK_NAME = "@debugbundle/sdk-react-native";
6
- const DEFAULT_SDK_VERSION = "3.0.1";
13
+ const DEFAULT_SDK_VERSION = "3.1.0";
7
14
  const MAX_PENDING_NATIVE_CALLS = 256;
8
15
  const MAX_PENDING_LOW_PRIORITY_CALLS = 224;
9
16
  const MAX_PENDING_NATIVE_BYTES = 4 * 1024 * 1024;
@@ -59,8 +66,10 @@ export class DebugBundleReactNativeClient {
59
66
  jsProbeActivationExpiresAt = 0;
60
67
  pendingNativeCalls = 0;
61
68
  pendingNativeBytes = 0;
69
+ evidence;
62
70
  constructor(config = {}) {
63
71
  this.config = resolveConfig(config);
72
+ this.evidence = new MobileEvidenceController(this.config);
64
73
  if (Object.keys(config).length > 0) {
65
74
  this.init(config);
66
75
  }
@@ -71,8 +80,34 @@ export class DebugBundleReactNativeClient {
71
80
  get lastEventAt() {
72
81
  return this.nativeState.lastEventAt ?? null;
73
82
  }
83
+ get evidenceCapabilities() {
84
+ return this.evidence.capabilities(this.nativeState, this.hasCanonicalBridge());
85
+ }
86
+ hasCanonicalBridge() {
87
+ return this.nativeModule?.enqueueCanonicalEventJson !== undefined || this.nativeModule?.enqueueCanonicalEvent !== undefined;
88
+ }
89
+ beginOperation() { return this.evidence.begin(); }
90
+ withOperationContext(operation) {
91
+ const scope = this.beginOperation();
92
+ try {
93
+ return operation(scope);
94
+ }
95
+ finally {
96
+ scope.close();
97
+ }
98
+ }
99
+ captureRecovery(observation) {
100
+ if (!this.canCapture("diagnostic_context", true) || !this.hasNativeCapacity(false) ||
101
+ !this.evidenceCapabilities.features.includes("recovery_v1"))
102
+ return;
103
+ const prepared = this.evidence.recovery(observation);
104
+ if (prepared)
105
+ this.enqueue("diagnostic_context", prepared.payload, null, true, undefined, true, true, prepared.correlation);
106
+ }
74
107
  init(config) {
108
+ this.evidence.close();
75
109
  this.config = resolveConfig(config);
110
+ this.evidence = new MobileEvidenceController(this.config);
76
111
  this.nativeModule = getNativeModule();
77
112
  if (!this.config.enabled) {
78
113
  this.nativeState = { status: "disconnected", lastEventAt: null, nativeModuleAvailable: Boolean(this.nativeModule) };
@@ -87,11 +122,14 @@ export class DebugBundleReactNativeClient {
87
122
  return;
88
123
  }
89
124
  try {
125
+ const initializedConfig = this.config;
90
126
  const initialization = this.nativeModule.initialize(nativeConfig(this.config));
91
127
  if (isPromiseLike(initialization)) {
92
128
  this.nativeState = { status: "healthy", lastEventAt: null, nativeModuleAvailable: true };
93
129
  void safeNativeCall(async () => {
94
- this.nativeState = normalizeNativeState(await initialization);
130
+ const state = await initialization;
131
+ if (this.config === initializedConfig)
132
+ this.nativeState = normalizeNativeState(state);
95
133
  }, undefined);
96
134
  }
97
135
  else {
@@ -106,17 +144,21 @@ export class DebugBundleReactNativeClient {
106
144
  if (!this.config.captureErrors || !this.canCapture("frontend_exception", false) ||
107
145
  !this.hasNativeCapacity(true))
108
146
  return;
109
- const mergedContext = this.mergeContext(context);
110
- const sanitizedError = asRecord(sanitizeValue(error, { redactFields: this.config.redactFields }));
147
+ const mergedContext = this.evidence.context(this.mergeContext(context), this.nativeState, this.hasCanonicalBridge());
148
+ const sanitizedError = snapshotError(error, this.config.redactFields);
149
+ const fatal = mergedContext.source === "react-native-global-error" && mergedContext.fatal === true;
150
+ const graph = this.evidenceCapabilities.features.includes("exception_evidence_v1")
151
+ ? snapshotExceptionEvidence(error, sanitizedError, this.config.redactFields, fatal) : undefined;
111
152
  this.enqueue("frontend_exception", {
112
153
  name: stringValue(sanitizedError.name) ?? "Error",
113
154
  message: stringValue(sanitizedError.message) ?? "Unknown error",
114
155
  stack: stringValue(sanitizedError.stack) ?? `${stringValue(sanitizedError.name) ?? "Error"}: ${stringValue(sanitizedError.message) ?? "Unknown error"}`,
115
156
  breadcrumbs: this.breadcrumbs.slice(),
116
- probe_data: this.snapshotProbeData()
157
+ probe_data: this.snapshotProbeData(),
158
+ ...(graph ? { exception_evidence: graph } : {})
117
159
  }, stringValue(mergedContext.trace_id), false, asRecord(sanitizeValue(mergedContext, {
118
160
  redactFields: this.config.redactFields
119
- })), this.config.captureErrors);
161
+ })), this.config.captureErrors, !fatal, correlationFromContext(mergedContext));
120
162
  }
121
163
  captureError(error, context = {}) {
122
164
  this.captureException(error, context);
@@ -236,7 +278,7 @@ export class DebugBundleReactNativeClient {
236
278
  await safeNativeCall(async () => {
237
279
  await this.nativeModule.flush();
238
280
  const state = await this.nativeModule.getStatus();
239
- this.nativeState = state;
281
+ this.nativeState = normalizeNativeState(state);
240
282
  }, undefined);
241
283
  }
242
284
  async activateProbeTriggerToken(token) {
@@ -258,7 +300,8 @@ export class DebugBundleReactNativeClient {
258
300
  this.breadcrumbs.push(breadcrumb);
259
301
  this.breadcrumbs = this.breadcrumbs.slice(-this.config.maxBreadcrumbs);
260
302
  this.enqueue("frontend_breadcrumb", {
261
- breadcrumb_type: type,
303
+ breadcrumb_type: ["route_change", "click", "form_submit", "console_log", "network_request"].includes(type) ? type
304
+ : type === "screen" ? "route_change" : type === "action" ? "click" : "console_log",
262
305
  data: breadcrumb.data
263
306
  }, null, true);
264
307
  }
@@ -272,7 +315,7 @@ export class DebugBundleReactNativeClient {
272
315
  source
273
316
  });
274
317
  }
275
- enqueue(eventType, payload, traceId, countTowardSession, context, localPolicyAllows = true) {
318
+ enqueue(eventType, payload, traceId, countTowardSession, context, localPolicyAllows = true, runBeforeSend = true, correlation) {
276
319
  if (!localPolicyAllows)
277
320
  return;
278
321
  if (!this.canCapture(eventType, countTowardSession)) {
@@ -285,6 +328,7 @@ export class DebugBundleReactNativeClient {
285
328
  this.pendingNativeCalls += 1;
286
329
  let handedOff = false;
287
330
  let reservedBytes = 0;
331
+ let owner = null;
288
332
  try {
289
333
  const authoredEvent = {
290
334
  schema_version: "2026-03-01",
@@ -299,15 +343,16 @@ export class DebugBundleReactNativeClient {
299
343
  framework: "react-native"
300
344
  },
301
345
  occurred_at: new Date().toISOString(),
302
- ...(traceId ? { correlation: { trace_id: traceId } } : {}),
346
+ ...(correlation ? { correlation } : traceId ? { correlation: { trace_id: traceId } } : {}),
303
347
  ...(context && Object.keys(context).length > 0 ? { context } : {}),
304
- payload: this.nativeState.device
305
- ? { ...payload, device: canonicalDevice(this.nativeState.device, this.config) }
306
- : payload
348
+ payload: eventType === "diagnostic_context" ? payload : { ...payload, device: canonicalDevice(this.nativeState.device, this.config) }
307
349
  };
308
350
  const initial = protectEventFields(authoredEvent, this.config.redactFields);
309
351
  if (!initial)
310
352
  return;
353
+ owner = initial.event_id;
354
+ if (initial.event_type === "diagnostic_context" && (!canonicalEvent(ownJSON(initial)) || !this.evidence.budget.reserve(initial, owner)))
355
+ return;
311
356
  const config = this.config;
312
357
  const nativeModule = this.nativeModule;
313
358
  const initialBytes = utf8ByteLength(JSON.stringify(initial));
@@ -321,26 +366,30 @@ export class DebugBundleReactNativeClient {
321
366
  return;
322
367
  }
323
368
  handedOff = true;
324
- this.sendAdmittedEvent(initial, config, nativeModule, reservedBytes);
369
+ this.sendAdmittedEvent(initial, config, nativeModule, reservedBytes, runBeforeSend, !this.nativeState.device);
325
370
  }
326
371
  catch {
327
372
  this.nativeState = degradedNativeState("event_preparation_failed");
328
373
  }
329
374
  finally {
330
- if (!handedOff)
375
+ if (!handedOff) {
331
376
  this.releaseNativeCapacity(reservedBytes);
377
+ if (owner)
378
+ this.evidence.budget.release(owner);
379
+ }
332
380
  }
333
381
  }
334
- sendAdmittedEvent(initial, config, nativeModule, reservedBytes) {
382
+ sendAdmittedEvent(initial, config, nativeModule, reservedBytes, runBeforeSend, hydrateNativeDevice) {
335
383
  // This activation receives only a protected snapshot: deferred closures must
336
384
  // not retain the raw capture payload/context through their lexical scope.
385
+ const evidence = this.evidence;
337
386
  const prepareAndSend = async () => {
338
387
  if (this.config !== config)
339
388
  return;
340
- const safeEvent = this.prepareAdmittedEvent(initial, config, nativeModule.enqueueCanonicalEvent !== undefined);
389
+ const safeEvent = this.prepareAdmittedEvent(initial, config, nativeModule.enqueueCanonicalEventJson !== undefined || nativeModule.enqueueCanonicalEvent !== undefined, runBeforeSend, hydrateNativeDevice);
341
390
  if (safeEvent === null)
342
391
  return;
343
- const finalPriority = safeEvent.event_type === "frontend_exception" ||
392
+ const finalPriority = ["frontend_exception", "backend_exception"].includes(safeEvent.event_type) ||
344
393
  safeEvent.event_type === "log_event" && ["error", "critical"].includes(String(safeEvent.payload.level));
345
394
  const eventBytes = utf8ByteLength(JSON.stringify(safeEvent));
346
395
  const finalLimit = finalPriority ? MAX_PENDING_NATIVE_BYTES : MAX_PENDING_LOW_PRIORITY_BYTES;
@@ -350,20 +399,33 @@ export class DebugBundleReactNativeClient {
350
399
  return;
351
400
  this.pendingNativeBytes += eventBytes;
352
401
  reservedBytes += eventBytes;
353
- if (nativeModule.enqueueCanonicalEvent)
402
+ if (safeEvent.event_type === "diagnostic_context") {
403
+ if (!evidence.budget.replace(initial.event_id, safeEvent))
404
+ return;
405
+ }
406
+ else {
407
+ evidence.budget.release(initial.event_id);
408
+ if (["frontend_exception", "backend_exception"].includes(safeEvent.event_type))
409
+ evidence.budget.register(safeEvent);
410
+ }
411
+ // iOS TurboModule object conversion removes nested nulls. A string carrier
412
+ // preserves the validated JSON shape; older binaries keep their existing API.
413
+ if (nativeModule.enqueueCanonicalEventJson)
414
+ await nativeModule.enqueueCanonicalEventJson(JSON.stringify(safeEvent));
415
+ else if (nativeModule.enqueueCanonicalEvent)
354
416
  await nativeModule.enqueueCanonicalEvent(safeEvent);
355
417
  else
356
418
  await nativeModule.enqueueEvent(safeEvent);
357
419
  };
358
- const operation = config.beforeSend === null
420
+ const operation = config.beforeSend === null || !runBeforeSend
359
421
  ? safeNativeCall(prepareAndSend, undefined)
360
422
  : Promise.resolve().then(() => safeNativeCall(prepareAndSend, undefined));
361
- void operation.finally(() => { this.releaseNativeCapacity(reservedBytes); });
423
+ void operation.finally(() => { this.releaseNativeCapacity(reservedBytes); evidence.budget.release(initial.event_id); });
362
424
  }
363
- prepareAdmittedEvent(initial, config, canonical) {
425
+ prepareAdmittedEvent(initial, config, canonical, runBeforeSend, hydrateNativeDevice) {
364
426
  // Exit this synchronous activation before awaiting the bridge. An application
365
427
  // replacement may be huge before projection and must not survive suspension.
366
- const event = applyBeforeSend(initial, config.beforeSend);
428
+ const event = applyBeforeSend(initial, runBeforeSend ? config.beforeSend : null);
367
429
  if (!event || this.config !== config)
368
430
  return null;
369
431
  const safeEvent = protectEventFields(event, config.redactFields);
@@ -372,9 +434,21 @@ export class DebugBundleReactNativeClient {
372
434
  if (safeEvent.event_type === "log_event" &&
373
435
  (!config.captureLogs || !(LOG_LEVELS[safeEvent.payload.level] >= LOG_LEVELS[config.logLevel])))
374
436
  return null;
375
- if (safeEvent.event_type === "frontend_exception" && !config.captureErrors)
437
+ if (["frontend_exception", "backend_exception"].includes(safeEvent.event_type) && !config.captureErrors)
376
438
  return null;
377
- return canonical ? safeEvent : toLegacyNativeEvent(safeEvent);
439
+ if (!canonicalEvent(ownJSON(safeEvent)))
440
+ return null;
441
+ const filtered = this.evidence.filter(safeEvent, this.nativeState, canonical);
442
+ if (!filtered)
443
+ return null;
444
+ const fitted = fitMobileEvent(filtered);
445
+ if (fitted && hydrateNativeDevice && fitted.sdk_name === SDK_NAME && fitted.event_type === "frontend_exception" &&
446
+ sameMobileDevice(initial.payload.device, fitted.payload.device)) {
447
+ // This is the private native carrier, whose existing worker supplies device
448
+ // metadata before wire delivery. Preserve an explicitly changed hook device.
449
+ delete fitted.payload.device;
450
+ }
451
+ return fitted ? canonical ? fitted : toLegacyNativeEvent(fitted) : null;
378
452
  }
379
453
  hasNativeCapacity(highPriority) {
380
454
  return this.pendingNativeCalls < (highPriority ? MAX_PENDING_NATIVE_CALLS : MAX_PENDING_LOW_PRIORITY_CALLS) &&
@@ -414,24 +488,6 @@ export class DebugBundleReactNativeClient {
414
488
  return this.jsProbeActivationExpiresAt > Date.now();
415
489
  }
416
490
  }
417
- function utf8ByteLength(value) {
418
- let bytes = 0;
419
- for (let index = 0; index < value.length; index += 1) {
420
- const code = value.charCodeAt(index);
421
- if (code < 0x80)
422
- bytes += 1;
423
- else if (code < 0x800)
424
- bytes += 2;
425
- else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length &&
426
- value.charCodeAt(index + 1) >= 0xdc00 && value.charCodeAt(index + 1) <= 0xdfff) {
427
- bytes += 4;
428
- index += 1;
429
- }
430
- else
431
- bytes += 3;
432
- }
433
- return bytes;
434
- }
435
491
  export function createDebugBundleClient(config = {}) {
436
492
  return new DebugBundleReactNativeClient(config);
437
493
  }
@@ -452,6 +508,9 @@ export function resolveConfig(config) {
452
508
  maxEventsPerSession: clampInteger(config.maxEventsPerSession, 100, 1, 10000),
453
509
  maxBreadcrumbs: clampInteger(config.maxBreadcrumbs, 20, 1, 200),
454
510
  captureErrors: config.captureErrors ?? true,
511
+ captureExceptionEvidence: config.captureExceptionEvidence ?? true,
512
+ captureRecoveryObservations: config.captureRecoveryObservations ?? false,
513
+ evidenceCapabilities: config.evidenceCapabilities ? normalizeCapabilities(config.evidenceCapabilities) : null,
455
514
  captureUnhandledRejections: config.captureUnhandledRejections ?? true,
456
515
  captureScreens: config.captureScreens ?? true,
457
516
  captureActions: config.captureActions ?? false,
@@ -501,165 +560,3 @@ function safeInvoke(producer) {
501
560
  function isPromiseLike(value) {
502
561
  return Boolean(value && typeof value.then === "function");
503
562
  }
504
- function normalizeNativeState(state) {
505
- return {
506
- ...state,
507
- status: state.status || "healthy",
508
- nativeModuleAvailable: state.nativeModuleAvailable ?? true
509
- };
510
- }
511
- function stringValue(value) {
512
- return typeof value === "string" && value.length > 0 ? value : null;
513
- }
514
- function asRecord(value) {
515
- if (value && typeof value === "object" && !Array.isArray(value)) {
516
- return value;
517
- }
518
- return {};
519
- }
520
- function objectWrap(value) {
521
- return value && typeof value === "object" && !Array.isArray(value)
522
- ? value
523
- : { value };
524
- }
525
- function parseRequestUrl(url) {
526
- try {
527
- const parsed = new URL(url);
528
- const query = {};
529
- for (const [key, value] of parsed.searchParams) {
530
- const existing = query[key];
531
- if (existing === undefined) {
532
- query[key] = value;
533
- }
534
- else if (Array.isArray(existing)) {
535
- existing.push(value);
536
- }
537
- else {
538
- query[key] = [existing, value];
539
- }
540
- }
541
- return { path: parsed.pathname || "/", query };
542
- }
543
- catch {
544
- const [path = "/", rawQuery = ""] = url.split("?", 2);
545
- const query = {};
546
- for (const segment of rawQuery.split("&")) {
547
- if (!segment) {
548
- continue;
549
- }
550
- const [rawKey = "", rawValue = ""] = segment.split("=", 2);
551
- const key = decodeQuerySegment(rawKey);
552
- const value = decodeQuerySegment(rawValue);
553
- const existing = query[key];
554
- if (existing === undefined) {
555
- query[key] = value;
556
- }
557
- else if (Array.isArray(existing)) {
558
- existing.push(value);
559
- }
560
- else {
561
- query[key] = [existing, value];
562
- }
563
- }
564
- return { path: path || "/", query };
565
- }
566
- }
567
- function decodeQuerySegment(value) {
568
- try {
569
- return decodeURIComponent(value.replace(/\+/g, " "));
570
- }
571
- catch {
572
- return value;
573
- }
574
- }
575
- function sanitizeScreenName(screenName) {
576
- return screenName.replace(/[^a-zA-Z0-9_.:/-]/g, "_").slice(0, 120);
577
- }
578
- function generateId() {
579
- const cryptoLike = globalThis.crypto;
580
- if (cryptoLike?.randomUUID) {
581
- return cryptoLike.randomUUID();
582
- }
583
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (marker) => {
584
- const random = Math.trunc(Math.random() * 16);
585
- const value = marker === "x" ? random : (random & 0x3) | 0x8;
586
- return value.toString(16);
587
- });
588
- }
589
- function canonicalDevice(nativeDevice, config) {
590
- const source = nativeDevice ?? {};
591
- const sourceOs = asRecord(source.os);
592
- const sourceScreen = asRecord(source.screen);
593
- const sourceViewport = asRecord(source.viewport);
594
- const width = nonNegativeInteger(sourceScreen.width ?? source.screen_width);
595
- const height = nonNegativeInteger(sourceScreen.height ?? source.screen_height);
596
- return {
597
- user_agent: stringValue(source.user_agent),
598
- os: {
599
- name: stringValue(sourceOs.name ?? source.os_name),
600
- version: stringValue(sourceOs.version ?? source.os_version)
601
- },
602
- device_type: canonicalDeviceType(source.device_type),
603
- screen: { width, height },
604
- viewport: {
605
- width: nonNegativeInteger(sourceViewport.width ?? width),
606
- height: nonNegativeInteger(sourceViewport.height ?? height)
607
- },
608
- device_pixel_ratio: positiveNumber(source.device_pixel_ratio),
609
- touch_capable: typeof source.touch_capable === "boolean" ? source.touch_capable : null,
610
- language: stringValue(source.language ?? source.locale),
611
- connection_type: stringValue(source.connection_type),
612
- color_scheme_preference: canonicalColorScheme(source.color_scheme_preference),
613
- app_version: stringValue(source.app_version) ?? config.appVersion,
614
- build_number: stringValue(source.build_number) ?? config.buildNumber,
615
- release_channel: stringValue(source.release_channel) ?? config.releaseChannel,
616
- api_level: nullableNonNegativeInteger(source.api_level),
617
- manufacturer: stringValue(source.manufacturer),
618
- model: stringValue(source.model),
619
- timezone: stringValue(source.timezone),
620
- battery_level: nonNegativeNumber(source.battery_level),
621
- battery_charging: typeof source.battery_charging === "boolean"
622
- ? source.battery_charging
623
- : typeof source.charging === "boolean" ? source.charging : null,
624
- free_disk_bytes: nullableNonNegativeInteger(source.free_disk_bytes),
625
- free_memory_bytes: nullableNonNegativeInteger(source.free_memory_bytes),
626
- jailbroken: typeof source.jailbroken === "boolean"
627
- ? source.jailbroken
628
- : typeof source.rooted === "boolean" ? source.rooted : null
629
- };
630
- }
631
- function toLegacyNativeEvent(event) {
632
- const context = event.context ?? {};
633
- const payload = { ...event.payload, context };
634
- if (event.event_type === "frontend_exception") {
635
- payload.error = {
636
- name: event.payload.name,
637
- message: event.payload.message,
638
- stack: event.payload.stack
639
- };
640
- }
641
- else if (event.event_type === "request_event") {
642
- payload.url = event.payload.path;
643
- }
644
- return { ...event, payload };
645
- }
646
- function canonicalDeviceType(value) {
647
- return value === "desktop" || value === "mobile" || value === "tablet" ? value : "unknown";
648
- }
649
- function canonicalColorScheme(value) {
650
- return value === "light" || value === "dark" || value === "no-preference" ? value : null;
651
- }
652
- function nonNegativeInteger(value) {
653
- return nullableNonNegativeInteger(value) ?? 0;
654
- }
655
- function nullableNonNegativeInteger(value) {
656
- return typeof value === "number" && Number.isFinite(value) && value >= 0
657
- ? Math.trunc(value)
658
- : null;
659
- }
660
- function positiveNumber(value) {
661
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
662
- }
663
- function nonNegativeNumber(value) {
664
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
665
- }
@@ -0,0 +1,16 @@
1
+ import type { DebugBundleEventEnvelope } from "./types.js";
2
+ /** Bridge ownership lasts until its promise settles; native offline ownership then
3
+ * remains with the native SDK. Observation/attempt allowances are never refunded. */
4
+ export declare class MobileDiagnosticBudget {
5
+ private windows;
6
+ private pending;
7
+ get accountedBytes(): number;
8
+ private prune;
9
+ register(event: DebugBundleEventEnvelope): void;
10
+ reserve(event: DebugBundleEventEnvelope, owner?: string): boolean;
11
+ replace(owner: string, event: DebugBundleEventEnvelope): boolean;
12
+ release(id: string): void;
13
+ clear(): void;
14
+ private fresh;
15
+ }
16
+ //# sourceMappingURL=diagnostic-budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostic-budget.d.ts","sourceRoot":"","sources":["../src/diagnostic-budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAa3D;qFACqF;AACrF,qBAAa,sBAAsB;IACjC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAA8B;IAC7C,IAAI,cAAc,IAAI,MAAM,CAK3B;IACD,OAAO,CAAC,KAAK;IAGb,QAAQ,CAAC,KAAK,EAAE,wBAAwB,GAAG,IAAI;IAY/C,OAAO,CAAC,KAAK,EAAE,wBAAwB,EAAE,KAAK,SAAiB,GAAG,OAAO;IAwBzE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,wBAAwB,GAAG,OAAO;IAmBhE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKzB,KAAK,IAAI,IAAI;IACb,OAAO,CAAC,KAAK;CACd"}
@@ -0,0 +1,120 @@
1
+ import { evidenceReference } from "./evidence-wire.js";
2
+ import { utf8Bytes } from "./owned-json.js";
3
+ const cost = (value) => utf8Bytes(JSON.stringify(value));
4
+ function scopeKey(event, reference) {
5
+ const scope = reference.scope;
6
+ return JSON.stringify([event.service.name, event.service.environment, event.correlation?.trace_id ?? null,
7
+ event.correlation?.session_id ?? null, reference.failure_ref, [scope.kind, scope.operation_id ?? scope.page_id]]);
8
+ }
9
+ /** Bridge ownership lasts until its promise settles; native offline ownership then
10
+ * remains with the native SDK. Observation/attempt allowances are never refunded. */
11
+ export class MobileDiagnosticBudget {
12
+ windows = new Map();
13
+ pending = new Map();
14
+ get accountedBytes() {
15
+ let bytes = 0;
16
+ for (const [key, window] of this.windows)
17
+ bytes += cost([key, { ...window, attempts: [...window.attempts] }]);
18
+ for (const [id, pending] of this.pending)
19
+ bytes += pending.bytes + cost([id, pending.key, pending.attempt, pending.kind]);
20
+ return bytes;
21
+ }
22
+ prune(now) {
23
+ for (const [key, window] of this.windows)
24
+ if (!window.pending && now >= window.startsAt + 300_000)
25
+ this.windows.delete(key);
26
+ }
27
+ register(event) {
28
+ const reference = event.context?.evidence_reference;
29
+ if (!evidenceReference(reference))
30
+ return;
31
+ const now = Date.parse(event.occurred_at);
32
+ if (!Number.isFinite(now))
33
+ return;
34
+ this.prune(now);
35
+ const key = scopeKey(event, reference), previous = this.windows.get(key);
36
+ if (!previous && this.windows.size >= 50)
37
+ return;
38
+ const next = { ...(previous ?? this.fresh(now)), failureAt: previous?.failureAt ?? now, failureId: event.event_id };
39
+ if (previous?.failureId && previous.failureId !== event.event_id)
40
+ next.ambiguous = true;
41
+ this.windows.set(key, next);
42
+ if (this.accountedBytes > 65_536) {
43
+ if (previous)
44
+ this.windows.set(key, previous);
45
+ else
46
+ this.windows.delete(key);
47
+ }
48
+ }
49
+ reserve(event, owner = event.event_id) {
50
+ const now = Date.parse(event.occurred_at);
51
+ if (!Number.isFinite(now) || this.pending.has(owner))
52
+ return false;
53
+ this.prune(now);
54
+ const payload = event.payload, key = scopeKey(event, payload), previous = this.windows.get(key);
55
+ if (!previous && this.windows.size >= 50 || previous?.ambiguous)
56
+ return false;
57
+ const next = { ...(previous ?? this.fresh(now)), attempts: new Set(previous?.attempts), pending: (previous?.pending ?? 0) + 1 };
58
+ if (now < (next.failureAt ?? next.startsAt) || now > (next.failureAt ?? next.startsAt) + 300_000 || next.observations >= 20)
59
+ return false;
60
+ const attempt = payload.kind === "recovery" ? String(payload.attempt_id) : null;
61
+ if (attempt !== null) {
62
+ if (!next.attempts.has(attempt) && next.attempts.size >= 5)
63
+ return false;
64
+ next.attempts.add(attempt);
65
+ }
66
+ else {
67
+ if (next.resource || next.failureId !== payload.source_event_id || next.failureAt === null || now > next.failureAt + 30_000)
68
+ return false;
69
+ next.resource = true;
70
+ }
71
+ next.observations++;
72
+ this.windows.set(key, next);
73
+ this.pending.set(owner, { key, attempt, kind: String(payload.kind), bytes: cost(event) });
74
+ if (this.accountedBytes > 65_536) {
75
+ this.pending.delete(owner);
76
+ if (previous)
77
+ this.windows.set(key, previous);
78
+ else
79
+ this.windows.delete(key);
80
+ return false;
81
+ }
82
+ return true;
83
+ }
84
+ replace(owner, event) {
85
+ const current = this.pending.get(owner), payload = event.payload, key = scopeKey(event, payload);
86
+ const attempt = payload.kind === "recovery" ? String(payload.attempt_id) : null;
87
+ if (!current || current.key !== key || current.attempt !== attempt || current.kind !== payload.kind) {
88
+ this.release(owner);
89
+ return this.reserve(event, owner);
90
+ }
91
+ // Hook identity may stay stable while occurrence time or resource identity
92
+ // changes. Revalidate association without spending another observation.
93
+ const window = this.windows.get(key), now = Date.parse(event.occurred_at);
94
+ if (!window || window.ambiguous || !Number.isFinite(now) ||
95
+ now < (window.failureAt ?? window.startsAt) || now > (window.failureAt ?? window.startsAt) + 300_000 ||
96
+ attempt === null && (window.failureAt === null || payload.source_event_id !== window.failureId || now > window.failureAt + 30_000)) {
97
+ this.release(owner);
98
+ return false;
99
+ }
100
+ const previous = current.bytes;
101
+ current.bytes = cost(event);
102
+ if (this.accountedBytes > 65_536) {
103
+ current.bytes = previous;
104
+ this.release(owner);
105
+ return false;
106
+ }
107
+ return true;
108
+ }
109
+ release(id) {
110
+ const pending = this.pending.get(id);
111
+ if (!pending)
112
+ return;
113
+ this.pending.delete(id);
114
+ const window = this.windows.get(pending.key);
115
+ if (window)
116
+ window.pending = Math.max(0, window.pending - 1);
117
+ }
118
+ clear() { this.windows.clear(); this.pending.clear(); }
119
+ fresh(now) { return { startsAt: now, failureAt: null, failureId: null, attempts: new Set(), observations: 0, resource: false, ambiguous: false, pending: 0 }; }
120
+ }
@@ -0,0 +1,2 @@
1
+ export declare const eventSchemaData: unknown;
2
+ //# sourceMappingURL=event-schema-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-schema-data.d.ts","sourceRoot":"","sources":["../src/event-schema-data.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,eAAe,EAAE,OA8CnB,CAAC"}