@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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/README.md +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ReactNode } from "react";
|
|
9
|
-
import {
|
|
9
|
+
import { _getRequestContext } from "../../server/request-context.js";
|
|
10
10
|
import type { StaticStore } from "../../prerender/store.js";
|
|
11
11
|
|
|
12
12
|
// Lazy-initialized static store for production Static handler interception.
|
|
@@ -57,7 +57,7 @@ export async function tryStaticLookup(
|
|
|
57
57
|
// The data was keyed by handlerId at build time; replay under segmentId
|
|
58
58
|
// so it matches the segment order used by useHandle on the client.
|
|
59
59
|
if (entry.handles && Object.keys(entry.handles).length > 0) {
|
|
60
|
-
const handleStore =
|
|
60
|
+
const handleStore = _getRequestContext()?._handleStore;
|
|
61
61
|
if (handleStore) {
|
|
62
62
|
handleStore.replaySegmentData(segmentId, entry.handles);
|
|
63
63
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// Barrel re-export -- see segment-resolution/ for implementations.
|
|
2
|
+
export { handleHandlerResult } from "./segment-resolution/helpers.js";
|
|
2
3
|
export {
|
|
3
|
-
handleHandlerResult,
|
|
4
4
|
resolveLoaders,
|
|
5
5
|
type ResolveSegmentOptions,
|
|
6
6
|
resolveSegment,
|
|
7
7
|
resolveOrphanLayout,
|
|
8
8
|
resolveParallelEntry,
|
|
9
|
-
resolveWithErrorHandling,
|
|
10
9
|
resolveAllSegments,
|
|
11
10
|
resolveLoadersOnly,
|
|
12
11
|
} from "./segment-resolution/fresh.js";
|
|
@@ -18,6 +17,5 @@ export {
|
|
|
18
17
|
resolveEntryHandlerWithRevalidation,
|
|
19
18
|
resolveSegmentWithRevalidation,
|
|
20
19
|
resolveOrphanLayoutWithRevalidation,
|
|
21
|
-
resolveWithRevalidationErrorHandling,
|
|
22
20
|
resolveAllSegmentsWithRevalidation,
|
|
23
21
|
} from "./segment-resolution/revalidation.js";
|
|
@@ -50,6 +50,7 @@ export interface SegmentWrappers<TEnv = any> {
|
|
|
50
50
|
actionResult?: any;
|
|
51
51
|
formData?: FormData;
|
|
52
52
|
},
|
|
53
|
+
stale?: boolean,
|
|
53
54
|
) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
|
|
54
55
|
buildEntryRevalidateMap: (
|
|
55
56
|
entries: EntryData[],
|
|
@@ -158,6 +159,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
158
159
|
actionResult?: any;
|
|
159
160
|
formData?: FormData;
|
|
160
161
|
},
|
|
162
|
+
stale?: boolean,
|
|
161
163
|
): ReturnType<typeof _resolveLoadersOnlyWithRevalidation> {
|
|
162
164
|
return _resolveLoadersOnlyWithRevalidation(
|
|
163
165
|
entries,
|
|
@@ -170,6 +172,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
170
172
|
routeKey,
|
|
171
173
|
segmentDeps,
|
|
172
174
|
actionContext,
|
|
175
|
+
stale,
|
|
173
176
|
);
|
|
174
177
|
}
|
|
175
178
|
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry Adapter for Router Telemetry
|
|
3
|
+
*
|
|
4
|
+
* Maps internal TelemetrySink events to OTel spans. The core router
|
|
5
|
+
* remains OTel-agnostic — this adapter bridges the gap by accepting
|
|
6
|
+
* a standard OTel Tracer and producing spans/events from it.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { trace } from "@opentelemetry/api";
|
|
10
|
+
* import { createOTelSink } from "@rangojs/router";
|
|
11
|
+
*
|
|
12
|
+
* const router = createRouter({
|
|
13
|
+
* telemetry: createOTelSink(trace.getTracer("my-app")),
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { TelemetrySink, TelemetryEvent } from "./telemetry.js";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Minimal OTel-compatible types (structurally typed, no import needed)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimal Span interface compatible with @opentelemetry/api Span.
|
|
25
|
+
* Only the methods used by the adapter are declared.
|
|
26
|
+
*/
|
|
27
|
+
export interface OTelSpan {
|
|
28
|
+
setAttribute(key: string, value: string | number | boolean): OTelSpan | void;
|
|
29
|
+
addEvent(
|
|
30
|
+
name: string,
|
|
31
|
+
attributes?: Record<string, string | number | boolean>,
|
|
32
|
+
): OTelSpan | void;
|
|
33
|
+
setStatus(status: { code: number; message?: string }): OTelSpan | void;
|
|
34
|
+
recordException(exception: Error): void;
|
|
35
|
+
end(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Minimal Tracer interface compatible with @opentelemetry/api Tracer.
|
|
40
|
+
*/
|
|
41
|
+
export interface OTelTracer {
|
|
42
|
+
startSpan(
|
|
43
|
+
name: string,
|
|
44
|
+
options?: {
|
|
45
|
+
attributes?: Record<string, string | number | boolean>;
|
|
46
|
+
},
|
|
47
|
+
): OTelSpan;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// OTel SpanStatusCode constants (mirrors @opentelemetry/api values)
|
|
51
|
+
const STATUS_OK = 1;
|
|
52
|
+
const STATUS_ERROR = 2;
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Span correlation helpers
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
// Build correlation keys using requestId.
|
|
59
|
+
// getRequestId() always returns a value (generated internally when no
|
|
60
|
+
// header is present), so concurrent requests to the same path each get
|
|
61
|
+
// their own correlation key and never mis-correlate.
|
|
62
|
+
|
|
63
|
+
function requestKey(event: {
|
|
64
|
+
requestId?: string;
|
|
65
|
+
pathname: string;
|
|
66
|
+
transaction: string;
|
|
67
|
+
}): string {
|
|
68
|
+
return `${event.requestId ?? ""}:${event.pathname}:${event.transaction}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function loaderKey(event: {
|
|
72
|
+
requestId?: string;
|
|
73
|
+
segmentId: string;
|
|
74
|
+
loaderName: string;
|
|
75
|
+
pathname: string;
|
|
76
|
+
}): string {
|
|
77
|
+
return `${event.requestId ?? ""}:${event.segmentId}:${event.loaderName}:${event.pathname}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function pushSpan(
|
|
81
|
+
map: Map<string, OTelSpan[]>,
|
|
82
|
+
key: string,
|
|
83
|
+
span: OTelSpan,
|
|
84
|
+
): void {
|
|
85
|
+
let stack = map.get(key);
|
|
86
|
+
if (!stack) {
|
|
87
|
+
stack = [];
|
|
88
|
+
map.set(key, stack);
|
|
89
|
+
}
|
|
90
|
+
stack.push(span);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function popSpan(
|
|
94
|
+
map: Map<string, OTelSpan[]>,
|
|
95
|
+
key: string,
|
|
96
|
+
): OTelSpan | undefined {
|
|
97
|
+
const stack = map.get(key);
|
|
98
|
+
if (!stack || stack.length === 0) return undefined;
|
|
99
|
+
const span = stack.pop()!;
|
|
100
|
+
if (stack.length === 0) map.delete(key);
|
|
101
|
+
return span;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Adapter factory
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a TelemetrySink that maps router lifecycle events to OTel spans.
|
|
110
|
+
*
|
|
111
|
+
* Span mapping:
|
|
112
|
+
* - request.start / request.end / request.error → "rango.request" span
|
|
113
|
+
* - loader.start / loader.end / loader.error → "rango.loader" span
|
|
114
|
+
* - handler.error → "rango.handler.error" instant span
|
|
115
|
+
* - cache.decision → "rango.cache.decision" instant span
|
|
116
|
+
* - revalidation.decision → "rango.revalidation.decision" instant span
|
|
117
|
+
*
|
|
118
|
+
* Attributes use the `rango.*` namespace for router-specific data and
|
|
119
|
+
* `http.method` / `http.route` for HTTP semantics.
|
|
120
|
+
*/
|
|
121
|
+
export function createOTelSink(tracer: OTelTracer): TelemetrySink {
|
|
122
|
+
const requestSpans = new Map<string, OTelSpan[]>();
|
|
123
|
+
const loaderSpans = new Map<string, OTelSpan[]>();
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
emit(event: TelemetryEvent): void {
|
|
127
|
+
switch (event.type) {
|
|
128
|
+
// -----------------------------------------------------------------
|
|
129
|
+
// Request lifecycle
|
|
130
|
+
// -----------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
case "request.start": {
|
|
133
|
+
const span = tracer.startSpan("rango.request", {
|
|
134
|
+
attributes: {
|
|
135
|
+
"http.method": event.method,
|
|
136
|
+
"http.route": event.pathname,
|
|
137
|
+
"rango.transaction": event.transaction,
|
|
138
|
+
"rango.is_partial": event.isPartial,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
pushSpan(requestSpans, requestKey(event), span);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case "request.end": {
|
|
146
|
+
const span = popSpan(requestSpans, requestKey(event));
|
|
147
|
+
if (span) {
|
|
148
|
+
span.setAttribute("rango.duration_ms", event.durationMs);
|
|
149
|
+
span.setAttribute("rango.segment_count", event.segmentCount);
|
|
150
|
+
span.setAttribute("rango.cache.hit", event.cacheHit);
|
|
151
|
+
span.setStatus({ code: STATUS_OK });
|
|
152
|
+
span.end();
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "request.error": {
|
|
158
|
+
const span = popSpan(requestSpans, requestKey(event));
|
|
159
|
+
if (span) {
|
|
160
|
+
span.setAttribute("rango.duration_ms", event.durationMs);
|
|
161
|
+
span.setAttribute("rango.phase", event.phase);
|
|
162
|
+
span.recordException(event.error);
|
|
163
|
+
span.setStatus({
|
|
164
|
+
code: STATUS_ERROR,
|
|
165
|
+
message: event.error.message,
|
|
166
|
+
});
|
|
167
|
+
span.end();
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// -----------------------------------------------------------------
|
|
173
|
+
// Loader lifecycle
|
|
174
|
+
// -----------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
case "loader.start": {
|
|
177
|
+
const span = tracer.startSpan("rango.loader", {
|
|
178
|
+
attributes: {
|
|
179
|
+
"rango.segment_id": event.segmentId,
|
|
180
|
+
"rango.loader_name": event.loaderName,
|
|
181
|
+
"http.route": event.pathname,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
pushSpan(loaderSpans, loaderKey(event), span);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case "loader.end": {
|
|
189
|
+
const key = loaderKey(event);
|
|
190
|
+
const span = popSpan(loaderSpans, key);
|
|
191
|
+
if (span) {
|
|
192
|
+
span.setAttribute("rango.duration_ms", event.durationMs);
|
|
193
|
+
span.setAttribute("rango.loader.ok", event.ok);
|
|
194
|
+
span.setStatus({ code: event.ok ? STATUS_OK : STATUS_ERROR });
|
|
195
|
+
span.end();
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case "loader.error": {
|
|
201
|
+
const key = loaderKey(event);
|
|
202
|
+
const span = popSpan(loaderSpans, key);
|
|
203
|
+
if (span) {
|
|
204
|
+
span.setAttribute(
|
|
205
|
+
"rango.handled_by_boundary",
|
|
206
|
+
event.handledByBoundary,
|
|
207
|
+
);
|
|
208
|
+
span.recordException(event.error);
|
|
209
|
+
span.setStatus({
|
|
210
|
+
code: STATUS_ERROR,
|
|
211
|
+
message: event.error.message,
|
|
212
|
+
});
|
|
213
|
+
span.end();
|
|
214
|
+
} else {
|
|
215
|
+
// No matching start — create a standalone error span
|
|
216
|
+
const errorSpan = tracer.startSpan("rango.loader", {
|
|
217
|
+
attributes: {
|
|
218
|
+
"rango.segment_id": event.segmentId,
|
|
219
|
+
"rango.loader_name": event.loaderName,
|
|
220
|
+
"http.route": event.pathname,
|
|
221
|
+
"rango.handled_by_boundary": event.handledByBoundary,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
errorSpan.recordException(event.error);
|
|
225
|
+
errorSpan.setStatus({
|
|
226
|
+
code: STATUS_ERROR,
|
|
227
|
+
message: event.error.message,
|
|
228
|
+
});
|
|
229
|
+
errorSpan.end();
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// -----------------------------------------------------------------
|
|
235
|
+
// Handler errors (instant span)
|
|
236
|
+
// -----------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
case "handler.error": {
|
|
239
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
240
|
+
"rango.handled_by_boundary": event.handledByBoundary,
|
|
241
|
+
};
|
|
242
|
+
if (event.segmentId) attrs["rango.segment_id"] = event.segmentId;
|
|
243
|
+
if (event.segmentType)
|
|
244
|
+
attrs["rango.segment_type"] = event.segmentType;
|
|
245
|
+
if (event.pathname) attrs["http.route"] = event.pathname;
|
|
246
|
+
if (event.routeKey) attrs["rango.route_key"] = event.routeKey;
|
|
247
|
+
if (event.params) {
|
|
248
|
+
attrs["rango.params"] = JSON.stringify(event.params);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const span = tracer.startSpan("rango.handler.error", {
|
|
252
|
+
attributes: attrs,
|
|
253
|
+
});
|
|
254
|
+
span.recordException(event.error);
|
|
255
|
+
span.setStatus({ code: STATUS_ERROR, message: event.error.message });
|
|
256
|
+
span.end();
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// -----------------------------------------------------------------
|
|
261
|
+
// Cache decision (instant span)
|
|
262
|
+
// -----------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
case "cache.decision": {
|
|
265
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
266
|
+
"http.route": event.pathname,
|
|
267
|
+
"rango.route_key": event.routeKey,
|
|
268
|
+
"rango.cache.hit": event.hit,
|
|
269
|
+
"rango.cache.should_revalidate": event.shouldRevalidate,
|
|
270
|
+
};
|
|
271
|
+
if (event.source) attrs["rango.cache.source"] = event.source;
|
|
272
|
+
|
|
273
|
+
const span = tracer.startSpan("rango.cache.decision", {
|
|
274
|
+
attributes: attrs,
|
|
275
|
+
});
|
|
276
|
+
span.end();
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// -----------------------------------------------------------------
|
|
281
|
+
// Revalidation decision (instant span)
|
|
282
|
+
// -----------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
case "revalidation.decision": {
|
|
285
|
+
const span = tracer.startSpan("rango.revalidation.decision", {
|
|
286
|
+
attributes: {
|
|
287
|
+
"rango.segment_id": event.segmentId,
|
|
288
|
+
"http.route": event.pathname,
|
|
289
|
+
"rango.route_key": event.routeKey,
|
|
290
|
+
"rango.revalidate": event.shouldRevalidate,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
span.end();
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Telemetry Sink
|
|
3
|
+
*
|
|
4
|
+
* Internal event model for structured lifecycle events.
|
|
5
|
+
* The sink is optional and zero-cost when not configured.
|
|
6
|
+
*
|
|
7
|
+
* Emit points:
|
|
8
|
+
* - request.start / request.end (match-handlers.ts)
|
|
9
|
+
* - request.error (match-handlers.ts catch blocks)
|
|
10
|
+
* - request.origin-rejected (rsc/handler.ts origin guard)
|
|
11
|
+
* - loader.start / loader.end / loader.error (loader-resolution.ts)
|
|
12
|
+
* - handler.error (trackHandler catch, segment-resolution/helpers.ts)
|
|
13
|
+
* - cache.decision (cache-lookup middleware)
|
|
14
|
+
* - revalidation.decision (revalidation evaluation)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Event types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
interface BaseEvent {
|
|
22
|
+
/** Monotonic timestamp from performance.now() */
|
|
23
|
+
timestamp: number;
|
|
24
|
+
/** Request ID (from header or generated) */
|
|
25
|
+
requestId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RequestStartEvent extends BaseEvent {
|
|
29
|
+
type: "request.start";
|
|
30
|
+
method: string;
|
|
31
|
+
pathname: string;
|
|
32
|
+
/** "match" for full document requests, "matchPartial" for navigation */
|
|
33
|
+
transaction: "match" | "matchPartial";
|
|
34
|
+
isPartial: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RequestEndEvent extends BaseEvent {
|
|
38
|
+
type: "request.end";
|
|
39
|
+
method: string;
|
|
40
|
+
pathname: string;
|
|
41
|
+
transaction: "match" | "matchPartial";
|
|
42
|
+
durationMs: number;
|
|
43
|
+
segmentCount: number;
|
|
44
|
+
cacheHit: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RequestErrorEvent extends BaseEvent {
|
|
48
|
+
type: "request.error";
|
|
49
|
+
method: string;
|
|
50
|
+
pathname: string;
|
|
51
|
+
transaction: "match" | "matchPartial";
|
|
52
|
+
error: Error;
|
|
53
|
+
phase: string;
|
|
54
|
+
durationMs: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LoaderStartEvent extends BaseEvent {
|
|
58
|
+
type: "loader.start";
|
|
59
|
+
segmentId: string;
|
|
60
|
+
loaderName: string;
|
|
61
|
+
pathname: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LoaderEndEvent extends BaseEvent {
|
|
65
|
+
type: "loader.end";
|
|
66
|
+
segmentId: string;
|
|
67
|
+
loaderName: string;
|
|
68
|
+
pathname: string;
|
|
69
|
+
durationMs: number;
|
|
70
|
+
ok: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LoaderErrorEvent extends BaseEvent {
|
|
74
|
+
type: "loader.error";
|
|
75
|
+
segmentId: string;
|
|
76
|
+
loaderName: string;
|
|
77
|
+
pathname: string;
|
|
78
|
+
error: Error;
|
|
79
|
+
handledByBoundary: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface HandlerErrorEvent extends BaseEvent {
|
|
83
|
+
type: "handler.error";
|
|
84
|
+
segmentId?: string;
|
|
85
|
+
segmentType?: string;
|
|
86
|
+
error: Error;
|
|
87
|
+
handledByBoundary: boolean;
|
|
88
|
+
pathname?: string;
|
|
89
|
+
routeKey?: string;
|
|
90
|
+
params?: Record<string, string>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface CacheDecisionEvent extends BaseEvent {
|
|
94
|
+
type: "cache.decision";
|
|
95
|
+
pathname: string;
|
|
96
|
+
routeKey: string;
|
|
97
|
+
hit: boolean;
|
|
98
|
+
/** Whether stale-while-revalidate was triggered */
|
|
99
|
+
shouldRevalidate: boolean;
|
|
100
|
+
source?: "runtime" | "prerender";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface RevalidationDecisionEvent extends BaseEvent {
|
|
104
|
+
type: "revalidation.decision";
|
|
105
|
+
segmentId: string;
|
|
106
|
+
pathname: string;
|
|
107
|
+
routeKey: string;
|
|
108
|
+
shouldRevalidate: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface RequestTimeoutEvent extends BaseEvent {
|
|
112
|
+
type: "request.timeout";
|
|
113
|
+
phase: import("./timeout.js").TimeoutPhase;
|
|
114
|
+
pathname: string;
|
|
115
|
+
routeKey?: string;
|
|
116
|
+
actionId?: string;
|
|
117
|
+
durationMs: number;
|
|
118
|
+
customHandler: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface OriginCheckRejectedEvent extends BaseEvent {
|
|
122
|
+
type: "request.origin-rejected";
|
|
123
|
+
method: string;
|
|
124
|
+
pathname: string;
|
|
125
|
+
phase: import("../rsc/origin-guard.js").OriginCheckPhase;
|
|
126
|
+
origin: string | null;
|
|
127
|
+
host: string | null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type TelemetryEvent =
|
|
131
|
+
| RequestStartEvent
|
|
132
|
+
| RequestEndEvent
|
|
133
|
+
| RequestErrorEvent
|
|
134
|
+
| LoaderStartEvent
|
|
135
|
+
| LoaderEndEvent
|
|
136
|
+
| LoaderErrorEvent
|
|
137
|
+
| HandlerErrorEvent
|
|
138
|
+
| CacheDecisionEvent
|
|
139
|
+
| RevalidationDecisionEvent
|
|
140
|
+
| RequestTimeoutEvent
|
|
141
|
+
| OriginCheckRejectedEvent;
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Sink interface
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Telemetry sink receives structured lifecycle events from the router.
|
|
149
|
+
* Implement this interface to integrate with any observability backend.
|
|
150
|
+
*
|
|
151
|
+
* All methods are fire-and-forget — exceptions are caught and logged.
|
|
152
|
+
*/
|
|
153
|
+
export interface TelemetrySink {
|
|
154
|
+
emit(event: TelemetryEvent): void;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// No-op singleton (zero-cost disabled state)
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
const noopSink: TelemetrySink = {
|
|
162
|
+
emit() {},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Returns the configured sink, or the no-op singleton.
|
|
167
|
+
* Call sites use this so they don't need null checks.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveSink(sink: TelemetrySink | undefined): TelemetrySink {
|
|
170
|
+
return sink ?? noopSink;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Safe emit — catches any error thrown by the sink to prevent
|
|
175
|
+
* telemetry failures from affecting request handling.
|
|
176
|
+
*/
|
|
177
|
+
export function safeEmit(sink: TelemetrySink, event: TelemetryEvent): void {
|
|
178
|
+
try {
|
|
179
|
+
sink.emit(event);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// Telemetry must never break request handling
|
|
182
|
+
if (process.env.NODE_ENV !== "production") {
|
|
183
|
+
console.error("[Router.telemetry] Sink error:", e);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Request ID extraction (for span correlation)
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
// Per-request memoization so the same Request object always maps to the
|
|
193
|
+
// same ID. WeakMap allows GC when the Request is no longer referenced.
|
|
194
|
+
const requestIds = new WeakMap<Request, string>();
|
|
195
|
+
let telemetryRequestCounter = 0;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get or create a request ID for telemetry correlation.
|
|
199
|
+
* Checks standard headers first (x-rsc-router-request-id, x-request-id,
|
|
200
|
+
* cf-ray), then generates an internal ID when none is present.
|
|
201
|
+
* Generated IDs use format "t-{base36}" to distinguish from header values.
|
|
202
|
+
*/
|
|
203
|
+
export function getRequestId(request: Request): string {
|
|
204
|
+
const existing = requestIds.get(request);
|
|
205
|
+
if (existing) return existing;
|
|
206
|
+
|
|
207
|
+
const candidate =
|
|
208
|
+
request.headers.get("x-rsc-router-request-id") ??
|
|
209
|
+
request.headers.get("x-request-id") ??
|
|
210
|
+
request.headers.get("cf-ray");
|
|
211
|
+
|
|
212
|
+
let id: string;
|
|
213
|
+
if (candidate) {
|
|
214
|
+
const trimmed = candidate.trim();
|
|
215
|
+
id =
|
|
216
|
+
trimmed.length > 0
|
|
217
|
+
? trimmed
|
|
218
|
+
: `t-${(++telemetryRequestCounter).toString(36)}`;
|
|
219
|
+
} else {
|
|
220
|
+
id = `t-${(++telemetryRequestCounter).toString(36)}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
requestIds.set(request, id);
|
|
224
|
+
return id;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Console sink (built-in, replaces ad-hoc console.log debug traces)
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Built-in console sink that logs events in a structured format.
|
|
233
|
+
* Designed as the default sink for development / debugging.
|
|
234
|
+
*/
|
|
235
|
+
export function createConsoleSink(): TelemetrySink {
|
|
236
|
+
return {
|
|
237
|
+
emit(event: TelemetryEvent): void {
|
|
238
|
+
switch (event.type) {
|
|
239
|
+
case "request.start":
|
|
240
|
+
console.log(
|
|
241
|
+
`[telemetry] ${event.type} ${event.method} ${event.pathname} (${event.transaction})`,
|
|
242
|
+
);
|
|
243
|
+
break;
|
|
244
|
+
case "request.end":
|
|
245
|
+
console.log(
|
|
246
|
+
`[telemetry] ${event.type} ${event.method} ${event.pathname} ${event.durationMs.toFixed(1)}ms segments=${event.segmentCount} cache=${event.cacheHit}`,
|
|
247
|
+
);
|
|
248
|
+
break;
|
|
249
|
+
case "request.error":
|
|
250
|
+
console.log(
|
|
251
|
+
`[telemetry] ${event.type} ${event.method} ${event.pathname} phase=${event.phase} ${event.durationMs.toFixed(1)}ms`,
|
|
252
|
+
event.error.message,
|
|
253
|
+
);
|
|
254
|
+
break;
|
|
255
|
+
case "loader.start":
|
|
256
|
+
console.log(
|
|
257
|
+
`[telemetry] ${event.type} ${event.loaderName} (${event.segmentId})`,
|
|
258
|
+
);
|
|
259
|
+
break;
|
|
260
|
+
case "loader.end":
|
|
261
|
+
console.log(
|
|
262
|
+
`[telemetry] ${event.type} ${event.loaderName} ${event.durationMs.toFixed(1)}ms ok=${event.ok}`,
|
|
263
|
+
);
|
|
264
|
+
break;
|
|
265
|
+
case "loader.error":
|
|
266
|
+
console.log(
|
|
267
|
+
`[telemetry] ${event.type} ${event.loaderName} boundary=${event.handledByBoundary}`,
|
|
268
|
+
event.error.message,
|
|
269
|
+
);
|
|
270
|
+
break;
|
|
271
|
+
case "handler.error":
|
|
272
|
+
console.log(
|
|
273
|
+
`[telemetry] ${event.type} segment=${event.segmentId ?? "unknown"} boundary=${event.handledByBoundary}${event.pathname ? ` ${event.pathname}` : ""}`,
|
|
274
|
+
event.error.message,
|
|
275
|
+
);
|
|
276
|
+
break;
|
|
277
|
+
case "cache.decision":
|
|
278
|
+
console.log(
|
|
279
|
+
`[telemetry] ${event.type} ${event.pathname} hit=${event.hit} swr=${event.shouldRevalidate}${event.source ? ` source=${event.source}` : ""}`,
|
|
280
|
+
);
|
|
281
|
+
break;
|
|
282
|
+
case "revalidation.decision":
|
|
283
|
+
console.log(
|
|
284
|
+
`[telemetry] ${event.type} ${event.segmentId} revalidate=${event.shouldRevalidate}`,
|
|
285
|
+
);
|
|
286
|
+
break;
|
|
287
|
+
case "request.timeout":
|
|
288
|
+
console.log(
|
|
289
|
+
`[telemetry] ${event.type} phase=${event.phase} ${event.pathname} ${event.durationMs.toFixed(1)}ms custom=${event.customHandler}`,
|
|
290
|
+
);
|
|
291
|
+
break;
|
|
292
|
+
case "request.origin-rejected":
|
|
293
|
+
console.log(
|
|
294
|
+
`[telemetry] ${event.type} ${event.method} ${event.pathname} phase=${event.phase} origin=${event.origin ?? "none"} host=${event.host ?? "none"}`,
|
|
295
|
+
);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|