@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.
- package/CHANGELOG.md +23 -0
- package/DebugBundleReactNative.podspec +2 -2
- package/MIGRATION-3.1.md +116 -0
- package/README.md +38 -2
- 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/dist/client.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { utf8Bytes as utf8ByteLength } from "./owned-json.js";
|
|
2
|
+
import { normalizeNativeState, stringValue, asRecord, objectWrap, parseRequestUrl, sanitizeScreenName, generateId, canonicalDevice, toLegacyNativeEvent, correlationFromContext, sameMobileDevice } from "./client-values.js";
|
|
3
|
+
import { MobileEvidenceController, fitMobileEvent } from "./evidence-controller.js";
|
|
4
|
+
import { normalizeCapabilities } from "./evidence-capabilities.js";
|
|
5
|
+
import { canonicalEvent } from "./evidence-wire.js";
|
|
6
|
+
import { ownJSON } from "./owned-json.js";
|
|
7
|
+
import { snapshotError, snapshotExceptionEvidence } from "./exception-evidence.js";
|
|
1
8
|
import { getNativeModule, degradedNativeState, safeNativeCall } from "./native.js";
|
|
2
9
|
import { applyBeforeSend } from "./before-send.js";
|
|
3
10
|
import { defaultRedactFields, DEFAULT_HEADER_ALLOWLIST, sanitizeHeaders, sanitizeValue } from "./redaction.js";
|
|
4
11
|
import { sanitizeTelemetry } from "./privacy-telemetry.js";
|
|
5
12
|
const SDK_NAME = "@debugbundle/sdk-react-native";
|
|
6
|
-
const DEFAULT_SDK_VERSION = "3.
|
|
13
|
+
const DEFAULT_SDK_VERSION = "3.1.0";
|
|
7
14
|
const MAX_PENDING_NATIVE_CALLS = 256;
|
|
8
15
|
const MAX_PENDING_LOW_PRIORITY_CALLS = 224;
|
|
9
16
|
const MAX_PENDING_NATIVE_BYTES = 4 * 1024 * 1024;
|
|
@@ -59,8 +66,10 @@ export class DebugBundleReactNativeClient {
|
|
|
59
66
|
jsProbeActivationExpiresAt = 0;
|
|
60
67
|
pendingNativeCalls = 0;
|
|
61
68
|
pendingNativeBytes = 0;
|
|
69
|
+
evidence;
|
|
62
70
|
constructor(config = {}) {
|
|
63
71
|
this.config = resolveConfig(config);
|
|
72
|
+
this.evidence = new MobileEvidenceController(this.config);
|
|
64
73
|
if (Object.keys(config).length > 0) {
|
|
65
74
|
this.init(config);
|
|
66
75
|
}
|
|
@@ -71,8 +80,34 @@ export class DebugBundleReactNativeClient {
|
|
|
71
80
|
get lastEventAt() {
|
|
72
81
|
return this.nativeState.lastEventAt ?? null;
|
|
73
82
|
}
|
|
83
|
+
get evidenceCapabilities() {
|
|
84
|
+
return this.evidence.capabilities(this.nativeState, this.hasCanonicalBridge());
|
|
85
|
+
}
|
|
86
|
+
hasCanonicalBridge() {
|
|
87
|
+
return this.nativeModule?.enqueueCanonicalEventJson !== undefined || this.nativeModule?.enqueueCanonicalEvent !== undefined;
|
|
88
|
+
}
|
|
89
|
+
beginOperation() { return this.evidence.begin(); }
|
|
90
|
+
withOperationContext(operation) {
|
|
91
|
+
const scope = this.beginOperation();
|
|
92
|
+
try {
|
|
93
|
+
return operation(scope);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
scope.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
captureRecovery(observation) {
|
|
100
|
+
if (!this.canCapture("diagnostic_context", true) || !this.hasNativeCapacity(false) ||
|
|
101
|
+
!this.evidenceCapabilities.features.includes("recovery_v1"))
|
|
102
|
+
return;
|
|
103
|
+
const prepared = this.evidence.recovery(observation);
|
|
104
|
+
if (prepared)
|
|
105
|
+
this.enqueue("diagnostic_context", prepared.payload, null, true, undefined, true, true, prepared.correlation);
|
|
106
|
+
}
|
|
74
107
|
init(config) {
|
|
108
|
+
this.evidence.close();
|
|
75
109
|
this.config = resolveConfig(config);
|
|
110
|
+
this.evidence = new MobileEvidenceController(this.config);
|
|
76
111
|
this.nativeModule = getNativeModule();
|
|
77
112
|
if (!this.config.enabled) {
|
|
78
113
|
this.nativeState = { status: "disconnected", lastEventAt: null, nativeModuleAvailable: Boolean(this.nativeModule) };
|
|
@@ -87,11 +122,14 @@ export class DebugBundleReactNativeClient {
|
|
|
87
122
|
return;
|
|
88
123
|
}
|
|
89
124
|
try {
|
|
125
|
+
const initializedConfig = this.config;
|
|
90
126
|
const initialization = this.nativeModule.initialize(nativeConfig(this.config));
|
|
91
127
|
if (isPromiseLike(initialization)) {
|
|
92
128
|
this.nativeState = { status: "healthy", lastEventAt: null, nativeModuleAvailable: true };
|
|
93
129
|
void safeNativeCall(async () => {
|
|
94
|
-
|
|
130
|
+
const state = await initialization;
|
|
131
|
+
if (this.config === initializedConfig)
|
|
132
|
+
this.nativeState = normalizeNativeState(state);
|
|
95
133
|
}, undefined);
|
|
96
134
|
}
|
|
97
135
|
else {
|
|
@@ -106,17 +144,21 @@ export class DebugBundleReactNativeClient {
|
|
|
106
144
|
if (!this.config.captureErrors || !this.canCapture("frontend_exception", false) ||
|
|
107
145
|
!this.hasNativeCapacity(true))
|
|
108
146
|
return;
|
|
109
|
-
const mergedContext = this.mergeContext(context);
|
|
110
|
-
const sanitizedError =
|
|
147
|
+
const mergedContext = this.evidence.context(this.mergeContext(context), this.nativeState, this.hasCanonicalBridge());
|
|
148
|
+
const sanitizedError = snapshotError(error, this.config.redactFields);
|
|
149
|
+
const fatal = mergedContext.source === "react-native-global-error" && mergedContext.fatal === true;
|
|
150
|
+
const graph = this.evidenceCapabilities.features.includes("exception_evidence_v1")
|
|
151
|
+
? snapshotExceptionEvidence(error, sanitizedError, this.config.redactFields, fatal) : undefined;
|
|
111
152
|
this.enqueue("frontend_exception", {
|
|
112
153
|
name: stringValue(sanitizedError.name) ?? "Error",
|
|
113
154
|
message: stringValue(sanitizedError.message) ?? "Unknown error",
|
|
114
155
|
stack: stringValue(sanitizedError.stack) ?? `${stringValue(sanitizedError.name) ?? "Error"}: ${stringValue(sanitizedError.message) ?? "Unknown error"}`,
|
|
115
156
|
breadcrumbs: this.breadcrumbs.slice(),
|
|
116
|
-
probe_data: this.snapshotProbeData()
|
|
157
|
+
probe_data: this.snapshotProbeData(),
|
|
158
|
+
...(graph ? { exception_evidence: graph } : {})
|
|
117
159
|
}, stringValue(mergedContext.trace_id), false, asRecord(sanitizeValue(mergedContext, {
|
|
118
160
|
redactFields: this.config.redactFields
|
|
119
|
-
})), this.config.captureErrors);
|
|
161
|
+
})), this.config.captureErrors, !fatal, correlationFromContext(mergedContext));
|
|
120
162
|
}
|
|
121
163
|
captureError(error, context = {}) {
|
|
122
164
|
this.captureException(error, context);
|
|
@@ -236,7 +278,7 @@ export class DebugBundleReactNativeClient {
|
|
|
236
278
|
await safeNativeCall(async () => {
|
|
237
279
|
await this.nativeModule.flush();
|
|
238
280
|
const state = await this.nativeModule.getStatus();
|
|
239
|
-
this.nativeState = state;
|
|
281
|
+
this.nativeState = normalizeNativeState(state);
|
|
240
282
|
}, undefined);
|
|
241
283
|
}
|
|
242
284
|
async activateProbeTriggerToken(token) {
|
|
@@ -258,7 +300,8 @@ export class DebugBundleReactNativeClient {
|
|
|
258
300
|
this.breadcrumbs.push(breadcrumb);
|
|
259
301
|
this.breadcrumbs = this.breadcrumbs.slice(-this.config.maxBreadcrumbs);
|
|
260
302
|
this.enqueue("frontend_breadcrumb", {
|
|
261
|
-
breadcrumb_type: type
|
|
303
|
+
breadcrumb_type: ["route_change", "click", "form_submit", "console_log", "network_request"].includes(type) ? type
|
|
304
|
+
: type === "screen" ? "route_change" : type === "action" ? "click" : "console_log",
|
|
262
305
|
data: breadcrumb.data
|
|
263
306
|
}, null, true);
|
|
264
307
|
}
|
|
@@ -272,7 +315,7 @@ export class DebugBundleReactNativeClient {
|
|
|
272
315
|
source
|
|
273
316
|
});
|
|
274
317
|
}
|
|
275
|
-
enqueue(eventType, payload, traceId, countTowardSession, context, localPolicyAllows = true) {
|
|
318
|
+
enqueue(eventType, payload, traceId, countTowardSession, context, localPolicyAllows = true, runBeforeSend = true, correlation) {
|
|
276
319
|
if (!localPolicyAllows)
|
|
277
320
|
return;
|
|
278
321
|
if (!this.canCapture(eventType, countTowardSession)) {
|
|
@@ -285,6 +328,7 @@ export class DebugBundleReactNativeClient {
|
|
|
285
328
|
this.pendingNativeCalls += 1;
|
|
286
329
|
let handedOff = false;
|
|
287
330
|
let reservedBytes = 0;
|
|
331
|
+
let owner = null;
|
|
288
332
|
try {
|
|
289
333
|
const authoredEvent = {
|
|
290
334
|
schema_version: "2026-03-01",
|
|
@@ -299,15 +343,16 @@ export class DebugBundleReactNativeClient {
|
|
|
299
343
|
framework: "react-native"
|
|
300
344
|
},
|
|
301
345
|
occurred_at: new Date().toISOString(),
|
|
302
|
-
...(traceId ? { correlation: { trace_id: traceId } } : {}),
|
|
346
|
+
...(correlation ? { correlation } : traceId ? { correlation: { trace_id: traceId } } : {}),
|
|
303
347
|
...(context && Object.keys(context).length > 0 ? { context } : {}),
|
|
304
|
-
payload: this.nativeState.device
|
|
305
|
-
? { ...payload, device: canonicalDevice(this.nativeState.device, this.config) }
|
|
306
|
-
: payload
|
|
348
|
+
payload: eventType === "diagnostic_context" ? payload : { ...payload, device: canonicalDevice(this.nativeState.device, this.config) }
|
|
307
349
|
};
|
|
308
350
|
const initial = protectEventFields(authoredEvent, this.config.redactFields);
|
|
309
351
|
if (!initial)
|
|
310
352
|
return;
|
|
353
|
+
owner = initial.event_id;
|
|
354
|
+
if (initial.event_type === "diagnostic_context" && (!canonicalEvent(ownJSON(initial)) || !this.evidence.budget.reserve(initial, owner)))
|
|
355
|
+
return;
|
|
311
356
|
const config = this.config;
|
|
312
357
|
const nativeModule = this.nativeModule;
|
|
313
358
|
const initialBytes = utf8ByteLength(JSON.stringify(initial));
|
|
@@ -321,26 +366,30 @@ export class DebugBundleReactNativeClient {
|
|
|
321
366
|
return;
|
|
322
367
|
}
|
|
323
368
|
handedOff = true;
|
|
324
|
-
this.sendAdmittedEvent(initial, config, nativeModule, reservedBytes);
|
|
369
|
+
this.sendAdmittedEvent(initial, config, nativeModule, reservedBytes, runBeforeSend, !this.nativeState.device);
|
|
325
370
|
}
|
|
326
371
|
catch {
|
|
327
372
|
this.nativeState = degradedNativeState("event_preparation_failed");
|
|
328
373
|
}
|
|
329
374
|
finally {
|
|
330
|
-
if (!handedOff)
|
|
375
|
+
if (!handedOff) {
|
|
331
376
|
this.releaseNativeCapacity(reservedBytes);
|
|
377
|
+
if (owner)
|
|
378
|
+
this.evidence.budget.release(owner);
|
|
379
|
+
}
|
|
332
380
|
}
|
|
333
381
|
}
|
|
334
|
-
sendAdmittedEvent(initial, config, nativeModule, reservedBytes) {
|
|
382
|
+
sendAdmittedEvent(initial, config, nativeModule, reservedBytes, runBeforeSend, hydrateNativeDevice) {
|
|
335
383
|
// This activation receives only a protected snapshot: deferred closures must
|
|
336
384
|
// not retain the raw capture payload/context through their lexical scope.
|
|
385
|
+
const evidence = this.evidence;
|
|
337
386
|
const prepareAndSend = async () => {
|
|
338
387
|
if (this.config !== config)
|
|
339
388
|
return;
|
|
340
|
-
const safeEvent = this.prepareAdmittedEvent(initial, config, nativeModule.enqueueCanonicalEvent !== undefined);
|
|
389
|
+
const safeEvent = this.prepareAdmittedEvent(initial, config, nativeModule.enqueueCanonicalEventJson !== undefined || nativeModule.enqueueCanonicalEvent !== undefined, runBeforeSend, hydrateNativeDevice);
|
|
341
390
|
if (safeEvent === null)
|
|
342
391
|
return;
|
|
343
|
-
const finalPriority = safeEvent.event_type
|
|
392
|
+
const finalPriority = ["frontend_exception", "backend_exception"].includes(safeEvent.event_type) ||
|
|
344
393
|
safeEvent.event_type === "log_event" && ["error", "critical"].includes(String(safeEvent.payload.level));
|
|
345
394
|
const eventBytes = utf8ByteLength(JSON.stringify(safeEvent));
|
|
346
395
|
const finalLimit = finalPriority ? MAX_PENDING_NATIVE_BYTES : MAX_PENDING_LOW_PRIORITY_BYTES;
|
|
@@ -350,20 +399,33 @@ export class DebugBundleReactNativeClient {
|
|
|
350
399
|
return;
|
|
351
400
|
this.pendingNativeBytes += eventBytes;
|
|
352
401
|
reservedBytes += eventBytes;
|
|
353
|
-
if (
|
|
402
|
+
if (safeEvent.event_type === "diagnostic_context") {
|
|
403
|
+
if (!evidence.budget.replace(initial.event_id, safeEvent))
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
evidence.budget.release(initial.event_id);
|
|
408
|
+
if (["frontend_exception", "backend_exception"].includes(safeEvent.event_type))
|
|
409
|
+
evidence.budget.register(safeEvent);
|
|
410
|
+
}
|
|
411
|
+
// iOS TurboModule object conversion removes nested nulls. A string carrier
|
|
412
|
+
// preserves the validated JSON shape; older binaries keep their existing API.
|
|
413
|
+
if (nativeModule.enqueueCanonicalEventJson)
|
|
414
|
+
await nativeModule.enqueueCanonicalEventJson(JSON.stringify(safeEvent));
|
|
415
|
+
else if (nativeModule.enqueueCanonicalEvent)
|
|
354
416
|
await nativeModule.enqueueCanonicalEvent(safeEvent);
|
|
355
417
|
else
|
|
356
418
|
await nativeModule.enqueueEvent(safeEvent);
|
|
357
419
|
};
|
|
358
|
-
const operation = config.beforeSend === null
|
|
420
|
+
const operation = config.beforeSend === null || !runBeforeSend
|
|
359
421
|
? safeNativeCall(prepareAndSend, undefined)
|
|
360
422
|
: Promise.resolve().then(() => safeNativeCall(prepareAndSend, undefined));
|
|
361
|
-
void operation.finally(() => { this.releaseNativeCapacity(reservedBytes); });
|
|
423
|
+
void operation.finally(() => { this.releaseNativeCapacity(reservedBytes); evidence.budget.release(initial.event_id); });
|
|
362
424
|
}
|
|
363
|
-
prepareAdmittedEvent(initial, config, canonical) {
|
|
425
|
+
prepareAdmittedEvent(initial, config, canonical, runBeforeSend, hydrateNativeDevice) {
|
|
364
426
|
// Exit this synchronous activation before awaiting the bridge. An application
|
|
365
427
|
// replacement may be huge before projection and must not survive suspension.
|
|
366
|
-
const event = applyBeforeSend(initial, config.beforeSend);
|
|
428
|
+
const event = applyBeforeSend(initial, runBeforeSend ? config.beforeSend : null);
|
|
367
429
|
if (!event || this.config !== config)
|
|
368
430
|
return null;
|
|
369
431
|
const safeEvent = protectEventFields(event, config.redactFields);
|
|
@@ -372,9 +434,21 @@ export class DebugBundleReactNativeClient {
|
|
|
372
434
|
if (safeEvent.event_type === "log_event" &&
|
|
373
435
|
(!config.captureLogs || !(LOG_LEVELS[safeEvent.payload.level] >= LOG_LEVELS[config.logLevel])))
|
|
374
436
|
return null;
|
|
375
|
-
if (safeEvent.event_type
|
|
437
|
+
if (["frontend_exception", "backend_exception"].includes(safeEvent.event_type) && !config.captureErrors)
|
|
376
438
|
return null;
|
|
377
|
-
|
|
439
|
+
if (!canonicalEvent(ownJSON(safeEvent)))
|
|
440
|
+
return null;
|
|
441
|
+
const filtered = this.evidence.filter(safeEvent, this.nativeState, canonical);
|
|
442
|
+
if (!filtered)
|
|
443
|
+
return null;
|
|
444
|
+
const fitted = fitMobileEvent(filtered);
|
|
445
|
+
if (fitted && hydrateNativeDevice && fitted.sdk_name === SDK_NAME && fitted.event_type === "frontend_exception" &&
|
|
446
|
+
sameMobileDevice(initial.payload.device, fitted.payload.device)) {
|
|
447
|
+
// This is the private native carrier, whose existing worker supplies device
|
|
448
|
+
// metadata before wire delivery. Preserve an explicitly changed hook device.
|
|
449
|
+
delete fitted.payload.device;
|
|
450
|
+
}
|
|
451
|
+
return fitted ? canonical ? fitted : toLegacyNativeEvent(fitted) : null;
|
|
378
452
|
}
|
|
379
453
|
hasNativeCapacity(highPriority) {
|
|
380
454
|
return this.pendingNativeCalls < (highPriority ? MAX_PENDING_NATIVE_CALLS : MAX_PENDING_LOW_PRIORITY_CALLS) &&
|
|
@@ -414,24 +488,6 @@ export class DebugBundleReactNativeClient {
|
|
|
414
488
|
return this.jsProbeActivationExpiresAt > Date.now();
|
|
415
489
|
}
|
|
416
490
|
}
|
|
417
|
-
function utf8ByteLength(value) {
|
|
418
|
-
let bytes = 0;
|
|
419
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
420
|
-
const code = value.charCodeAt(index);
|
|
421
|
-
if (code < 0x80)
|
|
422
|
-
bytes += 1;
|
|
423
|
-
else if (code < 0x800)
|
|
424
|
-
bytes += 2;
|
|
425
|
-
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length &&
|
|
426
|
-
value.charCodeAt(index + 1) >= 0xdc00 && value.charCodeAt(index + 1) <= 0xdfff) {
|
|
427
|
-
bytes += 4;
|
|
428
|
-
index += 1;
|
|
429
|
-
}
|
|
430
|
-
else
|
|
431
|
-
bytes += 3;
|
|
432
|
-
}
|
|
433
|
-
return bytes;
|
|
434
|
-
}
|
|
435
491
|
export function createDebugBundleClient(config = {}) {
|
|
436
492
|
return new DebugBundleReactNativeClient(config);
|
|
437
493
|
}
|
|
@@ -452,6 +508,9 @@ export function resolveConfig(config) {
|
|
|
452
508
|
maxEventsPerSession: clampInteger(config.maxEventsPerSession, 100, 1, 10000),
|
|
453
509
|
maxBreadcrumbs: clampInteger(config.maxBreadcrumbs, 20, 1, 200),
|
|
454
510
|
captureErrors: config.captureErrors ?? true,
|
|
511
|
+
captureExceptionEvidence: config.captureExceptionEvidence ?? true,
|
|
512
|
+
captureRecoveryObservations: config.captureRecoveryObservations ?? false,
|
|
513
|
+
evidenceCapabilities: config.evidenceCapabilities ? normalizeCapabilities(config.evidenceCapabilities) : null,
|
|
455
514
|
captureUnhandledRejections: config.captureUnhandledRejections ?? true,
|
|
456
515
|
captureScreens: config.captureScreens ?? true,
|
|
457
516
|
captureActions: config.captureActions ?? false,
|
|
@@ -501,165 +560,3 @@ function safeInvoke(producer) {
|
|
|
501
560
|
function isPromiseLike(value) {
|
|
502
561
|
return Boolean(value && typeof value.then === "function");
|
|
503
562
|
}
|
|
504
|
-
function normalizeNativeState(state) {
|
|
505
|
-
return {
|
|
506
|
-
...state,
|
|
507
|
-
status: state.status || "healthy",
|
|
508
|
-
nativeModuleAvailable: state.nativeModuleAvailable ?? true
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
function stringValue(value) {
|
|
512
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
513
|
-
}
|
|
514
|
-
function asRecord(value) {
|
|
515
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
516
|
-
return value;
|
|
517
|
-
}
|
|
518
|
-
return {};
|
|
519
|
-
}
|
|
520
|
-
function objectWrap(value) {
|
|
521
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
522
|
-
? value
|
|
523
|
-
: { value };
|
|
524
|
-
}
|
|
525
|
-
function parseRequestUrl(url) {
|
|
526
|
-
try {
|
|
527
|
-
const parsed = new URL(url);
|
|
528
|
-
const query = {};
|
|
529
|
-
for (const [key, value] of parsed.searchParams) {
|
|
530
|
-
const existing = query[key];
|
|
531
|
-
if (existing === undefined) {
|
|
532
|
-
query[key] = value;
|
|
533
|
-
}
|
|
534
|
-
else if (Array.isArray(existing)) {
|
|
535
|
-
existing.push(value);
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
query[key] = [existing, value];
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
return { path: parsed.pathname || "/", query };
|
|
542
|
-
}
|
|
543
|
-
catch {
|
|
544
|
-
const [path = "/", rawQuery = ""] = url.split("?", 2);
|
|
545
|
-
const query = {};
|
|
546
|
-
for (const segment of rawQuery.split("&")) {
|
|
547
|
-
if (!segment) {
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
const [rawKey = "", rawValue = ""] = segment.split("=", 2);
|
|
551
|
-
const key = decodeQuerySegment(rawKey);
|
|
552
|
-
const value = decodeQuerySegment(rawValue);
|
|
553
|
-
const existing = query[key];
|
|
554
|
-
if (existing === undefined) {
|
|
555
|
-
query[key] = value;
|
|
556
|
-
}
|
|
557
|
-
else if (Array.isArray(existing)) {
|
|
558
|
-
existing.push(value);
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
query[key] = [existing, value];
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
return { path: path || "/", query };
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
function decodeQuerySegment(value) {
|
|
568
|
-
try {
|
|
569
|
-
return decodeURIComponent(value.replace(/\+/g, " "));
|
|
570
|
-
}
|
|
571
|
-
catch {
|
|
572
|
-
return value;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
function sanitizeScreenName(screenName) {
|
|
576
|
-
return screenName.replace(/[^a-zA-Z0-9_.:/-]/g, "_").slice(0, 120);
|
|
577
|
-
}
|
|
578
|
-
function generateId() {
|
|
579
|
-
const cryptoLike = globalThis.crypto;
|
|
580
|
-
if (cryptoLike?.randomUUID) {
|
|
581
|
-
return cryptoLike.randomUUID();
|
|
582
|
-
}
|
|
583
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (marker) => {
|
|
584
|
-
const random = Math.trunc(Math.random() * 16);
|
|
585
|
-
const value = marker === "x" ? random : (random & 0x3) | 0x8;
|
|
586
|
-
return value.toString(16);
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
function canonicalDevice(nativeDevice, config) {
|
|
590
|
-
const source = nativeDevice ?? {};
|
|
591
|
-
const sourceOs = asRecord(source.os);
|
|
592
|
-
const sourceScreen = asRecord(source.screen);
|
|
593
|
-
const sourceViewport = asRecord(source.viewport);
|
|
594
|
-
const width = nonNegativeInteger(sourceScreen.width ?? source.screen_width);
|
|
595
|
-
const height = nonNegativeInteger(sourceScreen.height ?? source.screen_height);
|
|
596
|
-
return {
|
|
597
|
-
user_agent: stringValue(source.user_agent),
|
|
598
|
-
os: {
|
|
599
|
-
name: stringValue(sourceOs.name ?? source.os_name),
|
|
600
|
-
version: stringValue(sourceOs.version ?? source.os_version)
|
|
601
|
-
},
|
|
602
|
-
device_type: canonicalDeviceType(source.device_type),
|
|
603
|
-
screen: { width, height },
|
|
604
|
-
viewport: {
|
|
605
|
-
width: nonNegativeInteger(sourceViewport.width ?? width),
|
|
606
|
-
height: nonNegativeInteger(sourceViewport.height ?? height)
|
|
607
|
-
},
|
|
608
|
-
device_pixel_ratio: positiveNumber(source.device_pixel_ratio),
|
|
609
|
-
touch_capable: typeof source.touch_capable === "boolean" ? source.touch_capable : null,
|
|
610
|
-
language: stringValue(source.language ?? source.locale),
|
|
611
|
-
connection_type: stringValue(source.connection_type),
|
|
612
|
-
color_scheme_preference: canonicalColorScheme(source.color_scheme_preference),
|
|
613
|
-
app_version: stringValue(source.app_version) ?? config.appVersion,
|
|
614
|
-
build_number: stringValue(source.build_number) ?? config.buildNumber,
|
|
615
|
-
release_channel: stringValue(source.release_channel) ?? config.releaseChannel,
|
|
616
|
-
api_level: nullableNonNegativeInteger(source.api_level),
|
|
617
|
-
manufacturer: stringValue(source.manufacturer),
|
|
618
|
-
model: stringValue(source.model),
|
|
619
|
-
timezone: stringValue(source.timezone),
|
|
620
|
-
battery_level: nonNegativeNumber(source.battery_level),
|
|
621
|
-
battery_charging: typeof source.battery_charging === "boolean"
|
|
622
|
-
? source.battery_charging
|
|
623
|
-
: typeof source.charging === "boolean" ? source.charging : null,
|
|
624
|
-
free_disk_bytes: nullableNonNegativeInteger(source.free_disk_bytes),
|
|
625
|
-
free_memory_bytes: nullableNonNegativeInteger(source.free_memory_bytes),
|
|
626
|
-
jailbroken: typeof source.jailbroken === "boolean"
|
|
627
|
-
? source.jailbroken
|
|
628
|
-
: typeof source.rooted === "boolean" ? source.rooted : null
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
function toLegacyNativeEvent(event) {
|
|
632
|
-
const context = event.context ?? {};
|
|
633
|
-
const payload = { ...event.payload, context };
|
|
634
|
-
if (event.event_type === "frontend_exception") {
|
|
635
|
-
payload.error = {
|
|
636
|
-
name: event.payload.name,
|
|
637
|
-
message: event.payload.message,
|
|
638
|
-
stack: event.payload.stack
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
else if (event.event_type === "request_event") {
|
|
642
|
-
payload.url = event.payload.path;
|
|
643
|
-
}
|
|
644
|
-
return { ...event, payload };
|
|
645
|
-
}
|
|
646
|
-
function canonicalDeviceType(value) {
|
|
647
|
-
return value === "desktop" || value === "mobile" || value === "tablet" ? value : "unknown";
|
|
648
|
-
}
|
|
649
|
-
function canonicalColorScheme(value) {
|
|
650
|
-
return value === "light" || value === "dark" || value === "no-preference" ? value : null;
|
|
651
|
-
}
|
|
652
|
-
function nonNegativeInteger(value) {
|
|
653
|
-
return nullableNonNegativeInteger(value) ?? 0;
|
|
654
|
-
}
|
|
655
|
-
function nullableNonNegativeInteger(value) {
|
|
656
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
657
|
-
? Math.trunc(value)
|
|
658
|
-
: null;
|
|
659
|
-
}
|
|
660
|
-
function positiveNumber(value) {
|
|
661
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
662
|
-
}
|
|
663
|
-
function nonNegativeNumber(value) {
|
|
664
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
665
|
-
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DebugBundleEventEnvelope } from "./types.js";
|
|
2
|
+
/** Bridge ownership lasts until its promise settles; native offline ownership then
|
|
3
|
+
* remains with the native SDK. Observation/attempt allowances are never refunded. */
|
|
4
|
+
export declare class MobileDiagnosticBudget {
|
|
5
|
+
private windows;
|
|
6
|
+
private pending;
|
|
7
|
+
get accountedBytes(): number;
|
|
8
|
+
private prune;
|
|
9
|
+
register(event: DebugBundleEventEnvelope): void;
|
|
10
|
+
reserve(event: DebugBundleEventEnvelope, owner?: string): boolean;
|
|
11
|
+
replace(owner: string, event: DebugBundleEventEnvelope): boolean;
|
|
12
|
+
release(id: string): void;
|
|
13
|
+
clear(): void;
|
|
14
|
+
private fresh;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=diagnostic-budget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostic-budget.d.ts","sourceRoot":"","sources":["../src/diagnostic-budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAa3D;qFACqF;AACrF,qBAAa,sBAAsB;IACjC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAA8B;IAC7C,IAAI,cAAc,IAAI,MAAM,CAK3B;IACD,OAAO,CAAC,KAAK;IAGb,QAAQ,CAAC,KAAK,EAAE,wBAAwB,GAAG,IAAI;IAY/C,OAAO,CAAC,KAAK,EAAE,wBAAwB,EAAE,KAAK,SAAiB,GAAG,OAAO;IAwBzE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,wBAAwB,GAAG,OAAO;IAmBhE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAKzB,KAAK,IAAI,IAAI;IACb,OAAO,CAAC,KAAK;CACd"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { evidenceReference } from "./evidence-wire.js";
|
|
2
|
+
import { utf8Bytes } from "./owned-json.js";
|
|
3
|
+
const cost = (value) => utf8Bytes(JSON.stringify(value));
|
|
4
|
+
function scopeKey(event, reference) {
|
|
5
|
+
const scope = reference.scope;
|
|
6
|
+
return JSON.stringify([event.service.name, event.service.environment, event.correlation?.trace_id ?? null,
|
|
7
|
+
event.correlation?.session_id ?? null, reference.failure_ref, [scope.kind, scope.operation_id ?? scope.page_id]]);
|
|
8
|
+
}
|
|
9
|
+
/** Bridge ownership lasts until its promise settles; native offline ownership then
|
|
10
|
+
* remains with the native SDK. Observation/attempt allowances are never refunded. */
|
|
11
|
+
export class MobileDiagnosticBudget {
|
|
12
|
+
windows = new Map();
|
|
13
|
+
pending = new Map();
|
|
14
|
+
get accountedBytes() {
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
for (const [key, window] of this.windows)
|
|
17
|
+
bytes += cost([key, { ...window, attempts: [...window.attempts] }]);
|
|
18
|
+
for (const [id, pending] of this.pending)
|
|
19
|
+
bytes += pending.bytes + cost([id, pending.key, pending.attempt, pending.kind]);
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
prune(now) {
|
|
23
|
+
for (const [key, window] of this.windows)
|
|
24
|
+
if (!window.pending && now >= window.startsAt + 300_000)
|
|
25
|
+
this.windows.delete(key);
|
|
26
|
+
}
|
|
27
|
+
register(event) {
|
|
28
|
+
const reference = event.context?.evidence_reference;
|
|
29
|
+
if (!evidenceReference(reference))
|
|
30
|
+
return;
|
|
31
|
+
const now = Date.parse(event.occurred_at);
|
|
32
|
+
if (!Number.isFinite(now))
|
|
33
|
+
return;
|
|
34
|
+
this.prune(now);
|
|
35
|
+
const key = scopeKey(event, reference), previous = this.windows.get(key);
|
|
36
|
+
if (!previous && this.windows.size >= 50)
|
|
37
|
+
return;
|
|
38
|
+
const next = { ...(previous ?? this.fresh(now)), failureAt: previous?.failureAt ?? now, failureId: event.event_id };
|
|
39
|
+
if (previous?.failureId && previous.failureId !== event.event_id)
|
|
40
|
+
next.ambiguous = true;
|
|
41
|
+
this.windows.set(key, next);
|
|
42
|
+
if (this.accountedBytes > 65_536) {
|
|
43
|
+
if (previous)
|
|
44
|
+
this.windows.set(key, previous);
|
|
45
|
+
else
|
|
46
|
+
this.windows.delete(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
reserve(event, owner = event.event_id) {
|
|
50
|
+
const now = Date.parse(event.occurred_at);
|
|
51
|
+
if (!Number.isFinite(now) || this.pending.has(owner))
|
|
52
|
+
return false;
|
|
53
|
+
this.prune(now);
|
|
54
|
+
const payload = event.payload, key = scopeKey(event, payload), previous = this.windows.get(key);
|
|
55
|
+
if (!previous && this.windows.size >= 50 || previous?.ambiguous)
|
|
56
|
+
return false;
|
|
57
|
+
const next = { ...(previous ?? this.fresh(now)), attempts: new Set(previous?.attempts), pending: (previous?.pending ?? 0) + 1 };
|
|
58
|
+
if (now < (next.failureAt ?? next.startsAt) || now > (next.failureAt ?? next.startsAt) + 300_000 || next.observations >= 20)
|
|
59
|
+
return false;
|
|
60
|
+
const attempt = payload.kind === "recovery" ? String(payload.attempt_id) : null;
|
|
61
|
+
if (attempt !== null) {
|
|
62
|
+
if (!next.attempts.has(attempt) && next.attempts.size >= 5)
|
|
63
|
+
return false;
|
|
64
|
+
next.attempts.add(attempt);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (next.resource || next.failureId !== payload.source_event_id || next.failureAt === null || now > next.failureAt + 30_000)
|
|
68
|
+
return false;
|
|
69
|
+
next.resource = true;
|
|
70
|
+
}
|
|
71
|
+
next.observations++;
|
|
72
|
+
this.windows.set(key, next);
|
|
73
|
+
this.pending.set(owner, { key, attempt, kind: String(payload.kind), bytes: cost(event) });
|
|
74
|
+
if (this.accountedBytes > 65_536) {
|
|
75
|
+
this.pending.delete(owner);
|
|
76
|
+
if (previous)
|
|
77
|
+
this.windows.set(key, previous);
|
|
78
|
+
else
|
|
79
|
+
this.windows.delete(key);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
replace(owner, event) {
|
|
85
|
+
const current = this.pending.get(owner), payload = event.payload, key = scopeKey(event, payload);
|
|
86
|
+
const attempt = payload.kind === "recovery" ? String(payload.attempt_id) : null;
|
|
87
|
+
if (!current || current.key !== key || current.attempt !== attempt || current.kind !== payload.kind) {
|
|
88
|
+
this.release(owner);
|
|
89
|
+
return this.reserve(event, owner);
|
|
90
|
+
}
|
|
91
|
+
// Hook identity may stay stable while occurrence time or resource identity
|
|
92
|
+
// changes. Revalidate association without spending another observation.
|
|
93
|
+
const window = this.windows.get(key), now = Date.parse(event.occurred_at);
|
|
94
|
+
if (!window || window.ambiguous || !Number.isFinite(now) ||
|
|
95
|
+
now < (window.failureAt ?? window.startsAt) || now > (window.failureAt ?? window.startsAt) + 300_000 ||
|
|
96
|
+
attempt === null && (window.failureAt === null || payload.source_event_id !== window.failureId || now > window.failureAt + 30_000)) {
|
|
97
|
+
this.release(owner);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const previous = current.bytes;
|
|
101
|
+
current.bytes = cost(event);
|
|
102
|
+
if (this.accountedBytes > 65_536) {
|
|
103
|
+
current.bytes = previous;
|
|
104
|
+
this.release(owner);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
release(id) {
|
|
110
|
+
const pending = this.pending.get(id);
|
|
111
|
+
if (!pending)
|
|
112
|
+
return;
|
|
113
|
+
this.pending.delete(id);
|
|
114
|
+
const window = this.windows.get(pending.key);
|
|
115
|
+
if (window)
|
|
116
|
+
window.pending = Math.max(0, window.pending - 1);
|
|
117
|
+
}
|
|
118
|
+
clear() { this.windows.clear(); this.pending.clear(); }
|
|
119
|
+
fresh(now) { return { startsAt: now, failureAt: null, failureId: null, attempts: new Set(), observations: 0, resource: false, ambiguous: false, pending: 0 }; }
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-schema-data.d.ts","sourceRoot":"","sources":["../src/event-schema-data.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,eAAe,EAAE,OA8CnB,CAAC"}
|