crumbtrail-react-native 0.2.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/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/index.cjs +799 -0
- package/dist/index.d.cts +231 -0
- package/dist/index.d.ts +231 -0
- package/dist/index.js +766 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
// src/capabilities.ts
|
|
2
|
+
var REACT_NATIVE_CAPABILITY_BITS = {
|
|
3
|
+
asyncStorage: 1 << 0,
|
|
4
|
+
navigation: 1 << 1,
|
|
5
|
+
viewShot: 1 << 2
|
|
6
|
+
};
|
|
7
|
+
var OPTIONAL_MODULES = [
|
|
8
|
+
{
|
|
9
|
+
key: "asyncStorage",
|
|
10
|
+
packageName: "@react-native-async-storage/async-storage",
|
|
11
|
+
capability: "async-storage",
|
|
12
|
+
bit: REACT_NATIVE_CAPABILITY_BITS.asyncStorage
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
key: "navigation",
|
|
16
|
+
packageName: "@react-navigation/native",
|
|
17
|
+
capability: "navigation",
|
|
18
|
+
bit: REACT_NATIVE_CAPABILITY_BITS.navigation
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "viewShot",
|
|
22
|
+
packageName: "react-native-view-shot",
|
|
23
|
+
capability: "view-snapshot",
|
|
24
|
+
bit: REACT_NATIVE_CAPABILITY_BITS.viewShot
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
function detectReactNativeCapabilities(options = {}) {
|
|
28
|
+
const resolver = options.resolver ?? safeRequireOptionalModule;
|
|
29
|
+
const modules = {};
|
|
30
|
+
const capabilities = [];
|
|
31
|
+
let bitset = 0;
|
|
32
|
+
for (const optionalModule of OPTIONAL_MODULES) {
|
|
33
|
+
const present = isModulePresent(optionalModule.packageName, resolver);
|
|
34
|
+
modules[optionalModule.key] = {
|
|
35
|
+
packageName: optionalModule.packageName,
|
|
36
|
+
present,
|
|
37
|
+
status: present ? "present" : "absent"
|
|
38
|
+
};
|
|
39
|
+
if (present) {
|
|
40
|
+
bitset |= optionalModule.bit;
|
|
41
|
+
capabilities.push(optionalModule.capability);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { bitset, capabilities, modules };
|
|
45
|
+
}
|
|
46
|
+
function isModulePresent(packageName, resolver) {
|
|
47
|
+
try {
|
|
48
|
+
return resolver(packageName) !== void 0;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function safeRequireOptionalModule(packageName) {
|
|
54
|
+
try {
|
|
55
|
+
const requireFn = getRequire();
|
|
56
|
+
return requireFn ? requireFn(packageName) : void 0;
|
|
57
|
+
} catch {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function getRequire() {
|
|
62
|
+
try {
|
|
63
|
+
const maybeRequire = Function(
|
|
64
|
+
'return typeof require === "function" ? require : undefined'
|
|
65
|
+
)();
|
|
66
|
+
return typeof maybeRequire === "function" ? maybeRequire : void 0;
|
|
67
|
+
} catch {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/session-store.ts
|
|
73
|
+
import { DEFAULT_SESSION_STORAGE_KEY } from "crumbtrail-core";
|
|
74
|
+
function createReactNativeSessionStore(storage, key = DEFAULT_SESSION_STORAGE_KEY) {
|
|
75
|
+
if (!storage) return void 0;
|
|
76
|
+
let cached;
|
|
77
|
+
return {
|
|
78
|
+
read() {
|
|
79
|
+
return cached;
|
|
80
|
+
},
|
|
81
|
+
write(session) {
|
|
82
|
+
cached = session;
|
|
83
|
+
try {
|
|
84
|
+
const result = storage.setItem(key, JSON.stringify(session));
|
|
85
|
+
if (result && typeof result.catch === "function") {
|
|
86
|
+
void result.catch(() => {
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
async hydrate() {
|
|
93
|
+
try {
|
|
94
|
+
const raw = await storage.getItem(key);
|
|
95
|
+
cached = parsePersistedSession(raw);
|
|
96
|
+
return cached;
|
|
97
|
+
} catch {
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function parsePersistedSession(raw) {
|
|
104
|
+
if (!raw) return void 0;
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(raw);
|
|
107
|
+
if (typeof parsed.id !== "string" || parsed.id.length === 0)
|
|
108
|
+
return void 0;
|
|
109
|
+
return {
|
|
110
|
+
id: parsed.id,
|
|
111
|
+
lastActivity: typeof parsed.lastActivity === "number" ? parsed.lastActivity : 0
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/logger.ts
|
|
119
|
+
import { Crumbtrail } from "crumbtrail-core";
|
|
120
|
+
|
|
121
|
+
// src/target-descriptor.ts
|
|
122
|
+
function createReactNativeTargetDescriptor(input) {
|
|
123
|
+
const descriptor = {
|
|
124
|
+
role: input.role,
|
|
125
|
+
label: input.label ?? input.accessibilityLabel,
|
|
126
|
+
testID: input.testID ?? input.testId,
|
|
127
|
+
accessibilityId: input.accessibilityId,
|
|
128
|
+
componentName: input.componentName,
|
|
129
|
+
routePath: input.routePath,
|
|
130
|
+
ancestryHash: input.ancestryHash,
|
|
131
|
+
bounds: input.bounds,
|
|
132
|
+
redaction: input.redaction
|
|
133
|
+
};
|
|
134
|
+
const hasIdentity = Boolean(
|
|
135
|
+
descriptor.role || descriptor.label || descriptor.testID || descriptor.accessibilityId || descriptor.componentName || descriptor.routePath || descriptor.ancestryHash
|
|
136
|
+
);
|
|
137
|
+
return hasIdentity ? descriptor : void 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/replay-lite.ts
|
|
141
|
+
function createReactNativeReplayLite(options) {
|
|
142
|
+
const emit2 = (type, data, target) => {
|
|
143
|
+
options.logger.addEvent({
|
|
144
|
+
type,
|
|
145
|
+
data,
|
|
146
|
+
platform: "react-native",
|
|
147
|
+
sdk: { name: "crumbtrail-react-native" },
|
|
148
|
+
capabilities: options.capabilities,
|
|
149
|
+
...target ? { target } : {}
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
recordViewSnapshot(snapshot) {
|
|
154
|
+
emit2("view-snapshot", {
|
|
155
|
+
kind: "component-tree",
|
|
156
|
+
routePath: snapshot.routePath,
|
|
157
|
+
root: sanitizeNode(snapshot.root)
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
recordTouch(overlay) {
|
|
161
|
+
const target = overlay.target ? createReactNativeTargetDescriptor(
|
|
162
|
+
overlay.target
|
|
163
|
+
) : void 0;
|
|
164
|
+
emit2(
|
|
165
|
+
"touch",
|
|
166
|
+
{
|
|
167
|
+
kind: "overlay",
|
|
168
|
+
x: overlay.x,
|
|
169
|
+
y: overlay.y,
|
|
170
|
+
phase: overlay.phase ?? "press"
|
|
171
|
+
},
|
|
172
|
+
target
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
async captureCrashScreenshot(target) {
|
|
176
|
+
const capture = target !== void 0 && options.viewShot?.captureRef ? options.viewShot.captureRef(target, { format: "jpg", quality: 0.7 }) : options.viewShot?.captureScreen?.({ format: "jpg", quality: 0.7 });
|
|
177
|
+
if (!capture) return void 0;
|
|
178
|
+
try {
|
|
179
|
+
const uri = await capture;
|
|
180
|
+
emit2("view-snapshot", {
|
|
181
|
+
kind: "crash-screenshot",
|
|
182
|
+
uri,
|
|
183
|
+
capture: "react-native-view-shot"
|
|
184
|
+
});
|
|
185
|
+
return uri;
|
|
186
|
+
} catch {
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function sanitizeNode(node) {
|
|
193
|
+
return {
|
|
194
|
+
id: node.id,
|
|
195
|
+
componentName: node.componentName,
|
|
196
|
+
role: node.role,
|
|
197
|
+
label: node.label,
|
|
198
|
+
testID: node.testID,
|
|
199
|
+
accessibilityId: node.accessibilityId,
|
|
200
|
+
bounds: node.bounds,
|
|
201
|
+
children: node.children?.map(sanitizeNode)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/collectors.ts
|
|
206
|
+
var DEFAULT_COLLECTORS = {
|
|
207
|
+
console: true,
|
|
208
|
+
errors: true,
|
|
209
|
+
network: true,
|
|
210
|
+
appState: true,
|
|
211
|
+
environment: true,
|
|
212
|
+
navigation: true,
|
|
213
|
+
replayLite: true
|
|
214
|
+
};
|
|
215
|
+
function startReactNativeCollectors(logger, options) {
|
|
216
|
+
const enabled = resolveCollectorConfig(options.config);
|
|
217
|
+
const globalObject = options.globalObject ?? globalThis;
|
|
218
|
+
const reactNative = options.reactNative ?? resolveModule("react-native", options.resolver);
|
|
219
|
+
const cleanup = [];
|
|
220
|
+
if (enabled.console)
|
|
221
|
+
cleanup.push(
|
|
222
|
+
startConsoleCollector(logger, options.capabilities, globalObject)
|
|
223
|
+
);
|
|
224
|
+
if (enabled.errors)
|
|
225
|
+
cleanup.push(
|
|
226
|
+
startErrorCollector(
|
|
227
|
+
logger,
|
|
228
|
+
options.capabilities,
|
|
229
|
+
globalObject,
|
|
230
|
+
options.errorUtils
|
|
231
|
+
)
|
|
232
|
+
);
|
|
233
|
+
if (enabled.network)
|
|
234
|
+
cleanup.push(
|
|
235
|
+
startNetworkCollector(logger, options.capabilities, globalObject)
|
|
236
|
+
);
|
|
237
|
+
if (enabled.appState)
|
|
238
|
+
cleanup.push(
|
|
239
|
+
startAppStateCollector(
|
|
240
|
+
logger,
|
|
241
|
+
options.capabilities,
|
|
242
|
+
reactNative?.AppState
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
if (enabled.environment)
|
|
246
|
+
startEnvironmentCollector(logger, options.capabilities, reactNative);
|
|
247
|
+
if (enabled.navigation)
|
|
248
|
+
cleanup.push(
|
|
249
|
+
startNavigationCollector(
|
|
250
|
+
logger,
|
|
251
|
+
options.capabilities,
|
|
252
|
+
options.navigation
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
const viewShot = resolveModule(
|
|
256
|
+
"react-native-view-shot",
|
|
257
|
+
options.resolver
|
|
258
|
+
);
|
|
259
|
+
const replayLite = enabled.replayLite ? createReactNativeReplayLite({
|
|
260
|
+
logger,
|
|
261
|
+
capabilities: options.capabilities.capabilities,
|
|
262
|
+
viewShot
|
|
263
|
+
}) : void 0;
|
|
264
|
+
return {
|
|
265
|
+
cleanup() {
|
|
266
|
+
for (const stop of cleanup.splice(0).reverse()) stop();
|
|
267
|
+
},
|
|
268
|
+
replayLite
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function resolveCollectorConfig(config) {
|
|
272
|
+
if (config === false) {
|
|
273
|
+
return Object.fromEntries(
|
|
274
|
+
Object.keys(DEFAULT_COLLECTORS).map((key) => [key, false])
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (config === true || config === void 0) return { ...DEFAULT_COLLECTORS };
|
|
278
|
+
return { ...DEFAULT_COLLECTORS, ...config };
|
|
279
|
+
}
|
|
280
|
+
function emit(logger, capabilities, type, data, target) {
|
|
281
|
+
logger.addEvent({
|
|
282
|
+
type,
|
|
283
|
+
data,
|
|
284
|
+
platform: "react-native",
|
|
285
|
+
sdk: { name: "crumbtrail-react-native" },
|
|
286
|
+
capabilities: capabilities.capabilities,
|
|
287
|
+
...target ? { target } : {}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function startConsoleCollector(logger, capabilities, globalObject) {
|
|
291
|
+
const consoleObject = globalObject.console;
|
|
292
|
+
if (!consoleObject) return () => {
|
|
293
|
+
};
|
|
294
|
+
const mutableConsole = consoleObject;
|
|
295
|
+
const methods = ["log", "warn", "error", "debug", "info"];
|
|
296
|
+
const originals = /* @__PURE__ */ new Map();
|
|
297
|
+
const level = {
|
|
298
|
+
log: "log",
|
|
299
|
+
warn: "warn",
|
|
300
|
+
error: "err",
|
|
301
|
+
debug: "dbg",
|
|
302
|
+
info: "info"
|
|
303
|
+
};
|
|
304
|
+
for (const method of methods) {
|
|
305
|
+
if (typeof mutableConsole[method] !== "function") continue;
|
|
306
|
+
const original = mutableConsole[method].bind(consoleObject);
|
|
307
|
+
originals.set(method, original);
|
|
308
|
+
mutableConsole[method] = (...args) => {
|
|
309
|
+
emit(logger, capabilities, "con", {
|
|
310
|
+
lv: level[method],
|
|
311
|
+
args: args.map((arg) => safeStringify(arg))
|
|
312
|
+
});
|
|
313
|
+
original(...args);
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return () => {
|
|
317
|
+
for (const [method, original] of originals) {
|
|
318
|
+
mutableConsole[method] = original;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function startErrorCollector(logger, capabilities, globalObject, suppliedErrorUtils) {
|
|
323
|
+
const cleanup = [];
|
|
324
|
+
const errorUtils = suppliedErrorUtils ?? globalObject.ErrorUtils;
|
|
325
|
+
if (errorUtils?.setGlobalHandler && errorUtils.getGlobalHandler) {
|
|
326
|
+
const previous = errorUtils.getGlobalHandler();
|
|
327
|
+
if (typeof previous !== "function") return () => {
|
|
328
|
+
};
|
|
329
|
+
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
330
|
+
emit(logger, capabilities, "err", {
|
|
331
|
+
msg: error instanceof Error ? error.message : String(error),
|
|
332
|
+
stk: error instanceof Error ? error.stack : void 0,
|
|
333
|
+
fatal: Boolean(isFatal),
|
|
334
|
+
source: "react-native-global-handler"
|
|
335
|
+
});
|
|
336
|
+
previous?.(error, isFatal);
|
|
337
|
+
});
|
|
338
|
+
cleanup.push(() => {
|
|
339
|
+
if (previous) errorUtils.setGlobalHandler?.(previous);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const addEventListener = globalObject.addEventListener;
|
|
343
|
+
const removeEventListener = globalObject.removeEventListener;
|
|
344
|
+
if (addEventListener && removeEventListener) {
|
|
345
|
+
const onUnhandledRejection = (event) => {
|
|
346
|
+
const reason = event.reason;
|
|
347
|
+
emit(logger, capabilities, "rej", {
|
|
348
|
+
msg: reason instanceof Error ? reason.message : String(reason),
|
|
349
|
+
stk: reason instanceof Error ? reason.stack : void 0,
|
|
350
|
+
source: "react-native-unhandled-rejection"
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
addEventListener.call(
|
|
354
|
+
globalObject,
|
|
355
|
+
"unhandledrejection",
|
|
356
|
+
onUnhandledRejection
|
|
357
|
+
);
|
|
358
|
+
cleanup.push(
|
|
359
|
+
() => removeEventListener.call(
|
|
360
|
+
globalObject,
|
|
361
|
+
"unhandledrejection",
|
|
362
|
+
onUnhandledRejection
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
return () => {
|
|
367
|
+
for (const stop of cleanup.reverse()) stop();
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function startNetworkCollector(logger, capabilities, globalObject) {
|
|
371
|
+
const cleanup = [];
|
|
372
|
+
const originalFetch = globalObject.fetch;
|
|
373
|
+
if (typeof originalFetch === "function") {
|
|
374
|
+
globalObject.fetch = (async (input, init) => {
|
|
375
|
+
const startedAt = Date.now();
|
|
376
|
+
const method = init?.method?.toUpperCase() ?? "GET";
|
|
377
|
+
const url = extractUrl(input);
|
|
378
|
+
try {
|
|
379
|
+
const response = await originalFetch(input, init);
|
|
380
|
+
emit(logger, capabilities, "net", {
|
|
381
|
+
url,
|
|
382
|
+
method,
|
|
383
|
+
status: response.status,
|
|
384
|
+
ok: response.ok,
|
|
385
|
+
dur: Date.now() - startedAt,
|
|
386
|
+
source: "fetch"
|
|
387
|
+
});
|
|
388
|
+
return response;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
emit(logger, capabilities, "net", {
|
|
391
|
+
url,
|
|
392
|
+
method,
|
|
393
|
+
error: error instanceof Error ? error.message : String(error),
|
|
394
|
+
dur: Date.now() - startedAt,
|
|
395
|
+
source: "fetch"
|
|
396
|
+
});
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
cleanup.push(() => {
|
|
401
|
+
globalObject.fetch = originalFetch;
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const Xhr = globalObject.XMLHttpRequest;
|
|
405
|
+
if (Xhr?.prototype) {
|
|
406
|
+
const originalOpen = Xhr.prototype.open;
|
|
407
|
+
const originalSend = Xhr.prototype.send;
|
|
408
|
+
if (typeof originalOpen === "function" && typeof originalSend === "function") {
|
|
409
|
+
Xhr.prototype.open = function open(method, url, ...rest) {
|
|
410
|
+
this.__crumbtrailNetwork = { method, url };
|
|
411
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
412
|
+
};
|
|
413
|
+
Xhr.prototype.send = function send(body) {
|
|
414
|
+
const startedAt = Date.now();
|
|
415
|
+
const info = this.__crumbtrailNetwork;
|
|
416
|
+
const previous = this.onreadystatechange;
|
|
417
|
+
this.onreadystatechange = () => {
|
|
418
|
+
if (this.readyState === 4) {
|
|
419
|
+
emit(logger, capabilities, "net", {
|
|
420
|
+
url: info?.url,
|
|
421
|
+
method: info?.method?.toUpperCase(),
|
|
422
|
+
status: this.status,
|
|
423
|
+
dur: Date.now() - startedAt,
|
|
424
|
+
source: "xmlhttprequest"
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
previous?.call(this);
|
|
428
|
+
};
|
|
429
|
+
return originalSend.call(this, body);
|
|
430
|
+
};
|
|
431
|
+
cleanup.push(() => {
|
|
432
|
+
Xhr.prototype.open = originalOpen;
|
|
433
|
+
Xhr.prototype.send = originalSend;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return () => {
|
|
438
|
+
for (const stop of cleanup.reverse()) stop();
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function startAppStateCollector(logger, capabilities, appState) {
|
|
442
|
+
if (!appState?.addEventListener) return () => {
|
|
443
|
+
};
|
|
444
|
+
const subscription = appState.addEventListener("change", (state) => {
|
|
445
|
+
emit(logger, capabilities, "app-lifecycle", { state, source: "AppState" });
|
|
446
|
+
});
|
|
447
|
+
emit(logger, capabilities, "app-lifecycle", {
|
|
448
|
+
state: appState.currentState,
|
|
449
|
+
source: "AppState",
|
|
450
|
+
kind: "initial"
|
|
451
|
+
});
|
|
452
|
+
return toCleanup(subscription);
|
|
453
|
+
}
|
|
454
|
+
function startEnvironmentCollector(logger, capabilities, reactNative) {
|
|
455
|
+
const window = reactNative?.Dimensions?.get?.("window");
|
|
456
|
+
emit(logger, capabilities, "env", {
|
|
457
|
+
kind: "snapshot",
|
|
458
|
+
platform: {
|
|
459
|
+
os: reactNative?.Platform?.OS,
|
|
460
|
+
version: reactNative?.Platform?.Version,
|
|
461
|
+
constants: reactNative?.Platform?.constants
|
|
462
|
+
},
|
|
463
|
+
viewport: window ? {
|
|
464
|
+
w: window.width,
|
|
465
|
+
h: window.height,
|
|
466
|
+
scale: window.scale,
|
|
467
|
+
fontScale: window.fontScale
|
|
468
|
+
} : void 0
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
function startNavigationCollector(logger, capabilities, navigation) {
|
|
472
|
+
if (!navigation?.addListener) return () => {
|
|
473
|
+
};
|
|
474
|
+
let previousRouteKey;
|
|
475
|
+
const emitCurrentRoute = () => {
|
|
476
|
+
const route = navigation.getCurrentRoute?.();
|
|
477
|
+
if (!route || route.key === previousRouteKey) return;
|
|
478
|
+
previousRouteKey = route.key;
|
|
479
|
+
emit(logger, capabilities, "navigation", {
|
|
480
|
+
name: route.name,
|
|
481
|
+
path: route.path,
|
|
482
|
+
key: route.key
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
const subscription = navigation.addListener("state", emitCurrentRoute);
|
|
486
|
+
emitCurrentRoute();
|
|
487
|
+
return toCleanup(subscription);
|
|
488
|
+
}
|
|
489
|
+
function resolveModule(packageName, resolver) {
|
|
490
|
+
if (!resolver) return void 0;
|
|
491
|
+
try {
|
|
492
|
+
return resolver(packageName);
|
|
493
|
+
} catch {
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function extractUrl(input) {
|
|
498
|
+
if (typeof input === "string") return input;
|
|
499
|
+
if (input instanceof URL) return input.toString();
|
|
500
|
+
if (typeof Request !== "undefined" && input instanceof Request)
|
|
501
|
+
return input.url;
|
|
502
|
+
return String(input);
|
|
503
|
+
}
|
|
504
|
+
function safeStringify(value) {
|
|
505
|
+
if (typeof value === "string") return value;
|
|
506
|
+
try {
|
|
507
|
+
return JSON.stringify(value);
|
|
508
|
+
} catch {
|
|
509
|
+
return String(value);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function toCleanup(subscription) {
|
|
513
|
+
if (typeof subscription === "function") return subscription;
|
|
514
|
+
if (subscription?.remove) return () => subscription.remove?.();
|
|
515
|
+
return () => {
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// src/logger.ts
|
|
520
|
+
var REACT_NATIVE_DEFAULT_CONFIG = {
|
|
521
|
+
network: false,
|
|
522
|
+
interactions: false,
|
|
523
|
+
keystrokes: false,
|
|
524
|
+
scroll: false,
|
|
525
|
+
visibility: false,
|
|
526
|
+
clipboard: false,
|
|
527
|
+
cookies: false,
|
|
528
|
+
storage: false,
|
|
529
|
+
performance: false,
|
|
530
|
+
video: false,
|
|
531
|
+
audio: false,
|
|
532
|
+
widget: false,
|
|
533
|
+
sessionPersistence: "session"
|
|
534
|
+
};
|
|
535
|
+
function createReactNativeCrumbtrail(options = {}) {
|
|
536
|
+
const capabilities = detectReactNativeCapabilities({
|
|
537
|
+
resolver: options.resolver
|
|
538
|
+
});
|
|
539
|
+
const userConfig = options.config;
|
|
540
|
+
const sessionStore = options.asyncStorage ? createReactNativeSessionStore(options.asyncStorage) : userConfig?.sessionStore;
|
|
541
|
+
const config = {
|
|
542
|
+
...REACT_NATIVE_DEFAULT_CONFIG,
|
|
543
|
+
...userConfig,
|
|
544
|
+
...sessionStore ? { sessionStore } : {}
|
|
545
|
+
};
|
|
546
|
+
const logger = Crumbtrail.init(config);
|
|
547
|
+
if (options.reportCapabilities !== false) {
|
|
548
|
+
logger.addEvent({
|
|
549
|
+
type: "rn.capabilities",
|
|
550
|
+
data: {
|
|
551
|
+
bitset: capabilities.bitset,
|
|
552
|
+
capabilities: capabilities.capabilities,
|
|
553
|
+
modules: capabilities.modules
|
|
554
|
+
},
|
|
555
|
+
platform: "react-native",
|
|
556
|
+
sdk: { name: "crumbtrail-react-native" },
|
|
557
|
+
capabilities: capabilities.capabilities
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
const collectors = startReactNativeCollectors(logger, {
|
|
561
|
+
config: options.collectors,
|
|
562
|
+
capabilities,
|
|
563
|
+
resolver: options.resolver,
|
|
564
|
+
globalObject: options.globalObject,
|
|
565
|
+
reactNative: options.reactNative,
|
|
566
|
+
navigation: options.navigation,
|
|
567
|
+
errorUtils: options.errorUtils
|
|
568
|
+
});
|
|
569
|
+
wrapStopWithCollectorCleanup(logger, collectors);
|
|
570
|
+
return { logger, capabilities, collectors };
|
|
571
|
+
}
|
|
572
|
+
async function createReactNativeCrumbtrailAsync(options = {}) {
|
|
573
|
+
const sessionStore = options.asyncStorage ? createReactNativeSessionStore(options.asyncStorage) : void 0;
|
|
574
|
+
await sessionStore?.hydrate();
|
|
575
|
+
return createReactNativeCrumbtrail({
|
|
576
|
+
...options,
|
|
577
|
+
asyncStorage: void 0,
|
|
578
|
+
config: {
|
|
579
|
+
...options.config,
|
|
580
|
+
...sessionStore ? { sessionStore } : {}
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
function wrapStopWithCollectorCleanup(logger, collectors) {
|
|
585
|
+
const stop = logger.stop.bind(logger);
|
|
586
|
+
let cleaned = false;
|
|
587
|
+
logger.stop = async () => {
|
|
588
|
+
if (!cleaned) {
|
|
589
|
+
cleaned = true;
|
|
590
|
+
collectors.cleanup();
|
|
591
|
+
}
|
|
592
|
+
return stop();
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/provider.ts
|
|
597
|
+
import {
|
|
598
|
+
createContext,
|
|
599
|
+
createElement,
|
|
600
|
+
useContext,
|
|
601
|
+
useEffect,
|
|
602
|
+
useState
|
|
603
|
+
} from "react";
|
|
604
|
+
var CrumbtrailReactNativeContext = createContext(void 0);
|
|
605
|
+
function CrumbtrailReactNativeProvider(props) {
|
|
606
|
+
const [value, setValue] = useState(() => {
|
|
607
|
+
if (!props.logger && props.asyncStorage) return void 0;
|
|
608
|
+
return props.logger ? {
|
|
609
|
+
logger: props.logger,
|
|
610
|
+
capabilities: detectReactNativeCapabilities({
|
|
611
|
+
resolver: props.resolver
|
|
612
|
+
})
|
|
613
|
+
} : createReactNativeCrumbtrail({
|
|
614
|
+
config: props.config,
|
|
615
|
+
resolver: props.resolver,
|
|
616
|
+
reportCapabilities: props.reportCapabilities
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
useEffect(() => {
|
|
620
|
+
let active = true;
|
|
621
|
+
if (props.logger) {
|
|
622
|
+
setValue({
|
|
623
|
+
logger: props.logger,
|
|
624
|
+
capabilities: detectReactNativeCapabilities({
|
|
625
|
+
resolver: props.resolver
|
|
626
|
+
})
|
|
627
|
+
});
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (!props.asyncStorage) return;
|
|
631
|
+
let hydratedLogger;
|
|
632
|
+
createReactNativeCrumbtrailAsync({
|
|
633
|
+
config: props.config,
|
|
634
|
+
asyncStorage: props.asyncStorage,
|
|
635
|
+
resolver: props.resolver,
|
|
636
|
+
reportCapabilities: props.reportCapabilities
|
|
637
|
+
}).then((result) => {
|
|
638
|
+
if (!active) {
|
|
639
|
+
void result.logger.stop();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
hydratedLogger = result.logger;
|
|
643
|
+
setValue(result);
|
|
644
|
+
}).catch(() => {
|
|
645
|
+
});
|
|
646
|
+
return () => {
|
|
647
|
+
active = false;
|
|
648
|
+
if (hydratedLogger) void hydratedLogger.stop();
|
|
649
|
+
};
|
|
650
|
+
}, [
|
|
651
|
+
props.asyncStorage,
|
|
652
|
+
props.config,
|
|
653
|
+
props.logger,
|
|
654
|
+
props.reportCapabilities,
|
|
655
|
+
props.resolver
|
|
656
|
+
]);
|
|
657
|
+
if (!value) return props.fallback ?? null;
|
|
658
|
+
return createElement(
|
|
659
|
+
CrumbtrailReactNativeContext.Provider,
|
|
660
|
+
{ value },
|
|
661
|
+
props.children
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
function useCrumbtrailReactNative() {
|
|
665
|
+
const value = useContext(CrumbtrailReactNativeContext);
|
|
666
|
+
if (!value) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
"useCrumbtrailReactNative must be used within CrumbtrailReactNativeProvider"
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
return value;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/use-bug-state.ts
|
|
675
|
+
import { useEffect as useEffect2, useRef } from "react";
|
|
676
|
+
var REDACTED_VALUE = "[REDACTED]";
|
|
677
|
+
var SENSITIVE_NAME_RE = /(^|[^a-z0-9])(access[-_]?token|api[-_]?key|auth|authorization|bearer|card[-_]?number|client[-_]?secret|cookie|credential(s)?|creds|csrf|cvv|cvc|id[-_]?token|jsessionid|jwt|mfa|otp|pass[-_]?phrase|pass(code|word)?|passwd|password[-_]?confirmation|pin|private[-_]?key|pwd|refresh[-_]?token|secret|security[-_]?code|session|session[-_]?id|sid|ssn|token|verification[-_]?code|xsrf)([^a-z0-9]|$)/i;
|
|
678
|
+
var TOKEN_RE = /\b(?:Bearer|Token|Basic)\s+[A-Za-z0-9._~+/=-]{8,}\b|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b|(?:sk|pk|rk|ghp|gho|ghu|ghs|glpat|xox[baprs])[-_][A-Za-z0-9_.=-]{12,}|\b[A-Fa-f0-9]{32,}\b|\b[A-Za-z0-9_-]{40,}\b/gi;
|
|
679
|
+
function isSensitiveName(name) {
|
|
680
|
+
const normalized = name.replace(/([a-z])([A-Z])/g, "$1_$2");
|
|
681
|
+
return SENSITIVE_NAME_RE.test(name) || SENSITIVE_NAME_RE.test(normalized);
|
|
682
|
+
}
|
|
683
|
+
function redactReactNativeSnapshot(value, keyName) {
|
|
684
|
+
if (keyName && isSensitiveName(keyName)) return REDACTED_VALUE;
|
|
685
|
+
if (typeof value === "string") {
|
|
686
|
+
if (/^\s*[^:=\n\r]{1,120}\s*[:=]/.test(value)) {
|
|
687
|
+
return value.replace(
|
|
688
|
+
/^(\s*[^:=\n\r]{1,120}\s*[:=]).*$/s,
|
|
689
|
+
(_match, key) => {
|
|
690
|
+
return isSensitiveName(key) ? `${key}${REDACTED_VALUE}` : value.replace(TOKEN_RE, REDACTED_VALUE);
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
return value.replace(TOKEN_RE, REDACTED_VALUE);
|
|
695
|
+
}
|
|
696
|
+
if (Array.isArray(value))
|
|
697
|
+
return value.map((entry) => redactReactNativeSnapshot(entry));
|
|
698
|
+
if (value && typeof value === "object") {
|
|
699
|
+
const output = {};
|
|
700
|
+
for (const [key, entry] of Object.entries(
|
|
701
|
+
value
|
|
702
|
+
)) {
|
|
703
|
+
output[key] = redactReactNativeSnapshot(entry, key);
|
|
704
|
+
}
|
|
705
|
+
return output;
|
|
706
|
+
}
|
|
707
|
+
return value;
|
|
708
|
+
}
|
|
709
|
+
function useBugState(logger, name, value, options = {}) {
|
|
710
|
+
const valueRef = useRef(value);
|
|
711
|
+
valueRef.current = value;
|
|
712
|
+
useEffect2(() => {
|
|
713
|
+
if (!logger || typeof logger.registerStateProvider !== "function") return;
|
|
714
|
+
return logger.registerStateProvider(
|
|
715
|
+
name,
|
|
716
|
+
() => options.captureRawState ? valueRef.current : redactReactNativeSnapshot(valueRef.current)
|
|
717
|
+
);
|
|
718
|
+
}, [logger, name, options.captureRawState]);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/error-boundary.ts
|
|
722
|
+
import { Component } from "react";
|
|
723
|
+
var CrumbtrailReactNativeErrorBoundary = class extends Component {
|
|
724
|
+
constructor(props) {
|
|
725
|
+
super(props);
|
|
726
|
+
this.state = { hasError: false };
|
|
727
|
+
}
|
|
728
|
+
static getDerivedStateFromError(_error) {
|
|
729
|
+
return { hasError: true };
|
|
730
|
+
}
|
|
731
|
+
componentDidCatch(error, errorInfo) {
|
|
732
|
+
this.props.logger.addEvent({
|
|
733
|
+
type: "err",
|
|
734
|
+
data: {
|
|
735
|
+
msg: redactReactNativeSnapshot(error.message),
|
|
736
|
+
stack: redactReactNativeSnapshot(error.stack),
|
|
737
|
+
componentStack: redactReactNativeSnapshot(errorInfo.componentStack),
|
|
738
|
+
source: "react-native-error-boundary"
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
resetError() {
|
|
743
|
+
this.setState({ hasError: false });
|
|
744
|
+
}
|
|
745
|
+
render() {
|
|
746
|
+
if (this.state.hasError) {
|
|
747
|
+
return this.props.fallback ?? null;
|
|
748
|
+
}
|
|
749
|
+
return this.props.children;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
export {
|
|
753
|
+
CrumbtrailReactNativeErrorBoundary,
|
|
754
|
+
CrumbtrailReactNativeProvider,
|
|
755
|
+
REACT_NATIVE_CAPABILITY_BITS,
|
|
756
|
+
createReactNativeCrumbtrail,
|
|
757
|
+
createReactNativeCrumbtrailAsync,
|
|
758
|
+
createReactNativeReplayLite,
|
|
759
|
+
createReactNativeSessionStore,
|
|
760
|
+
createReactNativeTargetDescriptor,
|
|
761
|
+
detectReactNativeCapabilities,
|
|
762
|
+
redactReactNativeSnapshot,
|
|
763
|
+
startReactNativeCollectors,
|
|
764
|
+
useBugState,
|
|
765
|
+
useCrumbtrailReactNative
|
|
766
|
+
};
|