@debugbundle/sdk-react-native 3.0.0 → 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 +23 -0
  2. package/DebugBundleReactNative.podspec +2 -2
  3. package/MIGRATION-3.1.md +116 -0
  4. package/README.md +38 -2
  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/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### 3.1.0 — 2026-11-01
8
+
9
+ - Reuse bounded custom privacy patterns and sensitive-key lookups within each
10
+ protection operation, preserving literal matching and caller-policy isolation.
11
+ - Add complete-path evidence capabilities, bounded structured JS errors,
12
+ synchronous operation scopes, and opt-in explicit recovery observations.
13
+ - Forward evidence options and native proof through the existing Android and iOS
14
+ bridges, with Android 3.1.0 and Swift 3.1.1 or later in the compatible 3.x line.
15
+ - Require the Swift native deadline fix so cancellation does not wait for a late URLSession callback.
16
+ - Validate owned before-send replacements against the complete canonical event
17
+ contract, retain allowed event/SDK metadata changes, and repeat privacy and
18
+ bounded association checks before native handoff.
19
+ - Preserve native device hydration, reject unsafe error getters and recursive
20
+ causes, and document unavailable engine evidence without inventing locations.
21
+ - Keep fatal JS evidence smaller and bypass application hooks on that path.
22
+
23
+ ## [3.0.1] - 2026-09-26
24
+
25
+ ### Fixed
26
+
27
+ - Adopt Android 3.0.1 and require Swift 3.0.1 or later in the compatible 3.x line, bringing the native acknowledgement and bounded retry repairs into React Native installs. The TypeScript facade and native bridge APIs are unchanged.
28
+ - Align embedded SDK versions and published-consumer pins with the patch release, and refresh native release and persistence guidance.
29
+
7
30
  ## [3.0.0] - 2026-09-25
8
31
 
9
32
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = "DebugBundleReactNative"
3
- s.version = "3.0.0"
3
+ s.version = "3.1.0"
4
4
  s.summary = "React Native wrapper for the DebugBundle mobile SDKs."
5
5
  s.license = { :type => "Apache-2.0", :file => "LICENSE" }
6
6
  s.author = { "DebugBundle" => "support@debugbundle.com" }
@@ -12,5 +12,5 @@ Pod::Spec.new do |s|
12
12
 
13
13
  s.dependency "React-Core"
14
14
  install_modules_dependencies(s) if defined?(install_modules_dependencies)
15
- s.dependency "DebugBundle", "~> 3.0"
15
+ s.dependency "DebugBundle", "~> 3.0", ">= 3.1.1"
16
16
  end
@@ -0,0 +1,116 @@
1
+ # React Native 3.1 candidate
2
+
3
+ Version 3.1.0 is an unpublished candidate. Rebuild the native binary with the
4
+ coordinated Android 3.1.0 and Swift 3.1.0 foundations before enabling the new
5
+ evidence options. Existing capture methods and their return values are unchanged.
6
+ An older native binary keeps ordinary error capture and disables unproven optional
7
+ evidence. This document does not establish release or device qualification.
8
+
9
+ The current iOS wrapper provides a bounded JSON-string carrier for canonical
10
+ events. React Native's object conversion can remove nested nulls; the string
11
+ carrier preserves required nullable stack, cause and device fields. Android's
12
+ existing canonical map carrier preserves nested nulls and numeric values.
13
+ The JavaScript client prefers the string method when present and retains both
14
+ older canonical-object and legacy-native fallbacks. New methods are optional on
15
+ the structural `DebugBundleClient` interface so existing custom implementations
16
+ continue to typecheck; the returned concrete client and facade provide them.
17
+
18
+ ## Evidence and delivery proof
19
+
20
+ `captureExceptionEvidence` defaults to true, but structured details are emitted
21
+ only when the native bridge reports complete-path support. Set it to false to
22
+ retain the ordinary outer error without structured details.
23
+ `captureRecoveryObservations` defaults to false. Enable it explicitly to capture
24
+ recovery observations. Native minimal policy or an explicit diagnostic disable
25
+ still prevents them. These options do not depend on probes or analytics.
26
+
27
+ The read-only `evidenceCapabilities` client/facade property reports the effective
28
+ intersection of JS options, native support, and the verified downstream path.
29
+ The optional configuration value with the same name pins a verified complete
30
+ path: `{ version: 1, features: [...] }`. Supported features are
31
+ `exception_evidence_v1`, `resource_observations_v1`, `recovery_v1`, and
32
+ `evidence_provenance_v1`. Unknown versions, duplicate or unknown features, and
33
+ extra descriptor fields disable support. A JS pin cannot make an old native
34
+ bridge preserve new evidence. Native config refresh and ingestion acknowledgements
35
+ carry proof; capture does not make a discovery request.
36
+
37
+ ## Explicit operation and recovery APIs
38
+
39
+ Both the facade and instance client expose `beginOperation()`,
40
+ `withOperationContext(callback)`, and `captureRecovery(observation)`.
41
+ An operation returns `{ operationId, evidenceScope, close() }`. Close it in a
42
+ `finally` block. Nested synchronous operations restore their parent; closing a
43
+ parent revokes its children. Reinitialization revokes existing scopes.
44
+
45
+ `withOperationContext` closes the implicit scope when the callback returns,
46
+ including when it returns a promise. It does not provide asynchronous context
47
+ propagation. Across `await`, retain the opaque `evidenceScope` value and pass it
48
+ explicitly to each capture; do not leave an implicit scope open across unrelated
49
+ tasks. At most 32 implicit scopes can nest.
50
+
51
+ ```ts
52
+ const client = createDebugBundleClient({
53
+ projectToken: "YOUR_PROJECT_TOKEN",
54
+ captureRecoveryObservations: true,
55
+ });
56
+ const operation = client.beginOperation();
57
+ const evidenceScope = operation.evidenceScope;
58
+ operation.close(); // Use explicit ownership across asynchronous application work.
59
+ const failureRef = createApplicationUuidV4();
60
+ const attemptId = createApplicationUuidV4();
61
+
62
+ client.captureException(error, { failureRef, evidenceScope, session_id: sessionId });
63
+ client.captureRecovery({
64
+ failureRef, evidenceScope, attemptId, state: "attempt_started",
65
+ operation: "reload", correlation: { session_id: sessionId },
66
+ });
67
+ // After the application's request completes:
68
+ client.captureRecovery({
69
+ failureRef, evidenceScope, attemptId, state: "attempt_completed", statusCode: 200,
70
+ correlation: { session_id: sessionId },
71
+ });
72
+ // Send recovery_observed only after the application confirms semantic recovery.
73
+ ```
74
+
75
+ Use your application's UUID v4 generator for each new failure and retry attempt;
76
+ references must not contain user, URL, or resource identifiers. Correlation must
77
+ match the original service, environment, scope, trace, and session. A page scope
78
+ also requires a nonempty session; the string `"0"` is valid. Null trace only
79
+ matches null trace. Recovery may arrive before the original failure, but its event
80
+ time must be at or after that failure and no more than five minutes later.
81
+
82
+ Recovery states are `attempt_started`, `attempt_completed`, `attempt_failed`,
83
+ `recovery_observed`, and `failure_persisted`. HTTP 200 and attempt completion do
84
+ not establish application recovery. Optional fields are `operation` (64 UTF-8
85
+ bytes), `statusCode` (100–599), and finite nonnegative `durationMs`. There is no
86
+ arbitrary data extension. These observations cannot change incident lifecycle.
87
+
88
+ ## Runtime limits and compatibility
89
+
90
+ Hermes/JSC engines without native `Error.isError` retain the outer error and
91
+ report cause traversal as unsupported. With that intrinsic, own native `cause`
92
+ data properties can be followed; accessors and custom stack formatters are not
93
+ invoked. Aggregate traversal remains unsupported because portable JS cannot prove
94
+ that an aggregate array is free of proxy traps. Stack parsing supports V8, Hermes
95
+ and JSC text and labels available traces `runtime_reported`; absent traces remain
96
+ unavailable. No native throw-site trace is invented.
97
+
98
+ Ordinary graphs use at most eight nodes, four causal edges in a path, 32 frames
99
+ per node, and the shared 32-KiB combined addition limit. The JS fatal handler uses
100
+ at most two nodes, eight total frames, and 4 KiB of combined additions and skips
101
+ application `beforeSend`; it still calls the previously installed error handler.
102
+ This does not add a process signal or native crash handler.
103
+
104
+ Diagnostic accounting limits one SDK instance to 50 windows, five attempts and
105
+ 20 observations per failure, and 64 KiB of metadata plus pending observations.
106
+ Pending bridge calls prevent expired windows from renewing. Native SDKs retain
107
+ their own accounting during offline queue ownership. Dropped or acknowledged
108
+ observations do not refund attempt/observation allowances.
109
+
110
+ Existing 64-KiB bridge events and bounded native call queues remain enforced.
111
+ Before-send callbacks may replace any valid canonical field, including event kind
112
+ and SDK metadata. Invalid/throwing callbacks retain the protected original; valid
113
+ oversized or unsafe replacements are withheld. Final privacy, capability, policy,
114
+ scope and byte checks apply to the replacement. Unchanged JS device placeholders
115
+ are hydrated by the native worker; explicitly replaced device metadata is retained.
116
+ Older bridge methods remain available.
package/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  React Native SDK for DebugBundle mobile apps.
4
4
 
5
+ Version 3.1.0 adds proof-gated structured exception details and
6
+ opt-in explicit recovery through both native bridges. See the
7
+ [3.1 migration and API guide](MIGRATION-3.1.md) for options, operation scopes,
8
+ recovery examples, runtime limitations, and coordinated native dependencies.
9
+
5
10
  This package targets iOS and Android React Native apps. It is a mobile direct-ingestion SDK, not a browser relay host: it does not provide CORS, `allowedOrigins`, `transportMode`, or `/debugbundle/browser` helpers.
6
11
 
7
12
  ## Status
@@ -142,8 +147,39 @@ checks.
142
147
 
143
148
  ## Release
144
149
 
145
- The package publishes to npm from `v*` tags through GitHub Actions. Configure an npm GitHub Actions trusted publisher for `debugbundle/debugbundle-react-native` and `release.yml`, with a blank environment and direct `npm publish` enabled. Publishing uses OIDC without a long-lived npm token. Make sure the tag matches `package.json` exactly. The next protected release requires a matching 2.x wrapper/Swift pod line and a published Android 2.x BOM/module pin. `make check-protected-native-pins` refuses the current 1.x source line; update the wrapper version, native pins, and embedded SDK versions together only after the exact Android and Swift artifacts are available to Maven Central and CocoaPods consumers. The release workflow then compiles and runs clean apps against those published artifacts on Android API 37 and Xcode 27 before npm publication.
150
+ The package publishes to npm from `v*` tags through GitHub Actions. Configure an npm GitHub Actions trusted publisher for `debugbundle/debugbundle-react-native` and `release.yml`, with a blank environment and direct `npm publish` enabled. Publishing uses OIDC without a long-lived npm token. Make sure the tag matches `package.json` exactly. The 3.1.0 wrapper pins Android 3.1.0 and requires Swift 3.1.1 or later within the compatible 3.x CocoaPods line. `make check-protected-native-pins` checks the wrapper, native pins and embedded SDK versions together. Publish only after the exact Android and Swift artifacts are available and their published-consumer checks pass. The release workflow compiles and runs clean apps against those published artifacts on Android API 37 and Xcode 27 before npm publication.
146
151
 
147
- Probe and context native calls share the event bridge's pending count and byte limits, including reserved exception capacity. Saturated auxiliary calls are discarded without evaluating probe suppliers. Probe labels are limited to 128 characters. Persistent JS context retains at most 50 fields with keys up to 128 characters, and field-sensitive privacy runs before local retention and native handoff. Throwing capture context accessors are withheld without escaping into application code. These bounds do not move native SDK persistence off its caller thread.
152
+ Probe and context native calls share the event bridge's pending count and byte limits, including reserved exception capacity. Saturated auxiliary calls are discarded without evaluating probe suppliers. Probe labels are limited to 128 characters. Persistent JS context retains at most 50 fields with keys up to 128 characters, and field-sensitive privacy runs before local retention and native handoff. Throwing capture context accessors are withheld without escaping into application code. The native 3.x SDKs perform queue persistence on their bounded background workers; abrupt process termination before persistence completes can lose admitted events.
148
153
 
149
154
  Version 3 defers optional `beforeSend` callbacks until capture returns. Callbacks run on the JavaScript event loop and must return promptly. Pending preparation and native delivery share bounded count/byte limits; valid expanded replacements may be dropped under pressure. See [the version3 migration guide](MIGRATION-3.0.md).
155
+
156
+ ## Probe checkpoint guidance
157
+
158
+ Application instrumentation supplies probe labels and bounded data. Deploy a new
159
+ checkpoint before activating it. Remote activation affects future matching calls
160
+ when policy permits; it does not inject code or recover unretained measurements.
161
+ Built-in runtime/error/device collectors keep their documented SDK settings.
162
+
163
+ Use fixed labels and allowlisted technical states: `media.preview.checkpoint`
164
+ (stage/input kind/attempt), `auth.session.checkpoint` (observed session presence
165
+ and result), `job.dependency.checkpoint` (named dependency/attempt/result), or
166
+ `cache.preview.checkpoint` (hit/miss/coarse age bucket). Omit customer identifiers,
167
+ tokens/cookies, cache keys, URLs and raw payloads. HTTP 200 or attempt completion
168
+ alone does not establish semantic recovery.
169
+
170
+ Empty probe data means no matching retained observation for the selected sample
171
+ window. It does not confirm activation delivery to every SDK or execution of a
172
+ label. Check the deployed SDK/wrapper version, instrumentation, scope, expiry,
173
+ policy, sampling and delivery receipts. See the [probe recipes](https://debugbundle.com/docs/probes#bounded-checkpoint-recipes).
174
+
175
+ `make evidence-performance` compares 20 identical native JS error/recovery pairs
176
+ with evidence disabled/enabled and an empty/full low-priority bridge queue. It uses
177
+ built SDK modules, a held in-memory bridge, network-disabled Docker and Node/V8.
178
+ Ordinary timing runs are separate from allocation-sampled runs; the latter retain
179
+ major/minor-GC-collected samples and report estimated allocation, not exact byte
180
+ counts. Heap deltas, SDK-owned bytes and diagnostic budgets are reported separately.
181
+ Setup and error creation are excluded; all original failures, two-node causes and
182
+ queue release are checked. The [V8 protocol](https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json/js_protocol.json)
183
+ defines sampling semantics. These facade measurements do not establish Hermes,
184
+ Android ART or actual native bridge performance; the native qualification lanes
185
+ remain separate.
@@ -11,7 +11,7 @@ if (isReactNativeHostBuild) {
11
11
  def hostKotlinVersion = rootProject.ext.has("kotlinVersion")
12
12
  ? rootProject.ext.get("kotlinVersion")
13
13
  : "2.1.20"
14
- def debugBundleAndroidVersion = rootProject.findProperty("debugBundleAndroidVersion") ?: "3.0.0"
14
+ def debugBundleAndroidVersion = rootProject.findProperty("debugBundleAndroidVersion") ?: "3.1.0"
15
15
 
16
16
  android {
17
17
  namespace "com.debugbundle.reactnative"
@@ -3,6 +3,8 @@ package com.debugbundle.reactnative;
3
3
  import android.content.Context;
4
4
  import com.debugbundle.android.DebugBundle;
5
5
  import com.debugbundle.android.DebugBundleConfig;
6
+ import com.debugbundle.android.DebugBundleEvidenceOptions;
7
+ import com.debugbundle.android.DebugBundleEvidenceCapabilities;
6
8
  import com.debugbundle.android.DebugBundleLogLevel;
7
9
  import com.debugbundle.android.DebugBundleRequestInfo;
8
10
  import com.debugbundle.android.DebugBundleResponseInfo;
@@ -14,6 +16,7 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule;
14
16
  import com.facebook.react.bridge.ReactMethod;
15
17
  import com.facebook.react.bridge.ReadableMap;
16
18
  import com.facebook.react.bridge.WritableMap;
19
+ import com.facebook.react.bridge.WritableArray;
17
20
  import java.lang.reflect.Method;
18
21
  import java.util.ArrayList;
19
22
  import java.util.Collections;
@@ -199,12 +202,12 @@ public final class DebugBundleReactNativeModule extends ReactContextBaseJavaModu
199
202
  }
200
203
  }
201
204
 
202
- private String currentSdkVersion = "3.0.0";
205
+ private String currentSdkVersion = "3.1.0";
203
206
  private String currentService = "react-native-app";
204
207
  private String currentEnvironment = "production";
205
208
 
206
209
  private DebugBundleConfig createConfig(ReadableMap config) {
207
- currentSdkVersion = stringOrDefault(config, "sdkVersion", "3.0.0");
210
+ currentSdkVersion = stringOrDefault(config, "sdkVersion", "3.1.0");
208
211
  currentService = stringOrDefault(config, "service", "react-native-app");
209
212
  currentEnvironment = stringOrDefault(config, "environment", "production");
210
213
  return DebugBundleConfig.create(
@@ -236,7 +239,10 @@ public final class DebugBundleReactNativeModule extends ReactContextBaseJavaModu
236
239
  intOrDefault(config, "maxProbeEntriesPerLabel", 10),
237
240
  booleanOrDefault(config, "probeFlushOnError", true),
238
241
  stringSet(config, "redactFields", Collections.emptySet()),
239
- currentSdkVersion);
242
+ currentSdkVersion).withEvidenceOptions(new DebugBundleEvidenceOptions(
243
+ booleanOrDefault(config, "captureExceptionEvidence", true),
244
+ booleanOrDefault(config, "captureRecoveryObservations", false),
245
+ evidenceCapabilities(config)));
240
246
  }
241
247
 
242
248
  private WritableMap stateMap() {
@@ -250,6 +256,11 @@ public final class DebugBundleReactNativeModule extends ReactContextBaseJavaModu
250
256
  }
251
257
  state.putBoolean("nativeModuleAvailable", true);
252
258
  state.putNull("degradedReason");
259
+ DebugBundleEvidenceCapabilities proof = DebugBundleEvidenceCapabilities.normalize(nativeOperations.evidenceCapabilities());
260
+ WritableMap descriptor = writableMapFactory.create();
261
+ descriptor.putInt("version", proof.getVersion());
262
+ descriptor.putArray("features", writableMapFactory.array(proof.getFeatures()));
263
+ state.putMap("evidenceCapabilities", descriptor);
253
264
  return state;
254
265
  }
255
266
 
@@ -270,6 +281,22 @@ public final class DebugBundleReactNativeModule extends ReactContextBaseJavaModu
270
281
  return nested.toHashMap();
271
282
  }
272
283
 
284
+ private static DebugBundleEvidenceCapabilities evidenceCapabilities(ReadableMap config) {
285
+ if (!config.hasKey("evidenceCapabilities") || config.isNull("evidenceCapabilities")) return null;
286
+ Map<String, Object> descriptor = hashMap(config, "evidenceCapabilities");
287
+ if (descriptor.size() != 2 || !descriptor.containsKey("version") || !descriptor.containsKey("features") ||
288
+ !(descriptor.get("version") instanceof Number) || ((Number) descriptor.get("version")).doubleValue() != 1 ||
289
+ !(descriptor.get("features") instanceof List<?>)) return new DebugBundleEvidenceCapabilities();
290
+ List<?> raw = (List<?>) descriptor.get("features");
291
+ if (raw.size() > 4) return new DebugBundleEvidenceCapabilities();
292
+ List<String> features = new ArrayList<>();
293
+ for (Object value : raw) {
294
+ if (!(value instanceof String)) return new DebugBundleEvidenceCapabilities();
295
+ features.add((String) value);
296
+ }
297
+ return DebugBundleEvidenceCapabilities.normalize(new DebugBundleEvidenceCapabilities(1, features));
298
+ }
299
+
273
300
  private static String stringOrDefault(ReadableMap map, String key, String fallback) {
274
301
  if (!map.hasKey(key) || map.isNull(key)) {
275
302
  return fallback;
@@ -422,9 +449,11 @@ public final class DebugBundleReactNativeModule extends ReactContextBaseJavaModu
422
449
 
423
450
  interface WritableMapFactory {
424
451
  WritableMap create();
452
+ default WritableArray array(List<String> values) { return Arguments.fromList(values); }
425
453
  }
426
454
 
427
455
  interface NativeOperations {
456
+ default DebugBundleEvidenceCapabilities evidenceCapabilities() { return new DebugBundleEvidenceCapabilities(); }
428
457
  void initialize(Context context, DebugBundleConfig config);
429
458
 
430
459
  void captureException(Throwable error, Map<String, Object> context);
@@ -471,6 +500,8 @@ public final class DebugBundleReactNativeModule extends ReactContextBaseJavaModu
471
500
  }
472
501
 
473
502
  private static final class DefaultNativeOperations implements NativeOperations {
503
+ @Override
504
+ public DebugBundleEvidenceCapabilities evidenceCapabilities() { return DebugBundle.INSTANCE.getEvidenceCapabilities(); }
474
505
  @Override
475
506
  public void initialize(Context context, DebugBundleConfig config) {
476
507
  DebugBundle.init(context, config);
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", "3.0.0");
52
+ export default createRunOncePlugin(withDebugBundle, "@debugbundle/sdk-react-native", "3.1.0");
@@ -3,6 +3,7 @@ import type { UnsafeObject } from "react-native/Libraries/Types/CodegenTypes";
3
3
  export interface Spec extends TurboModule {
4
4
  initialize(config: UnsafeObject): Promise<UnsafeObject>;
5
5
  enqueueCanonicalEvent?(event: UnsafeObject): Promise<boolean>;
6
+ enqueueCanonicalEventJson?(event: string): Promise<boolean>;
6
7
  enqueueEvent(event: UnsafeObject): Promise<void>;
7
8
  captureProbe?(label: string, data: UnsafeObject, occurredAt: string): Promise<boolean>;
8
9
  isProbeActive?(label: string): boolean;
@@ -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,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"}
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,yBAAyB,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5D,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"}
@@ -1 +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,CAmBjC"}
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;AAIlF,wBAAgB,eAAe,CAAC,KAAK,EAAE,wBAAwB,EAAE,IAAI,EAAE,qBAAqB,GAAG,IAAI,GAAG,wBAAwB,GAAG,IAAI,CASpI"}
@@ -1,142 +1,20 @@
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;
1
+ import { canonicalEvent } from "./evidence-wire.js";
2
+ import { EventValidationUnavailable, ownJSON } from "./owned-json.js";
39
3
  export function applyBeforeSend(event, hook) {
40
- if (!hook) {
4
+ if (!hook)
41
5
  return event;
42
- }
43
6
  try {
44
- const result = hook(cloneEvent(event));
45
- // Hooks have synchronous return contracts. Contain mistaken async failures
46
- // before treating their Promise as invalid and keeping the protected original.
7
+ const result = hook(JSON.parse(JSON.stringify(event)));
47
8
  if (result instanceof Promise) {
48
9
  void result.catch(() => undefined);
49
10
  return event;
50
11
  }
51
- if (result === null) {
12
+ if (result === null)
52
13
  return null;
53
- }
54
- return isValidEvent(result) ? result : event;
14
+ const owned = ownJSON(result);
15
+ return canonicalEvent(owned) ? owned : event;
55
16
  }
56
- catch {
57
- return event;
58
- }
59
- }
60
- function cloneEvent(event) {
61
- return JSON.parse(JSON.stringify(event));
62
- }
63
- function isValidEvent(event) {
64
- if (!isRecord(event) || !Object.keys(event).every((key) => ROOT_KEYS.has(key))) {
65
- return false;
66
- }
67
- if (event.schema_version !== "2026-03-01" ||
68
- !UUID_PATTERN.test(stringValue(event.event_id)) ||
69
- !EVENT_TYPES.has(stringValue(event.event_type)) ||
70
- event.sdk_name !== "@debugbundle/sdk-react-native" ||
71
- !nonEmptyString(event.sdk_version) ||
72
- !isIsoTimestamp(event.occurred_at) ||
73
- !isRecord(event.service) ||
74
- !nonEmptyString(event.service.name) ||
75
- !nonEmptyString(event.service.environment) ||
76
- event.service.runtime !== "react-native" ||
77
- event.service.framework !== "react-native" ||
78
- !isRecord(event.payload)) {
79
- return false;
80
- }
81
- const eventType = event.event_type;
82
- return (REQUIRED_PAYLOAD_FIELDS[eventType].every((field) => Object.prototype.hasOwnProperty.call(event.payload, field)) &&
83
- Object.keys(event.payload).every((field) => ALLOWED_PAYLOAD_FIELDS[eventType].has(field)) &&
84
- hasValidPayloadShape(eventType, event.payload));
85
- }
86
- function hasValidPayloadShape(eventType, payload) {
87
- switch (eventType) {
88
- case "frontend_exception":
89
- return (hasNonEmptyStrings(payload, "name", "message", "stack") &&
90
- (!("breadcrumbs" in payload) || Array.isArray(payload.breadcrumbs)) &&
91
- optionalRecord(payload, "probe_data"));
92
- case "frontend_breadcrumb":
93
- return hasNonEmptyStrings(payload, "breadcrumb_type") && isRecord(payload.data);
94
- case "log_event":
95
- return hasNonEmptyStrings(payload, "level", "message") && isRecord(payload.attributes);
96
- case "request_event":
97
- return (hasNonEmptyStrings(payload, "method", "path") &&
98
- isRecord(payload.query) &&
99
- isRecord(payload.headers) &&
100
- isNonNegativeNumber(payload.response_status) &&
101
- isNonNegativeNumber(payload.duration_ms) &&
102
- optionalRecord(payload, "response_headers"));
103
- case "error_suppressed":
104
- return (hasNonEmptyStrings(payload, "fingerprint") &&
105
- isNonNegativeInteger(payload.suppressed_count) &&
106
- isPositiveInteger(payload.window_seconds) &&
107
- isIsoTimestamp(payload.first_seen) &&
108
- isIsoTimestamp(payload.last_seen));
109
- case "probe_event":
110
- return (hasNonEmptyStrings(payload, "label", "probe_label_pattern") &&
111
- isRecord(payload.data) &&
112
- (payload.activation_id === null ||
113
- nonEmptyString(payload.activation_id) && UUID_PATTERN.test(payload.activation_id)));
17
+ catch (error) {
18
+ return error instanceof EventValidationUnavailable ? null : event;
114
19
  }
115
20
  }
116
- function hasNonEmptyStrings(payload, ...fields) {
117
- return fields.every((field) => nonEmptyString(payload[field]));
118
- }
119
- function optionalRecord(payload, field) {
120
- return !(field in payload) || isRecord(payload[field]);
121
- }
122
- function isNonNegativeNumber(value) {
123
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
124
- }
125
- function isNonNegativeInteger(value) {
126
- return Number.isInteger(value) && value >= 0;
127
- }
128
- function isPositiveInteger(value) {
129
- return Number.isInteger(value) && value > 0;
130
- }
131
- function isRecord(value) {
132
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
133
- }
134
- function nonEmptyString(value) {
135
- return typeof value === "string" && value.trim().length > 0;
136
- }
137
- function stringValue(value) {
138
- return typeof value === "string" ? value : "";
139
- }
140
- function isIsoTimestamp(value) {
141
- return nonEmptyString(value) && Number.isFinite(Date.parse(value));
142
- }
@@ -0,0 +1,15 @@
1
+ import { type OwnedJSON } from "./owned-json.js";
2
+ export declare function canonicalTimestamp(text: string): boolean;
3
+ /** Small closed Draft-7 interpreter for the generated accepting contract. */
4
+ export declare class CanonicalSchema {
5
+ private refs;
6
+ private patterns;
7
+ private root;
8
+ constructor(schema: unknown);
9
+ private resolve;
10
+ private compile;
11
+ accepts(value: OwnedJSON): boolean;
12
+ private matches;
13
+ }
14
+ export declare function canonicalShape(value: OwnedJSON): boolean;
15
+ //# sourceMappingURL=canonical-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-schema.d.ts","sourceRoot":"","sources":["../src/canonical-schema.ts"],"names":[],"mappings":"AACA,OAAO,EAA0C,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAQzF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOxD;AAED,6EAA6E;AAC7E,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,IAAI,CAAY;gBACZ,MAAM,EAAE,OAAO;IAE3B,OAAO,CAAC,OAAO;IAcf,OAAO,CAAC,OAAO;IAaf,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO;IAwClC,OAAO,CAAC,OAAO;CAQhB;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAGxD"}