@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 +17 -8
- package/lib/router.d.ts.map +1 -1
- package/lib/router.js +35 -10
- package/lib/router.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/router.test.tsx +41 -2
- package/src/router.ts +39 -11
package/lib/router.d.ts
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Subscribe to `react-router` navigation
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
12
|
-
*
|
|
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`.
|
|
15
|
-
*
|
|
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
|
package/lib/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
15
|
-
*
|
|
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`.
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
4
|
-
"description": "React adapter for Sentori
|
|
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.
|
|
60
|
-
"@goliapkg/sentori-javascript": "0.3.
|
|
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(() =>
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
17
|
-
*
|
|
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`.
|
|
20
|
-
*
|
|
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
|
|
33
|
-
|
|
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
|
}
|