@rangojs/router 0.0.0-experimental.126 → 0.0.0-experimental.127
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/dist/bin/rango.js +5 -1
- package/dist/vite/index.js +55 -40
- package/package.json +23 -19
- package/skills/observability/SKILL.md +12 -3
- package/skills/prerender/SKILL.md +30 -11
- package/skills/router-setup/SKILL.md +11 -3
- package/src/build/route-types/codegen.ts +12 -1
- package/src/cache/cache-scope.ts +20 -0
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/index.rsc.ts +19 -2
- package/src/index.ts +16 -1
- package/src/route-definition/dsl-helpers.ts +19 -0
- package/src/router/instrument.ts +230 -0
- package/src/router/loader-resolution.ts +15 -10
- package/src/router/match-middleware/cache-lookup.ts +9 -14
- package/src/router/match-middleware/cache-store.ts +12 -0
- package/src/router/middleware.ts +23 -2
- package/src/router/prerender-match.ts +5 -2
- package/src/router/router-context.ts +2 -1
- package/src/router/router-interfaces.ts +8 -0
- package/src/router/router-options.ts +58 -4
- package/src/router/segment-resolution/fresh.ts +15 -18
- package/src/router/segment-resolution/helpers.ts +6 -0
- package/src/router/segment-resolution/loader-cache.ts +5 -0
- package/src/router/segment-resolution/revalidation.ts +9 -18
- package/src/router/segment-wrappers.ts +3 -2
- package/src/router/telemetry-otel.ts +161 -179
- package/src/router/tracing.ts +198 -0
- package/src/router.ts +9 -0
- package/src/rsc/handler.ts +132 -134
- package/src/rsc/loader-fetch.ts +7 -1
- package/src/rsc/progressive-enhancement.ts +9 -2
- package/src/rsc/rsc-rendering.ts +38 -14
- package/src/rsc/server-action.ts +28 -7
- package/src/segment-system.tsx +4 -1
- package/src/server/request-context.ts +13 -5
- package/src/vite/discovery/prerender-collection.ts +26 -37
- package/src/vite/discovery/state.ts +6 -0
- package/src/vite/plugin-types.ts +25 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +10 -0
- package/src/vite/rango.ts +1 -0
- package/src/vite/router-discovery.ts +9 -3
- package/src/vite/utils/prerender-utils.ts +36 -0
|
@@ -284,11 +284,17 @@ export async function resolveWithErrorBoundary<TEnv, TResult>(
|
|
|
284
284
|
deps: SegmentResolutionDeps<TEnv>,
|
|
285
285
|
report?: ErrorReportContext,
|
|
286
286
|
pathname?: string,
|
|
287
|
+
throwOnError?: boolean,
|
|
287
288
|
): Promise<TResult> {
|
|
288
289
|
try {
|
|
289
290
|
return await resolveFn();
|
|
290
291
|
} catch (error) {
|
|
291
292
|
if (error instanceof Response) throw error;
|
|
293
|
+
// Pre-render surfaces render failures to the build instead of baking the
|
|
294
|
+
// error boundary as a frozen 200 (issue #587). A `throw new Skip()` in a
|
|
295
|
+
// render fn also propagates here so the build can skip that URL rather than
|
|
296
|
+
// bake its error page. The live request path leaves throwOnError unset.
|
|
297
|
+
if (throwOnError) throw error;
|
|
292
298
|
const segment = catchSegmentError(
|
|
293
299
|
error,
|
|
294
300
|
entry,
|
|
@@ -113,6 +113,11 @@ function getLoaderStore(
|
|
|
113
113
|
*
|
|
114
114
|
* When the LoaderEntry has no cache config, delegates directly to ctx.use(loader).
|
|
115
115
|
* When cached, checks store first and stores on miss via waitUntil.
|
|
116
|
+
*
|
|
117
|
+
* Loader metering is NOT done here — it lives at the ctx.use execution funnel
|
|
118
|
+
* (observePhase; see instrument.ts). A cache HIT returns without calling ctx.use,
|
|
119
|
+
* so it emits no loader phase (the loader did not execute; the hit is only a
|
|
120
|
+
* LoaderCache debug log).
|
|
116
121
|
*/
|
|
117
122
|
export function resolveLoaderData<TEnv>(
|
|
118
123
|
loaderEntry: LoaderEntry,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
} from "./helpers.js";
|
|
44
44
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
45
45
|
import { getRouterContext } from "../router-context.js";
|
|
46
|
-
import {
|
|
46
|
+
import { observeEvent } from "../instrument.js";
|
|
47
47
|
import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
|
|
48
48
|
import {
|
|
49
49
|
track,
|
|
@@ -87,23 +87,14 @@ function emitRevalidationDecision(
|
|
|
87
87
|
routeKey: string,
|
|
88
88
|
shouldRevalidate: boolean,
|
|
89
89
|
): void {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
type: "revalidation.decision",
|
|
99
|
-
timestamp: performance.now(),
|
|
100
|
-
requestId: routerCtx.requestId,
|
|
101
|
-
segmentId,
|
|
102
|
-
pathname,
|
|
103
|
-
routeKey,
|
|
104
|
-
shouldRevalidate,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
90
|
+
observeEvent({
|
|
91
|
+
type: "revalidation.decision",
|
|
92
|
+
timestamp: performance.now(),
|
|
93
|
+
segmentId,
|
|
94
|
+
pathname,
|
|
95
|
+
routeKey,
|
|
96
|
+
shouldRevalidate,
|
|
97
|
+
});
|
|
107
98
|
}
|
|
108
99
|
|
|
109
100
|
// ---------------------------------------------------------------------------
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ShouldRevalidateFn,
|
|
6
6
|
} from "../types";
|
|
7
7
|
import type { SegmentResolutionDeps } from "./types.js";
|
|
8
|
+
import type { ResolveSegmentOptions } from "./segment-resolution.js";
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
11
|
resolveAllSegments as _resolveAllSegments,
|
|
@@ -29,7 +30,7 @@ export interface SegmentWrappers<TEnv = any> {
|
|
|
29
30
|
params: Record<string, string>,
|
|
30
31
|
context: HandlerContext<any, TEnv>,
|
|
31
32
|
loaderPromises: Map<string, Promise<any>>,
|
|
32
|
-
options?:
|
|
33
|
+
options?: ResolveSegmentOptions,
|
|
33
34
|
) => Promise<ResolvedSegment[]>;
|
|
34
35
|
resolveLoadersOnly: (
|
|
35
36
|
entries: EntryData[],
|
|
@@ -123,7 +124,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
123
124
|
params: Record<string, string>,
|
|
124
125
|
context: HandlerContext<any, TEnv>,
|
|
125
126
|
loaderPromises: Map<string, Promise<any>>,
|
|
126
|
-
options?:
|
|
127
|
+
options?: ResolveSegmentOptions,
|
|
127
128
|
): ReturnType<typeof _resolveAllSegments> {
|
|
128
129
|
return _resolveAllSegments(
|
|
129
130
|
entries,
|
|
@@ -1,20 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenTelemetry
|
|
2
|
+
* OpenTelemetry adapters for the router.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Two adapters, two surfaces — matching the split in instrument.ts:
|
|
5
|
+
*
|
|
6
|
+
* - createOTelTracing(tracer): the OTel adapter for the `tracing` SLOT — the
|
|
7
|
+
* canonical phase-span layer. It bridges observePhase's callback boundary
|
|
8
|
+
* onto OTel's callback-bound `startActiveSpan`, so the router's phase spans
|
|
9
|
+
* (rango.request/middleware/loader/render/ssr) nest by async context and the
|
|
10
|
+
* loader's own OTel spans (db/fetch) land under rango.loader. This is the
|
|
11
|
+
* OTel equivalent of createCloudflareTracing — pass it to
|
|
12
|
+
* `createRouter({ tracing })`.
|
|
13
|
+
*
|
|
14
|
+
* - createOTelSink(tracer): the OTel adapter for the `telemetry` SLOT — a
|
|
15
|
+
* TelemetrySink for the EVENT-shaped facts (handler errors, cache decisions,
|
|
16
|
+
* revalidation decisions, timeouts, origin rejections). It emits one instant
|
|
17
|
+
* OTel span per fact. It deliberately does NOT emit request/loader phase
|
|
18
|
+
* spans: those are owned by the tracing slot (createOTelTracing) so the two
|
|
19
|
+
* layers don't produce duplicate rango.request / rango.loader spans.
|
|
20
|
+
*
|
|
21
|
+
* The core router stays OTel-agnostic — these adapters accept a standard OTel
|
|
22
|
+
* Tracer (structurally typed, no import needed) and bridge the gap.
|
|
7
23
|
*
|
|
8
24
|
* Usage:
|
|
9
25
|
* import { trace } from "@opentelemetry/api";
|
|
10
|
-
* import { createOTelSink } from "@rangojs/router";
|
|
26
|
+
* import { createRouter, createOTelTracing, createOTelSink } from "@rangojs/router";
|
|
11
27
|
*
|
|
28
|
+
* const tracer = trace.getTracer("my-app");
|
|
12
29
|
* const router = createRouter({
|
|
13
|
-
*
|
|
30
|
+
* tracing: createOTelTracing(tracer), // phase spans (callback-bound)
|
|
31
|
+
* telemetry: createOTelSink(tracer), // discrete-fact instant spans
|
|
14
32
|
* });
|
|
33
|
+
*
|
|
34
|
+
* Faithful nesting requires an OTel async context manager
|
|
35
|
+
* (AsyncLocalStorageContextManager) configured in your OTel setup — standard for
|
|
36
|
+
* any startActiveSpan-based instrumentation.
|
|
15
37
|
*/
|
|
16
38
|
|
|
17
39
|
import type { TelemetrySink, TelemetryEvent } from "./telemetry.js";
|
|
40
|
+
import { runThenSettle } from "./tracing.js";
|
|
41
|
+
import type {
|
|
42
|
+
RouterTracingConfig,
|
|
43
|
+
SpanRunner,
|
|
44
|
+
TracePhaseToggles,
|
|
45
|
+
} from "./tracing.js";
|
|
18
46
|
|
|
19
47
|
// ---------------------------------------------------------------------------
|
|
20
48
|
// Minimal OTel-compatible types (structurally typed, no import needed)
|
|
@@ -22,21 +50,21 @@ import type { TelemetrySink, TelemetryEvent } from "./telemetry.js";
|
|
|
22
50
|
|
|
23
51
|
/**
|
|
24
52
|
* Minimal Span interface compatible with @opentelemetry/api Span.
|
|
25
|
-
* Only the methods used by the
|
|
53
|
+
* Only the methods used by the adapters are declared.
|
|
26
54
|
*/
|
|
27
55
|
export interface OTelSpan {
|
|
28
56
|
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
57
|
setStatus(status: { code: number; message?: string }): OTelSpan | void;
|
|
34
58
|
recordException(exception: Error): void;
|
|
35
59
|
end(): void;
|
|
36
60
|
}
|
|
37
61
|
|
|
38
62
|
/**
|
|
39
|
-
* Minimal Tracer interface
|
|
63
|
+
* Minimal Tracer interface for the EVENT sink (createOTelSink): only `startSpan`
|
|
64
|
+
* is used (one instant span per discrete fact). Kept narrow so a custom,
|
|
65
|
+
* event-only tracer that does not implement `startActiveSpan` still satisfies it.
|
|
66
|
+
* The real `@opentelemetry/api` Tracer has both methods, so it satisfies this and
|
|
67
|
+
* `OTelActiveSpanTracer` below.
|
|
40
68
|
*/
|
|
41
69
|
export interface OTelTracer {
|
|
42
70
|
startSpan(
|
|
@@ -47,182 +75,110 @@ export interface OTelTracer {
|
|
|
47
75
|
): OTelSpan;
|
|
48
76
|
}
|
|
49
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Minimal Tracer interface for the PHASE-SPAN adapter (createOTelTracing): only
|
|
80
|
+
* `startActiveSpan` is used (callback-bound, so the span is active for the work
|
|
81
|
+
* and child spans nest). Declared separately from `OTelTracer` so each factory
|
|
82
|
+
* requires exactly the method it calls.
|
|
83
|
+
*/
|
|
84
|
+
export interface OTelActiveSpanTracer {
|
|
85
|
+
startActiveSpan<T>(name: string, fn: (span: OTelSpan) => T): T;
|
|
86
|
+
}
|
|
87
|
+
|
|
50
88
|
// OTel SpanStatusCode constants (mirrors @opentelemetry/api values)
|
|
51
|
-
const STATUS_OK = 1;
|
|
52
89
|
const STATUS_ERROR = 2;
|
|
53
90
|
|
|
54
91
|
// ---------------------------------------------------------------------------
|
|
55
|
-
//
|
|
92
|
+
// Tracing adapter: phase spans via startActiveSpan (the `tracing` slot)
|
|
56
93
|
// ---------------------------------------------------------------------------
|
|
57
94
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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}`;
|
|
95
|
+
/** Options for createOTelTracing. */
|
|
96
|
+
export interface OTelTracingOptions {
|
|
97
|
+
/** Master switch. Defaults to true. */
|
|
98
|
+
enabled?: boolean;
|
|
99
|
+
/** Per-phase span toggles. Omitted phases default to enabled. */
|
|
100
|
+
spans?: TracePhaseToggles;
|
|
78
101
|
}
|
|
79
102
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Create the tracing config that maps the router's phases onto OTel spans via
|
|
105
|
+
* `tracer.startActiveSpan`, which runs the work inside the span's active context
|
|
106
|
+
* (so child spans nest) and returns the work's value unchanged. The span ends
|
|
107
|
+
* when that value — or, for async work, the returned promise — settles, matching
|
|
108
|
+
* observePhase's contract. When the work throws or rejects, the exception is
|
|
109
|
+
* recorded and the span status is set to ERROR before it ends, so a failed phase
|
|
110
|
+
* stays visible in the trace (the old createOTelSink error path is preserved here
|
|
111
|
+
* now that phase spans live in this adapter). Pass the result to
|
|
112
|
+
* `createRouter({ tracing })`.
|
|
113
|
+
*
|
|
114
|
+
* @see createCloudflareTracing (`@rangojs/router/cloudflare`) for the same slot
|
|
115
|
+
* using Cloudflare Workers native custom spans.
|
|
116
|
+
*/
|
|
117
|
+
export function createOTelTracing(
|
|
118
|
+
tracer: OTelActiveSpanTracer,
|
|
119
|
+
options: OTelTracingOptions = {},
|
|
120
|
+
): RouterTracingConfig {
|
|
121
|
+
const runner: SpanRunner = <T>(name: string, fn: (span: OTelSpan) => T): T =>
|
|
122
|
+
tracer.startActiveSpan(
|
|
123
|
+
name,
|
|
124
|
+
(span): T =>
|
|
125
|
+
runThenSettle(
|
|
126
|
+
() => fn(span),
|
|
127
|
+
(error) => {
|
|
128
|
+
// On failure record the exception + ERROR status before ending, so a
|
|
129
|
+
// failed phase stays visible in the trace.
|
|
130
|
+
if (error !== undefined) {
|
|
131
|
+
if (error instanceof Error) {
|
|
132
|
+
span.recordException(error);
|
|
133
|
+
span.setStatus({ code: STATUS_ERROR, message: error.message });
|
|
134
|
+
} else {
|
|
135
|
+
span.setStatus({ code: STATUS_ERROR });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
span.end();
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
);
|
|
92
142
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!stack || stack.length === 0) return undefined;
|
|
99
|
-
const span = stack.pop()!;
|
|
100
|
-
if (stack.length === 0) map.delete(key);
|
|
101
|
-
return span;
|
|
143
|
+
return {
|
|
144
|
+
runner,
|
|
145
|
+
enabled: options.enabled ?? true,
|
|
146
|
+
spans: options.spans,
|
|
147
|
+
};
|
|
102
148
|
}
|
|
103
149
|
|
|
104
150
|
// ---------------------------------------------------------------------------
|
|
105
|
-
//
|
|
151
|
+
// Telemetry sink: discrete-fact instant spans (the `telemetry` slot)
|
|
106
152
|
// ---------------------------------------------------------------------------
|
|
107
153
|
|
|
108
154
|
/**
|
|
109
|
-
* Create a TelemetrySink that maps router
|
|
155
|
+
* Create a TelemetrySink that maps the router's discrete-fact events to instant
|
|
156
|
+
* OTel spans. One span per fact; no duration spans.
|
|
157
|
+
*
|
|
158
|
+
* Fact mapping:
|
|
159
|
+
* - handler.error -> "rango.handler.error" (error)
|
|
160
|
+
* - cache.decision -> "rango.cache.decision"
|
|
161
|
+
* - revalidation.decision -> "rango.revalidation.decision"
|
|
162
|
+
* - request.timeout -> "rango.request.timeout" (error)
|
|
163
|
+
* - request.origin-rejected -> "rango.request.origin-rejected" (error)
|
|
110
164
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* - cache.decision → "rango.cache.decision" instant span
|
|
116
|
-
* - revalidation.decision → "rango.revalidation.decision" instant span
|
|
165
|
+
* Request and loader PHASE spans are intentionally NOT emitted here — they are
|
|
166
|
+
* owned by the tracing slot (createOTelTracing) so the two layers cannot produce
|
|
167
|
+
* duplicate rango.request / rango.loader spans. request.start/end and
|
|
168
|
+
* loader.start/end events are no-ops for this sink.
|
|
117
169
|
*
|
|
118
170
|
* Attributes use the `rango.*` namespace for router-specific data and
|
|
119
171
|
* `http.method` / `http.route` for HTTP semantics.
|
|
120
172
|
*/
|
|
121
173
|
export function createOTelSink(tracer: OTelTracer): TelemetrySink {
|
|
122
|
-
const
|
|
123
|
-
|
|
174
|
+
const instant = (
|
|
175
|
+
name: string,
|
|
176
|
+
attributes: Record<string, string | number | boolean>,
|
|
177
|
+
): OTelSpan => tracer.startSpan(name, { attributes });
|
|
124
178
|
|
|
125
179
|
return {
|
|
126
180
|
emit(event: TelemetryEvent): void {
|
|
127
181
|
switch (event.type) {
|
|
128
|
-
case "request.start": {
|
|
129
|
-
const span = tracer.startSpan("rango.request", {
|
|
130
|
-
attributes: {
|
|
131
|
-
"http.method": event.method,
|
|
132
|
-
"http.route": event.pathname,
|
|
133
|
-
"rango.transaction": event.transaction,
|
|
134
|
-
"rango.is_partial": event.isPartial,
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
pushSpan(requestSpans, requestKey(event), span);
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
case "request.end": {
|
|
142
|
-
const span = popSpan(requestSpans, requestKey(event));
|
|
143
|
-
if (span) {
|
|
144
|
-
span.setAttribute("rango.duration_ms", event.durationMs);
|
|
145
|
-
span.setAttribute("rango.segment_count", event.segmentCount);
|
|
146
|
-
span.setAttribute("rango.cache.hit", event.cacheHit);
|
|
147
|
-
span.setStatus({ code: STATUS_OK });
|
|
148
|
-
span.end();
|
|
149
|
-
}
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
case "request.error": {
|
|
154
|
-
const span = popSpan(requestSpans, requestKey(event));
|
|
155
|
-
if (span) {
|
|
156
|
-
span.setAttribute("rango.duration_ms", event.durationMs);
|
|
157
|
-
span.setAttribute("rango.phase", event.phase);
|
|
158
|
-
span.recordException(event.error);
|
|
159
|
-
span.setStatus({
|
|
160
|
-
code: STATUS_ERROR,
|
|
161
|
-
message: event.error.message,
|
|
162
|
-
});
|
|
163
|
-
span.end();
|
|
164
|
-
}
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
case "loader.start": {
|
|
169
|
-
const span = tracer.startSpan("rango.loader", {
|
|
170
|
-
attributes: {
|
|
171
|
-
"rango.segment_id": event.segmentId,
|
|
172
|
-
"rango.loader_name": event.loaderName,
|
|
173
|
-
"http.route": event.pathname,
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
pushSpan(loaderSpans, loaderKey(event), span);
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
case "loader.end": {
|
|
181
|
-
const key = loaderKey(event);
|
|
182
|
-
const span = popSpan(loaderSpans, key);
|
|
183
|
-
if (span) {
|
|
184
|
-
span.setAttribute("rango.duration_ms", event.durationMs);
|
|
185
|
-
span.setAttribute("rango.loader.ok", event.ok);
|
|
186
|
-
span.setStatus({ code: event.ok ? STATUS_OK : STATUS_ERROR });
|
|
187
|
-
span.end();
|
|
188
|
-
}
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
case "loader.error": {
|
|
193
|
-
const key = loaderKey(event);
|
|
194
|
-
const span = popSpan(loaderSpans, key);
|
|
195
|
-
if (span) {
|
|
196
|
-
span.setAttribute(
|
|
197
|
-
"rango.handled_by_boundary",
|
|
198
|
-
event.handledByBoundary,
|
|
199
|
-
);
|
|
200
|
-
span.recordException(event.error);
|
|
201
|
-
span.setStatus({
|
|
202
|
-
code: STATUS_ERROR,
|
|
203
|
-
message: event.error.message,
|
|
204
|
-
});
|
|
205
|
-
span.end();
|
|
206
|
-
} else {
|
|
207
|
-
// No matching start — create a standalone error span
|
|
208
|
-
const errorSpan = tracer.startSpan("rango.loader", {
|
|
209
|
-
attributes: {
|
|
210
|
-
"rango.segment_id": event.segmentId,
|
|
211
|
-
"rango.loader_name": event.loaderName,
|
|
212
|
-
"http.route": event.pathname,
|
|
213
|
-
"rango.handled_by_boundary": event.handledByBoundary,
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
errorSpan.recordException(event.error);
|
|
217
|
-
errorSpan.setStatus({
|
|
218
|
-
code: STATUS_ERROR,
|
|
219
|
-
message: event.error.message,
|
|
220
|
-
});
|
|
221
|
-
errorSpan.end();
|
|
222
|
-
}
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
182
|
case "handler.error": {
|
|
227
183
|
const attrs: Record<string, string | number | boolean> = {
|
|
228
184
|
"rango.handled_by_boundary": event.handledByBoundary,
|
|
@@ -232,13 +188,10 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
|
|
|
232
188
|
attrs["rango.segment_type"] = event.segmentType;
|
|
233
189
|
if (event.pathname) attrs["http.route"] = event.pathname;
|
|
234
190
|
if (event.routeKey) attrs["rango.route_key"] = event.routeKey;
|
|
235
|
-
if (event.params)
|
|
191
|
+
if (event.params)
|
|
236
192
|
attrs["rango.params"] = JSON.stringify(event.params);
|
|
237
|
-
}
|
|
238
193
|
|
|
239
|
-
const span =
|
|
240
|
-
attributes: attrs,
|
|
241
|
-
});
|
|
194
|
+
const span = instant("rango.handler.error", attrs);
|
|
242
195
|
span.recordException(event.error);
|
|
243
196
|
span.setStatus({ code: STATUS_ERROR, message: event.error.message });
|
|
244
197
|
span.end();
|
|
@@ -253,26 +206,55 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
|
|
|
253
206
|
"rango.cache.should_revalidate": event.shouldRevalidate,
|
|
254
207
|
};
|
|
255
208
|
if (event.source) attrs["rango.cache.source"] = event.source;
|
|
209
|
+
instant("rango.cache.decision", attrs).end();
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
256
212
|
|
|
257
|
-
|
|
258
|
-
|
|
213
|
+
case "revalidation.decision": {
|
|
214
|
+
instant("rango.revalidation.decision", {
|
|
215
|
+
"rango.segment_id": event.segmentId,
|
|
216
|
+
"http.route": event.pathname,
|
|
217
|
+
"rango.route_key": event.routeKey,
|
|
218
|
+
"rango.revalidate": event.shouldRevalidate,
|
|
219
|
+
}).end();
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case "request.timeout": {
|
|
224
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
225
|
+
"rango.phase": event.phase,
|
|
226
|
+
"http.route": event.pathname,
|
|
227
|
+
"rango.duration_ms": event.durationMs,
|
|
228
|
+
"rango.timeout.custom_handler": event.customHandler,
|
|
229
|
+
};
|
|
230
|
+
if (event.routeKey) attrs["rango.route_key"] = event.routeKey;
|
|
231
|
+
if (event.actionId) attrs["rango.action_id"] = event.actionId;
|
|
232
|
+
const span = instant("rango.request.timeout", attrs);
|
|
233
|
+
span.setStatus({
|
|
234
|
+
code: STATUS_ERROR,
|
|
235
|
+
message: `timeout: ${event.phase}`,
|
|
259
236
|
});
|
|
260
237
|
span.end();
|
|
261
238
|
break;
|
|
262
239
|
}
|
|
263
240
|
|
|
264
|
-
case "
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
241
|
+
case "request.origin-rejected": {
|
|
242
|
+
const attrs: Record<string, string | number | boolean> = {
|
|
243
|
+
"http.method": event.method,
|
|
244
|
+
"http.route": event.pathname,
|
|
245
|
+
"rango.phase": event.phase,
|
|
246
|
+
};
|
|
247
|
+
if (event.origin) attrs["rango.origin"] = event.origin;
|
|
248
|
+
if (event.host) attrs["http.host"] = event.host;
|
|
249
|
+
const span = instant("rango.request.origin-rejected", attrs);
|
|
250
|
+
span.setStatus({ code: STATUS_ERROR, message: "origin rejected" });
|
|
273
251
|
span.end();
|
|
274
252
|
break;
|
|
275
253
|
}
|
|
254
|
+
|
|
255
|
+
// request.start/end/error and loader.start/end/error are phase events;
|
|
256
|
+
// their spans are owned by the tracing slot (createOTelTracing), so this
|
|
257
|
+
// sink ignores them to avoid duplicate rango.request / rango.loader spans.
|
|
276
258
|
}
|
|
277
259
|
},
|
|
278
260
|
};
|