@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.
Files changed (47) hide show
  1. package/README.md +407 -18
  2. package/apollo.cjs +301 -0
  3. package/apollo.d.cts +70 -0
  4. package/apollo.d.ts +70 -0
  5. package/apollo.js +294 -0
  6. package/examples/apollo-link-spans.mjs +149 -0
  7. package/examples/index.mjs +29 -1
  8. package/examples/instrumentation-kit.mjs +304 -0
  9. package/examples/lifecycle-spans.mjs +131 -0
  10. package/examples/native-bridge-scope.mjs +107 -0
  11. package/examples/navigation-resource-spans.mjs +156 -0
  12. package/examples/package.json +8 -1
  13. package/examples/real-user-smoke.mjs +106 -9
  14. package/examples/resource-fetch-spans.mjs +133 -0
  15. package/examples/trace-correlation.mjs +135 -0
  16. package/index.cjs +625 -111
  17. package/index.d.cts +236 -0
  18. package/index.d.ts +236 -0
  19. package/index.js +613 -95
  20. package/index.native.js +18 -0
  21. package/instrumentation.cjs +639 -0
  22. package/instrumentation.d.cts +84 -0
  23. package/instrumentation.d.ts +84 -0
  24. package/instrumentation.js +634 -0
  25. package/lifecycle.cjs +129 -0
  26. package/lifecycle.d.cts +50 -0
  27. package/lifecycle.d.ts +50 -0
  28. package/lifecycle.js +121 -0
  29. package/metadata.cjs +175 -0
  30. package/metadata.js +165 -0
  31. package/metro.cjs +310 -0
  32. package/metro.d.cts +37 -0
  33. package/metro.d.ts +37 -0
  34. package/metro.js +6 -0
  35. package/native-bridge.cjs +127 -0
  36. package/native-bridge.d.cts +60 -0
  37. package/native-bridge.d.ts +60 -0
  38. package/native-bridge.js +125 -0
  39. package/package.json +113 -4
  40. package/release-artifacts.cjs +344 -0
  41. package/release-artifacts.d.cts +55 -0
  42. package/release-artifacts.d.ts +53 -0
  43. package/release-artifacts.js +8 -0
  44. package/resource-fetch.cjs +469 -0
  45. package/resource-fetch.d.cts +60 -0
  46. package/resource-fetch.d.ts +60 -0
  47. package/resource-fetch.js +464 -0
package/index.cjs CHANGED
@@ -1,14 +1,22 @@
1
1
  const React = require("react");
2
2
  const {
3
+ createIssueAttributesFromError,
3
4
  createTraceparent,
4
5
  LogBrewClient,
5
6
  parseTraceparent,
6
7
  SdkError
7
8
  } = require("@logbrew/sdk");
9
+ const {
10
+ runtimeReactNativeDebugIdMap,
11
+ sanitizeReactNativeIssueMetadata,
12
+ sanitizeReactNativeIssueStackFrames
13
+ } = require("./metadata.cjs");
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
  function createLogBrewReactNativeClient({
14
22
  apiKey,
@@ -24,12 +32,7 @@ function createLogBrewReactNativeClient({
24
32
  return LogBrewClient.create({ apiKey: authKey, sdkName, sdkVersion, maxRetries });
25
33
  }
26
34
 
27
- function createReactNativeTraceparent({
28
- randomValues = defaultRandomValues,
29
- spanId,
30
- traceFlags = "01",
31
- traceId
32
- } = {}) {
35
+ 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 @@ function createReactNativeTraceparent({
37
40
  });
38
41
  }
39
42
 
43
+ 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
+ function getActiveLogBrewTrace() {
79
+ return activeTraceScopes.length === 0 ? undefined : activeTraceScopes[activeTraceScopes.length - 1].trace;
80
+ }
81
+
82
+ 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
+ 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
+ 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
+ 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
+ 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
  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 @@ 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 @@ function shouldPropagateTraceparent(url, tracePropagationTargets = []) {
85
194
  }
86
195
  return tracePropagationTargets.some((target) => {
87
196
  if (typeof target === "string") {
88
- return url.includes(target);
197
+ return shouldPropagateToStringTarget(url, target);
89
198
  }
90
199
  if (target instanceof RegExp) {
91
200
  target.lastIndex = 0;
@@ -117,6 +226,7 @@ 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 @@ 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 @@ 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,260 @@ function captureAppStateChange(client, state, {
147
259
  metadata: {
148
260
  ...getReactNativeContext({ platform, appState }),
149
261
  appState: state,
150
- ...metadata
262
+ ...metadata,
263
+ ...getReactNativeTraceMetadata(trace ?? getActiveLogBrewTrace())
264
+ }
265
+ });
266
+ }
267
+
268
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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,
464
+ durationMs,
465
+ includeRouteKey,
466
+ metadata,
467
+ now,
468
+ platform,
469
+ appState,
470
+ previousRouteKey: previousRoute.key,
471
+ previousRouteName: previousRoute.name,
472
+ routeKey: route.key,
473
+ routeName: route.name,
474
+ routePath: route.path,
475
+ timestamp: pendingNavigation?.timestamp,
476
+ trace
477
+ });
478
+ previousRoute = route;
479
+ pendingNavigation = undefined;
480
+ } catch (error) {
481
+ if (typeof onError === "function") {
482
+ onError(error);
483
+ } else {
484
+ throw error;
485
+ }
151
486
  }
487
+ };
488
+
489
+ const actionSubscription = safeNavigationListener(container, "__unsafe_action__", (event) => {
490
+ pendingNavigation = {
491
+ actionType: navigationActionType(event),
492
+ startedAtMs: nowMs(),
493
+ timestamp: now()
494
+ };
152
495
  });
496
+ if (actionSubscription) {
497
+ removers.push(actionSubscription);
498
+ }
499
+ removers.push(addNavigationListener(container, "state", captureCurrentRoute));
500
+
501
+ if (captureInitialRoute) {
502
+ captureCurrentRoute();
503
+ }
504
+
505
+ return () => {
506
+ for (const remove of removers.splice(0).reverse()) {
507
+ remove();
508
+ }
509
+ };
153
510
  }
154
511
 
155
512
  function createReactNativeErrorEvent(error, {
513
+ debugIdMap,
514
+ environment,
515
+ fingerprint,
156
516
  id,
157
517
  idFactory = defaultErrorEventId,
158
518
  includeStack = false,
@@ -161,27 +521,43 @@ function createReactNativeErrorEvent(error, {
161
521
  now = () => new Date().toISOString(),
162
522
  platform,
163
523
  appState,
524
+ release,
525
+ runtime,
164
526
  screen,
165
- timestamp
527
+ service,
528
+ timestamp,
529
+ trace
166
530
  } = {}) {
167
- const details = errorDetails(error, includeStack);
168
- const eventMetadata = compactMetadata({
169
- ...getReactNativeContext({ platform, appState }),
170
- errorName: details.name,
171
- errorValueType: details.valueType,
531
+ const details = errorDetails(error);
532
+ const traceContext = resolveTraceContext(trace ?? getActiveLogBrewTrace());
533
+ const attributes = createIssueAttributesFromError(error?.reason ?? error?.error ?? error, {
534
+ debugIdMap: debugIdMap === undefined ? runtimeReactNativeDebugIdMap() : debugIdMap,
535
+ environment,
536
+ fingerprint,
537
+ includeErrorStack: includeStack,
538
+ level,
539
+ metadata: {
540
+ ...getReactNativeContext({ platform, appState }),
541
+ errorValueType: details.valueType,
542
+ screen,
543
+ ...metadata
544
+ },
545
+ release,
546
+ runtime,
547
+ service,
172
548
  source: "react-native.error",
173
- screen,
174
- ...(includeStack ? { errorStack: details.stack } : {}),
175
- ...metadata
549
+ trace: traceContext
176
550
  });
551
+ const stackFrames = sanitizeReactNativeIssueStackFrames(attributes.stackFrames);
177
552
  return {
178
553
  id: id ?? idFactory({ error, message: details.message, screen }),
179
554
  timestamp: timestamp ?? now(),
180
555
  attributes: {
556
+ ...attributes,
557
+ ...(stackFrames ? { stackFrames } : {}),
181
558
  title: `React Native error: ${details.message}`,
182
- level,
183
559
  message: details.message,
184
- metadata: eventMetadata
560
+ metadata: sanitizeReactNativeIssueMetadata(attributes.metadata, compactMetadata)
185
561
  }
186
562
  };
187
563
  }
@@ -206,22 +582,17 @@ function createAppStateListener(client, appState, options = {}) {
206
582
  });
207
583
  });
208
584
 
209
- if (typeof subscription === "function") {
210
- return subscription;
211
- }
212
- if (subscription && typeof subscription.remove === "function") {
213
- return () => subscription.remove();
214
- }
215
- return () => {};
585
+ return subscriptionRemover(subscription);
216
586
  }
217
587
 
218
- function LogBrewNativeProvider({ client, platform, appState, children }) {
588
+ function LogBrewNativeProvider({ client, platform, appState, trace, children }) {
219
589
  requireClient(client);
220
590
  const value = React.useMemo(() => ({
221
591
  client,
222
592
  platform,
223
- appState
224
- }), [appState, client, platform]);
593
+ appState,
594
+ trace: resolveTraceContext(trace)
595
+ }), [appState, client, platform, trace]);
225
596
  return React.createElement(LogBrewNativeContext.Provider, { value }, children);
226
597
  }
227
598
 
@@ -234,33 +605,27 @@ function useLogBrewNative() {
234
605
  }
235
606
 
236
607
  function useLogBrewNativeActions() {
237
- const { client, platform, appState } = useLogBrewNative();
608
+ const { client, platform, appState, trace } = useLogBrewNative();
609
+ const scoped = (options = {}) => ({ platform, appState, trace, ...options });
238
610
  return {
239
611
  release: client.release.bind(client),
240
612
  environment: client.environment.bind(client),
241
- issue: client.issue.bind(client),
242
- log: client.log.bind(client),
613
+ issue: (id, timestamp, attributes) => client.issue(id, timestamp, attributesWithTrace(attributes, trace)),
614
+ log: (id, timestamp, attributes) => client.log(id, timestamp, attributesWithTrace(attributes, trace)),
243
615
  span: client.span.bind(client),
244
- action: client.action.bind(client),
616
+ action: (id, timestamp, attributes) => client.action(id, timestamp, attributesWithTrace(attributes, trace)),
245
617
  flush: client.flush.bind(client),
246
618
  shutdown: client.shutdown.bind(client),
247
619
  previewJson: client.previewJson.bind(client),
248
620
  pendingEvents: client.pendingEvents.bind(client),
249
- captureScreenView: (screenName, options = {}) => captureScreenView(client, screenName, {
250
- platform,
251
- appState,
252
- ...options
253
- }),
254
- captureAppStateChange: (state, options = {}) => captureAppStateChange(client, state, {
255
- platform,
256
- appState,
257
- ...options
258
- }),
259
- captureReactNativeError: (error, options = {}) => captureReactNativeError(client, error, {
260
- platform,
261
- appState,
262
- ...options
263
- })
621
+ trace,
622
+ captureScreenView: (screenName, options = {}) => captureScreenView(client, screenName, scoped(options)),
623
+ captureAppStateChange: (state, options = {}) => captureAppStateChange(client, state, scoped(options)),
624
+ captureReactNativeAction: (input = {}) => captureReactNativeAction(client, scoped(input)),
625
+ captureReactNativeNetwork: (input = {}) => captureReactNativeNetwork(client, scoped(input)),
626
+ captureReactNativeNavigationSpan: (input = {}) => captureReactNativeNavigationSpan(client, scoped(input)),
627
+ captureReactNativeResourceSpan: (input = {}) => captureReactNativeResourceSpan(client, scoped(input)),
628
+ captureReactNativeError: (error, options = {}) => captureReactNativeError(client, error, scoped(options))
264
629
  };
265
630
  }
266
631
 
@@ -299,25 +664,26 @@ function compactMetadata(metadata) {
299
664
  return compacted;
300
665
  }
301
666
 
302
- function errorDetails(error, includeStack) {
303
- const candidate = error?.reason ?? error?.error ?? error;
304
- const message = errorMessage(candidate);
667
+ function attributesWithTrace(attributes, trace) {
668
+ const context = resolveTraceContext(trace) ?? getActiveLogBrewTrace();
669
+ if (!context) {
670
+ return attributes;
671
+ }
305
672
  return {
306
- message,
307
- name: errorName(candidate),
308
- stack: includeStack && typeof candidate?.stack === "string" ? candidate.stack : undefined,
309
- valueType: candidate === null ? "null" : typeof candidate
673
+ ...attributes,
674
+ metadata: compactMetadata({
675
+ ...(attributes?.metadata ?? {}),
676
+ ...getReactNativeTraceMetadata(context)
677
+ })
310
678
  };
311
679
  }
312
680
 
313
- function errorName(error) {
314
- if (error instanceof Error && typeof error.name === "string" && error.name.trim() !== "") {
315
- return error.name;
316
- }
317
- if (typeof error?.name === "string" && error.name.trim() !== "") {
318
- return error.name;
319
- }
320
- return undefined;
681
+ function errorDetails(error) {
682
+ const candidate = error?.reason ?? error?.error ?? error;
683
+ return {
684
+ message: errorMessage(candidate),
685
+ valueType: candidate === null ? "null" : typeof candidate
686
+ };
321
687
  }
322
688
 
323
689
  function errorMessage(error) {
@@ -337,6 +703,22 @@ function defaultErrorEventId({ message, screen }) {
337
703
  return `evt_native_error_${slugify(`${screen ?? "app"}_${message}`)}`;
338
704
  }
339
705
 
706
+ function defaultActionEventId({ name, screen }) {
707
+ return `evt_native_action_${slugify(`${screen ?? "app"}_${name ?? "event"}`)}`;
708
+ }
709
+
710
+ function defaultNetworkEventId({ method, routeTemplate, screen }) {
711
+ return `evt_native_network_${slugify([screen, method, routeTemplate].filter(Boolean).join("_") || "request")}`;
712
+ }
713
+
714
+ function defaultNavigationSpanEventId({ routeName, routePath, screen }) {
715
+ return `evt_native_navigation_${slugify([screen, routeName, routePath].filter(Boolean).join("_") || "route")}`;
716
+ }
717
+
718
+ function defaultResourceSpanEventId({ method, routeTemplate, screen }) {
719
+ return `evt_native_resource_${slugify([screen, method, routeTemplate].filter(Boolean).join("_") || "request")}`;
720
+ }
721
+
340
722
  function defaultFetch() {
341
723
  return typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : undefined;
342
724
  }
@@ -395,6 +777,99 @@ function randomHex(length, randomValues) {
395
777
  return bytes.map((value) => value.toString(16).padStart(2, "0")).join("");
396
778
  }
397
779
 
780
+ function resolveTraceContext(trace) {
781
+ if (trace === undefined || trace === null) {
782
+ return undefined;
783
+ }
784
+ if (typeof trace === "string") {
785
+ return createReactNativeTraceContext({ traceparent: trace });
786
+ }
787
+ if (typeof trace === "object") {
788
+ const {
789
+ parentSpanId,
790
+ sampled,
791
+ spanId,
792
+ traceFlags = "01",
793
+ traceId
794
+ } = trace;
795
+ if (typeof traceId !== "string" || typeof spanId !== "string") {
796
+ throw new SdkError("validation_error", "trace context requires traceId and spanId");
797
+ }
798
+ createTraceparent({ traceId, spanId, traceFlags });
799
+ if (parentSpanId !== undefined) {
800
+ createTraceparent({ traceId, spanId: parentSpanId, traceFlags });
801
+ }
802
+ return freezeTraceContext({
803
+ parentSpanId,
804
+ sampled: sampled ?? sampledFromTraceFlags(traceFlags),
805
+ spanId,
806
+ traceFlags,
807
+ traceId
808
+ });
809
+ }
810
+ throw new SdkError("validation_error", "trace must be a trace context or traceparent string");
811
+ }
812
+
813
+ function freezeTraceContext({ parentSpanId, sampled, spanId, traceFlags, traceId }) {
814
+ const normalized = {
815
+ traceId: String(traceId).toLowerCase(),
816
+ spanId: String(spanId).toLowerCase(),
817
+ parentSpanId: parentSpanId === undefined ? undefined : String(parentSpanId).toLowerCase(),
818
+ traceFlags: String(traceFlags).toLowerCase(),
819
+ sampled: Boolean(sampled)
820
+ };
821
+ return Object.freeze(normalized);
822
+ }
823
+
824
+ function sampledFromTraceFlags(traceFlags) {
825
+ return (Number.parseInt(traceFlags, 16) & 1) === 1;
826
+ }
827
+
828
+ function removeActiveTraceScope(scopeId) {
829
+ const index = activeTraceScopes.findIndex((scope) => scope.id === scopeId);
830
+ if (index >= 0) {
831
+ activeTraceScopes.splice(index, 1);
832
+ }
833
+ }
834
+
835
+ function shouldPropagateToStringTarget(url, target) {
836
+ const targetText = target.trim();
837
+ if (targetText === "") {
838
+ return false;
839
+ }
840
+ if (targetText.startsWith("/")) {
841
+ return url.startsWith(targetText);
842
+ }
843
+
844
+ const URLConstructor = globalThis.URL;
845
+ if (typeof URLConstructor === "function") {
846
+ try {
847
+ const targetUrl = new URLConstructor(targetText);
848
+ if (!hasUrlScheme(url)) {
849
+ return false;
850
+ }
851
+ const requestUrl = new URLConstructor(url, targetUrl.origin);
852
+ if (requestUrl.origin !== targetUrl.origin) {
853
+ return false;
854
+ }
855
+ const targetPath = targetUrl.pathname || "/";
856
+ return requestUrl.pathname === targetPath || requestUrl.pathname.startsWith(pathPrefix(targetPath));
857
+ } catch {
858
+ return url.startsWith(targetText);
859
+ }
860
+ }
861
+
862
+ return url.startsWith(targetText);
863
+ }
864
+
865
+ function pathPrefix(pathname) {
866
+ return pathname.endsWith("/") ? pathname : `${pathname}/`;
867
+ }
868
+
869
+ function hasUrlScheme(url) {
870
+ return /^[a-z][a-z0-9+.-]*:/iu.test(url);
871
+ }
872
+
398
873
  function requestHeaders(input, init) {
399
874
  if (init && init.headers !== undefined) {
400
875
  return init.headers;
@@ -417,57 +892,96 @@ function requestUrl(input) {
417
892
  }
418
893
 
419
894
  function slugify(value) {
420
- return value
895
+ return String(value)
421
896
  .toLowerCase()
422
897
  .replace(/[^a-z0-9]+/g, "_")
423
898
  .replace(/^_+|_+$/g, "") || "event";
424
899
  }
425
900
 
901
+ function statusFromStatusCode(statusCode) {
902
+ if (typeof statusCode === "number" && Number.isFinite(statusCode) && statusCode >= 400) {
903
+ return "failure";
904
+ }
905
+ return "success";
906
+ }
907
+
908
+ function spanStatusFromStatusCode(statusCode) {
909
+ if (typeof statusCode === "number" && Number.isFinite(statusCode) && statusCode >= 400) {
910
+ return "error";
911
+ }
912
+ return "ok";
913
+ }
914
+
915
+ function stripQueryAndHash(value) {
916
+ if (value === undefined || value === null) {
917
+ return undefined;
918
+ }
919
+ return String(value).split(/[?#]/u, 1)[0];
920
+ }
921
+
922
+ function resolveNavigationContainer(navigationContainer) {
923
+ if (navigationContainer && typeof navigationContainer === "object" && "current" in navigationContainer) {
924
+ return navigationContainer.current;
925
+ }
926
+ return navigationContainer;
927
+ }
928
+
929
+ function addNavigationListener(container, eventName, listener) {
930
+ const subscription = container.addListener(eventName, listener);
931
+ return subscriptionRemover(subscription);
932
+ }
933
+
934
+ function subscriptionRemover(subscription) {
935
+ if (typeof subscription === "function") {
936
+ return subscription;
937
+ }
938
+ if (subscription && typeof subscription.remove === "function") {
939
+ return () => subscription.remove();
940
+ }
941
+ return () => {};
942
+ }
943
+
944
+ function safeNavigationListener(container, eventName, listener) {
945
+ try {
946
+ return addNavigationListener(container, eventName, listener);
947
+ } catch {
948
+ return undefined;
949
+ }
950
+ }
951
+
952
+ function routeSnapshot(route) {
953
+ return {
954
+ key: normalizeMetadataValue(route?.key),
955
+ name: typeof route?.name === "string" && route.name.trim() !== "" ? route.name : undefined,
956
+ path: stripQueryAndHash(route?.path)
957
+ };
958
+ }
959
+
960
+ function navigationActionType(event) {
961
+ const action = event?.data?.action ?? event?.action ?? event;
962
+ return typeof action?.type === "string" && action.type.trim() !== "" ? action.type : undefined;
963
+ }
964
+
426
965
  function traceparentForRequest({
427
- init,
428
- input,
429
- randomValues,
430
- traceFlags,
431
- traceparent,
432
- traceparentFactory,
433
- url
966
+ init, input, randomValues, trace, traceFlags, traceparent, traceparentFactory, url
434
967
  }) {
968
+ const context = resolveTraceContext(trace) ?? getActiveLogBrewTrace();
435
969
  const nextTraceparent = typeof traceparentFactory === "function"
436
970
  ? traceparentFactory({ init, input, url })
437
- : traceparent ?? createReactNativeTraceparent({ randomValues, traceFlags });
971
+ : traceparent ?? (context ? createReactNativeTraceHeaders(context).traceparent : createReactNativeTraceparent({ randomValues, traceFlags }));
438
972
  parseTraceparent(nextTraceparent);
439
973
  return nextTraceparent;
440
974
  }
441
975
 
442
976
  const defaultExport = {
443
- LogBrewNativeProvider,
444
- captureAppStateChange,
445
- captureReactNativeError,
446
- captureScreenView,
447
- createAppStateListener,
448
- createLogBrewReactNativeClient,
449
- createReactNativeErrorEvent,
450
- createReactNativeTraceparent,
451
- createTraceparentFetch,
452
- getReactNativeContext,
453
- shouldPropagateTraceparent,
454
- useLogBrewNative,
455
- useLogBrewNativeActions
977
+ LogBrewNativeProvider, captureAppStateChange, captureReactNativeAction, captureReactNativeError,
978
+ captureReactNativeNetwork, captureReactNativeNavigationSpan, captureReactNativeResourceSpan, captureScreenView,
979
+ bindLogBrewTrace, createAppStateListener, createLogBrewReactNativeClient, createReactNavigationSpanListener,
980
+ createReactNativeSpanAttributes, createReactNativeTraceContext, createReactNativeTraceHeaders, createReactNativeActionEvent,
981
+ createReactNativeErrorEvent, createReactNativeNetworkEvent, createReactNativeNavigationSpanEvent,
982
+ createReactNativeResourceSpanEvent, createReactNativeTraceparent, createTraceparentFetch, getActiveLogBrewTrace,
983
+ getReactNativeContext, getReactNativeTraceMetadata, shouldPropagateTraceparent, useLogBrewNative,
984
+ useLogBrewNativeActions, withLogBrewTrace
456
985
  };
457
986
 
458
- module.exports = {
459
- LogBrewNativeProvider,
460
- captureAppStateChange,
461
- captureReactNativeError,
462
- captureScreenView,
463
- createAppStateListener,
464
- createLogBrewReactNativeClient,
465
- createReactNativeErrorEvent,
466
- createReactNativeTraceparent,
467
- createTraceparentFetch,
468
- default: defaultExport,
469
- getReactNativeContext,
470
- shouldPropagateTraceparent,
471
- useLogBrewNative,
472
- useLogBrewNativeActions
473
- };
987
+ module.exports = { ...defaultExport, default: defaultExport };