@debugbundle/sdk-react-native 1.0.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.
@@ -0,0 +1,474 @@
1
+ package com.debugbundle.reactnative;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertFalse;
5
+ import static org.junit.Assert.assertTrue;
6
+ import static org.mockito.ArgumentMatchers.any;
7
+ import static org.mockito.ArgumentMatchers.anyBoolean;
8
+ import static org.mockito.ArgumentMatchers.anyString;
9
+ import static org.mockito.ArgumentMatchers.eq;
10
+ import static org.mockito.ArgumentMatchers.isNull;
11
+ import static org.mockito.Mockito.doThrow;
12
+ import static org.mockito.Mockito.mock;
13
+ import static org.mockito.Mockito.never;
14
+ import static org.mockito.Mockito.reset;
15
+ import static org.mockito.Mockito.verify;
16
+ import static org.mockito.Mockito.when;
17
+
18
+ import android.content.Context;
19
+ import com.debugbundle.android.DebugBundleConfig;
20
+ import com.debugbundle.android.DebugBundleLogLevel;
21
+ import com.debugbundle.android.DebugBundleRequestInfo;
22
+ import com.debugbundle.android.DebugBundleResponseInfo;
23
+ import com.facebook.react.bridge.Dynamic;
24
+ import com.facebook.react.bridge.Promise;
25
+ import com.facebook.react.bridge.NativeModule;
26
+ import com.facebook.react.bridge.ReactApplicationContext;
27
+ import com.facebook.react.bridge.ReadableArray;
28
+ import com.facebook.react.bridge.ReadableMap;
29
+ import com.facebook.react.bridge.ReadableType;
30
+ import com.facebook.react.bridge.WritableMap;
31
+ import com.facebook.react.uimanager.ViewManager;
32
+ import java.util.HashMap;
33
+ import java.util.List;
34
+ import java.util.Map;
35
+ import org.junit.Before;
36
+ import org.junit.Test;
37
+ import org.mockito.ArgumentCaptor;
38
+
39
+ public final class DebugBundleReactNativeModuleTest {
40
+ private ReactApplicationContext reactContext;
41
+ private Context applicationContext;
42
+ private DebugBundleReactNativeModule.NativeOperations nativeOperations;
43
+ private DebugBundleReactNativeModule.WritableMapFactory writableMapFactory;
44
+ private WritableMap writableMap;
45
+ private DebugBundleReactNativeModule module;
46
+
47
+ @Before
48
+ public void setUp() {
49
+ reactContext = mock(ReactApplicationContext.class);
50
+ applicationContext = mock(Context.class);
51
+ nativeOperations = mock(DebugBundleReactNativeModule.NativeOperations.class);
52
+ writableMapFactory = mock(DebugBundleReactNativeModule.WritableMapFactory.class);
53
+ writableMap = mock(WritableMap.class);
54
+ when(reactContext.getApplicationContext()).thenReturn(applicationContext);
55
+ when(nativeOperations.statusName()).thenReturn("ONLINE");
56
+ when(writableMapFactory.create()).thenReturn(writableMap);
57
+ module = new DebugBundleReactNativeModule(
58
+ reactContext,
59
+ nativeOperations,
60
+ writableMapFactory);
61
+ }
62
+
63
+ @Test
64
+ public void initializesWithFullConfigurationAndReturnsNativeState() {
65
+ when(nativeOperations.lastEventAt()).thenReturn(1_234L);
66
+ Promise promise = mock(Promise.class);
67
+ ReadableMap config = readableMap(Map.ofEntries(
68
+ Map.entry("sdkVersion", "1.2.7"),
69
+ Map.entry("projectToken", "project-token"),
70
+ Map.entry("enabled", false),
71
+ Map.entry("environment", "staging"),
72
+ Map.entry("service", "checkout-mobile"),
73
+ Map.entry("endpoint", "https://ingest.example.test"),
74
+ Map.entry("batchSize", 4),
75
+ Map.entry("flushInterval", 1_250.0),
76
+ Map.entry("sampleRate", 0.5),
77
+ Map.entry("sessionSampleRate", 0.75),
78
+ Map.entry("requestTimeout", 2_500.0),
79
+ Map.entry("releaseChannel", "beta"),
80
+ Map.entry("appVersion", "4.2.0"),
81
+ Map.entry("buildNumber", "42"),
82
+ Map.entry("maxEventsPerSession", 55),
83
+ Map.entry("maxBreadcrumbs", 12),
84
+ Map.entry("captureScreens", false),
85
+ Map.entry("captureActions", true),
86
+ Map.entry("captureNetwork", false),
87
+ Map.entry("captureLogs", false),
88
+ Map.entry("logLevel", "debug"),
89
+ Map.entry("headerAllowlist", List.of("x-request-id", 4)),
90
+ Map.entry("offlineQueueMaxEvents", 75),
91
+ Map.entry("offlineQueueMaxBytes", 4_096.0),
92
+ Map.entry("offlineQueueTtl", 9_000.0),
93
+ Map.entry("maxProbeLabels", 8),
94
+ Map.entry("maxProbeEntriesPerLabel", 3),
95
+ Map.entry("probeFlushOnError", false),
96
+ Map.entry("redactFields", List.of("password"))));
97
+
98
+ module.initialize(config, promise);
99
+
100
+ verify(nativeOperations).initialize(eq(applicationContext), any(DebugBundleConfig.class));
101
+ verify(writableMap).putString("status", "online");
102
+ verify(writableMap).putDouble("lastEventAt", 1_234.0);
103
+ verify(writableMap).putBoolean("nativeModuleAvailable", true);
104
+ verify(writableMap).putNull("degradedReason");
105
+ verify(promise).resolve(writableMap);
106
+ }
107
+
108
+ @Test
109
+ public void initializationAndStatusFailOpenWithoutThrowingIntoReactNative() {
110
+ Promise failedPromise = mock(Promise.class);
111
+ doThrow(new IllegalStateException("native failure"))
112
+ .when(nativeOperations)
113
+ .initialize(any(Context.class), any(DebugBundleConfig.class));
114
+
115
+ module.initialize(readableMap(Map.of()), failedPromise);
116
+
117
+ verify(writableMap).putString("status", "degraded");
118
+ verify(writableMap).putNull("lastEventAt");
119
+ verify(writableMap).putString("degradedReason", "android_initialize_failed");
120
+ verify(failedPromise).resolve(writableMap);
121
+
122
+ reset(writableMap);
123
+ when(nativeOperations.lastEventAt()).thenReturn(null);
124
+ Promise statusPromise = mock(Promise.class);
125
+ module.getStatus(statusPromise);
126
+ verify(writableMap).putNull("lastEventAt");
127
+ verify(statusPromise).resolve(writableMap);
128
+ }
129
+
130
+ @Test
131
+ public void translatesEveryLegacyEventFamilyThroughNativeOperations() {
132
+ assertLegacyEventCalls(
133
+ Map.of(
134
+ "event_type", "frontend_exception",
135
+ "payload", Map.of("error", "checkout failed")),
136
+ () -> verify(nativeOperations).captureException(
137
+ any(RuntimeException.class),
138
+ any(Map.class)));
139
+ assertLegacyEventCalls(
140
+ Map.of(
141
+ "event_type", "frontend_breadcrumb",
142
+ "payload", Map.of("breadcrumb_type", "screen_transition")),
143
+ () -> verify(nativeOperations).captureBreadcrumb(
144
+ eq("screen_transition"),
145
+ isNull(),
146
+ any(Map.class)));
147
+
148
+ for (Map.Entry<String, DebugBundleLogLevel> entry : Map.of(
149
+ "debug", DebugBundleLogLevel.Debug,
150
+ "info", DebugBundleLogLevel.Info,
151
+ "error", DebugBundleLogLevel.Error,
152
+ "critical", DebugBundleLogLevel.Critical,
153
+ "warning", DebugBundleLogLevel.Warning).entrySet()) {
154
+ assertLegacyEventCalls(
155
+ Map.of(
156
+ "event_type", "log_event",
157
+ "payload", Map.of("message", "checkout", "level", entry.getKey())),
158
+ () -> verify(nativeOperations).captureLog(
159
+ eq("checkout"),
160
+ eq(entry.getValue()),
161
+ any(Map.class)));
162
+ }
163
+
164
+ assertLegacyEventCalls(
165
+ Map.of(
166
+ "event_type", "request_event",
167
+ "correlation", Map.of("trace_id", "trace-rn"),
168
+ "payload", Map.of(
169
+ "method", "POST",
170
+ "url", "https://example.test/checkout",
171
+ "route_template", "/checkout",
172
+ "response_status", 503,
173
+ "duration_ms", 12.5)),
174
+ () -> verify(nativeOperations).captureRequest(
175
+ any(DebugBundleRequestInfo.class),
176
+ any(DebugBundleResponseInfo.class),
177
+ any(Map.class),
178
+ eq(false)));
179
+ assertLegacyEventCalls(
180
+ Map.of("event_type", "custom_event"),
181
+ () -> verify(nativeOperations).captureMessage(
182
+ eq("react_native_event:custom_event"),
183
+ eq(DebugBundleLogLevel.Warning),
184
+ any(Map.class)));
185
+ }
186
+
187
+ @Test
188
+ public void canonicalEventsAndProbeOperationsReturnTruthfulResultsAndFailClosed() {
189
+ ReadableMap event = readableMap(Map.of("event_type", "frontend_exception"));
190
+ Promise canonicalPromise = mock(Promise.class);
191
+ when(nativeOperations.captureExternalEvent(any(Map.class))).thenReturn(true);
192
+ module.enqueueCanonicalEvent(event, canonicalPromise);
193
+ verify(canonicalPromise).resolve(true);
194
+
195
+ when(nativeOperations.isProbeActive("checkout.*")).thenReturn(true);
196
+ assertTrue(module.isProbeActive("checkout.*"));
197
+ doThrow(new IllegalStateException("probe failure"))
198
+ .when(nativeOperations)
199
+ .isProbeActive("broken");
200
+ assertFalse(module.isProbeActive("broken"));
201
+
202
+ Promise initializePromise = mock(Promise.class);
203
+ module.initialize(
204
+ readableMap(Map.of(
205
+ "sdkVersion", "1.2.7",
206
+ "service", "checkout-mobile",
207
+ "environment", "test")),
208
+ initializePromise);
209
+ Promise probePromise = mock(Promise.class);
210
+ when(nativeOperations.captureExternalProbe(
211
+ anyString(),
212
+ anyString(),
213
+ anyString(),
214
+ anyString(),
215
+ any(Map.class),
216
+ anyString())).thenReturn(true);
217
+ module.captureProbe(
218
+ "checkout.cart",
219
+ readableMap(Map.of("count", 2)),
220
+ "2026-07-28T12:00:00.000Z",
221
+ probePromise);
222
+ verify(nativeOperations).captureExternalProbe(
223
+ "1.2.7",
224
+ "checkout-mobile",
225
+ "test",
226
+ "checkout.cart",
227
+ Map.of("count", 2),
228
+ "2026-07-28T12:00:00.000Z");
229
+ verify(probePromise).resolve(true);
230
+
231
+ Promise failedCanonicalPromise = mock(Promise.class);
232
+ doThrow(new IllegalStateException("capture failure"))
233
+ .when(nativeOperations)
234
+ .captureExternalEvent(any(Map.class));
235
+ module.enqueueCanonicalEvent(event, failedCanonicalPromise);
236
+ verify(failedCanonicalPromise).resolve(false);
237
+
238
+ Promise failedProbePromise = mock(Promise.class);
239
+ doThrow(new IllegalStateException("probe capture failure"))
240
+ .when(nativeOperations)
241
+ .captureExternalProbe(anyString(), anyString(), anyString(), anyString(), any(Map.class), anyString());
242
+ module.captureProbe(
243
+ "checkout.cart",
244
+ readableMap(Map.of()),
245
+ "2026-07-28T12:00:00.000Z",
246
+ failedProbePromise);
247
+ verify(failedProbePromise).resolve(false);
248
+ }
249
+
250
+ @Test
251
+ public void canonicalEventsNormalizeIntegralReactNativeNumbersRecursively() {
252
+ ReadableMap event = readableMap(Map.of(
253
+ "event_type", "request_event",
254
+ "payload", Map.of(
255
+ "response_status", 503.0,
256
+ "duration_ms", 12.5,
257
+ "probe_data", Map.of(
258
+ "version", 1.0,
259
+ "values", List.of(2.0, 2.5)))));
260
+ Promise promise = mock(Promise.class);
261
+ when(nativeOperations.captureExternalEvent(any(Map.class))).thenReturn(true);
262
+
263
+ module.enqueueCanonicalEvent(event, promise);
264
+
265
+ @SuppressWarnings("unchecked")
266
+ ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
267
+ verify(nativeOperations).captureExternalEvent(captor.capture());
268
+ Map<String, Object> payload = (Map<String, Object>) captor.getValue().get("payload");
269
+ Map<String, Object> probeData = (Map<String, Object>) payload.get("probe_data");
270
+ assertEquals(503L, payload.get("response_status"));
271
+ assertEquals(12.5, payload.get("duration_ms"));
272
+ assertEquals(1L, probeData.get("version"));
273
+ assertEquals(List.of(2L, 2.5), probeData.get("values"));
274
+ verify(promise).resolve(true);
275
+ }
276
+
277
+ @Test
278
+ public void contextTriggerAndFlushOperationsAlwaysResolve() throws Exception {
279
+ Promise contextPromise = mock(Promise.class);
280
+ Dynamic tenant = dynamic(ReadableType.String, "checkout");
281
+ module.setContext("tenant", tenant, contextPromise);
282
+ verify(nativeOperations).setContext("tenant", "checkout");
283
+ verify(tenant).recycle();
284
+ verify(contextPromise).resolve(isNull());
285
+
286
+ Promise contextValuePromise = mock(Promise.class);
287
+ module.setContextValue(
288
+ "cart",
289
+ readableMap(Map.of("value", 2)),
290
+ contextValuePromise);
291
+ verify(nativeOperations).setContext("cart", 2);
292
+ verify(contextValuePromise).resolve(isNull());
293
+
294
+ Promise triggerPromise = mock(Promise.class);
295
+ when(nativeOperations.activateProbeTriggerToken("trigger")).thenReturn(true);
296
+ module.activateProbeTriggerToken("trigger", triggerPromise);
297
+ verify(triggerPromise).resolve(true);
298
+
299
+ Promise flushPromise = mock(Promise.class);
300
+ module.flush(flushPromise);
301
+ verify(nativeOperations).flush();
302
+ verify(flushPromise).resolve(isNull());
303
+
304
+ doThrow(new IllegalStateException("context failure"))
305
+ .when(nativeOperations)
306
+ .setContext(eq("broken"), any());
307
+ Promise failedContext = mock(Promise.class);
308
+ Dynamic failedValue = dynamic(ReadableType.String, "value");
309
+ module.setContext("broken", failedValue, failedContext);
310
+ verify(failedValue).recycle();
311
+ verify(failedContext).resolve(isNull());
312
+
313
+ doThrow(new ReflectiveOperationException("flush failure"))
314
+ .when(nativeOperations)
315
+ .flush();
316
+ Promise failedFlush = mock(Promise.class);
317
+ module.flush(failedFlush);
318
+ verify(failedFlush).resolve(isNull());
319
+
320
+ doThrow(new IllegalStateException("trigger failure"))
321
+ .when(nativeOperations)
322
+ .activateProbeTriggerToken("broken");
323
+ Promise failedTrigger = mock(Promise.class);
324
+ module.activateProbeTriggerToken("broken", failedTrigger);
325
+ verify(failedTrigger).resolve(false);
326
+ }
327
+
328
+ @Test
329
+ public void legacyDynamicContextPreservesEverySupportedValueShape() {
330
+ assertDynamicContext("null", dynamic(ReadableType.Null, null), null);
331
+ assertDynamicContext("boolean", dynamic(ReadableType.Boolean, true), true);
332
+ assertDynamicContext("number", dynamic(ReadableType.Number, 2.5), 2.5);
333
+ assertDynamicContext("string", dynamic(ReadableType.String, "checkout"), "checkout");
334
+ assertDynamicContext(
335
+ "map",
336
+ dynamic(ReadableType.Map, readableMap(Map.of("count", 2))),
337
+ Map.of("count", 2));
338
+ assertDynamicContext(
339
+ "array",
340
+ dynamic(ReadableType.Array, readableArray(List.of("cart", 2))),
341
+ List.of("cart", 2));
342
+ }
343
+
344
+ @Test
345
+ public void legacyTranslationFailureIsSwallowedAndModuleNameIsStable() {
346
+ Promise promise = mock(Promise.class);
347
+ doThrow(new IllegalStateException("legacy failure"))
348
+ .when(nativeOperations)
349
+ .captureMessage(anyString(), any(DebugBundleLogLevel.class), any(Map.class));
350
+
351
+ module.enqueueEvent(readableMap(Map.of("event_type", "custom_event")), promise);
352
+
353
+ verify(promise).resolve(isNull());
354
+ verify(promise, never()).reject(anyString(), anyString());
355
+ org.junit.Assert.assertEquals(DebugBundleReactNativeModule.NAME, module.getName());
356
+ }
357
+
358
+ @Test
359
+ public void reactPackageRegistersOneModuleAndNoViewManagers() {
360
+ DebugBundleReactNativePackage reactPackage = new DebugBundleReactNativePackage();
361
+
362
+ List<NativeModule> modules = reactPackage.createNativeModules(reactContext);
363
+ List<ViewManager> viewManagers = reactPackage.createViewManagers(reactContext);
364
+
365
+ org.junit.Assert.assertEquals(1, modules.size());
366
+ org.junit.Assert.assertEquals(DebugBundleReactNativeModule.NAME, modules.get(0).getName());
367
+ assertTrue(viewManagers.isEmpty());
368
+ }
369
+
370
+ @Test
371
+ public void newArchitectureInteropCanParseEveryExportedMethod() throws Exception {
372
+ Class<?> interop = Class.forName(
373
+ "com.facebook.react.internal.turbomodule.core.TurboModuleInteropUtils");
374
+ java.lang.reflect.Method parser = interop.getDeclaredMethod(
375
+ "getMethodDescriptorsFromModule",
376
+ NativeModule.class);
377
+ parser.setAccessible(true);
378
+
379
+ List<?> descriptors = (List<?>) parser.invoke(null, module);
380
+
381
+ assertFalse(descriptors.isEmpty());
382
+ }
383
+
384
+ private void assertLegacyEventCalls(
385
+ Map<String, Object> event,
386
+ Runnable verification) {
387
+ Promise promise = mock(Promise.class);
388
+ module.enqueueEvent(readableMap(event), promise);
389
+ verification.run();
390
+ verify(promise).resolve(isNull());
391
+ }
392
+
393
+ private void assertDynamicContext(String key, Dynamic value, Object expected) {
394
+ Promise promise = mock(Promise.class);
395
+ module.setContext(key, value, promise);
396
+ verify(nativeOperations).setContext(key, expected);
397
+ verify(value).recycle();
398
+ verify(promise).resolve(isNull());
399
+ }
400
+
401
+ private static Dynamic dynamic(ReadableType type, Object value) {
402
+ Dynamic dynamic = mock(Dynamic.class);
403
+ when(dynamic.getType()).thenReturn(type);
404
+ when(dynamic.isNull()).thenReturn(type == ReadableType.Null);
405
+ switch (type) {
406
+ case Boolean:
407
+ when(dynamic.asBoolean()).thenReturn((Boolean) value);
408
+ break;
409
+ case Number:
410
+ when(dynamic.asDouble()).thenReturn((Double) value);
411
+ break;
412
+ case String:
413
+ when(dynamic.asString()).thenReturn((String) value);
414
+ break;
415
+ case Map:
416
+ when(dynamic.asMap()).thenReturn((ReadableMap) value);
417
+ break;
418
+ case Array:
419
+ when(dynamic.asArray()).thenReturn((ReadableArray) value);
420
+ break;
421
+ default:
422
+ break;
423
+ }
424
+ return dynamic;
425
+ }
426
+
427
+ @SuppressWarnings({"unchecked", "rawtypes"})
428
+ private static ReadableArray readableArray(List<?> values) {
429
+ ReadableArray array = mock(ReadableArray.class);
430
+ when(array.toArrayList()).thenReturn((java.util.ArrayList) new java.util.ArrayList<>((List) values));
431
+ return array;
432
+ }
433
+
434
+ @SuppressWarnings({"unchecked", "rawtypes"})
435
+ private static ReadableMap readableMap(Map<String, ?> values) {
436
+ Map<String, Object> copy = new HashMap<>((Map) values);
437
+ ReadableMap readableMap = mock(ReadableMap.class);
438
+ when(readableMap.hasKey(anyString()))
439
+ .thenAnswer(invocation -> copy.containsKey(invocation.getArgument(0)));
440
+ when(readableMap.isNull(anyString()))
441
+ .thenAnswer(invocation -> copy.get(invocation.getArgument(0)) == null);
442
+ when(readableMap.getString(anyString()))
443
+ .thenAnswer(invocation -> (String) copy.get(invocation.getArgument(0)));
444
+ when(readableMap.getBoolean(anyString()))
445
+ .thenAnswer(invocation -> (Boolean) copy.get(invocation.getArgument(0)));
446
+ when(readableMap.getInt(anyString()))
447
+ .thenAnswer(invocation -> ((Number) copy.get(invocation.getArgument(0))).intValue());
448
+ when(readableMap.getDouble(anyString()))
449
+ .thenAnswer(invocation -> ((Number) copy.get(invocation.getArgument(0))).doubleValue());
450
+ when(readableMap.getMap(anyString()))
451
+ .thenAnswer(invocation -> {
452
+ Object value = copy.get(invocation.getArgument(0));
453
+ if (value instanceof ReadableMap) {
454
+ return value;
455
+ }
456
+ if (value instanceof Map) {
457
+ return readableMap((Map<String, ?>) value);
458
+ }
459
+ return null;
460
+ });
461
+ when(readableMap.getArray(anyString()))
462
+ .thenAnswer(invocation -> {
463
+ Object value = copy.get(invocation.getArgument(0));
464
+ if (!(value instanceof List)) {
465
+ return null;
466
+ }
467
+ ReadableArray array = mock(ReadableArray.class);
468
+ when(array.toArrayList()).thenReturn((java.util.ArrayList) new java.util.ArrayList<>((List) value));
469
+ return array;
470
+ });
471
+ when(readableMap.toHashMap()).thenReturn(new HashMap<>(copy));
472
+ return readableMap;
473
+ }
474
+ }
package/app.plugin.js CHANGED
@@ -49,4 +49,4 @@ function withAndroidDesugaring(contents) {
49
49
  return next;
50
50
  }
51
51
 
52
- export default createRunOncePlugin(withDebugBundle, "@debugbundle/sdk-react-native", "1.0.0");
52
+ export default createRunOncePlugin(withDebugBundle, "@debugbundle/sdk-react-native", "1.2.0");
@@ -2,10 +2,14 @@ import type { TurboModule } from "react-native";
2
2
  import type { UnsafeObject } from "react-native/Libraries/Types/CodegenTypes";
3
3
  export interface Spec extends TurboModule {
4
4
  initialize(config: UnsafeObject): Promise<UnsafeObject>;
5
+ enqueueCanonicalEvent?(event: UnsafeObject): Promise<boolean>;
5
6
  enqueueEvent(event: UnsafeObject): Promise<void>;
7
+ captureProbe?(label: string, data: UnsafeObject, occurredAt: string): Promise<boolean>;
8
+ isProbeActive?(label: string): boolean;
6
9
  flush(): Promise<void>;
7
10
  getStatus(): Promise<UnsafeObject>;
8
- setContext(key: string, value: unknown): Promise<void>;
11
+ setContext(key: string, value: UnsafeObject): Promise<void>;
12
+ setContextValue?(key: string, entry: UnsafeObject): Promise<void>;
9
13
  activateProbeTriggerToken?(token: string): Promise<boolean>;
10
14
  }
11
15
  declare const _default: Spec;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeDebugBundleReactNative.d.ts","sourceRoot":"","sources":["../src/NativeDebugBundleReactNative.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAG9E,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACxD,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IACnC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,yBAAyB,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7D;;AAED,wBAAgF"}
1
+ {"version":3,"file":"NativeDebugBundleReactNative.d.ts","sourceRoot":"","sources":["../src/NativeDebugBundleReactNative.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAG9E,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACxD,qBAAqB,CAAC,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,YAAY,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvF,aAAa,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IACnC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,eAAe,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,yBAAyB,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7D;;AAED,wBAAgF"}
@@ -0,0 +1,3 @@
1
+ import type { DebugBundleBeforeSend, DebugBundleEventEnvelope } from "./types.js";
2
+ export declare function applyBeforeSend(event: DebugBundleEventEnvelope, hook: DebugBundleBeforeSend | null): DebugBundleEventEnvelope | null;
3
+ //# sourceMappingURL=before-send.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"before-send.d.ts","sourceRoot":"","sources":["../src/before-send.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AA2ClF,wBAAgB,eAAe,CAC7B,KAAK,EAAE,wBAAwB,EAC/B,IAAI,EAAE,qBAAqB,GAAG,IAAI,GACjC,wBAAwB,GAAG,IAAI,CAajC"}
@@ -0,0 +1,136 @@
1
+ const ROOT_KEYS = new Set([
2
+ "schema_version",
3
+ "event_id",
4
+ "event_type",
5
+ "sdk_name",
6
+ "sdk_version",
7
+ "service",
8
+ "occurred_at",
9
+ "correlation",
10
+ "context",
11
+ "payload"
12
+ ]);
13
+ const REQUIRED_PAYLOAD_FIELDS = {
14
+ frontend_exception: ["name", "message", "stack"],
15
+ frontend_breadcrumb: ["breadcrumb_type", "data"],
16
+ log_event: ["level", "message", "attributes"],
17
+ request_event: ["method", "path", "query", "headers", "response_status", "duration_ms"],
18
+ error_suppressed: ["fingerprint", "suppressed_count", "window_seconds", "first_seen", "last_seen"],
19
+ probe_event: ["label", "data", "activation_id", "probe_label_pattern"]
20
+ };
21
+ const ALLOWED_PAYLOAD_FIELDS = {
22
+ frontend_exception: new Set([
23
+ "name", "message", "stack", "route", "browser", "breadcrumbs", "device",
24
+ "browser_event", "rejection_reason", "dom_context", "probe_data"
25
+ ]),
26
+ frontend_breadcrumb: new Set(["breadcrumb_type", "route", "data", "device"]),
27
+ log_event: new Set(["level", "message", "attributes", "device"]),
28
+ request_event: new Set([
29
+ "method", "path", "query", "headers", "body", "response_status", "duration_ms",
30
+ "route_template", "response_headers", "response_body", "device"
31
+ ]),
32
+ error_suppressed: new Set([
33
+ "fingerprint", "suppressed_count", "window_seconds", "first_seen", "last_seen", "device"
34
+ ]),
35
+ probe_event: new Set(["label", "data", "activation_id", "probe_label_pattern", "device"])
36
+ };
37
+ const EVENT_TYPES = new Set(Object.keys(REQUIRED_PAYLOAD_FIELDS));
38
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
39
+ export function applyBeforeSend(event, hook) {
40
+ if (!hook) {
41
+ return event;
42
+ }
43
+ try {
44
+ const result = hook(cloneEvent(event));
45
+ if (result === null) {
46
+ return null;
47
+ }
48
+ return isValidEvent(result) ? result : event;
49
+ }
50
+ catch {
51
+ return event;
52
+ }
53
+ }
54
+ function cloneEvent(event) {
55
+ return JSON.parse(JSON.stringify(event));
56
+ }
57
+ function isValidEvent(event) {
58
+ if (!isRecord(event) || !Object.keys(event).every((key) => ROOT_KEYS.has(key))) {
59
+ return false;
60
+ }
61
+ if (event.schema_version !== "2026-03-01" ||
62
+ !UUID_PATTERN.test(stringValue(event.event_id)) ||
63
+ !EVENT_TYPES.has(stringValue(event.event_type)) ||
64
+ event.sdk_name !== "@debugbundle/sdk-react-native" ||
65
+ !nonEmptyString(event.sdk_version) ||
66
+ !isIsoTimestamp(event.occurred_at) ||
67
+ !isRecord(event.service) ||
68
+ !nonEmptyString(event.service.name) ||
69
+ !nonEmptyString(event.service.environment) ||
70
+ event.service.runtime !== "react-native" ||
71
+ event.service.framework !== "react-native" ||
72
+ !isRecord(event.payload)) {
73
+ return false;
74
+ }
75
+ const eventType = event.event_type;
76
+ return (REQUIRED_PAYLOAD_FIELDS[eventType].every((field) => Object.prototype.hasOwnProperty.call(event.payload, field)) &&
77
+ Object.keys(event.payload).every((field) => ALLOWED_PAYLOAD_FIELDS[eventType].has(field)) &&
78
+ hasValidPayloadShape(eventType, event.payload));
79
+ }
80
+ function hasValidPayloadShape(eventType, payload) {
81
+ switch (eventType) {
82
+ case "frontend_exception":
83
+ return (hasNonEmptyStrings(payload, "name", "message", "stack") &&
84
+ (!("breadcrumbs" in payload) || Array.isArray(payload.breadcrumbs)) &&
85
+ optionalRecord(payload, "probe_data"));
86
+ case "frontend_breadcrumb":
87
+ return hasNonEmptyStrings(payload, "breadcrumb_type") && isRecord(payload.data);
88
+ case "log_event":
89
+ return hasNonEmptyStrings(payload, "level", "message") && isRecord(payload.attributes);
90
+ case "request_event":
91
+ return (hasNonEmptyStrings(payload, "method", "path") &&
92
+ isRecord(payload.query) &&
93
+ isRecord(payload.headers) &&
94
+ isNonNegativeNumber(payload.response_status) &&
95
+ isNonNegativeNumber(payload.duration_ms) &&
96
+ optionalRecord(payload, "response_headers"));
97
+ case "error_suppressed":
98
+ return (hasNonEmptyStrings(payload, "fingerprint") &&
99
+ isNonNegativeInteger(payload.suppressed_count) &&
100
+ isPositiveInteger(payload.window_seconds) &&
101
+ isIsoTimestamp(payload.first_seen) &&
102
+ isIsoTimestamp(payload.last_seen));
103
+ case "probe_event":
104
+ return (hasNonEmptyStrings(payload, "label", "probe_label_pattern") &&
105
+ isRecord(payload.data) &&
106
+ (payload.activation_id === null ||
107
+ nonEmptyString(payload.activation_id) && UUID_PATTERN.test(payload.activation_id)));
108
+ }
109
+ }
110
+ function hasNonEmptyStrings(payload, ...fields) {
111
+ return fields.every((field) => nonEmptyString(payload[field]));
112
+ }
113
+ function optionalRecord(payload, field) {
114
+ return !(field in payload) || isRecord(payload[field]);
115
+ }
116
+ function isNonNegativeNumber(value) {
117
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
118
+ }
119
+ function isNonNegativeInteger(value) {
120
+ return Number.isInteger(value) && value >= 0;
121
+ }
122
+ function isPositiveInteger(value) {
123
+ return Number.isInteger(value) && value > 0;
124
+ }
125
+ function isRecord(value) {
126
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
127
+ }
128
+ function nonEmptyString(value) {
129
+ return typeof value === "string" && value.trim().length > 0;
130
+ }
131
+ function stringValue(value) {
132
+ return typeof value === "string" ? value : "";
133
+ }
134
+ function isIsoTimestamp(value) {
135
+ return nonEmptyString(value) && Number.isFinite(Date.parse(value));
136
+ }
package/dist/client.d.ts CHANGED
@@ -7,8 +7,6 @@ export declare class DebugBundleReactNativeClient implements DebugBundleClient {
7
7
  private breadcrumbs;
8
8
  private probeBuffers;
9
9
  private jsProbeActivationExpiresAt;
10
- private sessionSampledIn;
11
- private sessionEventCount;
12
10
  constructor(config?: DebugBundleConfig);
13
11
  get status(): DebugBundleStatus;
14
12
  get lastEventAt(): number | null;
@@ -29,7 +27,6 @@ export declare class DebugBundleReactNativeClient implements DebugBundleClient {
29
27
  private mergeContext;
30
28
  private snapshotProbeData;
31
29
  private hasActiveJsProbeActivation;
32
- private shouldCaptureRequestEvent;
33
30
  }
34
31
  export declare function createDebugBundleClient(config?: DebugBundleConfig): DebugBundleReactNativeClient;
35
32
  export declare function resolveConfig(config: DebugBundleConfig): ResolvedDebugBundleConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,EAEjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAGjB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAYpB,qBAAa,4BAA6B,YAAW,iBAAiB;IACpE,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,WAAW,CAAkE;IACrF,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,WAAW,CAAsC;IACzD,OAAO,CAAC,YAAY,CAAqD;IACzE,OAAO,CAAC,0BAA0B,CAAK;IACvC,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,iBAAiB,CAAK;gBAElB,MAAM,GAAE,iBAAsB;IAO1C,IAAI,MAAM,IAAI,iBAAiB,CAE9B;IAED,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,IAAI,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IA8BrC,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,yBAA8B,GAAG,IAAI;IAU/E,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,yBAA8B,GAAG,IAAI;IAI3E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,mBAA+B,EAAE,OAAO,GAAE,yBAA8B,GAAG,IAAI;IAYlH,cAAc,CACZ,OAAO,EAAE,sBAAsB,EAC/B,QAAQ,EAAE,uBAAuB,EACjC,OAAO,GAAE,yBAA8B,GACtC,IAAI;IAsBP,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,mBAA+B,EAAE,OAAO,GAAE,yBAA8B,GAAG,IAAI;IAQtH,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAO7C,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,EAAE,OAAO,GAAE,uBAA4B,GAAG,IAAI;IAmB5F,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAWtB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWhE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI;IASxE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,GAAG,IAAW,EAAE,MAAM,SAAqB,GAAG,IAAI;IAWzG,OAAO,CAAC,OAAO;IAoCf,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,0BAA0B;IAIlC,OAAO,CAAC,yBAAyB;CAGlC;AAED,wBAAgB,uBAAuB,CAAC,MAAM,GAAE,iBAAsB,GAAG,4BAA4B,CAEpG;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,yBAAyB,CAqClF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,EAEjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAIjB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAYpB,qBAAa,4BAA6B,YAAW,iBAAiB;IACpE,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,YAAY,CAAwC;IAC5D,OAAO,CAAC,WAAW,CAAkE;IACrF,OAAO,CAAC,OAAO,CAA+B;IAC9C,OAAO,CAAC,WAAW,CAAsC;IACzD,OAAO,CAAC,YAAY,CAAqD;IACzE,OAAO,CAAC,0BAA0B,CAAK;gBAE3B,MAAM,GAAE,iBAAsB;IAO1C,IAAI,MAAM,IAAI,iBAAiB,CAE9B;IAED,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,IAAI,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;IA8BrC,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,yBAA8B,GAAG,IAAI;IAc/E,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,yBAA8B,GAAG,IAAI;IAI3E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,mBAA+B,EAAE,OAAO,GAAE,yBAA8B,GAAG,IAAI;IAYlH,cAAc,CACZ,OAAO,EAAE,sBAAsB,EAC/B,QAAQ,EAAE,uBAAuB,EACjC,OAAO,GAAE,yBAA8B,GACtC,IAAI;IA+BP,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,mBAA+B,EAAE,OAAO,GAAE,yBAA8B,GAAG,IAAI;IAQtH,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAS7C,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,EAAE,OAAO,GAAE,uBAA4B,GAAG,IAAI;IA4B5F,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAWtB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWhE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI;IAcxE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,GAAG,IAAW,EAAE,MAAM,SAAqB,GAAG,IAAI;IAWzG,OAAO,CAAC,OAAO;IA6Cf,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,0BAA0B;CAInC;AAED,wBAAgB,uBAAuB,CAAC,MAAM,GAAE,iBAAsB,GAAG,4BAA4B,CAEpG;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,yBAAyB,CAsClF"}