@debugbundle/sdk-react-native 1.3.0 → 2.0.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.
@@ -1,474 +0,0 @@
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
- }