@logbrew/react-native 0.1.0 → 0.1.1
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/README.md +407 -18
- package/apollo.cjs +301 -0
- package/apollo.d.cts +70 -0
- package/apollo.d.ts +70 -0
- package/apollo.js +294 -0
- package/examples/apollo-link-spans.mjs +149 -0
- package/examples/index.mjs +29 -1
- package/examples/instrumentation-kit.mjs +304 -0
- package/examples/lifecycle-spans.mjs +131 -0
- package/examples/native-bridge-scope.mjs +107 -0
- package/examples/navigation-resource-spans.mjs +156 -0
- package/examples/package.json +8 -1
- package/examples/real-user-smoke.mjs +106 -9
- package/examples/resource-fetch-spans.mjs +133 -0
- package/examples/trace-correlation.mjs +135 -0
- package/index.cjs +625 -111
- package/index.d.cts +236 -0
- package/index.d.ts +236 -0
- package/index.js +613 -95
- package/index.native.js +18 -0
- package/instrumentation.cjs +639 -0
- package/instrumentation.d.cts +84 -0
- package/instrumentation.d.ts +84 -0
- package/instrumentation.js +634 -0
- package/lifecycle.cjs +129 -0
- package/lifecycle.d.cts +50 -0
- package/lifecycle.d.ts +50 -0
- package/lifecycle.js +121 -0
- package/metadata.cjs +175 -0
- package/metadata.js +165 -0
- package/metro.cjs +310 -0
- package/metro.d.cts +37 -0
- package/metro.d.ts +37 -0
- package/metro.js +6 -0
- package/native-bridge.cjs +127 -0
- package/native-bridge.d.cts +60 -0
- package/native-bridge.d.ts +60 -0
- package/native-bridge.js +125 -0
- package/package.json +113 -4
- package/release-artifacts.cjs +344 -0
- package/release-artifacts.d.cts +55 -0
- package/release-artifacts.d.ts +53 -0
- package/release-artifacts.js +8 -0
- package/resource-fetch.cjs +469 -0
- package/resource-fetch.d.cts +60 -0
- package/resource-fetch.d.ts +60 -0
- package/resource-fetch.js +464 -0
package/index.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import {
|
|
3
|
+
createIssueAttributesFromError,
|
|
3
4
|
createTraceparent,
|
|
4
5
|
LogBrewClient,
|
|
5
6
|
parseTraceparent,
|
|
6
7
|
SdkError
|
|
7
8
|
} from "@logbrew/sdk";
|
|
9
|
+
import {
|
|
10
|
+
runtimeReactNativeDebugIdMap,
|
|
11
|
+
sanitizeReactNativeIssueMetadata,
|
|
12
|
+
sanitizeReactNativeIssueStackFrames
|
|
13
|
+
} from "./metadata.js";
|
|
8
14
|
|
|
9
15
|
const DEFAULT_SDK_NAME = "logbrew-react-native";
|
|
10
16
|
const DEFAULT_SDK_VERSION = "0.1.0";
|
|
11
17
|
const LogBrewNativeContext = React.createContext(null);
|
|
18
|
+
const activeTraceScopes = [];
|
|
19
|
+
let nextTraceScopeId = 0;
|
|
12
20
|
|
|
13
21
|
export function createLogBrewReactNativeClient({
|
|
14
22
|
apiKey,
|
|
@@ -24,12 +32,7 @@ export function createLogBrewReactNativeClient({
|
|
|
24
32
|
return LogBrewClient.create({ apiKey: authKey, sdkName, sdkVersion, maxRetries });
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
export function createReactNativeTraceparent({
|
|
28
|
-
randomValues = defaultRandomValues,
|
|
29
|
-
spanId,
|
|
30
|
-
traceFlags = "01",
|
|
31
|
-
traceId
|
|
32
|
-
} = {}) {
|
|
35
|
+
export function createReactNativeTraceparent({ randomValues = defaultRandomValues, spanId, traceFlags = "01", traceId } = {}) {
|
|
33
36
|
return createTraceparent({
|
|
34
37
|
spanId: spanId ?? randomHex(8, randomValues),
|
|
35
38
|
traceFlags,
|
|
@@ -37,9 +40,123 @@ export function createReactNativeTraceparent({
|
|
|
37
40
|
});
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
export function createReactNativeTraceContext({
|
|
44
|
+
parentSpanId, randomValues = defaultRandomValues, spanId, traceFlags = "01", traceId, traceparent
|
|
45
|
+
} = {}) {
|
|
46
|
+
if (traceparent !== undefined && traceparent !== null && String(traceparent).trim() !== "") {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = parseTraceparent(traceparent);
|
|
49
|
+
const localSpanId = spanId ?? randomHex(8, randomValues);
|
|
50
|
+
createTraceparent({ traceId: parsed.traceId, spanId: localSpanId, traceFlags: parsed.traceFlags });
|
|
51
|
+
return freezeTraceContext({
|
|
52
|
+
parentSpanId: parsed.parentSpanId,
|
|
53
|
+
sampled: parsed.sampled,
|
|
54
|
+
spanId: localSpanId,
|
|
55
|
+
traceFlags: parsed.traceFlags,
|
|
56
|
+
traceId: parsed.traceId
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
// Bad upstream propagation should not break mobile app flows.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const localTraceId = traceId ?? randomHex(16, randomValues);
|
|
64
|
+
const localSpanId = spanId ?? randomHex(8, randomValues);
|
|
65
|
+
createTraceparent({ traceId: localTraceId, spanId: localSpanId, traceFlags });
|
|
66
|
+
if (parentSpanId !== undefined) {
|
|
67
|
+
createTraceparent({ traceId: localTraceId, spanId: parentSpanId, traceFlags });
|
|
68
|
+
}
|
|
69
|
+
return freezeTraceContext({
|
|
70
|
+
parentSpanId,
|
|
71
|
+
sampled: sampledFromTraceFlags(traceFlags),
|
|
72
|
+
spanId: localSpanId,
|
|
73
|
+
traceFlags,
|
|
74
|
+
traceId: localTraceId
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getActiveLogBrewTrace() {
|
|
79
|
+
return activeTraceScopes.length === 0 ? undefined : activeTraceScopes[activeTraceScopes.length - 1].trace;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function withLogBrewTrace(trace, callback) {
|
|
83
|
+
if (typeof callback !== "function") {
|
|
84
|
+
throw new SdkError("configuration_error", "withLogBrewTrace requires a callback");
|
|
85
|
+
}
|
|
86
|
+
const context = resolveTraceContext(trace) ?? createReactNativeTraceContext();
|
|
87
|
+
const scope = {
|
|
88
|
+
id: ++nextTraceScopeId,
|
|
89
|
+
trace: context
|
|
90
|
+
};
|
|
91
|
+
activeTraceScopes.push(scope);
|
|
92
|
+
try {
|
|
93
|
+
return callback(context);
|
|
94
|
+
} finally {
|
|
95
|
+
removeActiveTraceScope(scope.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function bindLogBrewTrace(trace, callback) {
|
|
100
|
+
if (typeof callback !== "function") {
|
|
101
|
+
throw new SdkError("configuration_error", "bindLogBrewTrace requires a callback");
|
|
102
|
+
}
|
|
103
|
+
const context = resolveTraceContext(trace) ?? createReactNativeTraceContext();
|
|
104
|
+
return function logBrewTracedCallback(...args) {
|
|
105
|
+
return withLogBrewTrace(context, () => callback(...args));
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getReactNativeTraceMetadata(trace = getActiveLogBrewTrace()) {
|
|
110
|
+
const context = resolveTraceContext(trace);
|
|
111
|
+
if (!context) {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
return compactMetadata({
|
|
115
|
+
parentSpanId: context.parentSpanId,
|
|
116
|
+
spanId: context.spanId,
|
|
117
|
+
traceFlags: context.traceFlags,
|
|
118
|
+
traceId: context.traceId,
|
|
119
|
+
traceSampled: context.sampled
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createReactNativeSpanAttributes({
|
|
124
|
+
durationMs,
|
|
125
|
+
metadata = {},
|
|
126
|
+
name,
|
|
127
|
+
spanId,
|
|
128
|
+
status = "ok",
|
|
129
|
+
trace = getActiveLogBrewTrace()
|
|
130
|
+
} = {}) {
|
|
131
|
+
const context = resolveTraceContext(trace) ?? createReactNativeTraceContext();
|
|
132
|
+
return {
|
|
133
|
+
name,
|
|
134
|
+
traceId: context.traceId,
|
|
135
|
+
spanId: spanId ?? context.spanId,
|
|
136
|
+
status,
|
|
137
|
+
durationMs,
|
|
138
|
+
metadata: compactMetadata({
|
|
139
|
+
...metadata,
|
|
140
|
+
...getReactNativeTraceMetadata(context)
|
|
141
|
+
})
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createReactNativeTraceHeaders(trace = getActiveLogBrewTrace()) {
|
|
146
|
+
const context = resolveTraceContext(trace) ?? createReactNativeTraceContext();
|
|
147
|
+
return {
|
|
148
|
+
traceparent: createTraceparent({
|
|
149
|
+
traceFlags: context.traceFlags,
|
|
150
|
+
traceId: context.traceId,
|
|
151
|
+
spanId: context.spanId
|
|
152
|
+
})
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
40
156
|
export function createTraceparentFetch({
|
|
41
157
|
fetchImpl = defaultFetch(),
|
|
42
158
|
randomValues = defaultRandomValues,
|
|
159
|
+
trace,
|
|
43
160
|
traceFlags = "01",
|
|
44
161
|
traceparent,
|
|
45
162
|
traceparentFactory,
|
|
@@ -62,15 +179,7 @@ export function createTraceparentFetch({
|
|
|
62
179
|
}
|
|
63
180
|
|
|
64
181
|
const requestInit = init ?? {};
|
|
65
|
-
const nextTraceparent = traceparentForRequest({
|
|
66
|
-
init,
|
|
67
|
-
input,
|
|
68
|
-
randomValues,
|
|
69
|
-
traceFlags,
|
|
70
|
-
traceparent,
|
|
71
|
-
traceparentFactory,
|
|
72
|
-
url
|
|
73
|
-
});
|
|
182
|
+
const nextTraceparent = traceparentForRequest({ init, input, randomValues, trace, traceFlags, traceparent, traceparentFactory, url });
|
|
74
183
|
const nextInit = {
|
|
75
184
|
...requestInit,
|
|
76
185
|
headers: headersWithTraceparent(requestHeaders(input, requestInit), nextTraceparent)
|
|
@@ -85,7 +194,7 @@ export function shouldPropagateTraceparent(url, tracePropagationTargets = []) {
|
|
|
85
194
|
}
|
|
86
195
|
return tracePropagationTargets.some((target) => {
|
|
87
196
|
if (typeof target === "string") {
|
|
88
|
-
return url
|
|
197
|
+
return shouldPropagateToStringTarget(url, target);
|
|
89
198
|
}
|
|
90
199
|
if (target instanceof RegExp) {
|
|
91
200
|
target.lastIndex = 0;
|
|
@@ -117,6 +226,7 @@ export function captureScreenView(client, screenName, {
|
|
|
117
226
|
status = "success",
|
|
118
227
|
platform,
|
|
119
228
|
appState,
|
|
229
|
+
trace,
|
|
120
230
|
metadata = {}
|
|
121
231
|
} = {}) {
|
|
122
232
|
requireClient(client);
|
|
@@ -127,7 +237,8 @@ export function captureScreenView(client, screenName, {
|
|
|
127
237
|
metadata: {
|
|
128
238
|
...getReactNativeContext({ platform, appState }),
|
|
129
239
|
screen: screenName,
|
|
130
|
-
...metadata
|
|
240
|
+
...metadata,
|
|
241
|
+
...getReactNativeTraceMetadata(trace ?? getActiveLogBrewTrace())
|
|
131
242
|
}
|
|
132
243
|
});
|
|
133
244
|
}
|
|
@@ -137,6 +248,7 @@ export function captureAppStateChange(client, state, {
|
|
|
137
248
|
timestamp = new Date().toISOString(),
|
|
138
249
|
platform,
|
|
139
250
|
appState,
|
|
251
|
+
trace,
|
|
140
252
|
metadata = {}
|
|
141
253
|
} = {}) {
|
|
142
254
|
requireClient(client);
|
|
@@ -147,12 +259,249 @@ export function captureAppStateChange(client, state, {
|
|
|
147
259
|
metadata: {
|
|
148
260
|
...getReactNativeContext({ platform, appState }),
|
|
149
261
|
appState: state,
|
|
150
|
-
...metadata
|
|
262
|
+
...metadata,
|
|
263
|
+
...getReactNativeTraceMetadata(trace ?? getActiveLogBrewTrace())
|
|
151
264
|
}
|
|
152
265
|
});
|
|
153
266
|
}
|
|
154
267
|
|
|
268
|
+
export function createReactNativeActionEvent({
|
|
269
|
+
id,
|
|
270
|
+
idFactory = defaultActionEventId,
|
|
271
|
+
metadata = {},
|
|
272
|
+
name,
|
|
273
|
+
now = () => new Date().toISOString(),
|
|
274
|
+
platform,
|
|
275
|
+
appState,
|
|
276
|
+
screen,
|
|
277
|
+
sessionId,
|
|
278
|
+
status = "success",
|
|
279
|
+
timestamp,
|
|
280
|
+
trace,
|
|
281
|
+
traceId
|
|
282
|
+
} = {}) {
|
|
283
|
+
return {
|
|
284
|
+
id: id ?? idFactory({ name, screen }),
|
|
285
|
+
timestamp: timestamp ?? now(),
|
|
286
|
+
attributes: {
|
|
287
|
+
name,
|
|
288
|
+
status,
|
|
289
|
+
metadata: compactMetadata({
|
|
290
|
+
...getReactNativeContext({ platform, appState }),
|
|
291
|
+
source: "react-native.action",
|
|
292
|
+
screen,
|
|
293
|
+
sessionId,
|
|
294
|
+
traceId,
|
|
295
|
+
...metadata,
|
|
296
|
+
...getReactNativeTraceMetadata(trace ?? getActiveLogBrewTrace())
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function captureReactNativeAction(client, input = {}) {
|
|
303
|
+
requireClient(client);
|
|
304
|
+
const event = createReactNativeActionEvent(input);
|
|
305
|
+
client.action(event.id, event.timestamp, event.attributes);
|
|
306
|
+
return event;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function createReactNativeNetworkEvent({
|
|
310
|
+
durationMs,
|
|
311
|
+
id,
|
|
312
|
+
idFactory = defaultNetworkEventId,
|
|
313
|
+
metadata = {},
|
|
314
|
+
method,
|
|
315
|
+
name,
|
|
316
|
+
now = () => new Date().toISOString(),
|
|
317
|
+
platform,
|
|
318
|
+
appState,
|
|
319
|
+
routeTemplate,
|
|
320
|
+
screen,
|
|
321
|
+
sessionId,
|
|
322
|
+
status,
|
|
323
|
+
statusCode,
|
|
324
|
+
timestamp,
|
|
325
|
+
trace,
|
|
326
|
+
traceId
|
|
327
|
+
} = {}) {
|
|
328
|
+
const safeRouteTemplate = stripQueryAndHash(routeTemplate);
|
|
329
|
+
const safeMethod = method === undefined ? undefined : String(method).toUpperCase();
|
|
330
|
+
const actionName = name ?? [safeMethod, safeRouteTemplate].filter(Boolean).join(" ");
|
|
331
|
+
return {
|
|
332
|
+
id: id ?? idFactory({ method: safeMethod, routeTemplate: safeRouteTemplate, screen }),
|
|
333
|
+
timestamp: timestamp ?? now(),
|
|
334
|
+
attributes: {
|
|
335
|
+
name: actionName,
|
|
336
|
+
status: status ?? statusFromStatusCode(statusCode),
|
|
337
|
+
metadata: compactMetadata({
|
|
338
|
+
...getReactNativeContext({ platform, appState }),
|
|
339
|
+
source: "react-native.network",
|
|
340
|
+
durationMs,
|
|
341
|
+
method: safeMethod,
|
|
342
|
+
routeTemplate: safeRouteTemplate,
|
|
343
|
+
screen,
|
|
344
|
+
sessionId,
|
|
345
|
+
statusCode,
|
|
346
|
+
traceId,
|
|
347
|
+
...metadata,
|
|
348
|
+
...getReactNativeTraceMetadata(trace ?? getActiveLogBrewTrace())
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function captureReactNativeNetwork(client, input = {}) {
|
|
355
|
+
requireClient(client);
|
|
356
|
+
const event = createReactNativeNetworkEvent(input);
|
|
357
|
+
client.action(event.id, event.timestamp, event.attributes);
|
|
358
|
+
return event;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function createReactNativeNavigationSpanEvent({
|
|
362
|
+
actionType, durationMs, id, idFactory = defaultNavigationSpanEventId, includeRouteKey = false, metadata = {},
|
|
363
|
+
name, now = () => new Date().toISOString(), platform, appState, previousRouteKey, previousRouteName,
|
|
364
|
+
routeKey, routeName, routePath, screen, status = "ok", timestamp, trace
|
|
365
|
+
} = {}) {
|
|
366
|
+
const safeRoutePath = stripQueryAndHash(routePath);
|
|
367
|
+
const spanName = name ?? `navigation:${routeName ?? safeRoutePath ?? screen ?? "route"}`;
|
|
368
|
+
const context = resolveTraceContext(trace) ?? getActiveLogBrewTrace() ?? createReactNativeTraceContext();
|
|
369
|
+
return {
|
|
370
|
+
id: id ?? idFactory({ routeName, routePath: safeRoutePath, screen }),
|
|
371
|
+
timestamp: timestamp ?? now(),
|
|
372
|
+
attributes: createReactNativeSpanAttributes({
|
|
373
|
+
name: spanName,
|
|
374
|
+
status,
|
|
375
|
+
durationMs,
|
|
376
|
+
trace: context,
|
|
377
|
+
metadata: {
|
|
378
|
+
...getReactNativeContext({ platform, appState }),
|
|
379
|
+
source: "react-native.navigation",
|
|
380
|
+
actionType,
|
|
381
|
+
previousRouteName,
|
|
382
|
+
routeName,
|
|
383
|
+
routePath: safeRoutePath,
|
|
384
|
+
screen: screen ?? routeName,
|
|
385
|
+
...(includeRouteKey ? { previousRouteKey, routeKey } : {}),
|
|
386
|
+
...metadata
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function captureReactNativeNavigationSpan(client, input = {}) {
|
|
393
|
+
requireClient(client);
|
|
394
|
+
const event = createReactNativeNavigationSpanEvent(input);
|
|
395
|
+
client.span(event.id, event.timestamp, event.attributes);
|
|
396
|
+
return event;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function createReactNativeResourceSpanEvent({
|
|
400
|
+
durationMs, id, idFactory = defaultResourceSpanEventId, kind = "fetch", metadata = {}, method, name,
|
|
401
|
+
now = () => new Date().toISOString(), platform, appState, responseSizeBytes, routeTemplate, screen,
|
|
402
|
+
sessionId, status, statusCode, timestamp, trace
|
|
403
|
+
} = {}) {
|
|
404
|
+
const safeRouteTemplate = stripQueryAndHash(routeTemplate);
|
|
405
|
+
const safeMethod = method === undefined ? undefined : String(method).toUpperCase();
|
|
406
|
+
const defaultName = [safeMethod, safeRouteTemplate].filter(Boolean).join(" ");
|
|
407
|
+
const spanName = name ?? (defaultName || "mobile.resource");
|
|
408
|
+
const context = resolveTraceContext(trace) ?? getActiveLogBrewTrace() ?? createReactNativeTraceContext();
|
|
409
|
+
return {
|
|
410
|
+
id: id ?? idFactory({ method: safeMethod, routeTemplate: safeRouteTemplate, screen }),
|
|
411
|
+
timestamp: timestamp ?? now(),
|
|
412
|
+
attributes: createReactNativeSpanAttributes({
|
|
413
|
+
name: spanName,
|
|
414
|
+
status: status ?? spanStatusFromStatusCode(statusCode),
|
|
415
|
+
durationMs,
|
|
416
|
+
trace: context,
|
|
417
|
+
metadata: {
|
|
418
|
+
...getReactNativeContext({ platform, appState }),
|
|
419
|
+
source: "react-native.resource",
|
|
420
|
+
durationMs,
|
|
421
|
+
method: safeMethod,
|
|
422
|
+
resourceKind: kind,
|
|
423
|
+
responseSizeBytes,
|
|
424
|
+
routeTemplate: safeRouteTemplate,
|
|
425
|
+
screen,
|
|
426
|
+
sessionId,
|
|
427
|
+
statusCode,
|
|
428
|
+
...metadata
|
|
429
|
+
}
|
|
430
|
+
})
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function captureReactNativeResourceSpan(client, input = {}) {
|
|
435
|
+
requireClient(client);
|
|
436
|
+
const event = createReactNativeResourceSpanEvent(input);
|
|
437
|
+
client.span(event.id, event.timestamp, event.attributes);
|
|
438
|
+
return event;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function createReactNavigationSpanListener(client, navigationContainer, {
|
|
442
|
+
captureInitialRoute = false, includeRouteKey = false, metadata = {}, now = () => new Date().toISOString(),
|
|
443
|
+
nowMs = () => Date.now(), onError, platform, appState, trace
|
|
444
|
+
} = {}) {
|
|
445
|
+
requireClient(client);
|
|
446
|
+
const container = resolveNavigationContainer(navigationContainer);
|
|
447
|
+
if (!container || typeof container.addListener !== "function" || typeof container.getCurrentRoute !== "function") {
|
|
448
|
+
throw new SdkError("configuration_error", "createReactNavigationSpanListener requires a React Navigation container ref with addListener and getCurrentRoute");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let previousRoute = captureInitialRoute ? {} : routeSnapshot(container.getCurrentRoute());
|
|
452
|
+
let pendingNavigation;
|
|
453
|
+
const removers = [];
|
|
454
|
+
const captureCurrentRoute = () => {
|
|
455
|
+
try {
|
|
456
|
+
const route = routeSnapshot(container.getCurrentRoute());
|
|
457
|
+
if (!route.name && !route.path) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const startedAtMs = pendingNavigation?.startedAtMs;
|
|
461
|
+
const durationMs = startedAtMs === undefined ? undefined : Math.max(0, nowMs() - startedAtMs);
|
|
462
|
+
captureReactNativeNavigationSpan(client, {
|
|
463
|
+
actionType: pendingNavigation?.actionType, durationMs, includeRouteKey, metadata, now, platform, appState,
|
|
464
|
+
previousRouteKey: previousRoute.key, previousRouteName: previousRoute.name, routeKey: route.key,
|
|
465
|
+
routeName: route.name, routePath: route.path, timestamp: pendingNavigation?.timestamp, trace
|
|
466
|
+
});
|
|
467
|
+
previousRoute = route;
|
|
468
|
+
pendingNavigation = undefined;
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (typeof onError === "function") {
|
|
471
|
+
onError(error);
|
|
472
|
+
} else {
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const actionSubscription = safeNavigationListener(container, "__unsafe_action__", (event) => {
|
|
479
|
+
pendingNavigation = {
|
|
480
|
+
actionType: navigationActionType(event),
|
|
481
|
+
startedAtMs: nowMs(),
|
|
482
|
+
timestamp: now()
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
if (actionSubscription) {
|
|
486
|
+
removers.push(actionSubscription);
|
|
487
|
+
}
|
|
488
|
+
removers.push(addNavigationListener(container, "state", captureCurrentRoute));
|
|
489
|
+
|
|
490
|
+
if (captureInitialRoute) {
|
|
491
|
+
captureCurrentRoute();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return () => {
|
|
495
|
+
for (const remove of removers.splice(0).reverse()) {
|
|
496
|
+
remove();
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
155
501
|
export function createReactNativeErrorEvent(error, {
|
|
502
|
+
debugIdMap,
|
|
503
|
+
environment,
|
|
504
|
+
fingerprint,
|
|
156
505
|
id,
|
|
157
506
|
idFactory = defaultErrorEventId,
|
|
158
507
|
includeStack = false,
|
|
@@ -161,27 +510,43 @@ export function createReactNativeErrorEvent(error, {
|
|
|
161
510
|
now = () => new Date().toISOString(),
|
|
162
511
|
platform,
|
|
163
512
|
appState,
|
|
513
|
+
release,
|
|
514
|
+
runtime,
|
|
164
515
|
screen,
|
|
165
|
-
|
|
516
|
+
service,
|
|
517
|
+
timestamp,
|
|
518
|
+
trace
|
|
166
519
|
} = {}) {
|
|
167
|
-
const details = errorDetails(error
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
520
|
+
const details = errorDetails(error);
|
|
521
|
+
const traceContext = resolveTraceContext(trace ?? getActiveLogBrewTrace());
|
|
522
|
+
const attributes = createIssueAttributesFromError(error?.reason ?? error?.error ?? error, {
|
|
523
|
+
debugIdMap: debugIdMap === undefined ? runtimeReactNativeDebugIdMap() : debugIdMap,
|
|
524
|
+
environment,
|
|
525
|
+
fingerprint,
|
|
526
|
+
includeErrorStack: includeStack,
|
|
527
|
+
level,
|
|
528
|
+
metadata: {
|
|
529
|
+
...getReactNativeContext({ platform, appState }),
|
|
530
|
+
errorValueType: details.valueType,
|
|
531
|
+
screen,
|
|
532
|
+
...metadata
|
|
533
|
+
},
|
|
534
|
+
release,
|
|
535
|
+
runtime,
|
|
536
|
+
service,
|
|
172
537
|
source: "react-native.error",
|
|
173
|
-
|
|
174
|
-
...(includeStack ? { errorStack: details.stack } : {}),
|
|
175
|
-
...metadata
|
|
538
|
+
trace: traceContext
|
|
176
539
|
});
|
|
540
|
+
const stackFrames = sanitizeReactNativeIssueStackFrames(attributes.stackFrames);
|
|
177
541
|
return {
|
|
178
542
|
id: id ?? idFactory({ error, message: details.message, screen }),
|
|
179
543
|
timestamp: timestamp ?? now(),
|
|
180
544
|
attributes: {
|
|
545
|
+
...attributes,
|
|
546
|
+
...(stackFrames ? { stackFrames } : {}),
|
|
181
547
|
title: `React Native error: ${details.message}`,
|
|
182
|
-
level,
|
|
183
548
|
message: details.message,
|
|
184
|
-
metadata:
|
|
549
|
+
metadata: sanitizeReactNativeIssueMetadata(attributes.metadata, compactMetadata)
|
|
185
550
|
}
|
|
186
551
|
};
|
|
187
552
|
}
|
|
@@ -206,22 +571,17 @@ export function createAppStateListener(client, appState, options = {}) {
|
|
|
206
571
|
});
|
|
207
572
|
});
|
|
208
573
|
|
|
209
|
-
|
|
210
|
-
return subscription;
|
|
211
|
-
}
|
|
212
|
-
if (subscription && typeof subscription.remove === "function") {
|
|
213
|
-
return () => subscription.remove();
|
|
214
|
-
}
|
|
215
|
-
return () => {};
|
|
574
|
+
return subscriptionRemover(subscription);
|
|
216
575
|
}
|
|
217
576
|
|
|
218
|
-
export function LogBrewNativeProvider({ client, platform, appState, children }) {
|
|
577
|
+
export function LogBrewNativeProvider({ client, platform, appState, trace, children }) {
|
|
219
578
|
requireClient(client);
|
|
220
579
|
const value = React.useMemo(() => ({
|
|
221
580
|
client,
|
|
222
581
|
platform,
|
|
223
|
-
appState
|
|
224
|
-
|
|
582
|
+
appState,
|
|
583
|
+
trace: resolveTraceContext(trace)
|
|
584
|
+
}), [appState, client, platform, trace]);
|
|
225
585
|
return React.createElement(LogBrewNativeContext.Provider, { value }, children);
|
|
226
586
|
}
|
|
227
587
|
|
|
@@ -234,33 +594,27 @@ export function useLogBrewNative() {
|
|
|
234
594
|
}
|
|
235
595
|
|
|
236
596
|
export function useLogBrewNativeActions() {
|
|
237
|
-
const { client, platform, appState } = useLogBrewNative();
|
|
597
|
+
const { client, platform, appState, trace } = useLogBrewNative();
|
|
598
|
+
const scoped = (options = {}) => ({ platform, appState, trace, ...options });
|
|
238
599
|
return {
|
|
239
600
|
release: client.release.bind(client),
|
|
240
601
|
environment: client.environment.bind(client),
|
|
241
|
-
issue: client.issue
|
|
242
|
-
log: client.log
|
|
602
|
+
issue: (id, timestamp, attributes) => client.issue(id, timestamp, attributesWithTrace(attributes, trace)),
|
|
603
|
+
log: (id, timestamp, attributes) => client.log(id, timestamp, attributesWithTrace(attributes, trace)),
|
|
243
604
|
span: client.span.bind(client),
|
|
244
|
-
action: client.action
|
|
605
|
+
action: (id, timestamp, attributes) => client.action(id, timestamp, attributesWithTrace(attributes, trace)),
|
|
245
606
|
flush: client.flush.bind(client),
|
|
246
607
|
shutdown: client.shutdown.bind(client),
|
|
247
608
|
previewJson: client.previewJson.bind(client),
|
|
248
609
|
pendingEvents: client.pendingEvents.bind(client),
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}),
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
...options
|
|
258
|
-
}),
|
|
259
|
-
captureReactNativeError: (error, options = {}) => captureReactNativeError(client, error, {
|
|
260
|
-
platform,
|
|
261
|
-
appState,
|
|
262
|
-
...options
|
|
263
|
-
})
|
|
610
|
+
trace,
|
|
611
|
+
captureScreenView: (screenName, options = {}) => captureScreenView(client, screenName, scoped(options)),
|
|
612
|
+
captureAppStateChange: (state, options = {}) => captureAppStateChange(client, state, scoped(options)),
|
|
613
|
+
captureReactNativeAction: (input = {}) => captureReactNativeAction(client, scoped(input)),
|
|
614
|
+
captureReactNativeNetwork: (input = {}) => captureReactNativeNetwork(client, scoped(input)),
|
|
615
|
+
captureReactNativeNavigationSpan: (input = {}) => captureReactNativeNavigationSpan(client, scoped(input)),
|
|
616
|
+
captureReactNativeResourceSpan: (input = {}) => captureReactNativeResourceSpan(client, scoped(input)),
|
|
617
|
+
captureReactNativeError: (error, options = {}) => captureReactNativeError(client, error, scoped(options))
|
|
264
618
|
};
|
|
265
619
|
}
|
|
266
620
|
|
|
@@ -299,25 +653,26 @@ function compactMetadata(metadata) {
|
|
|
299
653
|
return compacted;
|
|
300
654
|
}
|
|
301
655
|
|
|
302
|
-
function
|
|
303
|
-
const
|
|
304
|
-
|
|
656
|
+
function attributesWithTrace(attributes, trace) {
|
|
657
|
+
const context = resolveTraceContext(trace) ?? getActiveLogBrewTrace();
|
|
658
|
+
if (!context) {
|
|
659
|
+
return attributes;
|
|
660
|
+
}
|
|
305
661
|
return {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
662
|
+
...attributes,
|
|
663
|
+
metadata: compactMetadata({
|
|
664
|
+
...(attributes?.metadata ?? {}),
|
|
665
|
+
...getReactNativeTraceMetadata(context)
|
|
666
|
+
})
|
|
310
667
|
};
|
|
311
668
|
}
|
|
312
669
|
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
return undefined;
|
|
670
|
+
function errorDetails(error) {
|
|
671
|
+
const candidate = error?.reason ?? error?.error ?? error;
|
|
672
|
+
return {
|
|
673
|
+
message: errorMessage(candidate),
|
|
674
|
+
valueType: candidate === null ? "null" : typeof candidate
|
|
675
|
+
};
|
|
321
676
|
}
|
|
322
677
|
|
|
323
678
|
function errorMessage(error) {
|
|
@@ -337,6 +692,22 @@ function defaultErrorEventId({ message, screen }) {
|
|
|
337
692
|
return `evt_native_error_${slugify(`${screen ?? "app"}_${message}`)}`;
|
|
338
693
|
}
|
|
339
694
|
|
|
695
|
+
function defaultActionEventId({ name, screen }) {
|
|
696
|
+
return `evt_native_action_${slugify(`${screen ?? "app"}_${name ?? "event"}`)}`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function defaultNetworkEventId({ method, routeTemplate, screen }) {
|
|
700
|
+
return `evt_native_network_${slugify([screen, method, routeTemplate].filter(Boolean).join("_") || "request")}`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function defaultNavigationSpanEventId({ routeName, routePath, screen }) {
|
|
704
|
+
return `evt_native_navigation_${slugify([screen, routeName, routePath].filter(Boolean).join("_") || "route")}`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function defaultResourceSpanEventId({ method, routeTemplate, screen }) {
|
|
708
|
+
return `evt_native_resource_${slugify([screen, method, routeTemplate].filter(Boolean).join("_") || "request")}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
340
711
|
function defaultFetch() {
|
|
341
712
|
return typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : undefined;
|
|
342
713
|
}
|
|
@@ -395,6 +766,99 @@ function randomHex(length, randomValues) {
|
|
|
395
766
|
return bytes.map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
396
767
|
}
|
|
397
768
|
|
|
769
|
+
function resolveTraceContext(trace) {
|
|
770
|
+
if (trace === undefined || trace === null) {
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
if (typeof trace === "string") {
|
|
774
|
+
return createReactNativeTraceContext({ traceparent: trace });
|
|
775
|
+
}
|
|
776
|
+
if (typeof trace === "object") {
|
|
777
|
+
const {
|
|
778
|
+
parentSpanId,
|
|
779
|
+
sampled,
|
|
780
|
+
spanId,
|
|
781
|
+
traceFlags = "01",
|
|
782
|
+
traceId
|
|
783
|
+
} = trace;
|
|
784
|
+
if (typeof traceId !== "string" || typeof spanId !== "string") {
|
|
785
|
+
throw new SdkError("validation_error", "trace context requires traceId and spanId");
|
|
786
|
+
}
|
|
787
|
+
createTraceparent({ traceId, spanId, traceFlags });
|
|
788
|
+
if (parentSpanId !== undefined) {
|
|
789
|
+
createTraceparent({ traceId, spanId: parentSpanId, traceFlags });
|
|
790
|
+
}
|
|
791
|
+
return freezeTraceContext({
|
|
792
|
+
parentSpanId,
|
|
793
|
+
sampled: sampled ?? sampledFromTraceFlags(traceFlags),
|
|
794
|
+
spanId,
|
|
795
|
+
traceFlags,
|
|
796
|
+
traceId
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
throw new SdkError("validation_error", "trace must be a trace context or traceparent string");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function freezeTraceContext({ parentSpanId, sampled, spanId, traceFlags, traceId }) {
|
|
803
|
+
const normalized = {
|
|
804
|
+
traceId: String(traceId).toLowerCase(),
|
|
805
|
+
spanId: String(spanId).toLowerCase(),
|
|
806
|
+
parentSpanId: parentSpanId === undefined ? undefined : String(parentSpanId).toLowerCase(),
|
|
807
|
+
traceFlags: String(traceFlags).toLowerCase(),
|
|
808
|
+
sampled: Boolean(sampled)
|
|
809
|
+
};
|
|
810
|
+
return Object.freeze(normalized);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function sampledFromTraceFlags(traceFlags) {
|
|
814
|
+
return (Number.parseInt(traceFlags, 16) & 1) === 1;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function removeActiveTraceScope(scopeId) {
|
|
818
|
+
const index = activeTraceScopes.findIndex((scope) => scope.id === scopeId);
|
|
819
|
+
if (index >= 0) {
|
|
820
|
+
activeTraceScopes.splice(index, 1);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function shouldPropagateToStringTarget(url, target) {
|
|
825
|
+
const targetText = target.trim();
|
|
826
|
+
if (targetText === "") {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
if (targetText.startsWith("/")) {
|
|
830
|
+
return url.startsWith(targetText);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const URLConstructor = globalThis.URL;
|
|
834
|
+
if (typeof URLConstructor === "function") {
|
|
835
|
+
try {
|
|
836
|
+
const targetUrl = new URLConstructor(targetText);
|
|
837
|
+
if (!hasUrlScheme(url)) {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
const requestUrl = new URLConstructor(url, targetUrl.origin);
|
|
841
|
+
if (requestUrl.origin !== targetUrl.origin) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
const targetPath = targetUrl.pathname || "/";
|
|
845
|
+
return requestUrl.pathname === targetPath || requestUrl.pathname.startsWith(pathPrefix(targetPath));
|
|
846
|
+
} catch {
|
|
847
|
+
return url.startsWith(targetText);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return url.startsWith(targetText);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function pathPrefix(pathname) {
|
|
855
|
+
return pathname.endsWith("/") ? pathname : `${pathname}/`;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function hasUrlScheme(url) {
|
|
859
|
+
return /^[a-z][a-z0-9+.-]*:/iu.test(url);
|
|
860
|
+
}
|
|
861
|
+
|
|
398
862
|
function requestHeaders(input, init) {
|
|
399
863
|
if (init && init.headers !== undefined) {
|
|
400
864
|
return init.headers;
|
|
@@ -417,40 +881,94 @@ function requestUrl(input) {
|
|
|
417
881
|
}
|
|
418
882
|
|
|
419
883
|
function slugify(value) {
|
|
420
|
-
return value
|
|
884
|
+
return String(value)
|
|
421
885
|
.toLowerCase()
|
|
422
886
|
.replace(/[^a-z0-9]+/g, "_")
|
|
423
887
|
.replace(/^_+|_+$/g, "") || "event";
|
|
424
888
|
}
|
|
425
889
|
|
|
890
|
+
function statusFromStatusCode(statusCode) {
|
|
891
|
+
if (typeof statusCode === "number" && Number.isFinite(statusCode) && statusCode >= 400) {
|
|
892
|
+
return "failure";
|
|
893
|
+
}
|
|
894
|
+
return "success";
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function spanStatusFromStatusCode(statusCode) {
|
|
898
|
+
if (typeof statusCode === "number" && Number.isFinite(statusCode) && statusCode >= 400) {
|
|
899
|
+
return "error";
|
|
900
|
+
}
|
|
901
|
+
return "ok";
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function stripQueryAndHash(value) {
|
|
905
|
+
if (value === undefined || value === null) {
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
return String(value).split(/[?#]/u, 1)[0];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function resolveNavigationContainer(navigationContainer) {
|
|
912
|
+
if (navigationContainer && typeof navigationContainer === "object" && "current" in navigationContainer) {
|
|
913
|
+
return navigationContainer.current;
|
|
914
|
+
}
|
|
915
|
+
return navigationContainer;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function addNavigationListener(container, eventName, listener) {
|
|
919
|
+
const subscription = container.addListener(eventName, listener);
|
|
920
|
+
return subscriptionRemover(subscription);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function subscriptionRemover(subscription) {
|
|
924
|
+
if (typeof subscription === "function") {
|
|
925
|
+
return subscription;
|
|
926
|
+
}
|
|
927
|
+
if (subscription && typeof subscription.remove === "function") {
|
|
928
|
+
return () => subscription.remove();
|
|
929
|
+
}
|
|
930
|
+
return () => {};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function safeNavigationListener(container, eventName, listener) {
|
|
934
|
+
try {
|
|
935
|
+
return addNavigationListener(container, eventName, listener);
|
|
936
|
+
} catch {
|
|
937
|
+
return undefined;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function routeSnapshot(route) {
|
|
942
|
+
return {
|
|
943
|
+
key: normalizeMetadataValue(route?.key),
|
|
944
|
+
name: typeof route?.name === "string" && route.name.trim() !== "" ? route.name : undefined,
|
|
945
|
+
path: stripQueryAndHash(route?.path)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function navigationActionType(event) {
|
|
950
|
+
const action = event?.data?.action ?? event?.action ?? event;
|
|
951
|
+
return typeof action?.type === "string" && action.type.trim() !== "" ? action.type : undefined;
|
|
952
|
+
}
|
|
953
|
+
|
|
426
954
|
function traceparentForRequest({
|
|
427
|
-
init,
|
|
428
|
-
input,
|
|
429
|
-
randomValues,
|
|
430
|
-
traceFlags,
|
|
431
|
-
traceparent,
|
|
432
|
-
traceparentFactory,
|
|
433
|
-
url
|
|
955
|
+
init, input, randomValues, trace, traceFlags, traceparent, traceparentFactory, url
|
|
434
956
|
}) {
|
|
957
|
+
const context = resolveTraceContext(trace) ?? getActiveLogBrewTrace();
|
|
435
958
|
const nextTraceparent = typeof traceparentFactory === "function"
|
|
436
959
|
? traceparentFactory({ init, input, url })
|
|
437
|
-
: traceparent ?? createReactNativeTraceparent({ randomValues, traceFlags });
|
|
960
|
+
: traceparent ?? (context ? createReactNativeTraceHeaders(context).traceparent : createReactNativeTraceparent({ randomValues, traceFlags }));
|
|
438
961
|
parseTraceparent(nextTraceparent);
|
|
439
962
|
return nextTraceparent;
|
|
440
963
|
}
|
|
441
964
|
|
|
442
965
|
export default {
|
|
443
|
-
LogBrewNativeProvider,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
createTraceparentFetch,
|
|
452
|
-
getReactNativeContext,
|
|
453
|
-
shouldPropagateTraceparent,
|
|
454
|
-
useLogBrewNative,
|
|
455
|
-
useLogBrewNativeActions
|
|
966
|
+
LogBrewNativeProvider, captureAppStateChange, captureReactNativeAction, captureReactNativeError,
|
|
967
|
+
captureReactNativeNetwork, captureReactNativeNavigationSpan, captureReactNativeResourceSpan, captureScreenView,
|
|
968
|
+
bindLogBrewTrace, createAppStateListener, createLogBrewReactNativeClient, createReactNavigationSpanListener,
|
|
969
|
+
createReactNativeSpanAttributes, createReactNativeTraceContext, createReactNativeTraceHeaders, createReactNativeActionEvent,
|
|
970
|
+
createReactNativeErrorEvent, createReactNativeNetworkEvent, createReactNativeNavigationSpanEvent,
|
|
971
|
+
createReactNativeResourceSpanEvent, createReactNativeTraceparent, createTraceparentFetch, getActiveLogBrewTrace,
|
|
972
|
+
getReactNativeContext, getReactNativeTraceMetadata, shouldPropagateTraceparent, useLogBrewNative,
|
|
973
|
+
useLogBrewNativeActions, withLogBrewTrace
|
|
456
974
|
};
|