@goliapkg/sentori-react 0.4.1 → 0.4.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.
package/lib/router.d.ts CHANGED
@@ -1,19 +1,28 @@
1
1
  /**
2
- * Subscribe to `react-router` navigation and push a `nav` breadcrumb
3
- * on every pathname/search/hash change. Mount once high in the tree
4
- * (inside the `Router` and inside `SentoriProvider`):
2
+ * Subscribe to `react-router` navigation. On every pathname/search/
3
+ * hash change this:
4
+ *
5
+ * - pushes a `nav` breadcrumb (`{ from, to }`)
6
+ * - opens a `react.navigation` span (a fresh trace root) for the new
7
+ * route and makes it the active span — so any `http.client` /
8
+ * other spans created while that route is mounted attach to it as
9
+ * children (one trace per route instead of one per request)
10
+ *
11
+ * Mount once high in the tree (inside the `Router` and inside
12
+ * `SentoriProvider`):
5
13
  *
6
14
  * function AppShell() {
7
15
  * useSentoriRouter()
8
16
  * return <Outlet />
9
17
  * }
10
18
  *
11
- * The first render does NOT emit a breadcrumb only actual
12
- * transitions are recorded.
19
+ * The first render does NOT emit a `nav` breadcrumb (there's no
20
+ * transition) but DOES open the route span for the landing page, so
21
+ * its requests are grouped too. On unmount the open route span is
22
+ * finished and the active span cleared.
13
23
  *
14
- * Peer dependency: `react-router >= 7`. This hook is in a separate
15
- * entry point so apps not using react-router don't pay the import
16
- * cost or trip a missing-module error.
24
+ * Peer dependency: `react-router >= 7`. Separate entry point so apps
25
+ * not using react-router don't pay the import cost.
17
26
  */
18
27
  export declare function useSentoriRouter(): void;
19
28
  //# sourceMappingURL=router.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAcvC"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAgCvC"}
package/lib/router.js CHANGED
@@ -1,34 +1,59 @@
1
+ import { setActiveSpan, startSpan } from '@goliapkg/sentori-core';
1
2
  import { useEffect, useRef } from 'react';
2
3
  import { useLocation } from 'react-router';
3
4
  import { useSentoriCtx } from './SentoriProvider.js';
4
5
  /**
5
- * Subscribe to `react-router` navigation and push a `nav` breadcrumb
6
- * on every pathname/search/hash change. Mount once high in the tree
7
- * (inside the `Router` and inside `SentoriProvider`):
6
+ * Subscribe to `react-router` navigation. On every pathname/search/
7
+ * hash change this:
8
+ *
9
+ * - pushes a `nav` breadcrumb (`{ from, to }`)
10
+ * - opens a `react.navigation` span (a fresh trace root) for the new
11
+ * route and makes it the active span — so any `http.client` /
12
+ * other spans created while that route is mounted attach to it as
13
+ * children (one trace per route instead of one per request)
14
+ *
15
+ * Mount once high in the tree (inside the `Router` and inside
16
+ * `SentoriProvider`):
8
17
  *
9
18
  * function AppShell() {
10
19
  * useSentoriRouter()
11
20
  * return <Outlet />
12
21
  * }
13
22
  *
14
- * The first render does NOT emit a breadcrumb only actual
15
- * transitions are recorded.
23
+ * The first render does NOT emit a `nav` breadcrumb (there's no
24
+ * transition) but DOES open the route span for the landing page, so
25
+ * its requests are grouped too. On unmount the open route span is
26
+ * finished and the active span cleared.
16
27
  *
17
- * Peer dependency: `react-router >= 7`. This hook is in a separate
18
- * entry point so apps not using react-router don't pay the import
19
- * cost or trip a missing-module error.
28
+ * Peer dependency: `react-router >= 7`. Separate entry point so apps
29
+ * not using react-router don't pay the import cost.
20
30
  */
21
31
  export function useSentoriRouter() {
22
32
  const { addBreadcrumb } = useSentoriCtx();
23
33
  const location = useLocation();
24
34
  const prevRef = useRef(null);
35
+ const openSpanRef = useRef(null);
25
36
  const next = location.pathname + location.search + location.hash;
26
37
  useEffect(() => {
27
38
  const prev = prevRef.current;
28
- if (prev !== null && prev !== next) {
39
+ if (prev === next)
40
+ return;
41
+ if (prev !== null)
29
42
  addBreadcrumb('nav', { from: prev, to: next });
30
- }
43
+ openSpanRef.current?.finish({ status: 'ok' });
44
+ const span = startSpan('react.navigation', {
45
+ name: prev ? `${prev} → ${next}` : next,
46
+ parent: null, // each route is its own trace root
47
+ tags: { 'nav.from': prev ?? '', 'nav.to': next },
48
+ });
49
+ openSpanRef.current = span;
50
+ setActiveSpan(span);
31
51
  prevRef.current = next;
32
52
  }, [addBreadcrumb, next]);
53
+ useEffect(() => () => {
54
+ openSpanRef.current?.finish({ status: 'ok' });
55
+ openSpanRef.current = null;
56
+ setActiveSpan(null);
57
+ }, []);
33
58
  }
34
59
  //# sourceMappingURL=router.js.map
package/lib/router.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEpD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;IACzC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAA;IAE3C,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAA;IAEhE,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAA;QAC5B,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACnC,aAAa,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;QAChD,CAAC;QACD,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;IACxB,CAAC,EAAE,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAA;AAC3B,CAAC"}
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAmB,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAClF,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAAA;IACzC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAA;IAC3C,MAAM,WAAW,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAA;IAEnD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAA;IAEhE,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAA;QAC5B,IAAI,IAAI,KAAK,IAAI;YAAE,OAAM;QACzB,IAAI,IAAI,KAAK,IAAI;YAAE,aAAa,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;QAEjE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAC7C,MAAM,IAAI,GAAG,SAAS,CAAC,kBAAkB,EAAE;YACzC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI;YACvC,MAAM,EAAE,IAAI,EAAE,mCAAmC;YACjD,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SACjD,CAAC,CAAA;QACF,WAAW,CAAC,OAAO,GAAG,IAAI,CAAA;QAC1B,aAAa,CAAC,IAAI,CAAC,CAAA;QACnB,OAAO,CAAC,OAAO,GAAG,IAAI,CAAA;IACxB,CAAC,EAAE,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAA;IAEzB,SAAS,CACP,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAC7C,WAAW,CAAC,OAAO,GAAG,IAAI,CAAA;QAC1B,aAAa,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC,EACD,EAAE,CACH,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-react",
3
- "version": "0.4.1",
4
- "description": "React adapter for Sentori Provider, ErrorBoundary (resetKeys + render-prop), Suspense, TraceRender, react-router breadcrumbs + auto-tracing.",
3
+ "version": "0.4.3",
4
+ "description": "React adapter for Sentori \u2014 Provider, ErrorBoundary (resetKeys + render-prop), Suspense, TraceRender, react-router breadcrumbs + auto-tracing.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentori.golia.jp",
7
7
  "repository": {
@@ -56,8 +56,8 @@
56
56
  }
57
57
  },
58
58
  "dependencies": {
59
- "@goliapkg/sentori-core": "0.3.0",
60
- "@goliapkg/sentori-javascript": "0.3.1"
59
+ "@goliapkg/sentori-core": "0.4.0",
60
+ "@goliapkg/sentori-javascript": "0.3.3"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@happy-dom/global-registrator": "^17",
@@ -1,5 +1,12 @@
1
+ import {
2
+ __resetTraceContextForTests,
3
+ __useFallbackTraceContextForTests,
4
+ clearSpans,
5
+ drainSpans,
6
+ setActiveSpan,
7
+ } from '@goliapkg/sentori-core'
1
8
  import { cleanup, fireEvent, render, screen } from '@testing-library/react'
2
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
9
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from 'bun:test'
3
10
  import { Link, MemoryRouter, Route, Routes } from 'react-router'
4
11
 
5
12
  import { clearBreadcrumbs, getBreadcrumbs } from '@goliapkg/sentori-javascript'
@@ -31,11 +38,23 @@ function Shell() {
31
38
  )
32
39
  }
33
40
 
41
+ const navSpans = () => drainSpans().filter((s) => s.op === 'react.navigation')
42
+
34
43
  describe('useSentoriRouter', () => {
35
- beforeEach(() => clearBreadcrumbs())
44
+ beforeEach(() => {
45
+ __useFallbackTraceContextForTests() // see navigation.test.ts
46
+ clearBreadcrumbs()
47
+ clearSpans()
48
+ setActiveSpan(null)
49
+ })
36
50
  afterEach(() => {
37
51
  cleanup()
38
52
  clearBreadcrumbs()
53
+ clearSpans()
54
+ setActiveSpan(null)
55
+ })
56
+ afterAll(() => {
57
+ __resetTraceContextForTests()
39
58
  })
40
59
 
41
60
  test('initial mount does NOT emit a nav breadcrumb', () => {
@@ -73,4 +92,24 @@ describe('useSentoriRouter', () => {
73
92
  expect(navsAfter).toHaveLength(2)
74
93
  expect(navsAfter[1]?.data).toEqual({ from: '/orders', to: '/billing' })
75
94
  })
95
+
96
+ test('opens a react.navigation span per route (initial + each transition)', () => {
97
+ render(
98
+ <SentoriProvider {...PROVIDER_PROPS}>
99
+ <MemoryRouter initialEntries={['/']}>
100
+ <Shell />
101
+ </MemoryRouter>
102
+ </SentoriProvider>,
103
+ )
104
+ fireEvent.click(screen.getByText('orders'))
105
+ fireEvent.click(screen.getByText('billing'))
106
+ cleanup() // unmount → finishes the last open span
107
+
108
+ const spans = navSpans()
109
+ expect(spans.map((s) => s.name)).toEqual(['/', '/ → /orders', '/orders → /billing'])
110
+ // each route is its own trace root
111
+ expect(spans.every((s) => s.parentSpanId === null)).toBe(true)
112
+ expect(new Set(spans.map((s) => s.traceId)).size).toBe(3)
113
+ expect(spans[1]?.tags).toEqual({ 'nav.from': '/', 'nav.to': '/orders' })
114
+ })
76
115
  })
package/src/router.ts CHANGED
@@ -1,37 +1,65 @@
1
+ import { setActiveSpan, type SpanHandle, startSpan } from '@goliapkg/sentori-core'
1
2
  import { useEffect, useRef } from 'react'
2
3
  import { useLocation } from 'react-router'
3
4
 
4
5
  import { useSentoriCtx } from './SentoriProvider.js'
5
6
 
6
7
  /**
7
- * Subscribe to `react-router` navigation and push a `nav` breadcrumb
8
- * on every pathname/search/hash change. Mount once high in the tree
9
- * (inside the `Router` and inside `SentoriProvider`):
8
+ * Subscribe to `react-router` navigation. On every pathname/search/
9
+ * hash change this:
10
+ *
11
+ * - pushes a `nav` breadcrumb (`{ from, to }`)
12
+ * - opens a `react.navigation` span (a fresh trace root) for the new
13
+ * route and makes it the active span — so any `http.client` /
14
+ * other spans created while that route is mounted attach to it as
15
+ * children (one trace per route instead of one per request)
16
+ *
17
+ * Mount once high in the tree (inside the `Router` and inside
18
+ * `SentoriProvider`):
10
19
  *
11
20
  * function AppShell() {
12
21
  * useSentoriRouter()
13
22
  * return <Outlet />
14
23
  * }
15
24
  *
16
- * The first render does NOT emit a breadcrumb only actual
17
- * transitions are recorded.
25
+ * The first render does NOT emit a `nav` breadcrumb (there's no
26
+ * transition) but DOES open the route span for the landing page, so
27
+ * its requests are grouped too. On unmount the open route span is
28
+ * finished and the active span cleared.
18
29
  *
19
- * Peer dependency: `react-router >= 7`. This hook is in a separate
20
- * entry point so apps not using react-router don't pay the import
21
- * cost or trip a missing-module error.
30
+ * Peer dependency: `react-router >= 7`. Separate entry point so apps
31
+ * not using react-router don't pay the import cost.
22
32
  */
23
33
  export function useSentoriRouter(): void {
24
34
  const { addBreadcrumb } = useSentoriCtx()
25
35
  const location = useLocation()
26
36
  const prevRef = useRef<null | string>(null)
37
+ const openSpanRef = useRef<null | SpanHandle>(null)
27
38
 
28
39
  const next = location.pathname + location.search + location.hash
29
40
 
30
41
  useEffect(() => {
31
42
  const prev = prevRef.current
32
- if (prev !== null && prev !== next) {
33
- addBreadcrumb('nav', { from: prev, to: next })
34
- }
43
+ if (prev === next) return
44
+ if (prev !== null) addBreadcrumb('nav', { from: prev, to: next })
45
+
46
+ openSpanRef.current?.finish({ status: 'ok' })
47
+ const span = startSpan('react.navigation', {
48
+ name: prev ? `${prev} → ${next}` : next,
49
+ parent: null, // each route is its own trace root
50
+ tags: { 'nav.from': prev ?? '', 'nav.to': next },
51
+ })
52
+ openSpanRef.current = span
53
+ setActiveSpan(span)
35
54
  prevRef.current = next
36
55
  }, [addBreadcrumb, next])
56
+
57
+ useEffect(
58
+ () => () => {
59
+ openSpanRef.current?.finish({ status: 'ok' })
60
+ openSpanRef.current = null
61
+ setActiveSpan(null)
62
+ },
63
+ [],
64
+ )
37
65
  }