@debugbundle/sdk-react-native 0.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/DebugBundleReactNative.podspec +15 -0
- package/README.md +124 -0
- package/android/build.gradle +37 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativeModule.java +281 -0
- package/android/src/main/java/com/debugbundle/reactnative/DebugBundleReactNativePackage.java +20 -0
- package/app.plugin.js +52 -0
- package/dist/NativeDebugBundleReactNative.d.ts +13 -0
- package/dist/NativeDebugBundleReactNative.d.ts.map +1 -0
- package/dist/NativeDebugBundleReactNative.js +2 -0
- package/dist/app-state.d.ts +9 -0
- package/dist/app-state.d.ts.map +1 -0
- package/dist/app-state.js +10 -0
- package/dist/client.d.ts +36 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +328 -0
- package/dist/console.d.ts +3 -0
- package/dist/console.d.ts.map +1 -0
- package/dist/console.js +35 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/native.d.ts +7 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +59 -0
- package/dist/navigation.d.ts +13 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +26 -0
- package/dist/network.d.ts +14 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +179 -0
- package/dist/probes.d.ts +4 -0
- package/dist/probes.d.ts.map +1 -0
- package/dist/probes.js +8 -0
- package/dist/react.d.ts +21 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +36 -0
- package/dist/redaction.d.ts +12 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +132 -0
- package/dist/testing.d.ts +9 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +36 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/ios/DebugBundleReactNative.swift +125 -0
- package/ios/DebugBundleReactNativeBridge.m +28 -0
- package/package.json +99 -0
- package/src/NativeDebugBundleReactNative.ts +14 -0
- package/src/app-state.ts +17 -0
- package/src/client.ts +385 -0
- package/src/console.ts +38 -0
- package/src/errors.ts +64 -0
- package/src/index.ts +82 -0
- package/src/native.ts +79 -0
- package/src/navigation.tsx +37 -0
- package/src/network.ts +240 -0
- package/src/probes.ts +11 -0
- package/src/react-native-shim.d.ts +14 -0
- package/src/react.tsx +60 -0
- package/src/redaction.ts +165 -0
- package/src/testing.ts +45 -0
- package/src/types.ts +173 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { getNativeModule, degradedNativeState, safeNativeCall } from "./native.js";
|
|
2
|
+
import { defaultRedactFields, DEFAULT_HEADER_ALLOWLIST, sanitizeHeaders, sanitizeValue } from "./redaction.js";
|
|
3
|
+
const SDK_NAME = "@debugbundle/sdk-react-native";
|
|
4
|
+
const DEFAULT_SDK_VERSION = "0.1.0";
|
|
5
|
+
const LOG_LEVELS = {
|
|
6
|
+
debug: 10,
|
|
7
|
+
info: 20,
|
|
8
|
+
warning: 30,
|
|
9
|
+
error: 40,
|
|
10
|
+
critical: 50
|
|
11
|
+
};
|
|
12
|
+
export class DebugBundleReactNativeClient {
|
|
13
|
+
config;
|
|
14
|
+
nativeModule = null;
|
|
15
|
+
nativeState = degradedNativeState("not_initialized");
|
|
16
|
+
context = {};
|
|
17
|
+
breadcrumbs = [];
|
|
18
|
+
probeBuffers = new Map();
|
|
19
|
+
jsProbeActivationExpiresAt = 0;
|
|
20
|
+
sessionSampledIn = true;
|
|
21
|
+
sessionEventCount = 0;
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
this.config = resolveConfig(config);
|
|
24
|
+
if (Object.keys(config).length > 0) {
|
|
25
|
+
this.init(config);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
get status() {
|
|
29
|
+
return this.nativeState.status;
|
|
30
|
+
}
|
|
31
|
+
get lastEventAt() {
|
|
32
|
+
return this.nativeState.lastEventAt ?? null;
|
|
33
|
+
}
|
|
34
|
+
init(config) {
|
|
35
|
+
this.config = resolveConfig(config);
|
|
36
|
+
this.nativeModule = getNativeModule();
|
|
37
|
+
this.sessionSampledIn =
|
|
38
|
+
this.config.enabled &&
|
|
39
|
+
(this.config.sessionSampleRate >= 1 || Math.random() <= this.config.sessionSampleRate);
|
|
40
|
+
if (!this.config.enabled) {
|
|
41
|
+
this.nativeState = { status: "disconnected", lastEventAt: null, nativeModuleAvailable: Boolean(this.nativeModule) };
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!this.config.projectToken || !this.config.endpoint) {
|
|
45
|
+
this.nativeState = degradedNativeState("missing_project_token_or_endpoint");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!this.nativeModule) {
|
|
49
|
+
this.nativeState = degradedNativeState("native_module_unavailable");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
void safeNativeCall(async () => {
|
|
53
|
+
this.nativeState = await this.nativeModule.initialize(this.config);
|
|
54
|
+
if (!this.nativeState.status) {
|
|
55
|
+
this.nativeState.status = "healthy";
|
|
56
|
+
}
|
|
57
|
+
}, undefined);
|
|
58
|
+
this.nativeState = { status: "healthy", lastEventAt: null, nativeModuleAvailable: true };
|
|
59
|
+
}
|
|
60
|
+
captureException(error, context = {}) {
|
|
61
|
+
const mergedContext = this.mergeContext(context);
|
|
62
|
+
this.enqueue("frontend_exception", {
|
|
63
|
+
error: sanitizeValue(error, { redactFields: this.config.redactFields }),
|
|
64
|
+
context: sanitizeValue(mergedContext, { redactFields: this.config.redactFields }),
|
|
65
|
+
breadcrumbs: this.breadcrumbs.slice(),
|
|
66
|
+
probe_data: this.snapshotProbeData()
|
|
67
|
+
}, stringValue(mergedContext.trace_id), false);
|
|
68
|
+
}
|
|
69
|
+
captureError(error, context = {}) {
|
|
70
|
+
this.captureException(error, context);
|
|
71
|
+
}
|
|
72
|
+
captureLog(message, level = "warning", context = {}) {
|
|
73
|
+
if (!this.config.captureLogs || LOG_LEVELS[level] < LOG_LEVELS[this.config.logLevel]) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.enqueue("log_event", {
|
|
77
|
+
level,
|
|
78
|
+
message: sanitizeValue(message, { redactFields: this.config.redactFields }),
|
|
79
|
+
context: sanitizeValue(this.mergeContext(context), { redactFields: this.config.redactFields }),
|
|
80
|
+
source: "javascript"
|
|
81
|
+
}, stringValue(context.trace_id), true);
|
|
82
|
+
}
|
|
83
|
+
captureRequest(request, response, context = {}) {
|
|
84
|
+
const traceId = request.traceId ?? stringValue(context.trace_id);
|
|
85
|
+
const payload = {
|
|
86
|
+
method: request.method,
|
|
87
|
+
url: sanitizeUrl(request.url),
|
|
88
|
+
route_template: request.routeTemplate ?? null,
|
|
89
|
+
request_headers: sanitizeHeaders(request.headers, this.config.headerAllowlist, {
|
|
90
|
+
redactFields: this.config.redactFields
|
|
91
|
+
}),
|
|
92
|
+
response_status: response.statusCode,
|
|
93
|
+
response_headers: sanitizeHeaders(response.headers, this.config.headerAllowlist, {
|
|
94
|
+
redactFields: this.config.redactFields
|
|
95
|
+
}),
|
|
96
|
+
duration_ms: response.durationMillis ?? null,
|
|
97
|
+
source: "react-native"
|
|
98
|
+
};
|
|
99
|
+
this.recordBreadcrumb("network", payload);
|
|
100
|
+
if (this.shouldCaptureRequestEvent(response.statusCode)) {
|
|
101
|
+
this.enqueue("request_event", payload, traceId, true);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
captureMessage(message, level = "warning", context = {}) {
|
|
105
|
+
this.recordBreadcrumb("message", {
|
|
106
|
+
level,
|
|
107
|
+
message: sanitizeValue(message, { redactFields: this.config.redactFields }),
|
|
108
|
+
context: sanitizeValue(this.mergeContext(context), { redactFields: this.config.redactFields })
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
setContext(key, value) {
|
|
112
|
+
this.context[key] = sanitizeValue(value, { redactFields: this.config.redactFields });
|
|
113
|
+
if (this.nativeModule?.setContext) {
|
|
114
|
+
void safeNativeCall(() => this.nativeModule.setContext(key, this.context[key]), undefined);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
probe(label, data, options = {}) {
|
|
118
|
+
if (!label || this.probeBuffers.size >= this.config.maxProbeLabels && !this.probeBuffers.has(label)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (options.heavy && !this.hasActiveJsProbeActivation()) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const value = typeof data === "function" ? safeInvoke(data) : data;
|
|
125
|
+
const entry = {
|
|
126
|
+
label,
|
|
127
|
+
data: sanitizeValue(value, { redactFields: this.config.redactFields }),
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
activation_id: null
|
|
130
|
+
};
|
|
131
|
+
const entries = this.probeBuffers.get(label) ?? [];
|
|
132
|
+
entries.push(entry);
|
|
133
|
+
this.probeBuffers.set(label, entries.slice(-this.config.maxProbeEntriesPerLabel));
|
|
134
|
+
}
|
|
135
|
+
async flush() {
|
|
136
|
+
if (!this.nativeModule) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
await safeNativeCall(async () => {
|
|
140
|
+
await this.nativeModule.flush();
|
|
141
|
+
const state = await this.nativeModule.getStatus();
|
|
142
|
+
this.nativeState = state;
|
|
143
|
+
}, undefined);
|
|
144
|
+
}
|
|
145
|
+
async activateProbeTriggerToken(token) {
|
|
146
|
+
if (!this.nativeModule?.activateProbeTriggerToken) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const activated = await safeNativeCall(() => this.nativeModule.activateProbeTriggerToken(token), false);
|
|
150
|
+
if (activated) {
|
|
151
|
+
this.jsProbeActivationExpiresAt = Date.now() + 60 * 60 * 1000;
|
|
152
|
+
}
|
|
153
|
+
return activated;
|
|
154
|
+
}
|
|
155
|
+
recordBreadcrumb(type, data = {}) {
|
|
156
|
+
this.breadcrumbs.push({
|
|
157
|
+
breadcrumb_type: type,
|
|
158
|
+
occurred_at: new Date().toISOString(),
|
|
159
|
+
data: sanitizeValue(data, { redactFields: this.config.redactFields })
|
|
160
|
+
});
|
|
161
|
+
this.breadcrumbs = this.breadcrumbs.slice(-this.config.maxBreadcrumbs);
|
|
162
|
+
}
|
|
163
|
+
recordScreen(screenName, previousScreen = null, source = "react-navigation") {
|
|
164
|
+
if (!this.config.captureScreens) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
this.recordBreadcrumb("screen", {
|
|
168
|
+
screen_name: sanitizeScreenName(screenName),
|
|
169
|
+
previous_screen: previousScreen ? sanitizeScreenName(previousScreen) : null,
|
|
170
|
+
source
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
enqueue(eventType, payload, traceId, countTowardSession) {
|
|
174
|
+
if (!this.canCapture(eventType, countTowardSession)) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const event = {
|
|
178
|
+
schema_version: "1",
|
|
179
|
+
event_id: generateId(),
|
|
180
|
+
event_type: eventType,
|
|
181
|
+
sdk_name: SDK_NAME,
|
|
182
|
+
sdk_version: this.config.sdkVersion,
|
|
183
|
+
service: {
|
|
184
|
+
name: this.config.service,
|
|
185
|
+
environment: this.config.environment,
|
|
186
|
+
runtime: "react-native",
|
|
187
|
+
framework: "react-native"
|
|
188
|
+
},
|
|
189
|
+
occurred_at: new Date().toISOString(),
|
|
190
|
+
correlation: traceId ? { trace_id: traceId } : null,
|
|
191
|
+
payload,
|
|
192
|
+
device: this.nativeState.device ?? deviceFromConfig(this.config)
|
|
193
|
+
};
|
|
194
|
+
if (countTowardSession) {
|
|
195
|
+
this.sessionEventCount += 1;
|
|
196
|
+
}
|
|
197
|
+
if (!this.nativeModule) {
|
|
198
|
+
this.nativeState = degradedNativeState("native_module_unavailable");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
void safeNativeCall(() => this.nativeModule.enqueueEvent(event), undefined);
|
|
202
|
+
}
|
|
203
|
+
canCapture(eventType, countTowardSession) {
|
|
204
|
+
if (!this.config.enabled || !this.config.projectToken || !this.sessionSampledIn) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (Math.random() > this.config.sampleRate) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (countTowardSession && this.sessionEventCount >= this.config.maxEventsPerSession) {
|
|
211
|
+
return eventType === "frontend_exception" || eventType === "probe_event";
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
mergeContext(context) {
|
|
216
|
+
return { ...this.context, ...context };
|
|
217
|
+
}
|
|
218
|
+
snapshotProbeData() {
|
|
219
|
+
if (!this.config.probeFlushOnError) {
|
|
220
|
+
return { version: 1, items: [] };
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
version: 1,
|
|
224
|
+
items: [...this.probeBuffers.values()].flat()
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
hasActiveJsProbeActivation() {
|
|
228
|
+
return this.jsProbeActivationExpiresAt > Date.now();
|
|
229
|
+
}
|
|
230
|
+
shouldCaptureRequestEvent(statusCode) {
|
|
231
|
+
return statusCode >= 500 || [408, 423, 424, 425, 429].includes(statusCode);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export function createDebugBundleClient(config = {}) {
|
|
235
|
+
return new DebugBundleReactNativeClient(config);
|
|
236
|
+
}
|
|
237
|
+
export function resolveConfig(config) {
|
|
238
|
+
return {
|
|
239
|
+
projectToken: config.projectToken ?? "",
|
|
240
|
+
enabled: config.enabled ?? true,
|
|
241
|
+
environment: config.environment ?? "production",
|
|
242
|
+
service: config.service ?? "react-native-app",
|
|
243
|
+
endpoint: config.endpoint ?? "https://api.debugbundle.com/v1/events",
|
|
244
|
+
releaseChannel: config.releaseChannel ?? "production",
|
|
245
|
+
appVersion: config.appVersion ?? null,
|
|
246
|
+
buildNumber: config.buildNumber ?? null,
|
|
247
|
+
batchSize: clampInteger(config.batchSize, 10, 1, 100),
|
|
248
|
+
flushInterval: clampInteger(config.flushInterval, 3000, 100, 60000),
|
|
249
|
+
sampleRate: clampRate(config.sampleRate),
|
|
250
|
+
sessionSampleRate: clampRate(config.sessionSampleRate),
|
|
251
|
+
maxEventsPerSession: clampInteger(config.maxEventsPerSession, 100, 1, 10000),
|
|
252
|
+
maxBreadcrumbs: clampInteger(config.maxBreadcrumbs, 20, 1, 200),
|
|
253
|
+
captureErrors: config.captureErrors ?? true,
|
|
254
|
+
captureUnhandledRejections: config.captureUnhandledRejections ?? true,
|
|
255
|
+
captureScreens: config.captureScreens ?? true,
|
|
256
|
+
captureActions: config.captureActions ?? false,
|
|
257
|
+
captureNetwork: config.captureNetwork ?? true,
|
|
258
|
+
captureConsole: config.captureConsole ?? false,
|
|
259
|
+
captureLogs: config.captureLogs ?? true,
|
|
260
|
+
logLevel: config.logLevel ?? "warning",
|
|
261
|
+
tracePropagationTargets: config.tracePropagationTargets ?? [],
|
|
262
|
+
networkFilter: config.networkFilter ?? {},
|
|
263
|
+
offlineQueueMaxEvents: clampInteger(config.offlineQueueMaxEvents, 500, 1, 10000),
|
|
264
|
+
offlineQueueMaxBytes: clampInteger(config.offlineQueueMaxBytes, 5 * 1024 * 1024, 1024, 100 * 1024 * 1024),
|
|
265
|
+
offlineQueueTtl: clampInteger(config.offlineQueueTtl, 72 * 60 * 60 * 1000, 60000, 30 * 24 * 60 * 60 * 1000),
|
|
266
|
+
requestTimeout: clampInteger(config.requestTimeout, 5000, 100, 60000),
|
|
267
|
+
maxProbeLabels: clampInteger(config.maxProbeLabels, 50, 1, 500),
|
|
268
|
+
maxProbeEntriesPerLabel: clampInteger(config.maxProbeEntriesPerLabel, 10, 1, 200),
|
|
269
|
+
probeFlushOnError: config.probeFlushOnError ?? true,
|
|
270
|
+
redactFields: config.redactFields ?? defaultRedactFields(),
|
|
271
|
+
headerAllowlist: config.headerAllowlist ?? DEFAULT_HEADER_ALLOWLIST,
|
|
272
|
+
sdkVersion: config.sdkVersion ?? DEFAULT_SDK_VERSION
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function clampInteger(value, fallback, min, max) {
|
|
276
|
+
if (!Number.isFinite(value)) {
|
|
277
|
+
return fallback;
|
|
278
|
+
}
|
|
279
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
280
|
+
}
|
|
281
|
+
function clampRate(value) {
|
|
282
|
+
if (!Number.isFinite(value)) {
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
return Math.min(1, Math.max(0, value));
|
|
286
|
+
}
|
|
287
|
+
function safeInvoke(producer) {
|
|
288
|
+
try {
|
|
289
|
+
return producer();
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
return { error: sanitizeValue(error) };
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function stringValue(value) {
|
|
296
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
297
|
+
}
|
|
298
|
+
function sanitizeUrl(url) {
|
|
299
|
+
try {
|
|
300
|
+
const parsed = new URL(url);
|
|
301
|
+
parsed.search = "";
|
|
302
|
+
return parsed.toString();
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return url.split("?")[0] ?? url;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function sanitizeScreenName(screenName) {
|
|
309
|
+
return screenName.replace(/[^a-zA-Z0-9_.:/-]/g, "_").slice(0, 120);
|
|
310
|
+
}
|
|
311
|
+
function generateId() {
|
|
312
|
+
const cryptoLike = globalThis.crypto;
|
|
313
|
+
if (cryptoLike?.randomUUID) {
|
|
314
|
+
return cryptoLike.randomUUID();
|
|
315
|
+
}
|
|
316
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (marker) => {
|
|
317
|
+
const random = Math.trunc(Math.random() * 16);
|
|
318
|
+
const value = marker === "x" ? random : (random & 0x3) | 0x8;
|
|
319
|
+
return value.toString(16);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function deviceFromConfig(config) {
|
|
323
|
+
return {
|
|
324
|
+
app_version: config.appVersion,
|
|
325
|
+
build_number: config.buildNumber,
|
|
326
|
+
release_channel: config.releaseChannel
|
|
327
|
+
};
|
|
328
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.d.ts","sourceRoot":"","sources":["../src/console.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAuB,MAAM,YAAY,CAAC;AAIzE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,GAAG,MAAM,IAAI,CAiCnG"}
|
package/dist/console.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
let restoreConsole = null;
|
|
2
|
+
export function captureDebugBundleConsole(client) {
|
|
3
|
+
if (restoreConsole) {
|
|
4
|
+
return restoreConsole;
|
|
5
|
+
}
|
|
6
|
+
const originalWarn = console.warn;
|
|
7
|
+
const originalError = console.error;
|
|
8
|
+
let inCapture = false;
|
|
9
|
+
console.warn = (...args) => {
|
|
10
|
+
capture("warning", args);
|
|
11
|
+
originalWarn(...args);
|
|
12
|
+
};
|
|
13
|
+
console.error = (...args) => {
|
|
14
|
+
capture("error", args);
|
|
15
|
+
originalError(...args);
|
|
16
|
+
};
|
|
17
|
+
restoreConsole = () => {
|
|
18
|
+
console.warn = originalWarn;
|
|
19
|
+
console.error = originalError;
|
|
20
|
+
restoreConsole = null;
|
|
21
|
+
};
|
|
22
|
+
return restoreConsole;
|
|
23
|
+
function capture(level, args) {
|
|
24
|
+
if (inCapture) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
inCapture = true;
|
|
28
|
+
try {
|
|
29
|
+
client.captureLog(args.map(String).join(" "), level, { source: "console" });
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
inCapture = false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DebugBundleClient } from "./types.js";
|
|
2
|
+
export interface ErrorHandlerInstallOptions {
|
|
3
|
+
captureUnhandledRejections?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function installDebugBundleErrorHandlers(client: Pick<DebugBundleClient, "captureException">, options?: ErrorHandlerInstallOptions): () => void;
|
|
6
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,0BAA0B;IACzC,0BAA0B,CAAC,EAAE,OAAO,CAAC;CACtC;AAID,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,EACnD,OAAO,GAAE,0BAA+B,GACvC,MAAM,IAAI,CAwCZ"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
let restoreHandlers = null;
|
|
2
|
+
export function installDebugBundleErrorHandlers(client, options = {}) {
|
|
3
|
+
if (restoreHandlers) {
|
|
4
|
+
return restoreHandlers;
|
|
5
|
+
}
|
|
6
|
+
const restores = [];
|
|
7
|
+
const errorUtils = globalThis.ErrorUtils;
|
|
8
|
+
if (errorUtils?.setGlobalHandler) {
|
|
9
|
+
const previousHandler = errorUtils.getGlobalHandler?.();
|
|
10
|
+
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
11
|
+
client.captureException(error, {
|
|
12
|
+
source: "react-native-global-error",
|
|
13
|
+
fatal: Boolean(isFatal)
|
|
14
|
+
});
|
|
15
|
+
previousHandler?.(error, isFatal);
|
|
16
|
+
});
|
|
17
|
+
restores.push(() => {
|
|
18
|
+
if (previousHandler) {
|
|
19
|
+
errorUtils.setGlobalHandler?.(previousHandler);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (options.captureUnhandledRejections !== false && hasAddEventListener(globalThis)) {
|
|
24
|
+
const listener = (event) => {
|
|
25
|
+
client.captureException(event.reason ?? new Error("Unhandled promise rejection"), {
|
|
26
|
+
source: "unhandledrejection"
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
globalThis.addEventListener("unhandledrejection", listener);
|
|
30
|
+
restores.push(() => globalThis.removeEventListener("unhandledrejection", listener));
|
|
31
|
+
}
|
|
32
|
+
restoreHandlers = () => {
|
|
33
|
+
for (const restore of restores.reverse()) {
|
|
34
|
+
restore();
|
|
35
|
+
}
|
|
36
|
+
restoreHandlers = null;
|
|
37
|
+
};
|
|
38
|
+
return restoreHandlers;
|
|
39
|
+
}
|
|
40
|
+
function hasAddEventListener(value) {
|
|
41
|
+
return "addEventListener" in value && "removeEventListener" in value;
|
|
42
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DebugBundleReactNativeClient, createDebugBundleClient } from "./client.js";
|
|
2
|
+
import { captureDebugBundleConsole } from "./console.js";
|
|
3
|
+
import { installDebugBundleErrorHandlers } from "./errors.js";
|
|
4
|
+
import type { DebugBundleCaptureContext, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus } from "./types.js";
|
|
5
|
+
export declare const DebugBundle: {
|
|
6
|
+
init(config: DebugBundleConfig): void;
|
|
7
|
+
captureException(error: unknown, context?: DebugBundleCaptureContext): void;
|
|
8
|
+
captureError(error: unknown, context?: DebugBundleCaptureContext): void;
|
|
9
|
+
captureLog(message: string, level?: DebugBundleLogLevel, context?: DebugBundleCaptureContext): void;
|
|
10
|
+
captureRequest(request: DebugBundleRequestInfo, response: DebugBundleResponseInfo, context?: DebugBundleCaptureContext): void;
|
|
11
|
+
captureMessage(message: string, level?: DebugBundleLogLevel, context?: DebugBundleCaptureContext): void;
|
|
12
|
+
setContext(key: string, value: unknown): void;
|
|
13
|
+
probe(label: string, data: unknown | (() => unknown), options?: DebugBundleProbeOptions): void;
|
|
14
|
+
flush(): Promise<void>;
|
|
15
|
+
activateProbeTriggerToken(token: string): Promise<boolean>;
|
|
16
|
+
recordBreadcrumb(type: string, data?: Record<string, unknown>): void;
|
|
17
|
+
recordScreen(screenName: string, previousScreen?: string | null, source?: string): void;
|
|
18
|
+
readonly status: DebugBundleStatus;
|
|
19
|
+
readonly lastEventAt: number | null;
|
|
20
|
+
};
|
|
21
|
+
export { createDebugBundleClient, DebugBundleReactNativeClient };
|
|
22
|
+
export { captureDebugBundleConsole };
|
|
23
|
+
export { installDebugBundleErrorHandlers };
|
|
24
|
+
export type { DebugBundleCaptureContext, DebugBundleClient, DebugBundleConfig, DebugBundleLogLevel, DebugBundleProbeOptions, DebugBundleRequestInfo, DebugBundleResponseInfo, DebugBundleStatus };
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DebugBundleReactNativeClient, createDebugBundleClient } from "./client.js";
|
|
2
|
+
import { captureDebugBundleConsole } from "./console.js";
|
|
3
|
+
import { installDebugBundleErrorHandlers } from "./errors.js";
|
|
4
|
+
const singleton = new DebugBundleReactNativeClient();
|
|
5
|
+
export const DebugBundle = {
|
|
6
|
+
init(config) {
|
|
7
|
+
singleton.init(config);
|
|
8
|
+
if (config.captureErrors !== false) {
|
|
9
|
+
installDebugBundleErrorHandlers(singleton, {
|
|
10
|
+
captureUnhandledRejections: config.captureUnhandledRejections !== false
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
if (config.captureConsole === true) {
|
|
14
|
+
captureDebugBundleConsole(singleton);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
captureException(error, context) {
|
|
18
|
+
singleton.captureException(error, context);
|
|
19
|
+
},
|
|
20
|
+
captureError(error, context) {
|
|
21
|
+
singleton.captureError(error, context);
|
|
22
|
+
},
|
|
23
|
+
captureLog(message, level, context) {
|
|
24
|
+
singleton.captureLog(message, level, context);
|
|
25
|
+
},
|
|
26
|
+
captureRequest(request, response, context) {
|
|
27
|
+
singleton.captureRequest(request, response, context);
|
|
28
|
+
},
|
|
29
|
+
captureMessage(message, level, context) {
|
|
30
|
+
singleton.captureMessage(message, level, context);
|
|
31
|
+
},
|
|
32
|
+
setContext(key, value) {
|
|
33
|
+
singleton.setContext(key, value);
|
|
34
|
+
},
|
|
35
|
+
probe(label, data, options) {
|
|
36
|
+
singleton.probe(label, data, options);
|
|
37
|
+
},
|
|
38
|
+
flush() {
|
|
39
|
+
return singleton.flush();
|
|
40
|
+
},
|
|
41
|
+
activateProbeTriggerToken(token) {
|
|
42
|
+
return singleton.activateProbeTriggerToken(token);
|
|
43
|
+
},
|
|
44
|
+
recordBreadcrumb(type, data) {
|
|
45
|
+
singleton.recordBreadcrumb(type, data);
|
|
46
|
+
},
|
|
47
|
+
recordScreen(screenName, previousScreen, source) {
|
|
48
|
+
singleton.recordScreen(screenName, previousScreen, source);
|
|
49
|
+
},
|
|
50
|
+
get status() {
|
|
51
|
+
return singleton.status;
|
|
52
|
+
},
|
|
53
|
+
get lastEventAt() {
|
|
54
|
+
return singleton.lastEventAt;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export { createDebugBundleClient, DebugBundleReactNativeClient };
|
|
58
|
+
export { captureDebugBundleConsole };
|
|
59
|
+
export { installDebugBundleErrorHandlers };
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NativeDebugBundleModule, NativeDebugBundleState } from "./types.js";
|
|
2
|
+
export declare function getNativeModule(): NativeDebugBundleModule | null;
|
|
3
|
+
export declare function setNativeModuleForTesting(module: NativeDebugBundleModule | null | undefined): void;
|
|
4
|
+
export declare function degradedNativeState(reason: string): NativeDebugBundleState;
|
|
5
|
+
export declare function safeNativeCall<T>(operation: () => Promise<T> | T, fallback: T): Promise<T>;
|
|
6
|
+
export declare function resetNativeModuleCacheForTesting(): void;
|
|
7
|
+
//# sourceMappingURL=native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA,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
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
let overrideModule;
|
|
2
|
+
let cachedReactNativeModule;
|
|
3
|
+
export function getNativeModule() {
|
|
4
|
+
if (overrideModule !== undefined) {
|
|
5
|
+
return overrideModule;
|
|
6
|
+
}
|
|
7
|
+
const globalValue = globalThis;
|
|
8
|
+
if (globalValue.__DebugBundleReactNativeModule) {
|
|
9
|
+
return globalValue.__DebugBundleReactNativeModule;
|
|
10
|
+
}
|
|
11
|
+
if (cachedReactNativeModule !== undefined) {
|
|
12
|
+
return cachedReactNativeModule;
|
|
13
|
+
}
|
|
14
|
+
cachedReactNativeModule = resolveReactNativeModule();
|
|
15
|
+
return cachedReactNativeModule;
|
|
16
|
+
}
|
|
17
|
+
export function setNativeModuleForTesting(module) {
|
|
18
|
+
overrideModule = module;
|
|
19
|
+
}
|
|
20
|
+
export function degradedNativeState(reason) {
|
|
21
|
+
return {
|
|
22
|
+
status: "degraded",
|
|
23
|
+
lastEventAt: null,
|
|
24
|
+
device: null,
|
|
25
|
+
nativeModuleAvailable: false,
|
|
26
|
+
degradedReason: reason
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function safeNativeCall(operation, fallback) {
|
|
30
|
+
try {
|
|
31
|
+
return await operation();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function resetNativeModuleCacheForTesting() {
|
|
38
|
+
cachedReactNativeModule = undefined;
|
|
39
|
+
}
|
|
40
|
+
function resolveReactNativeModule() {
|
|
41
|
+
const required = requireReactNative();
|
|
42
|
+
const turboModule = required?.TurboModuleRegistry?.get?.("DebugBundleReactNative");
|
|
43
|
+
if (turboModule) {
|
|
44
|
+
return turboModule;
|
|
45
|
+
}
|
|
46
|
+
return required?.NativeModules?.DebugBundleReactNative ?? null;
|
|
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
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DebugBundleClient } from "./types.js";
|
|
2
|
+
export interface DebugBundleNavigationRef {
|
|
3
|
+
current?: unknown;
|
|
4
|
+
getCurrentRoute?: () => {
|
|
5
|
+
name?: string;
|
|
6
|
+
key?: string;
|
|
7
|
+
params?: unknown;
|
|
8
|
+
} | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare function createDebugBundleNavigationRef(): DebugBundleNavigationRef;
|
|
11
|
+
export declare function onDebugBundleNavigationReady(ref: DebugBundleNavigationRef, client?: DebugBundleClient): void;
|
|
12
|
+
export declare function onDebugBundleNavigationStateChange(ref: DebugBundleNavigationRef, client?: DebugBundleClient): void;
|
|
13
|
+
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CAAC;CACvF;AAID,wBAAgB,8BAA8B,IAAI,wBAAwB,CAEzE;AAED,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,wBAAwB,EAAE,MAAM,GAAE,iBAA+B,GAAG,IAAI,CAKzH;AAED,wBAAgB,kCAAkC,CAAC,GAAG,EAAE,wBAAwB,EAAE,MAAM,GAAE,iBAA+B,GAAG,IAAI,CAO/H"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DebugBundle } from "./index.js";
|
|
2
|
+
let previousRouteName = null;
|
|
3
|
+
export function createDebugBundleNavigationRef() {
|
|
4
|
+
return { current: undefined };
|
|
5
|
+
}
|
|
6
|
+
export function onDebugBundleNavigationReady(ref, client = DebugBundle) {
|
|
7
|
+
previousRouteName = routeName(ref);
|
|
8
|
+
if (previousRouteName) {
|
|
9
|
+
client.recordScreen(previousRouteName, null, "react-navigation");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function onDebugBundleNavigationStateChange(ref, client = DebugBundle) {
|
|
13
|
+
const currentRouteName = routeName(ref);
|
|
14
|
+
if (!currentRouteName || currentRouteName === previousRouteName) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
client.recordScreen(currentRouteName, previousRouteName, "react-navigation");
|
|
18
|
+
previousRouteName = currentRouteName;
|
|
19
|
+
}
|
|
20
|
+
function routeName(ref) {
|
|
21
|
+
const route = ref.getCurrentRoute?.();
|
|
22
|
+
if (!route?.name) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return route.name.replace(/[^a-zA-Z0-9_.:/-]/g, "_").slice(0, 120);
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DebugBundleClient, DebugBundleConfig, DebugBundleNetworkFilter } from "./types.js";
|
|
2
|
+
export interface NetworkInstrumentationOptions {
|
|
3
|
+
client?: DebugBundleClient;
|
|
4
|
+
tracePropagationTargets?: string[];
|
|
5
|
+
captureNetwork?: boolean;
|
|
6
|
+
networkFilter?: DebugBundleNetworkFilter;
|
|
7
|
+
headerAllowlist?: string[];
|
|
8
|
+
redactFields?: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function instrumentDebugBundleNetwork(options?: NetworkInstrumentationOptions): () => void;
|
|
11
|
+
export declare function createInstrumentedFetch(originalFetch: typeof fetch, client: Pick<DebugBundleClient, "captureRequest">, options: NetworkInstrumentationOptions): typeof fetch;
|
|
12
|
+
export declare function shouldInjectTrace(url: string, targets: string[]): boolean;
|
|
13
|
+
export type { DebugBundleConfig };
|
|
14
|
+
//# sourceMappingURL=network.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAMjG,MAAM,WAAW,6BAA6B;IAC5C,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,6BAAkC,GAAG,MAAM,IAAI,CAkBpG;AAED,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,OAAO,KAAK,EAC3B,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,EACjD,OAAO,EAAE,6BAA6B,GACrC,OAAO,KAAK,CAgBd;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAKzE;AA+KD,YAAY,EAAE,iBAAiB,EAAE,CAAC"}
|