@debugbundle/sdk-react-native 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { getNativeModule, degradedNativeState, safeNativeCall } from "./native.js";
2
+ import { applyBeforeSend } from "./before-send.js";
2
3
  import { defaultRedactFields, DEFAULT_HEADER_ALLOWLIST, sanitizeHeaders, sanitizeValue } from "./redaction.js";
3
4
  const SDK_NAME = "@debugbundle/sdk-react-native";
4
- const DEFAULT_SDK_VERSION = "1.1.0";
5
+ const DEFAULT_SDK_VERSION = "1.2.0";
5
6
  const LOG_LEVELS = {
6
7
  debug: 10,
7
8
  info: 20,
@@ -17,8 +18,6 @@ export class DebugBundleReactNativeClient {
17
18
  breadcrumbs = [];
18
19
  probeBuffers = new Map();
19
20
  jsProbeActivationExpiresAt = 0;
20
- sessionSampledIn = true;
21
- sessionEventCount = 0;
22
21
  constructor(config = {}) {
23
22
  this.config = resolveConfig(config);
24
23
  if (Object.keys(config).length > 0) {
@@ -34,9 +33,6 @@ export class DebugBundleReactNativeClient {
34
33
  init(config) {
35
34
  this.config = resolveConfig(config);
36
35
  this.nativeModule = getNativeModule();
37
- this.sessionSampledIn =
38
- this.config.enabled &&
39
- (this.config.sessionSampleRate >= 1 || Math.random() <= this.config.sessionSampleRate);
40
36
  if (!this.config.enabled) {
41
37
  this.nativeState = { status: "disconnected", lastEventAt: null, nativeModuleAvailable: Boolean(this.nativeModule) };
42
38
  return;
@@ -49,57 +45,70 @@ export class DebugBundleReactNativeClient {
49
45
  this.nativeState = degradedNativeState("native_module_unavailable");
50
46
  return;
51
47
  }
52
- void safeNativeCall(async () => {
53
- this.nativeState = await this.nativeModule.initialize(this.config);
54
- if (!this.nativeState.status) {
55
- this.nativeState.status = "healthy";
48
+ try {
49
+ const initialization = this.nativeModule.initialize(nativeConfig(this.config));
50
+ if (isPromiseLike(initialization)) {
51
+ this.nativeState = { status: "healthy", lastEventAt: null, nativeModuleAvailable: true };
52
+ void safeNativeCall(async () => {
53
+ this.nativeState = normalizeNativeState(await initialization);
54
+ }, undefined);
56
55
  }
57
- }, undefined);
58
- this.nativeState = { status: "healthy", lastEventAt: null, nativeModuleAvailable: true };
56
+ else {
57
+ this.nativeState = normalizeNativeState(initialization);
58
+ }
59
+ }
60
+ catch {
61
+ this.nativeState = degradedNativeState("native_initialize_failed");
62
+ }
59
63
  }
60
64
  captureException(error, context = {}) {
61
65
  const mergedContext = this.mergeContext(context);
66
+ const sanitizedError = asRecord(sanitizeValue(error, { redactFields: this.config.redactFields }));
62
67
  this.enqueue("frontend_exception", {
63
- error: sanitizeValue(error, { redactFields: this.config.redactFields }),
64
- context: sanitizeValue(mergedContext, { redactFields: this.config.redactFields }),
68
+ name: stringValue(sanitizedError.name) ?? "Error",
69
+ message: stringValue(sanitizedError.message) ?? "Unknown error",
70
+ stack: stringValue(sanitizedError.stack) ?? `${stringValue(sanitizedError.name) ?? "Error"}: ${stringValue(sanitizedError.message) ?? "Unknown error"}`,
65
71
  breadcrumbs: this.breadcrumbs.slice(),
66
72
  probe_data: this.snapshotProbeData()
67
- }, stringValue(mergedContext.trace_id), false);
73
+ }, stringValue(mergedContext.trace_id), false, asRecord(sanitizeValue(mergedContext, {
74
+ redactFields: this.config.redactFields
75
+ })), this.config.captureErrors);
68
76
  }
69
77
  captureError(error, context = {}) {
70
78
  this.captureException(error, context);
71
79
  }
72
80
  captureLog(message, level = "warning", context = {}) {
73
- if (!this.config.captureLogs || LOG_LEVELS[level] < LOG_LEVELS[this.config.logLevel]) {
74
- return;
75
- }
76
81
  this.enqueue("log_event", {
77
82
  level,
78
83
  message: sanitizeValue(message, { redactFields: this.config.redactFields }),
79
- context: sanitizeValue(this.mergeContext(context), { redactFields: this.config.redactFields }),
80
- source: "javascript"
81
- }, stringValue(context.trace_id), true);
84
+ attributes: {
85
+ ...asRecord(sanitizeValue(this.mergeContext(context), { redactFields: this.config.redactFields })),
86
+ source: "javascript"
87
+ }
88
+ }, stringValue(context.trace_id), true, undefined, this.config.captureLogs && LOG_LEVELS[level] >= LOG_LEVELS[this.config.logLevel]);
82
89
  }
83
90
  captureRequest(request, response, context = {}) {
84
- const traceId = request.traceId ?? stringValue(context.trace_id);
91
+ const mergedContext = this.mergeContext(context);
92
+ const traceId = request.traceId ?? stringValue(mergedContext.trace_id);
93
+ const parsedUrl = parseRequestUrl(request.url);
85
94
  const payload = {
86
95
  method: request.method,
87
- url: sanitizeUrl(request.url),
96
+ path: parsedUrl.path,
97
+ query: parsedUrl.query,
88
98
  route_template: request.routeTemplate ?? null,
89
- request_headers: sanitizeHeaders(request.headers, this.config.headerAllowlist, {
99
+ headers: sanitizeHeaders(request.headers, this.config.headerAllowlist, {
90
100
  redactFields: this.config.redactFields
91
101
  }),
92
102
  response_status: response.statusCode,
93
103
  response_headers: sanitizeHeaders(response.headers, this.config.headerAllowlist, {
94
104
  redactFields: this.config.redactFields
95
105
  }),
96
- duration_ms: response.durationMillis ?? null,
97
- source: "react-native"
106
+ duration_ms: response.durationMillis ?? 0
98
107
  };
99
- this.recordBreadcrumb("network", payload);
100
- if (this.shouldCaptureRequestEvent(response.statusCode)) {
101
- this.enqueue("request_event", payload, traceId, true);
108
+ if (this.config.captureNetwork) {
109
+ this.recordBreadcrumb("network_request", payload);
102
110
  }
111
+ this.enqueue("request_event", payload, traceId, true, asRecord(sanitizeValue(mergedContext, { redactFields: this.config.redactFields })), this.config.captureNetwork);
103
112
  }
104
113
  captureMessage(message, level = "warning", context = {}) {
105
114
  this.recordBreadcrumb("message", {
@@ -110,7 +119,10 @@ export class DebugBundleReactNativeClient {
110
119
  }
111
120
  setContext(key, value) {
112
121
  this.context[key] = sanitizeValue(value, { redactFields: this.config.redactFields });
113
- if (this.nativeModule?.setContext) {
122
+ if (this.nativeModule?.setContextValue) {
123
+ void safeNativeCall(() => this.nativeModule.setContextValue(key, { value: this.context[key] }), undefined);
124
+ }
125
+ else if (this.nativeModule?.setContext) {
114
126
  void safeNativeCall(() => this.nativeModule.setContext(key, this.context[key]), undefined);
115
127
  }
116
128
  }
@@ -118,19 +130,25 @@ export class DebugBundleReactNativeClient {
118
130
  if (!label || this.probeBuffers.size >= this.config.maxProbeLabels && !this.probeBuffers.has(label)) {
119
131
  return;
120
132
  }
121
- if (options.heavy && !this.hasActiveJsProbeActivation()) {
133
+ const nativeProbeActive = this.nativeModule?.isProbeActive?.(label) ?? false;
134
+ if (options.heavy && !nativeProbeActive && !this.hasActiveJsProbeActivation()) {
122
135
  return;
123
136
  }
124
137
  const value = typeof data === "function" ? safeInvoke(data) : data;
138
+ const sanitizedData = objectWrap(sanitizeValue(value, { redactFields: this.config.redactFields }));
139
+ const occurredAt = new Date().toISOString();
125
140
  const entry = {
126
141
  label,
127
- data: sanitizeValue(value, { redactFields: this.config.redactFields }),
128
- timestamp: new Date().toISOString(),
142
+ data: sanitizedData,
143
+ timestamp: occurredAt,
129
144
  activation_id: null
130
145
  };
131
146
  const entries = this.probeBuffers.get(label) ?? [];
132
147
  entries.push(entry);
133
148
  this.probeBuffers.set(label, entries.slice(-this.config.maxProbeEntriesPerLabel));
149
+ if (this.nativeModule?.captureProbe) {
150
+ void safeNativeCall(() => this.nativeModule.captureProbe(label, sanitizedData, occurredAt), false);
151
+ }
134
152
  }
135
153
  async flush() {
136
154
  if (!this.nativeModule) {
@@ -153,12 +171,17 @@ export class DebugBundleReactNativeClient {
153
171
  return activated;
154
172
  }
155
173
  recordBreadcrumb(type, data = {}) {
156
- this.breadcrumbs.push({
174
+ const breadcrumb = {
157
175
  breadcrumb_type: type,
158
- occurred_at: new Date().toISOString(),
176
+ ts: new Date().toISOString(),
159
177
  data: sanitizeValue(data, { redactFields: this.config.redactFields })
160
- });
178
+ };
179
+ this.breadcrumbs.push(breadcrumb);
161
180
  this.breadcrumbs = this.breadcrumbs.slice(-this.config.maxBreadcrumbs);
181
+ this.enqueue("frontend_breadcrumb", {
182
+ breadcrumb_type: type,
183
+ data: breadcrumb.data
184
+ }, null, true);
162
185
  }
163
186
  recordScreen(screenName, previousScreen = null, source = "react-navigation") {
164
187
  if (!this.config.captureScreens) {
@@ -170,12 +193,12 @@ export class DebugBundleReactNativeClient {
170
193
  source
171
194
  });
172
195
  }
173
- enqueue(eventType, payload, traceId, countTowardSession) {
196
+ enqueue(eventType, payload, traceId, countTowardSession, context, localPolicyAllows = true) {
174
197
  if (!this.canCapture(eventType, countTowardSession)) {
175
198
  return;
176
199
  }
177
- const event = {
178
- schema_version: "1",
200
+ const authoredEvent = {
201
+ schema_version: "2026-03-01",
179
202
  event_id: generateId(),
180
203
  event_type: eventType,
181
204
  sdk_name: SDK_NAME,
@@ -187,29 +210,33 @@ export class DebugBundleReactNativeClient {
187
210
  framework: "react-native"
188
211
  },
189
212
  occurred_at: new Date().toISOString(),
190
- correlation: traceId ? { trace_id: traceId } : null,
191
- payload,
192
- device: this.nativeState.device ?? deviceFromConfig(this.config)
213
+ ...(traceId ? { correlation: { trace_id: traceId } } : {}),
214
+ ...(context && Object.keys(context).length > 0 ? { context } : {}),
215
+ payload: this.nativeState.device
216
+ ? { ...payload, device: canonicalDevice(this.nativeState.device, this.config) }
217
+ : payload
193
218
  };
194
- if (countTowardSession) {
195
- this.sessionEventCount += 1;
219
+ const event = applyBeforeSend(authoredEvent, this.config.beforeSend);
220
+ if (!event || !localPolicyAllows) {
221
+ return;
196
222
  }
197
223
  if (!this.nativeModule) {
198
224
  this.nativeState = degradedNativeState("native_module_unavailable");
199
225
  return;
200
226
  }
201
- void safeNativeCall(() => this.nativeModule.enqueueEvent(event), undefined);
227
+ if (this.nativeModule.enqueueCanonicalEvent) {
228
+ void safeNativeCall(() => this.nativeModule.enqueueCanonicalEvent(event), undefined);
229
+ }
230
+ else {
231
+ void safeNativeCall(() => this.nativeModule.enqueueEvent(toLegacyNativeEvent(event)), undefined);
232
+ }
202
233
  }
203
234
  canCapture(eventType, countTowardSession) {
204
- if (!this.config.enabled || !this.config.projectToken || !this.sessionSampledIn) {
235
+ if (!this.config.enabled || !this.config.projectToken) {
205
236
  return false;
206
237
  }
207
- if (Math.random() > this.config.sampleRate) {
208
- return false;
209
- }
210
- if (countTowardSession && this.sessionEventCount >= this.config.maxEventsPerSession) {
211
- return eventType === "frontend_exception" || eventType === "probe_event";
212
- }
238
+ void eventType;
239
+ void countTowardSession;
213
240
  return true;
214
241
  }
215
242
  mergeContext(context) {
@@ -227,9 +254,6 @@ export class DebugBundleReactNativeClient {
227
254
  hasActiveJsProbeActivation() {
228
255
  return this.jsProbeActivationExpiresAt > Date.now();
229
256
  }
230
- shouldCaptureRequestEvent(statusCode) {
231
- return statusCode >= 500 || [408, 423, 424, 425, 429].includes(statusCode);
232
- }
233
257
  }
234
258
  export function createDebugBundleClient(config = {}) {
235
259
  return new DebugBundleReactNativeClient(config);
@@ -269,9 +293,14 @@ export function resolveConfig(config) {
269
293
  probeFlushOnError: config.probeFlushOnError ?? true,
270
294
  redactFields: config.redactFields ?? defaultRedactFields(),
271
295
  headerAllowlist: config.headerAllowlist ?? DEFAULT_HEADER_ALLOWLIST,
272
- sdkVersion: config.sdkVersion ?? DEFAULT_SDK_VERSION
296
+ sdkVersion: config.sdkVersion ?? DEFAULT_SDK_VERSION,
297
+ beforeSend: config.beforeSend ?? null
273
298
  };
274
299
  }
300
+ function nativeConfig(config) {
301
+ const { beforeSend: _beforeSend, ...native } = config;
302
+ return native;
303
+ }
275
304
  function clampInteger(value, fallback, min, max) {
276
305
  if (!Number.isFinite(value)) {
277
306
  return fallback;
@@ -292,17 +321,78 @@ function safeInvoke(producer) {
292
321
  return { error: sanitizeValue(error) };
293
322
  }
294
323
  }
324
+ function isPromiseLike(value) {
325
+ return Boolean(value && typeof value.then === "function");
326
+ }
327
+ function normalizeNativeState(state) {
328
+ return {
329
+ ...state,
330
+ status: state.status || "healthy",
331
+ nativeModuleAvailable: state.nativeModuleAvailable ?? true
332
+ };
333
+ }
295
334
  function stringValue(value) {
296
335
  return typeof value === "string" && value.length > 0 ? value : null;
297
336
  }
298
- function sanitizeUrl(url) {
337
+ function asRecord(value) {
338
+ if (value && typeof value === "object" && !Array.isArray(value)) {
339
+ return value;
340
+ }
341
+ return {};
342
+ }
343
+ function objectWrap(value) {
344
+ return value && typeof value === "object" && !Array.isArray(value)
345
+ ? value
346
+ : { value };
347
+ }
348
+ function parseRequestUrl(url) {
299
349
  try {
300
350
  const parsed = new URL(url);
301
- parsed.search = "";
302
- return parsed.toString();
351
+ const query = {};
352
+ for (const [key, value] of parsed.searchParams) {
353
+ const existing = query[key];
354
+ if (existing === undefined) {
355
+ query[key] = value;
356
+ }
357
+ else if (Array.isArray(existing)) {
358
+ existing.push(value);
359
+ }
360
+ else {
361
+ query[key] = [existing, value];
362
+ }
363
+ }
364
+ return { path: parsed.pathname || "/", query };
365
+ }
366
+ catch {
367
+ const [path = "/", rawQuery = ""] = url.split("?", 2);
368
+ const query = {};
369
+ for (const segment of rawQuery.split("&")) {
370
+ if (!segment) {
371
+ continue;
372
+ }
373
+ const [rawKey = "", rawValue = ""] = segment.split("=", 2);
374
+ const key = decodeQuerySegment(rawKey);
375
+ const value = decodeQuerySegment(rawValue);
376
+ const existing = query[key];
377
+ if (existing === undefined) {
378
+ query[key] = value;
379
+ }
380
+ else if (Array.isArray(existing)) {
381
+ existing.push(value);
382
+ }
383
+ else {
384
+ query[key] = [existing, value];
385
+ }
386
+ }
387
+ return { path: path || "/", query };
388
+ }
389
+ }
390
+ function decodeQuerySegment(value) {
391
+ try {
392
+ return decodeURIComponent(value.replace(/\+/g, " "));
303
393
  }
304
394
  catch {
305
- return url.split("?")[0] ?? url;
395
+ return value;
306
396
  }
307
397
  }
308
398
  function sanitizeScreenName(screenName) {
@@ -319,10 +409,80 @@ function generateId() {
319
409
  return value.toString(16);
320
410
  });
321
411
  }
322
- function deviceFromConfig(config) {
412
+ function canonicalDevice(nativeDevice, config) {
413
+ const source = nativeDevice ?? {};
414
+ const sourceOs = asRecord(source.os);
415
+ const sourceScreen = asRecord(source.screen);
416
+ const sourceViewport = asRecord(source.viewport);
417
+ const width = nonNegativeInteger(sourceScreen.width ?? source.screen_width);
418
+ const height = nonNegativeInteger(sourceScreen.height ?? source.screen_height);
323
419
  return {
324
- app_version: config.appVersion,
325
- build_number: config.buildNumber,
326
- release_channel: config.releaseChannel
420
+ user_agent: stringValue(source.user_agent),
421
+ os: {
422
+ name: stringValue(sourceOs.name ?? source.os_name),
423
+ version: stringValue(sourceOs.version ?? source.os_version)
424
+ },
425
+ device_type: canonicalDeviceType(source.device_type),
426
+ screen: { width, height },
427
+ viewport: {
428
+ width: nonNegativeInteger(sourceViewport.width ?? width),
429
+ height: nonNegativeInteger(sourceViewport.height ?? height)
430
+ },
431
+ device_pixel_ratio: positiveNumber(source.device_pixel_ratio),
432
+ touch_capable: typeof source.touch_capable === "boolean" ? source.touch_capable : null,
433
+ language: stringValue(source.language ?? source.locale),
434
+ connection_type: stringValue(source.connection_type),
435
+ color_scheme_preference: canonicalColorScheme(source.color_scheme_preference),
436
+ app_version: stringValue(source.app_version) ?? config.appVersion,
437
+ build_number: stringValue(source.build_number) ?? config.buildNumber,
438
+ release_channel: stringValue(source.release_channel) ?? config.releaseChannel,
439
+ api_level: nullableNonNegativeInteger(source.api_level),
440
+ manufacturer: stringValue(source.manufacturer),
441
+ model: stringValue(source.model),
442
+ timezone: stringValue(source.timezone),
443
+ battery_level: nonNegativeNumber(source.battery_level),
444
+ battery_charging: typeof source.battery_charging === "boolean"
445
+ ? source.battery_charging
446
+ : typeof source.charging === "boolean" ? source.charging : null,
447
+ free_disk_bytes: nullableNonNegativeInteger(source.free_disk_bytes),
448
+ free_memory_bytes: nullableNonNegativeInteger(source.free_memory_bytes),
449
+ jailbroken: typeof source.jailbroken === "boolean"
450
+ ? source.jailbroken
451
+ : typeof source.rooted === "boolean" ? source.rooted : null
327
452
  };
328
453
  }
454
+ function toLegacyNativeEvent(event) {
455
+ const context = event.context ?? {};
456
+ const payload = { ...event.payload, context };
457
+ if (event.event_type === "frontend_exception") {
458
+ payload.error = {
459
+ name: event.payload.name,
460
+ message: event.payload.message,
461
+ stack: event.payload.stack
462
+ };
463
+ }
464
+ else if (event.event_type === "request_event") {
465
+ payload.url = event.payload.path;
466
+ }
467
+ return { ...event, payload };
468
+ }
469
+ function canonicalDeviceType(value) {
470
+ return value === "desktop" || value === "mobile" || value === "tablet" ? value : "unknown";
471
+ }
472
+ function canonicalColorScheme(value) {
473
+ return value === "light" || value === "dark" || value === "no-preference" ? value : null;
474
+ }
475
+ function nonNegativeInteger(value) {
476
+ return nullableNonNegativeInteger(value) ?? 0;
477
+ }
478
+ function nullableNonNegativeInteger(value) {
479
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
480
+ ? Math.trunc(value)
481
+ : null;
482
+ }
483
+ function positiveNumber(value) {
484
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
485
+ }
486
+ function nonNegativeNumber(value) {
487
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
488
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DebugBundleReactNativeClient, createDebugBundleClient } from "./client.js";
2
2
  import { captureDebugBundleConsole } from "./console.js";
3
3
  import { installDebugBundleErrorHandlers } from "./errors.js";
4
- import type { DebugBundleCaptureContext, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus } from "./types.js";
4
+ import type { DebugBundleCaptureContext, DebugBundleBeforeSend, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus } from "./types.js";
5
5
  export declare const DebugBundle: {
6
6
  init(config: DebugBundleConfig): void;
7
7
  captureException(error: unknown, context?: DebugBundleCaptureContext): void;
@@ -21,5 +21,5 @@ export declare const DebugBundle: {
21
21
  export { createDebugBundleClient, DebugBundleReactNativeClient };
22
22
  export { captureDebugBundleConsole };
23
23
  export { installDebugBundleErrorHandlers };
24
- export type { DebugBundleCaptureContext, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus };
24
+ export type { DebugBundleCaptureContext, DebugBundleBeforeSend, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus };
25
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,WAAW;iBACT,iBAAiB,GAAG,IAAI;4BAWb,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGvD,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGnD,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;4BAG3E,sBAAsB,YAAY,uBAAuB,YAAY,yBAAyB,GAAG,IAAI;4BAGrG,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;oBAGvF,MAAM,SAAS,OAAO,GAAG,IAAI;iBAGhC,MAAM,QAAQ,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,uBAAuB,GAAG,IAAI;aAGrF,OAAO,CAAC,IAAI,CAAC;qCAGW,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;2BAGnC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;6BAG3C,MAAM,mBAAmB,MAAM,GAAG,IAAI,WAAW,MAAM,GAAG,IAAI;qBAGzE,iBAAiB;0BAGZ,MAAM,GAAG,IAAI;CAGjC,CAAC;AAEF,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,CAAC;AACjE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AACrC,OAAO,EAAE,+BAA+B,EAAE,CAAC;AAC3C,YAAY,EACV,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EACV,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,WAAW;iBACT,iBAAiB,GAAG,IAAI;4BAWb,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGvD,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGnD,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;4BAG3E,sBAAsB,YAAY,uBAAuB,YAAY,yBAAyB,GAAG,IAAI;4BAGrG,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;oBAGvF,MAAM,SAAS,OAAO,GAAG,IAAI;iBAGhC,MAAM,QAAQ,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,uBAAuB,GAAG,IAAI;aAGrF,OAAO,CAAC,IAAI,CAAC;qCAGW,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;2BAGnC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;6BAG3C,MAAM,mBAAmB,MAAM,GAAG,IAAI,WAAW,MAAM,GAAG,IAAI;qBAGzE,iBAAiB;0BAGZ,MAAM,GAAG,IAAI;CAGjC,CAAC;AAEF,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,CAAC;AACjE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AACrC,OAAO,EAAE,+BAA+B,EAAE,CAAC;AAC3C,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAKlF,wBAAgB,eAAe,IAAI,uBAAuB,GAAG,IAAI,CAgBhE;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAElG;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,CAQ1E;AAED,wBAAsB,cAAc,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMhG;AAED,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD"}
1
+ {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAKlF,wBAAgB,eAAe,IAAI,uBAAuB,GAAG,IAAI,CAgBhE;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAElG;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,CAQ1E;AAED,wBAAsB,cAAc,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMhG;AAED,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD"}
package/dist/native.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { NativeModules, TurboModuleRegistry } from "react-native";
1
2
  let overrideModule;
2
3
  let cachedReactNativeModule;
3
4
  export function getNativeModule() {
@@ -38,22 +39,9 @@ export function resetNativeModuleCacheForTesting() {
38
39
  cachedReactNativeModule = undefined;
39
40
  }
40
41
  function resolveReactNativeModule() {
41
- const required = requireReactNative();
42
- const turboModule = required?.TurboModuleRegistry?.get?.("DebugBundleReactNative");
42
+ const turboModule = TurboModuleRegistry.get("DebugBundleReactNative");
43
43
  if (turboModule) {
44
44
  return turboModule;
45
45
  }
46
- return required?.NativeModules?.DebugBundleReactNative ?? null;
47
- }
48
- function requireReactNative() {
49
- try {
50
- const maybeRequire = Function("return typeof require === 'function' ? require : null")();
51
- if (!maybeRequire) {
52
- return null;
53
- }
54
- return maybeRequire("react-native");
55
- }
56
- catch {
57
- return null;
58
- }
46
+ return NativeModules.DebugBundleReactNative ?? null;
59
47
  }
package/dist/testing.d.ts CHANGED
@@ -1,7 +1,14 @@
1
- import type { DebugBundleEventEnvelope, NativeDebugBundleModule, NativeDebugBundleState, ResolvedDebugBundleConfig } from "./types.js";
1
+ import type { DebugBundleEventEnvelope, NativeDebugBundleConfig, NativeDebugBundleModule, NativeDebugBundleState } from "./types.js";
2
2
  export interface RecordingNativeModule extends NativeDebugBundleModule {
3
3
  events: DebugBundleEventEnvelope[];
4
- config: ResolvedDebugBundleConfig | null;
4
+ probes: Array<{
5
+ label: string;
6
+ data: unknown;
7
+ occurredAt: string;
8
+ }>;
9
+ context: Record<string, unknown>;
10
+ config: NativeDebugBundleConfig | null;
11
+ activeProbeLabels: Set<string>;
5
12
  }
6
13
  export declare function createRecordingNativeModule(initialState?: NativeDebugBundleState): RecordingNativeModule;
7
14
  export declare function installRecordingNativeModule(module?: RecordingNativeModule): RecordingNativeModule;
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAEvI,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IACpE,MAAM,EAAE,wBAAwB,EAAE,CAAC;IACnC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAAC;CAC1C;AAED,wBAAgB,2BAA2B,CAAC,YAAY,GAAE,sBAAiE,GAAG,qBAAqB,CA0BlJ;AAED,wBAAgB,4BAA4B,CAAC,MAAM,GAAE,qBAAqD,GAAG,qBAAqB,CAGjI;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAGnD"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IACpE,MAAM,EAAE,wBAAwB,EAAE,CAAC;IACnC,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACvC,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAChC;AAED,wBAAgB,2BAA2B,CAAC,YAAY,GAAE,sBAAiE,GAAG,qBAAqB,CA4ClJ;AAED,wBAAgB,4BAA4B,CAAC,MAAM,GAAE,qBAAqD,GAAG,qBAAqB,CAGjI;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAGnD"}
package/dist/testing.js CHANGED
@@ -3,15 +3,30 @@ export function createRecordingNativeModule(initialState = { status: "healthy",
3
3
  const state = { ...initialState };
4
4
  return {
5
5
  events: [],
6
+ probes: [],
7
+ context: {},
6
8
  config: null,
9
+ activeProbeLabels: new Set(),
7
10
  initialize(config) {
8
11
  this.config = config;
9
12
  return state;
10
13
  },
14
+ enqueueCanonicalEvent(event) {
15
+ this.events.push(event);
16
+ state.lastEventAt = Date.now();
17
+ return true;
18
+ },
11
19
  enqueueEvent(event) {
12
20
  this.events.push(event);
13
21
  state.lastEventAt = Date.now();
14
22
  },
23
+ captureProbe(label, data, occurredAt) {
24
+ this.probes.push({ label, data, occurredAt });
25
+ return this.activeProbeLabels.has(label);
26
+ },
27
+ isProbeActive(label) {
28
+ return this.activeProbeLabels.has(label);
29
+ },
15
30
  flush() {
16
31
  state.status = "healthy";
17
32
  },
@@ -21,6 +36,9 @@ export function createRecordingNativeModule(initialState = { status: "healthy",
21
36
  setContext() {
22
37
  return undefined;
23
38
  },
39
+ setContextValue(key, entry) {
40
+ this.context[key] = entry.value;
41
+ },
24
42
  activateProbeTriggerToken() {
25
43
  return true;
26
44
  }
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type DebugBundleStatus = "healthy" | "degraded" | "disconnected";
2
2
  export type DebugBundleLogLevel = "debug" | "info" | "warning" | "error" | "critical";
3
+ export type DebugBundleBeforeSend = (event: DebugBundleEventEnvelope) => DebugBundleEventEnvelope | null;
3
4
  export interface DebugBundleConfig {
4
5
  projectToken?: string;
5
6
  enabled?: boolean;
@@ -35,6 +36,7 @@ export interface DebugBundleConfig {
35
36
  redactFields?: string[];
36
37
  headerAllowlist?: string[];
37
38
  sdkVersion?: string;
39
+ beforeSend?: DebugBundleBeforeSend;
38
40
  }
39
41
  export interface ResolvedDebugBundleConfig {
40
42
  projectToken: string;
@@ -71,6 +73,7 @@ export interface ResolvedDebugBundleConfig {
71
73
  redactFields: string[];
72
74
  headerAllowlist: string[];
73
75
  sdkVersion: string;
76
+ beforeSend: DebugBundleBeforeSend | null;
74
77
  }
75
78
  export interface DebugBundleNetworkFilter {
76
79
  urlAllowlist?: string[];
@@ -97,7 +100,7 @@ export interface DebugBundleProbeOptions {
97
100
  heavy?: boolean;
98
101
  }
99
102
  export interface DebugBundleEventEnvelope {
100
- schema_version: "1";
103
+ schema_version: "2026-03-01";
101
104
  event_id: string;
102
105
  event_type: "frontend_exception" | "frontend_breadcrumb" | "log_event" | "request_event" | "error_suppressed" | "probe_event";
103
106
  sdk_name: "@debugbundle/sdk-react-native";
@@ -109,14 +112,14 @@ export interface DebugBundleEventEnvelope {
109
112
  framework: "react-native";
110
113
  };
111
114
  occurred_at: string;
112
- correlation: {
115
+ correlation?: {
113
116
  trace_id: string | null;
114
117
  request_id?: string | null;
115
118
  session_id?: string | null;
116
119
  user_id_hash?: string | null;
117
- } | null;
120
+ };
121
+ context?: Record<string, unknown>;
118
122
  payload: Record<string, unknown>;
119
- device: Record<string, unknown> | null;
120
123
  }
121
124
  export interface DebugBundleClient {
122
125
  readonly status: DebugBundleStatus;
@@ -135,13 +138,21 @@ export interface DebugBundleClient {
135
138
  recordScreen(screenName: string, previousScreen?: string | null, source?: string): void;
136
139
  }
137
140
  export interface NativeDebugBundleModule {
138
- initialize(config: ResolvedDebugBundleConfig): Promise<NativeDebugBundleState> | NativeDebugBundleState;
141
+ initialize(config: NativeDebugBundleConfig): Promise<NativeDebugBundleState> | NativeDebugBundleState;
142
+ enqueueCanonicalEvent?(event: DebugBundleEventEnvelope): Promise<boolean | void> | boolean | void;
143
+ /** Compatibility fallback implemented by native binaries released before the canonical bridge. */
139
144
  enqueueEvent(event: DebugBundleEventEnvelope): Promise<void> | void;
145
+ captureProbe?(label: string, data: unknown, occurredAt: string): Promise<boolean> | boolean;
146
+ isProbeActive?(label: string): boolean;
140
147
  flush(): Promise<void> | void;
141
148
  getStatus(): Promise<NativeDebugBundleState> | NativeDebugBundleState;
142
149
  setContext?(key: string, value: unknown): Promise<void> | void;
150
+ setContextValue?(key: string, entry: {
151
+ value: unknown;
152
+ }): Promise<void> | void;
143
153
  activateProbeTriggerToken?(token: string): Promise<boolean> | boolean;
144
154
  }
155
+ export type NativeDebugBundleConfig = Omit<ResolvedDebugBundleConfig, "beforeSend">;
145
156
  export interface NativeDebugBundleState {
146
157
  status: DebugBundleStatus;
147
158
  lastEventAt?: number | null;