@debugbundle/sdk-react-native 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/DebugBundleReactNative.podspec +2 -2
- package/MIGRATION-3.1.md +116 -0
- package/README.md +37 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +34 -3
- package/app.plugin.js +1 -1
- package/dist/NativeDebugBundleReactNative.d.ts +1 -0
- package/dist/NativeDebugBundleReactNative.d.ts.map +1 -1
- package/dist/before-send.d.ts.map +1 -1
- package/dist/before-send.js +9 -131
- package/dist/canonical-schema.d.ts +15 -0
- package/dist/canonical-schema.d.ts.map +1 -0
- package/dist/canonical-schema.js +139 -0
- package/dist/client-values.d.ts +17 -0
- package/dist/client-values.d.ts.map +1 -0
- package/dist/client-values.js +185 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +102 -205
- package/dist/diagnostic-budget.d.ts +16 -0
- package/dist/diagnostic-budget.d.ts.map +1 -0
- package/dist/diagnostic-budget.js +120 -0
- package/dist/event-schema-data.d.ts +2 -0
- package/dist/event-schema-data.d.ts.map +1 -0
- package/dist/event-schema-data.js +49 -0
- package/dist/evidence-capabilities.d.ts +5 -0
- package/dist/evidence-capabilities.d.ts.map +1 -0
- package/dist/evidence-capabilities.js +21 -0
- package/dist/evidence-controller.d.ts +24 -0
- package/dist/evidence-controller.d.ts.map +1 -0
- package/dist/evidence-controller.js +113 -0
- package/dist/evidence-types.d.ts +33 -0
- package/dist/evidence-types.d.ts.map +1 -0
- package/dist/evidence-types.js +1 -0
- package/dist/evidence-wire.d.ts +7 -0
- package/dist/evidence-wire.d.ts.map +1 -0
- package/dist/evidence-wire.js +132 -0
- package/dist/exception-evidence.d.ts +25 -0
- package/dist/exception-evidence.d.ts.map +1 -0
- package/dist/exception-evidence.js +175 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/owned-json.d.ts +11 -0
- package/dist/owned-json.d.ts.map +1 -0
- package/dist/owned-json.js +85 -0
- package/dist/privacy-keys.d.ts +2 -0
- package/dist/privacy-keys.d.ts.map +1 -1
- package/dist/privacy-keys.js +10 -2
- package/dist/privacy-telemetry.js +7 -7
- package/dist/privacy-text.d.ts +2 -2
- package/dist/privacy-text.d.ts.map +1 -1
- package/dist/privacy-text.js +35 -33
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +9 -7
- package/dist/safe-error-fields.d.ts +4 -0
- package/dist/safe-error-fields.d.ts.map +1 -0
- package/dist/safe-error-fields.js +54 -0
- package/dist/types.d.ts +21 -6
- package/dist/types.d.ts.map +1 -1
- package/ios/DebugBundleReactNative.swift +58 -4
- package/ios/DebugBundleReactNativeBridge.mm +4 -0
- package/package.json +3 -2
- package/src/NativeDebugBundleReactNative.ts +1 -0
- package/src/before-send.ts +10 -174
- package/src/canonical-schema.ts +108 -0
- package/src/client-values.ts +196 -0
- package/src/client.ts +92 -213
- package/src/diagnostic-budget.ts +90 -0
- package/src/event-schema-data.ts +49 -0
- package/src/evidence-capabilities.ts +19 -0
- package/src/evidence-controller.ts +89 -0
- package/src/evidence-types.ts +14 -0
- package/src/evidence-wire.ts +100 -0
- package/src/exception-evidence.ts +112 -0
- package/src/index.ts +6 -0
- package/src/owned-json.ts +60 -0
- package/src/privacy-keys.ts +12 -2
- package/src/privacy-telemetry.ts +9 -9
- package/src/privacy-text.ts +32 -31
- package/src/redaction.ts +8 -7
- package/src/safe-error-fields.ts +46 -0
- package/src/types.ts +23 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ 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
|
+
|
|
7
23
|
## [3.0.1] - 2026-09-26
|
|
8
24
|
|
|
9
25
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Pod::Spec.new do |s|
|
|
2
2
|
s.name = "DebugBundleReactNative"
|
|
3
|
-
s.version = "3.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", ">= 3.
|
|
15
|
+
s.dependency "DebugBundle", "~> 3.0", ">= 3.1.1"
|
|
16
16
|
end
|
package/MIGRATION-3.1.md
ADDED
|
@@ -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 3.0
|
|
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
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.
|
package/android/build.gradle
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
@@ -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;
|
|
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"}
|
package/dist/before-send.js
CHANGED
|
@@ -1,142 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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(
|
|
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
|
|
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"}
|