@logbrew/react-native 0.1.0 → 0.1.2

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 (51) hide show
  1. package/README.md +431 -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/global-errors.cjs +366 -0
  17. package/global-errors.d.cts +63 -0
  18. package/global-errors.d.ts +63 -0
  19. package/global-errors.js +7 -0
  20. package/index.cjs +625 -111
  21. package/index.d.cts +236 -0
  22. package/index.d.ts +236 -0
  23. package/index.js +613 -95
  24. package/index.native.js +18 -0
  25. package/instrumentation.cjs +639 -0
  26. package/instrumentation.d.cts +84 -0
  27. package/instrumentation.d.ts +84 -0
  28. package/instrumentation.js +634 -0
  29. package/lifecycle.cjs +129 -0
  30. package/lifecycle.d.cts +50 -0
  31. package/lifecycle.d.ts +50 -0
  32. package/lifecycle.js +121 -0
  33. package/metadata.cjs +175 -0
  34. package/metadata.js +165 -0
  35. package/metro.cjs +310 -0
  36. package/metro.d.cts +37 -0
  37. package/metro.d.ts +37 -0
  38. package/metro.js +6 -0
  39. package/native-bridge.cjs +127 -0
  40. package/native-bridge.d.cts +60 -0
  41. package/native-bridge.d.ts +60 -0
  42. package/native-bridge.js +125 -0
  43. package/package.json +128 -4
  44. package/release-artifacts.cjs +344 -0
  45. package/release-artifacts.d.cts +55 -0
  46. package/release-artifacts.d.ts +53 -0
  47. package/release-artifacts.js +8 -0
  48. package/resource-fetch.cjs +469 -0
  49. package/resource-fetch.d.cts +60 -0
  50. package/resource-fetch.d.ts +60 -0
  51. 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.includes(target);
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
- timestamp
516
+ service,
517
+ timestamp,
518
+ trace
166
519
  } = {}) {
167
- const details = errorDetails(error, includeStack);
168
- const eventMetadata = compactMetadata({
169
- ...getReactNativeContext({ platform, appState }),
170
- errorName: details.name,
171
- errorValueType: details.valueType,
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
- screen,
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: eventMetadata
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
- if (typeof subscription === "function") {
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
- }), [appState, client, platform]);
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.bind(client),
242
- log: client.log.bind(client),
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.bind(client),
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
- 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
- })
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 errorDetails(error, includeStack) {
303
- const candidate = error?.reason ?? error?.error ?? error;
304
- const message = errorMessage(candidate);
656
+ function attributesWithTrace(attributes, trace) {
657
+ const context = resolveTraceContext(trace) ?? getActiveLogBrewTrace();
658
+ if (!context) {
659
+ return attributes;
660
+ }
305
661
  return {
306
- message,
307
- name: errorName(candidate),
308
- stack: includeStack && typeof candidate?.stack === "string" ? candidate.stack : undefined,
309
- valueType: candidate === null ? "null" : typeof candidate
662
+ ...attributes,
663
+ metadata: compactMetadata({
664
+ ...(attributes?.metadata ?? {}),
665
+ ...getReactNativeTraceMetadata(context)
666
+ })
310
667
  };
311
668
  }
312
669
 
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;
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
- captureAppStateChange,
445
- captureReactNativeError,
446
- captureScreenView,
447
- createAppStateListener,
448
- createLogBrewReactNativeClient,
449
- createReactNativeErrorEvent,
450
- createReactNativeTraceparent,
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
  };