@debugbundle/sdk-react-native 1.1.0 → 1.3.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 +43 -0
- package/DebugBundleReactNative.podspec +4 -3
- package/LICENSE +202 -0
- package/README.md +13 -3
- package/android/build.gradle +70 -1
- package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +369 -78
- package/android/src/test/java/com/debugbundle/reactnative/DebugBundleReactNativeModuleTest.java +474 -0
- package/app.plugin.js +1 -1
- package/dist/NativeDebugBundleReactNative.d.ts +5 -1
- package/dist/NativeDebugBundleReactNative.d.ts.map +1 -1
- package/dist/before-send.d.ts +3 -0
- package/dist/before-send.d.ts.map +1 -0
- package/dist/before-send.js +136 -0
- package/dist/client.d.ts +0 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +224 -64
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +3 -15
- package/dist/testing.d.ts +9 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +18 -0
- package/dist/types.d.ts +16 -5
- package/dist/types.d.ts.map +1 -1
- package/ios/DebugBundleReactNative.swift +120 -13
- package/ios/DebugBundleReactNativeBridge.mm +73 -0
- package/package.json +7 -3
- package/src/NativeDebugBundleReactNative.ts +5 -1
- package/src/before-send.ts +172 -0
- package/src/client.ts +245 -68
- package/src/index.ts +2 -0
- package/src/native.ts +3 -25
- package/src/testing.ts +28 -2
- package/src/types.ts +22 -5
- package/ios/DebugBundleReactNativeBridge.m +0 -28
package/dist/client.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getNativeModule, degradedNativeState, safeNativeCall } from "./native.js";
|
|
2
|
+
import { applyBeforeSend } from "./before-send.js";
|
|
2
3
|
import { defaultRedactFields, DEFAULT_HEADER_ALLOWLIST, sanitizeHeaders, sanitizeValue } from "./redaction.js";
|
|
3
4
|
const SDK_NAME = "@debugbundle/sdk-react-native";
|
|
4
|
-
const DEFAULT_SDK_VERSION = "1.
|
|
5
|
+
const DEFAULT_SDK_VERSION = "1.3.0";
|
|
5
6
|
const LOG_LEVELS = {
|
|
6
7
|
debug: 10,
|
|
7
8
|
info: 20,
|
|
@@ -17,8 +18,6 @@ export class DebugBundleReactNativeClient {
|
|
|
17
18
|
breadcrumbs = [];
|
|
18
19
|
probeBuffers = new Map();
|
|
19
20
|
jsProbeActivationExpiresAt = 0;
|
|
20
|
-
sessionSampledIn = true;
|
|
21
|
-
sessionEventCount = 0;
|
|
22
21
|
constructor(config = {}) {
|
|
23
22
|
this.config = resolveConfig(config);
|
|
24
23
|
if (Object.keys(config).length > 0) {
|
|
@@ -34,9 +33,6 @@ export class DebugBundleReactNativeClient {
|
|
|
34
33
|
init(config) {
|
|
35
34
|
this.config = resolveConfig(config);
|
|
36
35
|
this.nativeModule = getNativeModule();
|
|
37
|
-
this.sessionSampledIn =
|
|
38
|
-
this.config.enabled &&
|
|
39
|
-
(this.config.sessionSampleRate >= 1 || Math.random() <= this.config.sessionSampleRate);
|
|
40
36
|
if (!this.config.enabled) {
|
|
41
37
|
this.nativeState = { status: "disconnected", lastEventAt: null, nativeModuleAvailable: Boolean(this.nativeModule) };
|
|
42
38
|
return;
|
|
@@ -49,57 +45,70 @@ export class DebugBundleReactNativeClient {
|
|
|
49
45
|
this.nativeState = degradedNativeState("native_module_unavailable");
|
|
50
46
|
return;
|
|
51
47
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
this.nativeState
|
|
48
|
+
try {
|
|
49
|
+
const initialization = this.nativeModule.initialize(nativeConfig(this.config));
|
|
50
|
+
if (isPromiseLike(initialization)) {
|
|
51
|
+
this.nativeState = { status: "healthy", lastEventAt: null, nativeModuleAvailable: true };
|
|
52
|
+
void safeNativeCall(async () => {
|
|
53
|
+
this.nativeState = normalizeNativeState(await initialization);
|
|
54
|
+
}, undefined);
|
|
56
55
|
}
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
else {
|
|
57
|
+
this.nativeState = normalizeNativeState(initialization);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
this.nativeState = degradedNativeState("native_initialize_failed");
|
|
62
|
+
}
|
|
59
63
|
}
|
|
60
64
|
captureException(error, context = {}) {
|
|
61
65
|
const mergedContext = this.mergeContext(context);
|
|
66
|
+
const sanitizedError = asRecord(sanitizeValue(error, { redactFields: this.config.redactFields }));
|
|
62
67
|
this.enqueue("frontend_exception", {
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
name: stringValue(sanitizedError.name) ?? "Error",
|
|
69
|
+
message: stringValue(sanitizedError.message) ?? "Unknown error",
|
|
70
|
+
stack: stringValue(sanitizedError.stack) ?? `${stringValue(sanitizedError.name) ?? "Error"}: ${stringValue(sanitizedError.message) ?? "Unknown error"}`,
|
|
65
71
|
breadcrumbs: this.breadcrumbs.slice(),
|
|
66
72
|
probe_data: this.snapshotProbeData()
|
|
67
|
-
}, stringValue(mergedContext.trace_id), false
|
|
73
|
+
}, stringValue(mergedContext.trace_id), false, asRecord(sanitizeValue(mergedContext, {
|
|
74
|
+
redactFields: this.config.redactFields
|
|
75
|
+
})), this.config.captureErrors);
|
|
68
76
|
}
|
|
69
77
|
captureError(error, context = {}) {
|
|
70
78
|
this.captureException(error, context);
|
|
71
79
|
}
|
|
72
80
|
captureLog(message, level = "warning", context = {}) {
|
|
73
|
-
if (!this.config.captureLogs || LOG_LEVELS[level] < LOG_LEVELS[this.config.logLevel]) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
81
|
this.enqueue("log_event", {
|
|
77
82
|
level,
|
|
78
83
|
message: sanitizeValue(message, { redactFields: this.config.redactFields }),
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
attributes: {
|
|
85
|
+
...asRecord(sanitizeValue(this.mergeContext(context), { redactFields: this.config.redactFields })),
|
|
86
|
+
source: "javascript"
|
|
87
|
+
}
|
|
88
|
+
}, stringValue(context.trace_id), true, undefined, this.config.captureLogs && LOG_LEVELS[level] >= LOG_LEVELS[this.config.logLevel]);
|
|
82
89
|
}
|
|
83
90
|
captureRequest(request, response, context = {}) {
|
|
84
|
-
const
|
|
91
|
+
const mergedContext = this.mergeContext(context);
|
|
92
|
+
const traceId = request.traceId ?? stringValue(mergedContext.trace_id);
|
|
93
|
+
const parsedUrl = parseRequestUrl(request.url);
|
|
85
94
|
const payload = {
|
|
86
95
|
method: request.method,
|
|
87
|
-
|
|
96
|
+
path: parsedUrl.path,
|
|
97
|
+
query: parsedUrl.query,
|
|
88
98
|
route_template: request.routeTemplate ?? null,
|
|
89
|
-
|
|
99
|
+
headers: sanitizeHeaders(request.headers, this.config.headerAllowlist, {
|
|
90
100
|
redactFields: this.config.redactFields
|
|
91
101
|
}),
|
|
92
102
|
response_status: response.statusCode,
|
|
93
103
|
response_headers: sanitizeHeaders(response.headers, this.config.headerAllowlist, {
|
|
94
104
|
redactFields: this.config.redactFields
|
|
95
105
|
}),
|
|
96
|
-
duration_ms: response.durationMillis ??
|
|
97
|
-
source: "react-native"
|
|
106
|
+
duration_ms: response.durationMillis ?? 0
|
|
98
107
|
};
|
|
99
|
-
this.
|
|
100
|
-
|
|
101
|
-
this.enqueue("request_event", payload, traceId, true);
|
|
108
|
+
if (this.config.captureNetwork) {
|
|
109
|
+
this.recordBreadcrumb("network_request", payload);
|
|
102
110
|
}
|
|
111
|
+
this.enqueue("request_event", payload, traceId, true, asRecord(sanitizeValue(mergedContext, { redactFields: this.config.redactFields })), this.config.captureNetwork);
|
|
103
112
|
}
|
|
104
113
|
captureMessage(message, level = "warning", context = {}) {
|
|
105
114
|
this.recordBreadcrumb("message", {
|
|
@@ -110,7 +119,10 @@ export class DebugBundleReactNativeClient {
|
|
|
110
119
|
}
|
|
111
120
|
setContext(key, value) {
|
|
112
121
|
this.context[key] = sanitizeValue(value, { redactFields: this.config.redactFields });
|
|
113
|
-
if (this.nativeModule?.
|
|
122
|
+
if (this.nativeModule?.setContextValue) {
|
|
123
|
+
void safeNativeCall(() => this.nativeModule.setContextValue(key, { value: this.context[key] }), undefined);
|
|
124
|
+
}
|
|
125
|
+
else if (this.nativeModule?.setContext) {
|
|
114
126
|
void safeNativeCall(() => this.nativeModule.setContext(key, this.context[key]), undefined);
|
|
115
127
|
}
|
|
116
128
|
}
|
|
@@ -118,19 +130,25 @@ export class DebugBundleReactNativeClient {
|
|
|
118
130
|
if (!label || this.probeBuffers.size >= this.config.maxProbeLabels && !this.probeBuffers.has(label)) {
|
|
119
131
|
return;
|
|
120
132
|
}
|
|
121
|
-
|
|
133
|
+
const nativeProbeActive = this.nativeModule?.isProbeActive?.(label) ?? false;
|
|
134
|
+
if (options.heavy && !nativeProbeActive && !this.hasActiveJsProbeActivation()) {
|
|
122
135
|
return;
|
|
123
136
|
}
|
|
124
137
|
const value = typeof data === "function" ? safeInvoke(data) : data;
|
|
138
|
+
const sanitizedData = objectWrap(sanitizeValue(value, { redactFields: this.config.redactFields }));
|
|
139
|
+
const occurredAt = new Date().toISOString();
|
|
125
140
|
const entry = {
|
|
126
141
|
label,
|
|
127
|
-
data:
|
|
128
|
-
timestamp:
|
|
142
|
+
data: sanitizedData,
|
|
143
|
+
timestamp: occurredAt,
|
|
129
144
|
activation_id: null
|
|
130
145
|
};
|
|
131
146
|
const entries = this.probeBuffers.get(label) ?? [];
|
|
132
147
|
entries.push(entry);
|
|
133
148
|
this.probeBuffers.set(label, entries.slice(-this.config.maxProbeEntriesPerLabel));
|
|
149
|
+
if (this.nativeModule?.captureProbe) {
|
|
150
|
+
void safeNativeCall(() => this.nativeModule.captureProbe(label, sanitizedData, occurredAt), false);
|
|
151
|
+
}
|
|
134
152
|
}
|
|
135
153
|
async flush() {
|
|
136
154
|
if (!this.nativeModule) {
|
|
@@ -153,12 +171,17 @@ export class DebugBundleReactNativeClient {
|
|
|
153
171
|
return activated;
|
|
154
172
|
}
|
|
155
173
|
recordBreadcrumb(type, data = {}) {
|
|
156
|
-
|
|
174
|
+
const breadcrumb = {
|
|
157
175
|
breadcrumb_type: type,
|
|
158
|
-
|
|
176
|
+
ts: new Date().toISOString(),
|
|
159
177
|
data: sanitizeValue(data, { redactFields: this.config.redactFields })
|
|
160
|
-
}
|
|
178
|
+
};
|
|
179
|
+
this.breadcrumbs.push(breadcrumb);
|
|
161
180
|
this.breadcrumbs = this.breadcrumbs.slice(-this.config.maxBreadcrumbs);
|
|
181
|
+
this.enqueue("frontend_breadcrumb", {
|
|
182
|
+
breadcrumb_type: type,
|
|
183
|
+
data: breadcrumb.data
|
|
184
|
+
}, null, true);
|
|
162
185
|
}
|
|
163
186
|
recordScreen(screenName, previousScreen = null, source = "react-navigation") {
|
|
164
187
|
if (!this.config.captureScreens) {
|
|
@@ -170,12 +193,12 @@ export class DebugBundleReactNativeClient {
|
|
|
170
193
|
source
|
|
171
194
|
});
|
|
172
195
|
}
|
|
173
|
-
enqueue(eventType, payload, traceId, countTowardSession) {
|
|
196
|
+
enqueue(eventType, payload, traceId, countTowardSession, context, localPolicyAllows = true) {
|
|
174
197
|
if (!this.canCapture(eventType, countTowardSession)) {
|
|
175
198
|
return;
|
|
176
199
|
}
|
|
177
|
-
const
|
|
178
|
-
schema_version: "
|
|
200
|
+
const authoredEvent = {
|
|
201
|
+
schema_version: "2026-03-01",
|
|
179
202
|
event_id: generateId(),
|
|
180
203
|
event_type: eventType,
|
|
181
204
|
sdk_name: SDK_NAME,
|
|
@@ -187,29 +210,33 @@ export class DebugBundleReactNativeClient {
|
|
|
187
210
|
framework: "react-native"
|
|
188
211
|
},
|
|
189
212
|
occurred_at: new Date().toISOString(),
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
213
|
+
...(traceId ? { correlation: { trace_id: traceId } } : {}),
|
|
214
|
+
...(context && Object.keys(context).length > 0 ? { context } : {}),
|
|
215
|
+
payload: this.nativeState.device
|
|
216
|
+
? { ...payload, device: canonicalDevice(this.nativeState.device, this.config) }
|
|
217
|
+
: payload
|
|
193
218
|
};
|
|
194
|
-
|
|
195
|
-
|
|
219
|
+
const event = applyBeforeSend(authoredEvent, this.config.beforeSend);
|
|
220
|
+
if (!event || !localPolicyAllows) {
|
|
221
|
+
return;
|
|
196
222
|
}
|
|
197
223
|
if (!this.nativeModule) {
|
|
198
224
|
this.nativeState = degradedNativeState("native_module_unavailable");
|
|
199
225
|
return;
|
|
200
226
|
}
|
|
201
|
-
|
|
227
|
+
if (this.nativeModule.enqueueCanonicalEvent) {
|
|
228
|
+
void safeNativeCall(() => this.nativeModule.enqueueCanonicalEvent(event), undefined);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
void safeNativeCall(() => this.nativeModule.enqueueEvent(toLegacyNativeEvent(event)), undefined);
|
|
232
|
+
}
|
|
202
233
|
}
|
|
203
234
|
canCapture(eventType, countTowardSession) {
|
|
204
|
-
if (!this.config.enabled || !this.config.projectToken
|
|
235
|
+
if (!this.config.enabled || !this.config.projectToken) {
|
|
205
236
|
return false;
|
|
206
237
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
if (countTowardSession && this.sessionEventCount >= this.config.maxEventsPerSession) {
|
|
211
|
-
return eventType === "frontend_exception" || eventType === "probe_event";
|
|
212
|
-
}
|
|
238
|
+
void eventType;
|
|
239
|
+
void countTowardSession;
|
|
213
240
|
return true;
|
|
214
241
|
}
|
|
215
242
|
mergeContext(context) {
|
|
@@ -227,9 +254,6 @@ export class DebugBundleReactNativeClient {
|
|
|
227
254
|
hasActiveJsProbeActivation() {
|
|
228
255
|
return this.jsProbeActivationExpiresAt > Date.now();
|
|
229
256
|
}
|
|
230
|
-
shouldCaptureRequestEvent(statusCode) {
|
|
231
|
-
return statusCode >= 500 || [408, 423, 424, 425, 429].includes(statusCode);
|
|
232
|
-
}
|
|
233
257
|
}
|
|
234
258
|
export function createDebugBundleClient(config = {}) {
|
|
235
259
|
return new DebugBundleReactNativeClient(config);
|
|
@@ -269,9 +293,14 @@ export function resolveConfig(config) {
|
|
|
269
293
|
probeFlushOnError: config.probeFlushOnError ?? true,
|
|
270
294
|
redactFields: config.redactFields ?? defaultRedactFields(),
|
|
271
295
|
headerAllowlist: config.headerAllowlist ?? DEFAULT_HEADER_ALLOWLIST,
|
|
272
|
-
sdkVersion: config.sdkVersion ?? DEFAULT_SDK_VERSION
|
|
296
|
+
sdkVersion: config.sdkVersion ?? DEFAULT_SDK_VERSION,
|
|
297
|
+
beforeSend: config.beforeSend ?? null
|
|
273
298
|
};
|
|
274
299
|
}
|
|
300
|
+
function nativeConfig(config) {
|
|
301
|
+
const { beforeSend: _beforeSend, ...native } = config;
|
|
302
|
+
return native;
|
|
303
|
+
}
|
|
275
304
|
function clampInteger(value, fallback, min, max) {
|
|
276
305
|
if (!Number.isFinite(value)) {
|
|
277
306
|
return fallback;
|
|
@@ -292,17 +321,78 @@ function safeInvoke(producer) {
|
|
|
292
321
|
return { error: sanitizeValue(error) };
|
|
293
322
|
}
|
|
294
323
|
}
|
|
324
|
+
function isPromiseLike(value) {
|
|
325
|
+
return Boolean(value && typeof value.then === "function");
|
|
326
|
+
}
|
|
327
|
+
function normalizeNativeState(state) {
|
|
328
|
+
return {
|
|
329
|
+
...state,
|
|
330
|
+
status: state.status || "healthy",
|
|
331
|
+
nativeModuleAvailable: state.nativeModuleAvailable ?? true
|
|
332
|
+
};
|
|
333
|
+
}
|
|
295
334
|
function stringValue(value) {
|
|
296
335
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
297
336
|
}
|
|
298
|
-
function
|
|
337
|
+
function asRecord(value) {
|
|
338
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
343
|
+
function objectWrap(value) {
|
|
344
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
345
|
+
? value
|
|
346
|
+
: { value };
|
|
347
|
+
}
|
|
348
|
+
function parseRequestUrl(url) {
|
|
299
349
|
try {
|
|
300
350
|
const parsed = new URL(url);
|
|
301
|
-
|
|
302
|
-
|
|
351
|
+
const query = {};
|
|
352
|
+
for (const [key, value] of parsed.searchParams) {
|
|
353
|
+
const existing = query[key];
|
|
354
|
+
if (existing === undefined) {
|
|
355
|
+
query[key] = value;
|
|
356
|
+
}
|
|
357
|
+
else if (Array.isArray(existing)) {
|
|
358
|
+
existing.push(value);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
query[key] = [existing, value];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return { path: parsed.pathname || "/", query };
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
const [path = "/", rawQuery = ""] = url.split("?", 2);
|
|
368
|
+
const query = {};
|
|
369
|
+
for (const segment of rawQuery.split("&")) {
|
|
370
|
+
if (!segment) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const [rawKey = "", rawValue = ""] = segment.split("=", 2);
|
|
374
|
+
const key = decodeQuerySegment(rawKey);
|
|
375
|
+
const value = decodeQuerySegment(rawValue);
|
|
376
|
+
const existing = query[key];
|
|
377
|
+
if (existing === undefined) {
|
|
378
|
+
query[key] = value;
|
|
379
|
+
}
|
|
380
|
+
else if (Array.isArray(existing)) {
|
|
381
|
+
existing.push(value);
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
query[key] = [existing, value];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return { path: path || "/", query };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function decodeQuerySegment(value) {
|
|
391
|
+
try {
|
|
392
|
+
return decodeURIComponent(value.replace(/\+/g, " "));
|
|
303
393
|
}
|
|
304
394
|
catch {
|
|
305
|
-
return
|
|
395
|
+
return value;
|
|
306
396
|
}
|
|
307
397
|
}
|
|
308
398
|
function sanitizeScreenName(screenName) {
|
|
@@ -319,10 +409,80 @@ function generateId() {
|
|
|
319
409
|
return value.toString(16);
|
|
320
410
|
});
|
|
321
411
|
}
|
|
322
|
-
function
|
|
412
|
+
function canonicalDevice(nativeDevice, config) {
|
|
413
|
+
const source = nativeDevice ?? {};
|
|
414
|
+
const sourceOs = asRecord(source.os);
|
|
415
|
+
const sourceScreen = asRecord(source.screen);
|
|
416
|
+
const sourceViewport = asRecord(source.viewport);
|
|
417
|
+
const width = nonNegativeInteger(sourceScreen.width ?? source.screen_width);
|
|
418
|
+
const height = nonNegativeInteger(sourceScreen.height ?? source.screen_height);
|
|
323
419
|
return {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
420
|
+
user_agent: stringValue(source.user_agent),
|
|
421
|
+
os: {
|
|
422
|
+
name: stringValue(sourceOs.name ?? source.os_name),
|
|
423
|
+
version: stringValue(sourceOs.version ?? source.os_version)
|
|
424
|
+
},
|
|
425
|
+
device_type: canonicalDeviceType(source.device_type),
|
|
426
|
+
screen: { width, height },
|
|
427
|
+
viewport: {
|
|
428
|
+
width: nonNegativeInteger(sourceViewport.width ?? width),
|
|
429
|
+
height: nonNegativeInteger(sourceViewport.height ?? height)
|
|
430
|
+
},
|
|
431
|
+
device_pixel_ratio: positiveNumber(source.device_pixel_ratio),
|
|
432
|
+
touch_capable: typeof source.touch_capable === "boolean" ? source.touch_capable : null,
|
|
433
|
+
language: stringValue(source.language ?? source.locale),
|
|
434
|
+
connection_type: stringValue(source.connection_type),
|
|
435
|
+
color_scheme_preference: canonicalColorScheme(source.color_scheme_preference),
|
|
436
|
+
app_version: stringValue(source.app_version) ?? config.appVersion,
|
|
437
|
+
build_number: stringValue(source.build_number) ?? config.buildNumber,
|
|
438
|
+
release_channel: stringValue(source.release_channel) ?? config.releaseChannel,
|
|
439
|
+
api_level: nullableNonNegativeInteger(source.api_level),
|
|
440
|
+
manufacturer: stringValue(source.manufacturer),
|
|
441
|
+
model: stringValue(source.model),
|
|
442
|
+
timezone: stringValue(source.timezone),
|
|
443
|
+
battery_level: nonNegativeNumber(source.battery_level),
|
|
444
|
+
battery_charging: typeof source.battery_charging === "boolean"
|
|
445
|
+
? source.battery_charging
|
|
446
|
+
: typeof source.charging === "boolean" ? source.charging : null,
|
|
447
|
+
free_disk_bytes: nullableNonNegativeInteger(source.free_disk_bytes),
|
|
448
|
+
free_memory_bytes: nullableNonNegativeInteger(source.free_memory_bytes),
|
|
449
|
+
jailbroken: typeof source.jailbroken === "boolean"
|
|
450
|
+
? source.jailbroken
|
|
451
|
+
: typeof source.rooted === "boolean" ? source.rooted : null
|
|
327
452
|
};
|
|
328
453
|
}
|
|
454
|
+
function toLegacyNativeEvent(event) {
|
|
455
|
+
const context = event.context ?? {};
|
|
456
|
+
const payload = { ...event.payload, context };
|
|
457
|
+
if (event.event_type === "frontend_exception") {
|
|
458
|
+
payload.error = {
|
|
459
|
+
name: event.payload.name,
|
|
460
|
+
message: event.payload.message,
|
|
461
|
+
stack: event.payload.stack
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
else if (event.event_type === "request_event") {
|
|
465
|
+
payload.url = event.payload.path;
|
|
466
|
+
}
|
|
467
|
+
return { ...event, payload };
|
|
468
|
+
}
|
|
469
|
+
function canonicalDeviceType(value) {
|
|
470
|
+
return value === "desktop" || value === "mobile" || value === "tablet" ? value : "unknown";
|
|
471
|
+
}
|
|
472
|
+
function canonicalColorScheme(value) {
|
|
473
|
+
return value === "light" || value === "dark" || value === "no-preference" ? value : null;
|
|
474
|
+
}
|
|
475
|
+
function nonNegativeInteger(value) {
|
|
476
|
+
return nullableNonNegativeInteger(value) ?? 0;
|
|
477
|
+
}
|
|
478
|
+
function nullableNonNegativeInteger(value) {
|
|
479
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
480
|
+
? Math.trunc(value)
|
|
481
|
+
: null;
|
|
482
|
+
}
|
|
483
|
+
function positiveNumber(value) {
|
|
484
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
485
|
+
}
|
|
486
|
+
function nonNegativeNumber(value) {
|
|
487
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
488
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DebugBundleReactNativeClient, createDebugBundleClient } from "./client.js";
|
|
2
2
|
import { captureDebugBundleConsole } from "./console.js";
|
|
3
3
|
import { installDebugBundleErrorHandlers } from "./errors.js";
|
|
4
|
-
import type { DebugBundleCaptureContext, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus } from "./types.js";
|
|
4
|
+
import type { DebugBundleCaptureContext, DebugBundleBeforeSend, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus } from "./types.js";
|
|
5
5
|
export declare const DebugBundle: {
|
|
6
6
|
init(config: DebugBundleConfig): void;
|
|
7
7
|
captureException(error: unknown, context?: DebugBundleCaptureContext): void;
|
|
@@ -21,5 +21,5 @@ export declare const DebugBundle: {
|
|
|
21
21
|
export { createDebugBundleClient, DebugBundleReactNativeClient };
|
|
22
22
|
export { captureDebugBundleConsole };
|
|
23
23
|
export { installDebugBundleErrorHandlers };
|
|
24
|
-
export type { DebugBundleCaptureContext, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus };
|
|
24
|
+
export type { DebugBundleCaptureContext, DebugBundleBeforeSend, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus };
|
|
25
25
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,WAAW;iBACT,iBAAiB,GAAG,IAAI;4BAWb,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGvD,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGnD,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;4BAG3E,sBAAsB,YAAY,uBAAuB,YAAY,yBAAyB,GAAG,IAAI;4BAGrG,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;oBAGvF,MAAM,SAAS,OAAO,GAAG,IAAI;iBAGhC,MAAM,QAAQ,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,uBAAuB,GAAG,IAAI;aAGrF,OAAO,CAAC,IAAI,CAAC;qCAGW,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;2BAGnC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;6BAG3C,MAAM,mBAAmB,MAAM,GAAG,IAAI,WAAW,MAAM,GAAG,IAAI;qBAGzE,iBAAiB;0BAGZ,MAAM,GAAG,IAAI;CAGjC,CAAC;AAEF,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,CAAC;AACjE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AACrC,OAAO,EAAE,+BAA+B,EAAE,CAAC;AAC3C,YAAY,EACV,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,+BAA+B,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EACV,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,WAAW;iBACT,iBAAiB,GAAG,IAAI;4BAWb,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGvD,OAAO,YAAY,yBAAyB,GAAG,IAAI;wBAGnD,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;4BAG3E,sBAAsB,YAAY,uBAAuB,YAAY,yBAAyB,GAAG,IAAI;4BAGrG,MAAM,UAAU,mBAAmB,YAAY,yBAAyB,GAAG,IAAI;oBAGvF,MAAM,SAAS,OAAO,GAAG,IAAI;iBAGhC,MAAM,QAAQ,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,uBAAuB,GAAG,IAAI;aAGrF,OAAO,CAAC,IAAI,CAAC;qCAGW,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;2BAGnC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;6BAG3C,MAAM,mBAAmB,MAAM,GAAG,IAAI,WAAW,MAAM,GAAG,IAAI;qBAGzE,iBAAiB;0BAGZ,MAAM,GAAG,IAAI;CAGjC,CAAC;AAEF,OAAO,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,CAAC;AACjE,OAAO,EAAE,yBAAyB,EAAE,CAAC;AACrC,OAAO,EAAE,+BAA+B,EAAE,CAAC;AAC3C,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,iBAAiB,EAClB,CAAC"}
|
package/dist/native.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAKlF,wBAAgB,eAAe,IAAI,uBAAuB,GAAG,IAAI,CAgBhE;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CAElG;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,CAQ1E;AAED,wBAAsB,cAAc,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMhG;AAED,wBAAgB,gCAAgC,IAAI,IAAI,CAEvD"}
|
package/dist/native.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NativeModules, TurboModuleRegistry } from "react-native";
|
|
1
2
|
let overrideModule;
|
|
2
3
|
let cachedReactNativeModule;
|
|
3
4
|
export function getNativeModule() {
|
|
@@ -38,22 +39,9 @@ export function resetNativeModuleCacheForTesting() {
|
|
|
38
39
|
cachedReactNativeModule = undefined;
|
|
39
40
|
}
|
|
40
41
|
function resolveReactNativeModule() {
|
|
41
|
-
const
|
|
42
|
-
const turboModule = required?.TurboModuleRegistry?.get?.("DebugBundleReactNative");
|
|
42
|
+
const turboModule = TurboModuleRegistry.get("DebugBundleReactNative");
|
|
43
43
|
if (turboModule) {
|
|
44
44
|
return turboModule;
|
|
45
45
|
}
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
function requireReactNative() {
|
|
49
|
-
try {
|
|
50
|
-
const maybeRequire = Function("return typeof require === 'function' ? require : null")();
|
|
51
|
-
if (!maybeRequire) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
return maybeRequire("react-native");
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
46
|
+
return NativeModules.DebugBundleReactNative ?? null;
|
|
59
47
|
}
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import type { DebugBundleEventEnvelope, NativeDebugBundleModule, NativeDebugBundleState
|
|
1
|
+
import type { DebugBundleEventEnvelope, NativeDebugBundleConfig, NativeDebugBundleModule, NativeDebugBundleState } from "./types.js";
|
|
2
2
|
export interface RecordingNativeModule extends NativeDebugBundleModule {
|
|
3
3
|
events: DebugBundleEventEnvelope[];
|
|
4
|
-
|
|
4
|
+
probes: Array<{
|
|
5
|
+
label: string;
|
|
6
|
+
data: unknown;
|
|
7
|
+
occurredAt: string;
|
|
8
|
+
}>;
|
|
9
|
+
context: Record<string, unknown>;
|
|
10
|
+
config: NativeDebugBundleConfig | null;
|
|
11
|
+
activeProbeLabels: Set<string>;
|
|
5
12
|
}
|
|
6
13
|
export declare function createRecordingNativeModule(initialState?: NativeDebugBundleState): RecordingNativeModule;
|
|
7
14
|
export declare function installRecordingNativeModule(module?: RecordingNativeModule): RecordingNativeModule;
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IACpE,MAAM,EAAE,wBAAwB,EAAE,CAAC;IACnC,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACvC,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAChC;AAED,wBAAgB,2BAA2B,CAAC,YAAY,GAAE,sBAAiE,GAAG,qBAAqB,CA4ClJ;AAED,wBAAgB,4BAA4B,CAAC,MAAM,GAAE,qBAAqD,GAAG,qBAAqB,CAGjI;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAGnD"}
|
package/dist/testing.js
CHANGED
|
@@ -3,15 +3,30 @@ export function createRecordingNativeModule(initialState = { status: "healthy",
|
|
|
3
3
|
const state = { ...initialState };
|
|
4
4
|
return {
|
|
5
5
|
events: [],
|
|
6
|
+
probes: [],
|
|
7
|
+
context: {},
|
|
6
8
|
config: null,
|
|
9
|
+
activeProbeLabels: new Set(),
|
|
7
10
|
initialize(config) {
|
|
8
11
|
this.config = config;
|
|
9
12
|
return state;
|
|
10
13
|
},
|
|
14
|
+
enqueueCanonicalEvent(event) {
|
|
15
|
+
this.events.push(event);
|
|
16
|
+
state.lastEventAt = Date.now();
|
|
17
|
+
return true;
|
|
18
|
+
},
|
|
11
19
|
enqueueEvent(event) {
|
|
12
20
|
this.events.push(event);
|
|
13
21
|
state.lastEventAt = Date.now();
|
|
14
22
|
},
|
|
23
|
+
captureProbe(label, data, occurredAt) {
|
|
24
|
+
this.probes.push({ label, data, occurredAt });
|
|
25
|
+
return this.activeProbeLabels.has(label);
|
|
26
|
+
},
|
|
27
|
+
isProbeActive(label) {
|
|
28
|
+
return this.activeProbeLabels.has(label);
|
|
29
|
+
},
|
|
15
30
|
flush() {
|
|
16
31
|
state.status = "healthy";
|
|
17
32
|
},
|
|
@@ -21,6 +36,9 @@ export function createRecordingNativeModule(initialState = { status: "healthy",
|
|
|
21
36
|
setContext() {
|
|
22
37
|
return undefined;
|
|
23
38
|
},
|
|
39
|
+
setContextValue(key, entry) {
|
|
40
|
+
this.context[key] = entry.value;
|
|
41
|
+
},
|
|
24
42
|
activateProbeTriggerToken() {
|
|
25
43
|
return true;
|
|
26
44
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type DebugBundleStatus = "healthy" | "degraded" | "disconnected";
|
|
2
2
|
export type DebugBundleLogLevel = "debug" | "info" | "warning" | "error" | "critical";
|
|
3
|
+
export type DebugBundleBeforeSend = (event: DebugBundleEventEnvelope) => DebugBundleEventEnvelope | null;
|
|
3
4
|
export interface DebugBundleConfig {
|
|
4
5
|
projectToken?: string;
|
|
5
6
|
enabled?: boolean;
|
|
@@ -35,6 +36,7 @@ export interface DebugBundleConfig {
|
|
|
35
36
|
redactFields?: string[];
|
|
36
37
|
headerAllowlist?: string[];
|
|
37
38
|
sdkVersion?: string;
|
|
39
|
+
beforeSend?: DebugBundleBeforeSend;
|
|
38
40
|
}
|
|
39
41
|
export interface ResolvedDebugBundleConfig {
|
|
40
42
|
projectToken: string;
|
|
@@ -71,6 +73,7 @@ export interface ResolvedDebugBundleConfig {
|
|
|
71
73
|
redactFields: string[];
|
|
72
74
|
headerAllowlist: string[];
|
|
73
75
|
sdkVersion: string;
|
|
76
|
+
beforeSend: DebugBundleBeforeSend | null;
|
|
74
77
|
}
|
|
75
78
|
export interface DebugBundleNetworkFilter {
|
|
76
79
|
urlAllowlist?: string[];
|
|
@@ -97,7 +100,7 @@ export interface DebugBundleProbeOptions {
|
|
|
97
100
|
heavy?: boolean;
|
|
98
101
|
}
|
|
99
102
|
export interface DebugBundleEventEnvelope {
|
|
100
|
-
schema_version: "
|
|
103
|
+
schema_version: "2026-03-01";
|
|
101
104
|
event_id: string;
|
|
102
105
|
event_type: "frontend_exception" | "frontend_breadcrumb" | "log_event" | "request_event" | "error_suppressed" | "probe_event";
|
|
103
106
|
sdk_name: "@debugbundle/sdk-react-native";
|
|
@@ -109,14 +112,14 @@ export interface DebugBundleEventEnvelope {
|
|
|
109
112
|
framework: "react-native";
|
|
110
113
|
};
|
|
111
114
|
occurred_at: string;
|
|
112
|
-
correlation
|
|
115
|
+
correlation?: {
|
|
113
116
|
trace_id: string | null;
|
|
114
117
|
request_id?: string | null;
|
|
115
118
|
session_id?: string | null;
|
|
116
119
|
user_id_hash?: string | null;
|
|
117
|
-
}
|
|
120
|
+
};
|
|
121
|
+
context?: Record<string, unknown>;
|
|
118
122
|
payload: Record<string, unknown>;
|
|
119
|
-
device: Record<string, unknown> | null;
|
|
120
123
|
}
|
|
121
124
|
export interface DebugBundleClient {
|
|
122
125
|
readonly status: DebugBundleStatus;
|
|
@@ -135,13 +138,21 @@ export interface DebugBundleClient {
|
|
|
135
138
|
recordScreen(screenName: string, previousScreen?: string | null, source?: string): void;
|
|
136
139
|
}
|
|
137
140
|
export interface NativeDebugBundleModule {
|
|
138
|
-
initialize(config:
|
|
141
|
+
initialize(config: NativeDebugBundleConfig): Promise<NativeDebugBundleState> | NativeDebugBundleState;
|
|
142
|
+
enqueueCanonicalEvent?(event: DebugBundleEventEnvelope): Promise<boolean | void> | boolean | void;
|
|
143
|
+
/** Compatibility fallback implemented by native binaries released before the canonical bridge. */
|
|
139
144
|
enqueueEvent(event: DebugBundleEventEnvelope): Promise<void> | void;
|
|
145
|
+
captureProbe?(label: string, data: unknown, occurredAt: string): Promise<boolean> | boolean;
|
|
146
|
+
isProbeActive?(label: string): boolean;
|
|
140
147
|
flush(): Promise<void> | void;
|
|
141
148
|
getStatus(): Promise<NativeDebugBundleState> | NativeDebugBundleState;
|
|
142
149
|
setContext?(key: string, value: unknown): Promise<void> | void;
|
|
150
|
+
setContextValue?(key: string, entry: {
|
|
151
|
+
value: unknown;
|
|
152
|
+
}): Promise<void> | void;
|
|
143
153
|
activateProbeTriggerToken?(token: string): Promise<boolean> | boolean;
|
|
144
154
|
}
|
|
155
|
+
export type NativeDebugBundleConfig = Omit<ResolvedDebugBundleConfig, "beforeSend">;
|
|
145
156
|
export interface NativeDebugBundleState {
|
|
146
157
|
status: DebugBundleStatus;
|
|
147
158
|
lastEventAt?: number | null;
|