@goliapkg/sentori-react-native 0.5.1 → 0.5.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,qBAAqB,QAAO,IAKxC,CAAC"}
1
+ {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,qBAAqB,QAAO,IAKxC,CAAC"}
@@ -1,7 +1,14 @@
1
- import { startSpan } from '@goliapkg/sentori-core';
1
+ import { normalizeUrl, startSpan } from '@goliapkg/sentori-core';
2
2
  import { addBreadcrumb } from '../breadcrumbs';
3
+ import { getConfig } from '../config';
3
4
  let _installed = false;
4
5
  const AUTH_PARAMS = ['token', 'key', 'password', 'secret', 'access_token'];
6
+ // Requests to our own ingest endpoint shouldn't be traced — otherwise
7
+ // every span upload spawns another http.client span, and so on.
8
+ const isIngestUrl = (url) => {
9
+ const base = getConfig()?.ingestUrl;
10
+ return !!base && url.startsWith(base);
11
+ };
5
12
  export const installNetworkHandler = () => {
6
13
  if (_installed)
7
14
  return;
@@ -17,6 +24,8 @@ function patchFetch() {
17
24
  globalThis.fetch = (async (input, init) => {
18
25
  const start = Date.now();
19
26
  const url = extractUrl(input);
27
+ if (isIngestUrl(url))
28
+ return original(input, init);
20
29
  const scrubbed = scrubUrl(url);
21
30
  const method = (init?.method ??
22
31
  (typeof input !== 'string' && 'method' in input
@@ -27,7 +36,7 @@ function patchFetch() {
27
36
  // attached to error events at capture time and serve a different
28
37
  // surface (the "last 100 things" timeline on the issue page).
29
38
  const span = startSpan('http.client', {
30
- name: `${method.toUpperCase()} ${scrubbed}`,
39
+ name: `${method.toUpperCase()} ${normalizeUrl(scrubbed)}`,
31
40
  tags: { 'http.method': method.toUpperCase(), 'http.url': scrubbed },
32
41
  });
33
42
  // Inject traceparent header on outbound requests.
@@ -87,10 +96,12 @@ function patchXhr() {
87
96
  return originalOpen.call(this, method, url, ...rest);
88
97
  };
89
98
  proto.send = function (body) {
99
+ if (isIngestUrl(this.__sentoriUrl ?? ''))
100
+ return originalSend.call(this, body);
90
101
  const method = this.__sentoriMethod ?? 'GET';
91
102
  const url = scrubUrl(this.__sentoriUrl ?? '');
92
103
  const span = startSpan('http.client', {
93
- name: `${method} ${url}`,
104
+ name: `${method} ${normalizeUrl(url)}`,
94
105
  tags: { 'http.method': method, 'http.url': url },
95
106
  });
96
107
  this.__sentoriSpan = span;
@@ -1 +1 @@
1
- {"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"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"}
@@ -9,9 +9,9 @@ export type NavigationRefLike = {
9
9
  };
10
10
  /**
11
11
  * Subscribe to react-navigation state changes and emit a
12
- * `react.navigation` span per transition. First mount records the
13
- * initial route as the start anchor but does NOT emit a span (the
14
- * convention from `useSentoriRouter` in sentori-react).
12
+ * `react.navigation` span per screen (including the initial one),
13
+ * each a fresh trace root. The span is kept active while the screen
14
+ * is current; child spans created in that window attribute up to it.
15
15
  *
16
16
  * import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'
17
17
  * import { useTraceNavigation } from '@goliapkg/sentori-react-native'
@@ -22,8 +22,7 @@ export type NavigationRefLike = {
22
22
  * return <NavigationContainer ref={navigationRef}>{...}</NavigationContainer>
23
23
  * }
24
24
  *
25
- * Each span carries `{ from, to }` as tags and uses the destination
26
- * route name as the span name.
25
+ * Each span carries `{ nav.from, nav.to }` tags.
27
26
  */
28
27
  export declare function useTraceNavigation(navigationRef: NavigationRefLike): void;
29
28
  //# sourceMappingURL=navigation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAiBA;;kDAEkD;AAClD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAClE,eAAe,EAAE,MAAM;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACrD,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,iBAAiB,GAAG,IAAI,CAyCzE"}
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AA0BA;;kDAEkD;AAClD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAClE,eAAe,EAAE,MAAM;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACrD,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,iBAAiB,GAAG,IAAI,CA+CzE"}
package/lib/navigation.js CHANGED
@@ -1,22 +1,31 @@
1
1
  // Phase 35 sub-C: react-navigation auto-instrumentation.
2
+ // Phase 39 sub-B: the open `react.navigation` span is also made the
3
+ // active span for that screen's lifetime, so the screen's
4
+ // `http.client` spans (and any other startSpan() calls) attach to it
5
+ // as children — one trace per screen instead of one per request.
2
6
  //
3
7
  // Mount `useTraceNavigation(navigationRef)` next to your
4
- // `<NavigationContainer ref={navigationRef}>` and every route
5
- // transition becomes a `react.navigation` span. Span names are
6
- // `<from> → <to>` so the trace list reads as a navigation log.
8
+ // `<NavigationContainer ref={navigationRef}>`. Span names are
9
+ // `<from> <to>` (or just the route name for the first screen) so
10
+ // the trace list reads as a navigation log.
7
11
  //
8
- // react-navigation is an OPTIONAL peer dependency — apps that
9
- // don't use it never have to install it. The hook itself doesn't
10
- // import from @react-navigation/native; consumers pass in the ref
11
- // they already have, and we read its state via the public
12
- // `getCurrentRoute()` API. That keeps the dep edge optional.
12
+ // react-navigation is an OPTIONAL peer dependency — apps that don't
13
+ // use it never have to install it. The hook doesn't import from
14
+ // @react-navigation/native; consumers pass in the ref they already
15
+ // have, and we read its state via the public `getCurrentRoute()`.
16
+ //
17
+ // Caveat (active-span on RN is a module variable): requests fired
18
+ // from a `setTimeout` / background poll / detached promise after the
19
+ // screen settled may not see the nav span as active. If you want such
20
+ // a request parented to the current screen, pass it explicitly:
21
+ // `startSpan(op, { parent: activeSpan() })`.
13
22
  import { useEffect, useRef } from 'react';
14
- import { startSpan } from '@goliapkg/sentori-core';
23
+ import { setActiveSpan, startSpan } from '@goliapkg/sentori-core';
15
24
  /**
16
25
  * Subscribe to react-navigation state changes and emit a
17
- * `react.navigation` span per transition. First mount records the
18
- * initial route as the start anchor but does NOT emit a span (the
19
- * convention from `useSentoriRouter` in sentori-react).
26
+ * `react.navigation` span per screen (including the initial one),
27
+ * each a fresh trace root. The span is kept active while the screen
28
+ * is current; child spans created in that window attribute up to it.
20
29
  *
21
30
  * import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'
22
31
  * import { useTraceNavigation } from '@goliapkg/sentori-react-native'
@@ -27,45 +36,53 @@ import { startSpan } from '@goliapkg/sentori-core';
27
36
  * return <NavigationContainer ref={navigationRef}>{...}</NavigationContainer>
28
37
  * }
29
38
  *
30
- * Each span carries `{ from, to }` as tags and uses the destination
31
- * route name as the span name.
39
+ * Each span carries `{ nav.from, nav.to }` tags.
32
40
  */
33
41
  export function useTraceNavigation(navigationRef) {
34
- // Latest route name we've observed. `null` means "no transition
35
- // recorded yet" (initial mount).
42
+ // Latest route name we've observed.
36
43
  const lastRouteRef = useRef(null);
37
- // Span that started when this route was entered. Finished when the
38
- // NEXT route transition arrives.
44
+ // Span for the screen the user is currently on. Finished when the
45
+ // next screen is entered (or on unmount).
39
46
  const openSpanRef = useRef(null);
40
47
  useEffect(() => {
41
48
  if (typeof navigationRef.addListener !== 'function')
42
49
  return;
43
50
  if (typeof navigationRef.getCurrentRoute !== 'function')
44
51
  return;
45
- // Seed the "last route" reference from the current state so the
46
- // first transition emits a span with the right `from`.
52
+ // Each screen gets its own trace root detach from whatever the
53
+ // previous screen's span was (we keep it active, so without
54
+ // `parent: null` the new one would nest under it).
55
+ const openScreenSpan = (from, to) => {
56
+ const span = startSpan('react.navigation', {
57
+ name: from ? `${from} → ${to}` : to,
58
+ parent: null,
59
+ tags: { 'nav.from': from ?? '', 'nav.to': to },
60
+ });
61
+ openSpanRef.current = span;
62
+ setActiveSpan(span);
63
+ lastRouteRef.current = to;
64
+ };
65
+ // Open a span for the initial screen so its requests are grouped
66
+ // too (auth / config / first data load are usually the busiest
67
+ // screen of a session).
47
68
  const initial = navigationRef.getCurrentRoute()?.name ?? null;
48
- lastRouteRef.current = initial;
69
+ if (initial !== null)
70
+ openScreenSpan(null, initial);
71
+ else
72
+ lastRouteRef.current = null;
49
73
  const unsubscribe = navigationRef.addListener('state', () => {
50
74
  const next = navigationRef.getCurrentRoute()?.name ?? null;
51
75
  const prev = lastRouteRef.current;
52
76
  if (next === null || next === prev)
53
77
  return;
54
- // Close the prior span (if any) before opening the new one so
55
- // the trace looks like a sequence, not nested.
56
78
  openSpanRef.current?.finish({ status: 'ok' });
57
- const span = startSpan('react.navigation', {
58
- name: prev ? `${prev} → ${next}` : next,
59
- tags: { 'nav.from': prev ?? '', 'nav.to': next },
60
- });
61
- openSpanRef.current = span;
62
- lastRouteRef.current = next;
79
+ openScreenSpan(prev, next);
63
80
  });
64
81
  return () => {
65
82
  unsubscribe();
66
- // Close any still-open span on unmount so we don't leak it.
67
83
  openSpanRef.current?.finish({ status: 'ok' });
68
84
  openSpanRef.current = null;
85
+ setActiveSpan(null);
69
86
  };
70
87
  }, [navigationRef]);
71
88
  }
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,EAAE;AACF,yDAAyD;AACzD,8DAA8D;AAC9D,+DAA+D;AAC/D,+DAA+D;AAC/D,EAAE;AACF,8DAA8D;AAC9D,iEAAiE;AACjE,kEAAkE;AAClE,0DAA0D;AAC1D,6DAA6D;AAE7D,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE1C,OAAO,EAAE,SAAS,EAAmB,MAAM,wBAAwB,CAAC;AAUpE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,aAAgC;IACjE,gEAAgE;IAChE,iCAAiC;IACjC,MAAM,YAAY,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IACjD,mEAAmE;IACnE,iCAAiC;IACjC,MAAM,WAAW,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAEpD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,aAAa,CAAC,WAAW,KAAK,UAAU;YAAE,OAAO;QAC5D,IAAI,OAAO,aAAa,CAAC,eAAe,KAAK,UAAU;YAAE,OAAO;QAEhE,gEAAgE;QAChE,uDAAuD;QACvD,MAAM,OAAO,GAAG,aAAa,CAAC,eAAe,EAAE,EAAE,IAAI,IAAI,IAAI,CAAC;QAC9D,YAAY,CAAC,OAAO,GAAG,OAAO,CAAC;QAE/B,MAAM,WAAW,GAAG,aAAa,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE;YAC1D,MAAM,IAAI,GAAG,aAAa,CAAC,eAAe,EAAE,EAAE,IAAI,IAAI,IAAI,CAAC;YAC3D,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC;YAClC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO;YAE3C,8DAA8D;YAC9D,+CAA+C;YAC/C,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAE9C,MAAM,IAAI,GAAG,SAAS,CAAC,kBAAkB,EAAE;gBACzC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI;gBACvC,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;aACjD,CAAC,CAAC;YACH,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;YAC3B,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,WAAW,EAAE,CAAC;YACd,4DAA4D;YAC5D,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9C,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC7B,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;AACtB,CAAC"}
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,oEAAoE;AACpE,0DAA0D;AAC1D,qEAAqE;AACrE,iEAAiE;AACjE,EAAE;AACF,yDAAyD;AACzD,8DAA8D;AAC9D,mEAAmE;AACnE,4CAA4C;AAC5C,EAAE;AACF,oEAAoE;AACpE,gEAAgE;AAChE,mEAAmE;AACnE,kEAAkE;AAClE,EAAE;AACF,kEAAkE;AAClE,qEAAqE;AACrE,sEAAsE;AACtE,gEAAgE;AAChE,6CAA6C;AAE7C,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE1C,OAAO,EAAE,aAAa,EAAE,SAAS,EAAmB,MAAM,wBAAwB,CAAC;AAUnF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAAC,aAAgC;IACjE,oCAAoC;IACpC,MAAM,YAAY,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAC;IACjD,kEAAkE;IAClE,0CAA0C;IAC1C,MAAM,WAAW,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAEpD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,aAAa,CAAC,WAAW,KAAK,UAAU;YAAE,OAAO;QAC5D,IAAI,OAAO,aAAa,CAAC,eAAe,KAAK,UAAU;YAAE,OAAO;QAEhE,iEAAiE;QACjE,4DAA4D;QAC5D,mDAAmD;QACnD,MAAM,cAAc,GAAG,CAAC,IAAmB,EAAE,EAAU,EAAE,EAAE;YACzD,MAAM,IAAI,GAAG,SAAS,CAAC,kBAAkB,EAAE;gBACzC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE;gBACnC,MAAM,EAAE,IAAI;gBACZ,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;aAC/C,CAAC,CAAC;YACH,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;YAC3B,aAAa,CAAC,IAAI,CAAC,CAAC;YACpB,YAAY,CAAC,OAAO,GAAG,EAAE,CAAC;QAC5B,CAAC,CAAC;QAEF,iEAAiE;QACjE,+DAA+D;QAC/D,wBAAwB;QACxB,MAAM,OAAO,GAAG,aAAa,CAAC,eAAe,EAAE,EAAE,IAAI,IAAI,IAAI,CAAC;QAC9D,IAAI,OAAO,KAAK,IAAI;YAAE,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;;YAC/C,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;QAEjC,MAAM,WAAW,GAAG,aAAa,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE;YAC1D,MAAM,IAAI,GAAG,aAAa,CAAC,eAAe,EAAE,EAAE,IAAI,IAAI,IAAI,CAAC;YAC3D,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC;YAClC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO;YAC3C,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9C,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,EAAE;YACV,WAAW,EAAE,CAAC;YACd,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9C,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;YAC3B,aAAa,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC;AACtB,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import type { Event } from './types';
2
2
  export declare const enqueue: (event: Event) => void;
3
3
  export declare const startTransport: () => void;
4
+ export declare const flushSpans: () => Promise<void>;
4
5
  export declare const flush: () => Promise<void>;
5
6
  export declare const drainOfflineQueue: () => Promise<void>;
6
7
  export declare const __resetForTests: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAcrC,eAAO,MAAM,OAAO,GAAI,OAAO,KAAK,KAAG,IAUtC,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,IAEjC,CAAC;AAEF,eAAO,MAAM,KAAK,QAAa,OAAO,CAAC,IAAI,CAkB1C,CAAC;AA4FF,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAatD,CAAC;AAEF,eAAO,MAAM,eAAe,QAAO,IAKlC,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,SAAS,KAAK,EAAY,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,MAAM,EACjB,OAAO,MAAM,EACb,MAAM,OAAO,KACZ,OAAO,CAAC,IAAI,CAcd,CAAC"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAqBrC,eAAO,MAAM,OAAO,GAAI,OAAO,KAAK,KAAG,IAUtC,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,IAOjC,CAAC;AAEF,eAAO,MAAM,UAAU,QAAa,OAAO,CAAC,IAAI,CAe/C,CAAC;AAqBF,eAAO,MAAM,KAAK,QAAa,OAAO,CAAC,IAAI,CAkB1C,CAAC;AA4FF,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAatD,CAAC;AAEF,eAAO,MAAM,eAAe,QAAO,IAOlC,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,SAAS,KAAK,EAAY,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,MAAM,EACjB,OAAO,MAAM,EACb,MAAM,OAAO,KACZ,OAAO,CAAC,IAAI,CAcd,CAAC"}
package/lib/transport.js CHANGED
@@ -1,11 +1,18 @@
1
+ import { drainSpans } from '@goliapkg/sentori-core';
1
2
  import { getConfig } from './config';
2
3
  const FLUSH_INTERVAL_MS = 5_000;
3
4
  const BATCH_SIZE = 10;
4
5
  const MAX_RETRY = 3;
5
6
  const STORAGE_KEY = '@sentori/pending';
6
7
  const MAX_PERSISTED = 1000;
8
+ // Spans are higher-volume and lower-value than error events: we batch
9
+ // them on their own timer, cap each request at the server's per-batch
10
+ // limit, and drop on failure rather than persisting offline.
11
+ const SPAN_FLUSH_INTERVAL_MS = 10_000;
12
+ const SPAN_BATCH_MAX = 200;
7
13
  let _queue = [];
8
14
  let _flushTimer = null;
15
+ let _spanTimer = null;
9
16
  let _started = false;
10
17
  const SDK_VERSION = '0.0.0';
11
18
  export const enqueue = (event) => {
@@ -22,6 +29,46 @@ export const enqueue = (event) => {
22
29
  };
23
30
  export const startTransport = () => {
24
31
  _started = true;
32
+ if (!_spanTimer) {
33
+ _spanTimer = setInterval(() => {
34
+ void flushSpans();
35
+ }, SPAN_FLUSH_INTERVAL_MS);
36
+ }
37
+ };
38
+ export const flushSpans = async () => {
39
+ if (!_started)
40
+ return;
41
+ const spans = drainSpans();
42
+ if (spans.length === 0)
43
+ return;
44
+ const config = getConfig();
45
+ if (!config)
46
+ return;
47
+ for (let i = 0; i < spans.length; i += SPAN_BATCH_MAX) {
48
+ const chunk = spans.slice(i, i + SPAN_BATCH_MAX);
49
+ try {
50
+ await sendSpansOnce(chunk, config.ingestUrl, config.token);
51
+ }
52
+ catch {
53
+ // drop the remaining chunks — span uploads aren't worth retrying
54
+ break;
55
+ }
56
+ }
57
+ };
58
+ const sendSpansOnce = async (spans, ingestUrl, token) => {
59
+ const resp = await fetch(`${ingestUrl}/v1/spans:batch`, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/json',
63
+ Authorization: `Bearer ${token}`,
64
+ 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
65
+ },
66
+ body: JSON.stringify({ spans }),
67
+ });
68
+ // 5xx: server overloaded. 4xx (bad token / quota / oversized): also
69
+ // pointless to keep sending. Either way, stop the rest of the batch.
70
+ if (resp.status >= 400)
71
+ throw new Error(`spans-${resp.status}`);
25
72
  };
26
73
  export const flush = async () => {
27
74
  if (!_started)
@@ -137,6 +184,9 @@ export const __resetForTests = () => {
137
184
  if (_flushTimer)
138
185
  clearTimeout(_flushTimer);
139
186
  _flushTimer = null;
187
+ if (_spanTimer)
188
+ clearInterval(_spanTimer);
189
+ _spanTimer = null;
140
190
  _started = false;
141
191
  };
142
192
  export const __peekQueue = () => _queue;
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-react-native",
3
- "version": "0.5.1",
4
- "description": "Sentori SDK for React Native JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
3
+ "version": "0.5.3",
4
+ "description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentori.golia.jp",
7
7
  "repository": {
@@ -64,6 +64,6 @@
64
64
  "access": "public"
65
65
  },
66
66
  "dependencies": {
67
- "@goliapkg/sentori-core": "0.3.0"
67
+ "@goliapkg/sentori-core": "0.4.0"
68
68
  }
69
69
  }
@@ -1,16 +1,22 @@
1
- import { clearSpans, drainSpans } from '@goliapkg/sentori-core';
2
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
1
+ import {
2
+ __resetTraceContextForTests,
3
+ __useFallbackTraceContextForTests,
4
+ activeSpan,
5
+ clearSpans,
6
+ drainSpans,
7
+ setActiveSpan,
8
+ startSpan,
9
+ } from '@goliapkg/sentori-core';
10
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from 'bun:test';
3
11
 
4
12
  import { type NavigationRefLike, useTraceNavigation } from '../navigation';
5
13
 
6
14
  // We test the hook without a React renderer (sdk/react-native has no
7
- // react-test-renderer or @testing-library dev-dep). Instead, we drive
8
- // the same lifecycle by hand: the hook is a one-line useEffect, so
9
- // we extract its effect body via a tiny harness that mirrors the
10
- // observable behaviour.
15
+ // react-test-renderer / @testing-library dev-dep). `harness` mirrors
16
+ // the hook's effect body 1:1 when production changes, this changes
17
+ // too. The point is to verify the observable contract: spans pushed
18
+ // in the right order + the open screen span left active.
11
19
 
12
- // FakeNav mirrors @react-navigation/native's NavigationContainerRef
13
- // surface — `addListener('state', cb)` + `getCurrentRoute()`.
14
20
  class FakeNav implements NavigationRefLike {
15
21
  private listeners: Array<() => void> = [];
16
22
  private route: { name: string } | undefined;
@@ -18,131 +24,152 @@ class FakeNav implements NavigationRefLike {
18
24
  setInitialRoute(name: string): void {
19
25
  this.route = { name };
20
26
  }
21
-
22
27
  addListener(_event: 'state', listener: () => void): () => void {
23
28
  this.listeners.push(listener);
24
29
  return () => {
25
30
  this.listeners = this.listeners.filter((l) => l !== listener);
26
31
  };
27
32
  }
28
-
29
33
  getCurrentRoute(): { name: string } | undefined {
30
34
  return this.route;
31
35
  }
32
-
33
36
  go(name: string): void {
34
37
  this.route = { name };
35
38
  this.listeners.forEach((l) => l());
36
39
  }
37
40
  }
38
41
 
39
- // Re-implements the hook's effect body for test purposes. Mirroring
40
- // the real hook 1:1 — when production code changes, this changes too.
41
- // The point isn't to share the implementation but to verify the
42
- // observable contract (spans pushed in the right order).
43
42
  function harness(navigationRef: NavigationRefLike): () => void {
44
- let lastRoute: null | string =
45
- navigationRef.getCurrentRoute()?.name ?? null;
46
- let openSpan: ReturnType<typeof import('@goliapkg/sentori-core').startSpan> | null = null;
43
+ let lastRoute: null | string = null;
44
+ let openSpan: ReturnType<typeof startSpan> | null = null;
47
45
 
48
- // Inline import to avoid hoisting.
49
- // eslint-disable-next-line @typescript-eslint/no-require-imports
50
- const { startSpan } = require('@goliapkg/sentori-core') as typeof import('@goliapkg/sentori-core');
46
+ const openScreenSpan = (from: null | string, to: string) => {
47
+ const span = startSpan('react.navigation', {
48
+ name: from ? `${from} ${to}` : to,
49
+ parent: null,
50
+ tags: { 'nav.from': from ?? '', 'nav.to': to },
51
+ });
52
+ openSpan = span;
53
+ setActiveSpan(span);
54
+ lastRoute = to;
55
+ };
56
+
57
+ const initial = navigationRef.getCurrentRoute()?.name ?? null;
58
+ if (initial !== null) openScreenSpan(null, initial);
59
+ else lastRoute = null;
51
60
 
52
61
  const unsub = navigationRef.addListener('state', () => {
53
62
  const next = navigationRef.getCurrentRoute()?.name ?? null;
54
63
  if (next === null || next === lastRoute) return;
55
-
56
64
  openSpan?.finish({ status: 'ok' });
57
- const span = startSpan('react.navigation', {
58
- name: lastRoute ? `${lastRoute} → ${next}` : next,
59
- tags: { 'nav.from': lastRoute ?? '', 'nav.to': next },
60
- });
61
- openSpan = span;
62
- lastRoute = next;
65
+ openScreenSpan(lastRoute, next);
63
66
  });
64
67
 
65
68
  return () => {
66
69
  unsub();
67
70
  openSpan?.finish({ status: 'ok' });
68
71
  openSpan = null;
72
+ setActiveSpan(null);
69
73
  };
70
74
  }
71
75
 
72
- beforeEach(() => clearSpans());
73
- afterEach(() => clearSpans());
76
+ beforeEach(() => {
77
+ // Navigation relies on setActiveSpan, which is a no-op on the
78
+ // ALS impl that bun (Node) picks. In production this runs on RN,
79
+ // where the fallback impl is in effect — exercise that one.
80
+ __useFallbackTraceContextForTests();
81
+ clearSpans();
82
+ });
83
+ afterEach(() => {
84
+ setActiveSpan(null);
85
+ clearSpans();
86
+ });
87
+ afterAll(() => {
88
+ __resetTraceContextForTests();
89
+ });
74
90
 
75
91
  describe('useTraceNavigation', () => {
76
- test('hook + production module both export', () => {
77
- // Sanity: the exported symbol exists with the right shape.
92
+ test('exports a hook', () => {
78
93
  expect(typeof useTraceNavigation).toBe('function');
79
94
  });
80
95
 
81
- test('initial mount does NOT emit a span', () => {
96
+ test('initial mount opens a span for the first screen and leaves it active', () => {
82
97
  const nav = new FakeNav();
83
98
  nav.setInitialRoute('Home');
84
99
  const cleanup = harness(nav);
100
+
101
+ // span not finished yet (still on Home) → buffer empty
85
102
  expect(drainSpans()).toHaveLength(0);
103
+ expect(activeSpan()).not.toBeNull();
104
+
86
105
  cleanup();
106
+ const spans = drainSpans();
107
+ expect(spans).toHaveLength(1);
108
+ expect(spans[0]?.op).toBe('react.navigation');
109
+ expect(spans[0]?.name).toBe('Home');
110
+ expect(spans[0]?.tags).toEqual({ 'nav.from': '', 'nav.to': 'Home' });
87
111
  });
88
112
 
89
- test('one transition + one more closes the first span', () => {
90
- const nav = new FakeNav();
91
- nav.setInitialRoute('Home');
113
+ test('mount with no current route no span, no active', () => {
114
+ const nav = new FakeNav(); // no initial route
92
115
  const cleanup = harness(nav);
93
-
94
- nav.go('Settings');
95
- nav.go('Profile');
96
-
97
- const spans = drainSpans();
98
- expect(spans.length).toBeGreaterThanOrEqual(1);
99
- const first = spans[0]!;
100
- expect(first.op).toBe('react.navigation');
101
- expect(first.name).toBe('Home → Settings');
102
- expect(first.tags).toEqual({ 'nav.from': 'Home', 'nav.to': 'Settings' });
103
- expect(first.status).toBe('ok');
104
-
116
+ expect(activeSpan()).toBeNull();
105
117
  cleanup();
118
+ expect(drainSpans()).toHaveLength(0);
106
119
  });
107
120
 
108
- test('same-route state event does not emit a span', () => {
121
+ test('http.client span during a screen becomes a child of the nav span', () => {
109
122
  const nav = new FakeNav();
110
123
  nav.setInitialRoute('Home');
111
124
  const cleanup = harness(nav);
125
+ const navSpan = activeSpan()!;
112
126
 
113
- nav.go('Home');
114
- expect(drainSpans()).toHaveLength(0);
127
+ // simulate the network handler opening a request span (no explicit
128
+ // parent → inherits the active nav span)
129
+ const req = startSpan('http.client', { name: 'GET /x' });
130
+ expect(req.parentSpanId).toBe(navSpan.spanId);
131
+ expect(req.traceId).toBe(navSpan.traceId);
132
+ req.finish({ status: 'ok' });
115
133
 
116
134
  cleanup();
135
+ const spans = drainSpans();
136
+ const navTrace = spans.find((s) => s.op === 'react.navigation')!.traceId;
137
+ expect(spans.every((s) => s.traceId === navTrace)).toBe(true);
117
138
  });
118
139
 
119
- test('chained transitions emit per-hop spans', () => {
140
+ test('each screen is its own trace root (transitions do not nest)', () => {
120
141
  const nav = new FakeNav();
121
142
  nav.setInitialRoute('A');
122
143
  const cleanup = harness(nav);
123
-
124
144
  nav.go('B');
125
145
  nav.go('C');
126
- nav.go('D');
146
+ cleanup();
127
147
 
128
- const spans = drainSpans();
129
- expect(spans.length).toBeGreaterThanOrEqual(2);
130
- expect(spans[0]?.name).toBe('A → B');
131
- expect(spans[1]?.name).toBe('B C');
148
+ const spans = drainSpans().filter((s) => s.op === 'react.navigation');
149
+ expect(spans).toHaveLength(3);
150
+ expect(spans.map((s) => s.name)).toEqual(['A', 'A → B', 'B → C']);
151
+ expect(spans.every((s) => s.parentSpanId === null)).toBe(true);
152
+ expect(new Set(spans.map((s) => s.traceId)).size).toBe(3);
153
+ });
132
154
 
155
+ test('same-route state event does not open a new span', () => {
156
+ const nav = new FakeNav();
157
+ nav.setInitialRoute('Home');
158
+ const cleanup = harness(nav);
159
+ nav.go('Home');
133
160
  cleanup();
161
+ expect(drainSpans().filter((s) => s.op === 'react.navigation')).toHaveLength(1);
134
162
  });
135
163
 
136
- test('cleanup closes the still-open span', () => {
164
+ test('cleanup finishes the open span and clears the active span', () => {
137
165
  const nav = new FakeNav();
138
166
  nav.setInitialRoute('Home');
139
167
  const cleanup = harness(nav);
140
-
141
168
  nav.go('Settings');
142
169
  cleanup();
143
170
 
144
- const spans = drainSpans();
145
- expect(spans).toHaveLength(1);
146
- expect(spans[0]?.name).toBe('Home → Settings');
171
+ expect(activeSpan()).toBeNull();
172
+ const spans = drainSpans().filter((s) => s.op === 'react.navigation');
173
+ expect(spans.map((s) => s.name)).toEqual(['Home', 'Home → Settings']);
147
174
  });
148
175
  });
@@ -105,6 +105,15 @@ describe('RN network handler tracing', () => {
105
105
  expect(tp).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/);
106
106
  });
107
107
 
108
+ test('span name normalizes id-like segments; tag keeps full scrubbed url', async () => {
109
+ await fetch('https://api.example.com/users/123/orders/456?token=abc');
110
+ const sp = drainSpans()[0]!;
111
+ expect(sp.name).toBe('GET https://api.example.com/users/{id}/orders/{id}');
112
+ // RN's scrubUrl redacts auth params in the tag, but keeps path + host.
113
+ expect(sp.tags['http.url']).toContain('https://api.example.com/users/123/orders/456');
114
+ expect(sp.tags['http.url']).not.toContain('token=abc');
115
+ });
116
+
108
117
  test('5xx → span.status = "error"', async () => {
109
118
  recorderQueue = [{ status: 503 }];
110
119
  await fetch('https://api.example.com/x');
@@ -7,10 +7,13 @@ import {
7
7
  mock,
8
8
  } from 'bun:test';
9
9
 
10
+ import { clearSpans, startSpan } from '@goliapkg/sentori-core';
11
+
10
12
  import { setConfig, __resetForTests as resetConfig } from '../config';
11
13
  import {
12
14
  enqueue,
13
15
  flush,
16
+ flushSpans,
14
17
  startTransport,
15
18
  __resetForTests as resetTransport,
16
19
  __peekQueue,
@@ -168,3 +171,77 @@ describe('transport', () => {
168
171
  expect(__peekQueue()).toHaveLength(0);
169
172
  });
170
173
  });
174
+
175
+ describe('span flush', () => {
176
+ beforeEach(() => {
177
+ resetConfig();
178
+ resetTransport();
179
+ clearSpans();
180
+ setConfig({
181
+ token: 'st_pk_test',
182
+ release: 'app@1.0.0+1',
183
+ environment: 'test',
184
+ ingestUrl: 'http://localhost:8080',
185
+ enabled: true,
186
+ });
187
+ startTransport();
188
+ });
189
+ afterEach(() => {
190
+ globalThis.fetch = originalFetch;
191
+ clearSpans();
192
+ });
193
+
194
+ it('POSTs buffered spans to /v1/spans:batch', async () => {
195
+ const calls: { url: string; init?: RequestInit }[] = [];
196
+ globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
197
+ calls.push({ url: String(url), init });
198
+ return new Response(null, { status: 202 });
199
+ }) as typeof fetch;
200
+
201
+ startSpan('http.client', { name: 'GET /a' }).finish({ status: 'ok' });
202
+ startSpan('http.client', { name: 'GET /b' }).finish({ status: 'error' });
203
+ await flushSpans();
204
+
205
+ expect(calls).toHaveLength(1);
206
+ expect(calls[0]?.url).toBe('http://localhost:8080/v1/spans:batch');
207
+ const body = JSON.parse((calls[0]?.init?.body as string) ?? '{}');
208
+ expect(body.spans).toHaveLength(2);
209
+ expect(body.spans[0]).toMatchObject({ op: 'http.client', name: 'GET /a', status: 'ok' });
210
+ });
211
+
212
+ it('no-op when the span buffer is empty', async () => {
213
+ let attempts = 0;
214
+ globalThis.fetch = mock(async () => {
215
+ attempts++;
216
+ return new Response(null, { status: 202 });
217
+ }) as typeof fetch;
218
+ await flushSpans();
219
+ expect(attempts).toBe(0);
220
+ });
221
+
222
+ it('splits >200 spans into multiple batches', async () => {
223
+ const sizes: number[] = [];
224
+ globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
225
+ const body = JSON.parse((init?.body as string) ?? '{}');
226
+ sizes.push(body.spans.length);
227
+ return new Response(null, { status: 202 });
228
+ }) as typeof fetch;
229
+
230
+ for (let i = 0; i < 450; i++) startSpan('http.client', { name: `GET /${i}` }).finish();
231
+ await flushSpans();
232
+
233
+ expect(sizes).toEqual([200, 200, 50]);
234
+ });
235
+
236
+ it('drops on 5xx without retrying', async () => {
237
+ let attempts = 0;
238
+ globalThis.fetch = mock(async () => {
239
+ attempts++;
240
+ return new Response('boom', { status: 503 });
241
+ }) as typeof fetch;
242
+
243
+ startSpan('http.client', { name: 'GET /a' }).finish();
244
+ await flushSpans();
245
+ expect(attempts).toBe(1);
246
+ });
247
+ });
@@ -1,11 +1,19 @@
1
- import { startSpan } from '@goliapkg/sentori-core';
1
+ import { normalizeUrl, startSpan } from '@goliapkg/sentori-core';
2
2
 
3
3
  import { addBreadcrumb } from '../breadcrumbs';
4
+ import { getConfig } from '../config';
4
5
 
5
6
  let _installed = false;
6
7
 
7
8
  const AUTH_PARAMS = ['token', 'key', 'password', 'secret', 'access_token'];
8
9
 
10
+ // Requests to our own ingest endpoint shouldn't be traced — otherwise
11
+ // every span upload spawns another http.client span, and so on.
12
+ const isIngestUrl = (url: string): boolean => {
13
+ const base = getConfig()?.ingestUrl;
14
+ return !!base && url.startsWith(base);
15
+ };
16
+
9
17
  export const installNetworkHandler = (): void => {
10
18
  if (_installed) return;
11
19
  _installed = true;
@@ -22,6 +30,7 @@ function patchFetch(): void {
22
30
  globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
23
31
  const start = Date.now();
24
32
  const url = extractUrl(input);
33
+ if (isIngestUrl(url)) return original(input, init);
25
34
  const scrubbed = scrubUrl(url);
26
35
  const method = (init?.method ??
27
36
  (typeof input !== 'string' && 'method' in (input as Request)
@@ -33,7 +42,7 @@ function patchFetch(): void {
33
42
  // attached to error events at capture time and serve a different
34
43
  // surface (the "last 100 things" timeline on the issue page).
35
44
  const span = startSpan('http.client', {
36
- name: `${method.toUpperCase()} ${scrubbed}`,
45
+ name: `${method.toUpperCase()} ${normalizeUrl(scrubbed)}`,
37
46
  tags: { 'http.method': method.toUpperCase(), 'http.url': scrubbed },
38
47
  });
39
48
 
@@ -117,10 +126,11 @@ function patchXhr(): void {
117
126
  };
118
127
 
119
128
  proto.send = function (this: TracedXhr, body?: Document | XMLHttpRequestBodyInit | null): void {
129
+ if (isIngestUrl(this.__sentoriUrl ?? '')) return originalSend.call(this, body);
120
130
  const method = this.__sentoriMethod ?? 'GET';
121
131
  const url = scrubUrl(this.__sentoriUrl ?? '');
122
132
  const span = startSpan('http.client', {
123
- name: `${method} ${url}`,
133
+ name: `${method} ${normalizeUrl(url)}`,
124
134
  tags: { 'http.method': method, 'http.url': url },
125
135
  });
126
136
  this.__sentoriSpan = span;
package/src/navigation.ts CHANGED
@@ -1,19 +1,28 @@
1
1
  // Phase 35 sub-C: react-navigation auto-instrumentation.
2
+ // Phase 39 sub-B: the open `react.navigation` span is also made the
3
+ // active span for that screen's lifetime, so the screen's
4
+ // `http.client` spans (and any other startSpan() calls) attach to it
5
+ // as children — one trace per screen instead of one per request.
2
6
  //
3
7
  // Mount `useTraceNavigation(navigationRef)` next to your
4
- // `<NavigationContainer ref={navigationRef}>` and every route
5
- // transition becomes a `react.navigation` span. Span names are
6
- // `<from> → <to>` so the trace list reads as a navigation log.
8
+ // `<NavigationContainer ref={navigationRef}>`. Span names are
9
+ // `<from> <to>` (or just the route name for the first screen) so
10
+ // the trace list reads as a navigation log.
7
11
  //
8
- // react-navigation is an OPTIONAL peer dependency — apps that
9
- // don't use it never have to install it. The hook itself doesn't
10
- // import from @react-navigation/native; consumers pass in the ref
11
- // they already have, and we read its state via the public
12
- // `getCurrentRoute()` API. That keeps the dep edge optional.
12
+ // react-navigation is an OPTIONAL peer dependency — apps that don't
13
+ // use it never have to install it. The hook doesn't import from
14
+ // @react-navigation/native; consumers pass in the ref they already
15
+ // have, and we read its state via the public `getCurrentRoute()`.
16
+ //
17
+ // Caveat (active-span on RN is a module variable): requests fired
18
+ // from a `setTimeout` / background poll / detached promise after the
19
+ // screen settled may not see the nav span as active. If you want such
20
+ // a request parented to the current screen, pass it explicitly:
21
+ // `startSpan(op, { parent: activeSpan() })`.
13
22
 
14
23
  import { useEffect, useRef } from 'react';
15
24
 
16
- import { startSpan, type SpanHandle } from '@goliapkg/sentori-core';
25
+ import { setActiveSpan, startSpan, type SpanHandle } from '@goliapkg/sentori-core';
17
26
 
18
27
  /** Minimal contract: anything with `addListener('state', cb)` and
19
28
  * `getCurrentRoute()` works. The real @react-navigation/native
@@ -25,9 +34,9 @@ export type NavigationRefLike = {
25
34
 
26
35
  /**
27
36
  * Subscribe to react-navigation state changes and emit a
28
- * `react.navigation` span per transition. First mount records the
29
- * initial route as the start anchor but does NOT emit a span (the
30
- * convention from `useSentoriRouter` in sentori-react).
37
+ * `react.navigation` span per screen (including the initial one),
38
+ * each a fresh trace root. The span is kept active while the screen
39
+ * is current; child spans created in that window attribute up to it.
31
40
  *
32
41
  * import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'
33
42
  * import { useTraceNavigation } from '@goliapkg/sentori-react-native'
@@ -38,48 +47,53 @@ export type NavigationRefLike = {
38
47
  * return <NavigationContainer ref={navigationRef}>{...}</NavigationContainer>
39
48
  * }
40
49
  *
41
- * Each span carries `{ from, to }` as tags and uses the destination
42
- * route name as the span name.
50
+ * Each span carries `{ nav.from, nav.to }` tags.
43
51
  */
44
52
  export function useTraceNavigation(navigationRef: NavigationRefLike): void {
45
- // Latest route name we've observed. `null` means "no transition
46
- // recorded yet" (initial mount).
53
+ // Latest route name we've observed.
47
54
  const lastRouteRef = useRef<null | string>(null);
48
- // Span that started when this route was entered. Finished when the
49
- // NEXT route transition arrives.
55
+ // Span for the screen the user is currently on. Finished when the
56
+ // next screen is entered (or on unmount).
50
57
  const openSpanRef = useRef<null | SpanHandle>(null);
51
58
 
52
59
  useEffect(() => {
53
60
  if (typeof navigationRef.addListener !== 'function') return;
54
61
  if (typeof navigationRef.getCurrentRoute !== 'function') return;
55
62
 
56
- // Seed the "last route" reference from the current state so the
57
- // first transition emits a span with the right `from`.
63
+ // Each screen gets its own trace root detach from whatever the
64
+ // previous screen's span was (we keep it active, so without
65
+ // `parent: null` the new one would nest under it).
66
+ const openScreenSpan = (from: null | string, to: string) => {
67
+ const span = startSpan('react.navigation', {
68
+ name: from ? `${from} → ${to}` : to,
69
+ parent: null,
70
+ tags: { 'nav.from': from ?? '', 'nav.to': to },
71
+ });
72
+ openSpanRef.current = span;
73
+ setActiveSpan(span);
74
+ lastRouteRef.current = to;
75
+ };
76
+
77
+ // Open a span for the initial screen so its requests are grouped
78
+ // too (auth / config / first data load are usually the busiest
79
+ // screen of a session).
58
80
  const initial = navigationRef.getCurrentRoute()?.name ?? null;
59
- lastRouteRef.current = initial;
81
+ if (initial !== null) openScreenSpan(null, initial);
82
+ else lastRouteRef.current = null;
60
83
 
61
84
  const unsubscribe = navigationRef.addListener('state', () => {
62
85
  const next = navigationRef.getCurrentRoute()?.name ?? null;
63
86
  const prev = lastRouteRef.current;
64
87
  if (next === null || next === prev) return;
65
-
66
- // Close the prior span (if any) before opening the new one so
67
- // the trace looks like a sequence, not nested.
68
88
  openSpanRef.current?.finish({ status: 'ok' });
69
-
70
- const span = startSpan('react.navigation', {
71
- name: prev ? `${prev} → ${next}` : next,
72
- tags: { 'nav.from': prev ?? '', 'nav.to': next },
73
- });
74
- openSpanRef.current = span;
75
- lastRouteRef.current = next;
89
+ openScreenSpan(prev, next);
76
90
  });
77
91
 
78
92
  return () => {
79
93
  unsubscribe();
80
- // Close any still-open span on unmount so we don't leak it.
81
94
  openSpanRef.current?.finish({ status: 'ok' });
82
95
  openSpanRef.current = null;
96
+ setActiveSpan(null);
83
97
  };
84
98
  }, [navigationRef]);
85
99
  }
package/src/transport.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { drainSpans } from '@goliapkg/sentori-core';
2
+
1
3
  import { getConfig } from './config';
2
4
  import type { Event } from './types';
3
5
 
@@ -7,8 +9,15 @@ const MAX_RETRY = 3;
7
9
  const STORAGE_KEY = '@sentori/pending';
8
10
  const MAX_PERSISTED = 1000;
9
11
 
12
+ // Spans are higher-volume and lower-value than error events: we batch
13
+ // them on their own timer, cap each request at the server's per-batch
14
+ // limit, and drop on failure rather than persisting offline.
15
+ const SPAN_FLUSH_INTERVAL_MS = 10_000;
16
+ const SPAN_BATCH_MAX = 200;
17
+
10
18
  let _queue: Event[] = [];
11
19
  let _flushTimer: ReturnType<typeof setTimeout> | null = null;
20
+ let _spanTimer: ReturnType<typeof setInterval> | null = null;
12
21
  let _started = false;
13
22
 
14
23
  const SDK_VERSION = '0.0.0';
@@ -27,6 +36,47 @@ export const enqueue = (event: Event): void => {
27
36
 
28
37
  export const startTransport = (): void => {
29
38
  _started = true;
39
+ if (!_spanTimer) {
40
+ _spanTimer = setInterval(() => {
41
+ void flushSpans();
42
+ }, SPAN_FLUSH_INTERVAL_MS);
43
+ }
44
+ };
45
+
46
+ export const flushSpans = async (): Promise<void> => {
47
+ if (!_started) return;
48
+ const spans = drainSpans();
49
+ if (spans.length === 0) return;
50
+ const config = getConfig();
51
+ if (!config) return;
52
+ for (let i = 0; i < spans.length; i += SPAN_BATCH_MAX) {
53
+ const chunk = spans.slice(i, i + SPAN_BATCH_MAX);
54
+ try {
55
+ await sendSpansOnce(chunk, config.ingestUrl, config.token);
56
+ } catch {
57
+ // drop the remaining chunks — span uploads aren't worth retrying
58
+ break;
59
+ }
60
+ }
61
+ };
62
+
63
+ const sendSpansOnce = async (
64
+ spans: unknown[],
65
+ ingestUrl: string,
66
+ token: string,
67
+ ): Promise<void> => {
68
+ const resp = await fetch(`${ingestUrl}/v1/spans:batch`, {
69
+ method: 'POST',
70
+ headers: {
71
+ 'Content-Type': 'application/json',
72
+ Authorization: `Bearer ${token}`,
73
+ 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
74
+ },
75
+ body: JSON.stringify({ spans }),
76
+ });
77
+ // 5xx: server overloaded. 4xx (bad token / quota / oversized): also
78
+ // pointless to keep sending. Either way, stop the rest of the batch.
79
+ if (resp.status >= 400) throw new Error(`spans-${resp.status}`);
30
80
  };
31
81
 
32
82
  export const flush = async (): Promise<void> => {
@@ -158,6 +208,8 @@ export const __resetForTests = (): void => {
158
208
  _queue = [];
159
209
  if (_flushTimer) clearTimeout(_flushTimer);
160
210
  _flushTimer = null;
211
+ if (_spanTimer) clearInterval(_spanTimer);
212
+ _spanTimer = null;
161
213
  _started = false;
162
214
  };
163
215