@rangojs/router 0.0.0-experimental.142 → 0.0.0-experimental.144
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/vite/index.js +25 -6
- package/package.json +4 -2
- package/skills/cache-guide/SKILL.md +3 -1
- package/skills/caching/SKILL.md +41 -2
- package/skills/catalog.json +6 -0
- package/skills/composability/SKILL.md +32 -0
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/loader/SKILL.md +5 -0
- package/skills/migrate-nextjs/SKILL.md +4 -2
- package/skills/observability/SKILL.md +8 -0
- package/skills/parallel/SKILL.md +4 -0
- package/skills/ppr/SKILL.md +110 -20
- package/skills/rango/SKILL.md +10 -0
- package/skills/route/SKILL.md +8 -0
- package/skills/typesafety/SKILL.md +1 -0
- package/skills/typesafety/generated-files-and-cli.md +30 -0
- package/skills/use-cache/SKILL.md +12 -2
- package/src/browser/partial-update.ts +7 -0
- package/src/cache/cache-key-utils.ts +29 -0
- package/src/cache/cache-scope.ts +2 -17
- package/src/cache/cache-tag.ts +60 -14
- package/src/cache/cf/cf-cache-store.ts +54 -20
- package/src/cache/document-cache.ts +17 -11
- package/src/cache/vercel/vercel-cache-store.ts +9 -19
- package/src/cloudflare/tracing.ts +7 -8
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -8
- package/src/redirect-origin.ts +14 -0
- package/src/route-definition/helpers-types.ts +5 -4
- package/src/route-map-builder.ts +41 -4
- package/src/router/find-match.ts +15 -1
- package/src/router/instrument.ts +9 -4
- package/src/router/lazy-includes.ts +8 -2
- package/src/router/loader-resolution.ts +14 -2
- package/src/router/match-handlers.ts +175 -133
- package/src/router/middleware.ts +40 -30
- package/src/router/router-interfaces.ts +9 -0
- package/src/router/segment-resolution/loader-snapshot.ts +98 -17
- package/src/router/telemetry-otel.ts +6 -8
- package/src/router/telemetry.ts +9 -1
- package/src/router/tracing.ts +14 -5
- package/src/router.ts +22 -14
- package/src/rsc/handler.ts +55 -32
- package/src/rsc/redirect-guard.ts +2 -1
- package/src/rsc/rsc-rendering.ts +35 -2
- package/src/rsc/shell-capture.ts +98 -20
- package/src/server/context.ts +47 -9
- package/src/server/cookie-store.ts +26 -5
- package/src/server/request-context.ts +22 -0
- package/src/ssr/index.tsx +145 -107
- package/src/testing/dispatch.ts +149 -37
- package/src/urls/path-helper-types.ts +9 -4
- package/src/vercel/tracing.ts +7 -7
- package/src/vite/inject-client-debug.ts +64 -12
- package/src/vite/router-discovery.ts +9 -1
|
@@ -41,6 +41,109 @@ import {
|
|
|
41
41
|
} from "./telemetry.js";
|
|
42
42
|
import { _getRequestContext } from "../server/request-context.js";
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Per-call telemetry lifecycle emitter for match()/matchPartial(). Each method
|
|
46
|
+
* reproduces the exact event object the two functions used to emit inline and is
|
|
47
|
+
* gated on the same per-call flag (`enabled` = the request's `emitTelemetry`), so
|
|
48
|
+
* a PPR shell-capture run (enabled=false) emits nothing while a foreground run
|
|
49
|
+
* emits byte-identical events. Extracted so the two transactions can't drift;
|
|
50
|
+
* pinned by thrown-response-telemetry.test.ts and
|
|
51
|
+
* shell-capture-telemetry-suppression.test.ts.
|
|
52
|
+
*/
|
|
53
|
+
interface LifecycleEmitter {
|
|
54
|
+
start(): void;
|
|
55
|
+
end(segmentCount: number, cacheHit: boolean, status?: number): void;
|
|
56
|
+
cacheDecision(
|
|
57
|
+
routeKey: string,
|
|
58
|
+
state: {
|
|
59
|
+
cacheHit: boolean;
|
|
60
|
+
cacheSource?: "runtime" | "prerender";
|
|
61
|
+
shouldRevalidate?: boolean;
|
|
62
|
+
},
|
|
63
|
+
segments: CacheSegmentSignal[],
|
|
64
|
+
): void;
|
|
65
|
+
error(error: Error, phase: string): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createLifecycleEmitter(args: {
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
sink: TelemetrySink;
|
|
71
|
+
requestId: string | undefined;
|
|
72
|
+
method: string;
|
|
73
|
+
pathname: string;
|
|
74
|
+
transaction: "match" | "matchPartial";
|
|
75
|
+
isPartial: boolean;
|
|
76
|
+
matchStart: number;
|
|
77
|
+
}): LifecycleEmitter {
|
|
78
|
+
return {
|
|
79
|
+
start(): void {
|
|
80
|
+
if (!args.enabled) return;
|
|
81
|
+
safeEmit(args.sink, {
|
|
82
|
+
type: "request.start",
|
|
83
|
+
timestamp: args.matchStart,
|
|
84
|
+
requestId: args.requestId,
|
|
85
|
+
method: args.method,
|
|
86
|
+
pathname: args.pathname,
|
|
87
|
+
transaction: args.transaction,
|
|
88
|
+
isPartial: args.isPartial,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
end(segmentCount: number, cacheHit: boolean, status?: number): void {
|
|
92
|
+
if (!args.enabled) return;
|
|
93
|
+
safeEmit(args.sink, {
|
|
94
|
+
type: "request.end",
|
|
95
|
+
timestamp: performance.now(),
|
|
96
|
+
requestId: args.requestId,
|
|
97
|
+
method: args.method,
|
|
98
|
+
pathname: args.pathname,
|
|
99
|
+
transaction: args.transaction,
|
|
100
|
+
durationMs: performance.now() - args.matchStart,
|
|
101
|
+
segmentCount,
|
|
102
|
+
cacheHit,
|
|
103
|
+
// Only a thrown-Response short-circuit passes a status; a normal render
|
|
104
|
+
// completion omits it (the Response is built after match()).
|
|
105
|
+
...(status !== undefined && { status }),
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
cacheDecision(
|
|
109
|
+
routeKey: string,
|
|
110
|
+
state: {
|
|
111
|
+
cacheHit: boolean;
|
|
112
|
+
cacheSource?: "runtime" | "prerender";
|
|
113
|
+
shouldRevalidate?: boolean;
|
|
114
|
+
},
|
|
115
|
+
segments: CacheSegmentSignal[],
|
|
116
|
+
): void {
|
|
117
|
+
if (!args.enabled) return;
|
|
118
|
+
safeEmit(args.sink, {
|
|
119
|
+
type: "cache.decision",
|
|
120
|
+
timestamp: performance.now(),
|
|
121
|
+
requestId: args.requestId,
|
|
122
|
+
pathname: args.pathname,
|
|
123
|
+
routeKey,
|
|
124
|
+
hit: state.cacheHit,
|
|
125
|
+
shouldRevalidate: !!state.shouldRevalidate,
|
|
126
|
+
source: state.cacheSource,
|
|
127
|
+
segments,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
error(error: Error, phase: string): void {
|
|
131
|
+
if (!args.enabled) return;
|
|
132
|
+
safeEmit(args.sink, {
|
|
133
|
+
type: "request.error",
|
|
134
|
+
timestamp: performance.now(),
|
|
135
|
+
requestId: args.requestId,
|
|
136
|
+
method: args.method,
|
|
137
|
+
pathname: args.pathname,
|
|
138
|
+
transaction: args.transaction,
|
|
139
|
+
error,
|
|
140
|
+
phase,
|
|
141
|
+
durationMs: performance.now() - args.matchStart,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
44
147
|
export interface MatchHandlerDeps<TEnv = any> {
|
|
45
148
|
buildRouterContext: () => RouterContext<TEnv>;
|
|
46
149
|
callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
|
|
@@ -155,9 +258,20 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
155
258
|
}
|
|
156
259
|
|
|
157
260
|
async function match(request: Request, env: TEnv): Promise<MatchResult> {
|
|
158
|
-
|
|
261
|
+
// Silence telemetry for the PPR background shell capture: it re-runs match()
|
|
262
|
+
// under a derived request context flagged _shellCaptureRun (shell-capture.ts
|
|
263
|
+
// attemptCapture), re-using the foreground Request — a second request.start/
|
|
264
|
+
// cache.decision/request.end stamped with the same WeakMap-keyed requestId
|
|
265
|
+
// would double-count dashboards. Derived here (inside the capture's active
|
|
266
|
+
// request-context ALS) so the read sees the derived context, not module state.
|
|
267
|
+
const emitTelemetry =
|
|
268
|
+
hasTelemetry && !_getRequestContext()?._shellCaptureRun;
|
|
269
|
+
const requestId = emitTelemetry ? getRequestId(request) : undefined;
|
|
159
270
|
return runWithRouterLogContext({ request, transaction: "match" }, () => {
|
|
160
271
|
const routerCtx = buildRouterContext();
|
|
272
|
+
// Also mute in-pipeline observeEvent emitters (revalidation.decision,
|
|
273
|
+
// cache-lookup's cache.decision) which read routerCtx.telemetry.
|
|
274
|
+
if (!emitTelemetry) routerCtx.telemetry = undefined;
|
|
161
275
|
routerCtx.requestId = requestId;
|
|
162
276
|
return runWithRouterContext(routerCtx, async () =>
|
|
163
277
|
withRouterLogScope("match", async () => {
|
|
@@ -165,34 +279,22 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
165
279
|
const pathname =
|
|
166
280
|
_getRequestContext()?.url?.pathname ??
|
|
167
281
|
new URL(request.url).pathname;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
282
|
+
const emitter = createLifecycleEmitter({
|
|
283
|
+
enabled: emitTelemetry,
|
|
284
|
+
sink: telemetry,
|
|
285
|
+
requestId,
|
|
286
|
+
method: request.method,
|
|
287
|
+
pathname,
|
|
288
|
+
transaction: "match",
|
|
289
|
+
isPartial: false,
|
|
290
|
+
matchStart,
|
|
291
|
+
});
|
|
292
|
+
emitter.start();
|
|
179
293
|
|
|
180
294
|
const result = await createMatchContextForFull(request, env);
|
|
181
295
|
|
|
182
296
|
if ("type" in result && result.type === "redirect") {
|
|
183
|
-
|
|
184
|
-
safeEmit(telemetry, {
|
|
185
|
-
type: "request.end",
|
|
186
|
-
timestamp: performance.now(),
|
|
187
|
-
requestId,
|
|
188
|
-
method: request.method,
|
|
189
|
-
pathname,
|
|
190
|
-
transaction: "match",
|
|
191
|
-
durationMs: performance.now() - matchStart,
|
|
192
|
-
segmentCount: 0,
|
|
193
|
-
cacheHit: false,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
297
|
+
emitter.end(0, false);
|
|
196
298
|
return {
|
|
197
299
|
segments: [],
|
|
198
300
|
matched: [],
|
|
@@ -212,51 +314,26 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
212
314
|
if (hasTelemetry || cacheSignalEnabled) {
|
|
213
315
|
const signalSegments = buildSignal(ctx.routeKey, state);
|
|
214
316
|
recordSignalIfEnabled(signalSegments);
|
|
215
|
-
|
|
216
|
-
safeEmit(telemetry, {
|
|
217
|
-
type: "cache.decision",
|
|
218
|
-
timestamp: performance.now(),
|
|
219
|
-
requestId,
|
|
220
|
-
pathname,
|
|
221
|
-
routeKey: ctx.routeKey,
|
|
222
|
-
hit: state.cacheHit,
|
|
223
|
-
shouldRevalidate: !!state.shouldRevalidate,
|
|
224
|
-
source: state.cacheSource,
|
|
225
|
-
segments: signalSegments,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (hasTelemetry) {
|
|
230
|
-
safeEmit(telemetry, {
|
|
231
|
-
type: "request.end",
|
|
232
|
-
timestamp: performance.now(),
|
|
233
|
-
requestId,
|
|
234
|
-
method: request.method,
|
|
235
|
-
pathname,
|
|
236
|
-
transaction: "match",
|
|
237
|
-
durationMs: performance.now() - matchStart,
|
|
238
|
-
segmentCount: matchResult.segments.length,
|
|
239
|
-
cacheHit: state.cacheHit,
|
|
240
|
-
});
|
|
317
|
+
emitter.cacheDecision(ctx.routeKey, state, signalSegments);
|
|
241
318
|
}
|
|
319
|
+
emitter.end(matchResult.segments.length, state.cacheHit);
|
|
242
320
|
return matchResult;
|
|
243
321
|
} catch (error) {
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
error: errorObj,
|
|
255
|
-
phase: error instanceof Response ? "redirect" : "routing",
|
|
256
|
-
durationMs: performance.now() - matchStart,
|
|
257
|
-
});
|
|
322
|
+
if (error instanceof Response) {
|
|
323
|
+
// A thrown Response (middleware short-circuit — redirect / auth
|
|
324
|
+
// gate) is a COMPLETED request from the consumer's seat, not an
|
|
325
|
+
// error: emit request.end (the same shape the non-thrown redirect
|
|
326
|
+
// result above already emits), never request.error with a
|
|
327
|
+
// synthetic "[object Response]" error. Rethrow so the caller
|
|
328
|
+
// drives the redirect. Carry the Response's status so a sink can
|
|
329
|
+
// tell a 3xx short-circuit from a 2xx completion.
|
|
330
|
+
emitter.end(0, false, error.status);
|
|
331
|
+
throw error;
|
|
258
332
|
}
|
|
259
|
-
|
|
333
|
+
emitter.error(
|
|
334
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
335
|
+
"routing",
|
|
336
|
+
);
|
|
260
337
|
callOnError(error, "routing", {
|
|
261
338
|
request,
|
|
262
339
|
url: ctx.url,
|
|
@@ -296,11 +373,16 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
296
373
|
context: TEnv,
|
|
297
374
|
actionContext?: ActionContext,
|
|
298
375
|
): Promise<MatchResult | null> {
|
|
299
|
-
|
|
376
|
+
// See match() above: the PPR shell capture re-runs matchPartial() under a
|
|
377
|
+
// _shellCaptureRun context and must stay invisible to the sink.
|
|
378
|
+
const emitTelemetry =
|
|
379
|
+
hasTelemetry && !_getRequestContext()?._shellCaptureRun;
|
|
380
|
+
const partialRequestId = emitTelemetry ? getRequestId(request) : undefined;
|
|
300
381
|
return runWithRouterLogContext(
|
|
301
382
|
{ request, transaction: "matchPartial" },
|
|
302
383
|
() => {
|
|
303
384
|
const routerCtx = buildRouterContext();
|
|
385
|
+
if (!emitTelemetry) routerCtx.telemetry = undefined;
|
|
304
386
|
routerCtx.requestId = partialRequestId;
|
|
305
387
|
return runWithRouterContext(routerCtx, async () =>
|
|
306
388
|
withRouterLogScope("matchPartial", async () => {
|
|
@@ -308,17 +390,17 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
308
390
|
const pathname =
|
|
309
391
|
_getRequestContext()?.url?.pathname ??
|
|
310
392
|
new URL(request.url).pathname;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
393
|
+
const emitter = createLifecycleEmitter({
|
|
394
|
+
enabled: emitTelemetry,
|
|
395
|
+
sink: telemetry,
|
|
396
|
+
requestId: partialRequestId,
|
|
397
|
+
method: request.method,
|
|
398
|
+
pathname,
|
|
399
|
+
transaction: "matchPartial",
|
|
400
|
+
isPartial: true,
|
|
401
|
+
matchStart,
|
|
402
|
+
});
|
|
403
|
+
emitter.start();
|
|
322
404
|
|
|
323
405
|
const ctx = await createMatchContextForPartial(
|
|
324
406
|
request,
|
|
@@ -326,19 +408,7 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
326
408
|
actionContext,
|
|
327
409
|
);
|
|
328
410
|
if (!ctx) {
|
|
329
|
-
|
|
330
|
-
safeEmit(telemetry, {
|
|
331
|
-
type: "request.end",
|
|
332
|
-
timestamp: performance.now(),
|
|
333
|
-
requestId: partialRequestId,
|
|
334
|
-
method: request.method,
|
|
335
|
-
pathname,
|
|
336
|
-
transaction: "matchPartial",
|
|
337
|
-
durationMs: performance.now() - matchStart,
|
|
338
|
-
segmentCount: 0,
|
|
339
|
-
cacheHit: false,
|
|
340
|
-
});
|
|
341
|
-
}
|
|
411
|
+
emitter.end(0, false);
|
|
342
412
|
return null;
|
|
343
413
|
}
|
|
344
414
|
|
|
@@ -365,53 +435,25 @@ export function createMatchHandlers<TEnv = any>(
|
|
|
365
435
|
if (hasTelemetry || cacheSignalEnabled) {
|
|
366
436
|
const signalSegments = buildSignal(ctx.routeKey, state);
|
|
367
437
|
recordSignalIfEnabled(signalSegments);
|
|
368
|
-
|
|
369
|
-
safeEmit(telemetry, {
|
|
370
|
-
type: "cache.decision",
|
|
371
|
-
timestamp: performance.now(),
|
|
372
|
-
requestId: partialRequestId,
|
|
373
|
-
pathname,
|
|
374
|
-
routeKey: ctx.routeKey,
|
|
375
|
-
hit: state.cacheHit,
|
|
376
|
-
shouldRevalidate: !!state.shouldRevalidate,
|
|
377
|
-
source: state.cacheSource,
|
|
378
|
-
segments: signalSegments,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (hasTelemetry) {
|
|
383
|
-
safeEmit(telemetry, {
|
|
384
|
-
type: "request.end",
|
|
385
|
-
timestamp: performance.now(),
|
|
386
|
-
requestId: partialRequestId,
|
|
387
|
-
method: request.method,
|
|
388
|
-
pathname,
|
|
389
|
-
transaction: "matchPartial",
|
|
390
|
-
durationMs: performance.now() - matchStart,
|
|
391
|
-
segmentCount: matchResult.segments.length,
|
|
392
|
-
cacheHit: state.cacheHit,
|
|
393
|
-
});
|
|
438
|
+
emitter.cacheDecision(ctx.routeKey, state, signalSegments);
|
|
394
439
|
}
|
|
440
|
+
emitter.end(matchResult.segments.length, state.cacheHit);
|
|
395
441
|
return matchResult;
|
|
396
442
|
} catch (error) {
|
|
397
443
|
flushRevalidationTrace();
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
method: request.method,
|
|
407
|
-
pathname,
|
|
408
|
-
transaction: "matchPartial",
|
|
409
|
-
error: errorObj,
|
|
410
|
-
phase: error instanceof Response ? "redirect" : phase,
|
|
411
|
-
durationMs: performance.now() - matchStart,
|
|
412
|
-
});
|
|
444
|
+
if (error instanceof Response) {
|
|
445
|
+
// A thrown Response (middleware short-circuit — redirect / auth
|
|
446
|
+
// gate) is a COMPLETED request, not an error: emit request.end
|
|
447
|
+
// (parity with match()), never request.error. Rethrow so the
|
|
448
|
+
// caller drives the redirect. Carry the Response's status so a
|
|
449
|
+
// sink can tell a 3xx short-circuit from a 2xx completion.
|
|
450
|
+
emitter.end(0, false, error.status);
|
|
451
|
+
throw error;
|
|
413
452
|
}
|
|
414
|
-
|
|
453
|
+
emitter.error(
|
|
454
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
455
|
+
actionContext ? "action" : "revalidation",
|
|
456
|
+
);
|
|
415
457
|
callOnError(error, actionContext ? "action" : "revalidation", {
|
|
416
458
|
request,
|
|
417
459
|
url: ctx.url,
|
package/src/router/middleware.ts
CHANGED
|
@@ -312,7 +312,10 @@ export function createMiddlewareContext<TEnv>(
|
|
|
312
312
|
const reqCtx = _getRequestContext();
|
|
313
313
|
if (reqCtx) {
|
|
314
314
|
reqCtx._debugPerformance = true;
|
|
315
|
-
reqCtx.
|
|
315
|
+
// Anchor to the true request entry (reqCtx._handlerStart) so phases that
|
|
316
|
+
// began before this opt-in report non-negative offsets; undefined falls
|
|
317
|
+
// back to performance.now() in createMetricsStore.
|
|
318
|
+
reqCtx._metricsStore ??= createMetricsStore(true, reqCtx._handlerStart);
|
|
316
319
|
}
|
|
317
320
|
},
|
|
318
321
|
};
|
|
@@ -550,22 +553,28 @@ export async function executeMiddleware<TEnv>(
|
|
|
550
553
|
// when neither surface is active.
|
|
551
554
|
let result: Response | void;
|
|
552
555
|
try {
|
|
553
|
-
result = await observePhase(PHASES.middleware(metricLabel), () =>
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
556
|
+
result = await observePhase(PHASES.middleware(metricLabel), async () => {
|
|
557
|
+
try {
|
|
558
|
+
return await entry.handler(ctx, wrappedNext);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
// Thrown Response is short-circuit control flow, not an error —
|
|
561
|
+
// absorb it INSIDE the span so the tracing runner settles the
|
|
562
|
+
// rango.middleware span as success, not STATUS_ERROR (every auth
|
|
563
|
+
// redirect would otherwise inflate trace error rates). Returning it
|
|
564
|
+
// routes through the `if (result instanceof Response)` branch below,
|
|
565
|
+
// so stub headers and request-context cookies merge identically to an
|
|
566
|
+
// explicit `return new Response(...)`. Segment handlers already follow
|
|
567
|
+
// this convention (segment-resolution/helpers.ts keeps result handling
|
|
568
|
+
// outside the span). Real errors propagate past the span.
|
|
569
|
+
if (error instanceof Response) return error;
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
} finally {
|
|
574
|
+
// Settle the middleware own-time metric once on both the success and
|
|
575
|
+
// error paths (idempotent guard in finishMiddleware).
|
|
576
|
+
finishMiddleware();
|
|
567
577
|
}
|
|
568
|
-
finishMiddleware();
|
|
569
578
|
|
|
570
579
|
// Record post-next() processing time when middleware did work after
|
|
571
580
|
// the downstream chain resolved (e.g. adding headers, logging).
|
|
@@ -725,20 +734,21 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
725
734
|
ordinal,
|
|
726
735
|
);
|
|
727
736
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
737
|
+
const result: Response | void = await observePhase(
|
|
738
|
+
PHASES.middleware(label),
|
|
739
|
+
async () => {
|
|
740
|
+
try {
|
|
741
|
+
return await middleware(ctx, guardedNext);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
744
|
+
// explicit-return path below. Absorb it INSIDE the span so the tracing
|
|
745
|
+
// runner settles rango.middleware as success, not STATUS_ERROR (same
|
|
746
|
+
// reasoning as executeMiddleware's main chain). Real errors propagate.
|
|
747
|
+
if (error instanceof Response) return error;
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
);
|
|
742
752
|
|
|
743
753
|
if (result instanceof Response) {
|
|
744
754
|
earlyResponse = result;
|
|
@@ -16,6 +16,7 @@ import type { RangoOptions, RootLayoutProps } from "./router-options.js";
|
|
|
16
16
|
import type { DefaultVars } from "../types/global-namespace.js";
|
|
17
17
|
import type { ResolvedTimeouts, OnTimeoutCallback } from "./timeout.js";
|
|
18
18
|
import type { ResolvedTracing } from "./tracing.js";
|
|
19
|
+
import type { TelemetrySink } from "./telemetry.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Options passed to router.fetch(), router.match(), and other request entrypoints.
|
|
@@ -350,6 +351,14 @@ export interface RangoInternal<
|
|
|
350
351
|
*/
|
|
351
352
|
readonly tracing?: ResolvedTracing;
|
|
352
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Raw telemetry sink from RangoOptions, exposed so handler-level emitters
|
|
356
|
+
* (rsc/handler.ts timeout/origin/late-handle) can emit WITHOUT the
|
|
357
|
+
* RouterContext ALS, which only match()/matchPartial() enter. See
|
|
358
|
+
* observeEvent's emitter list in router/instrument.ts.
|
|
359
|
+
*/
|
|
360
|
+
readonly telemetry?: TelemetrySink;
|
|
361
|
+
|
|
353
362
|
/**
|
|
354
363
|
* Whether ?__debug_manifest is allowed in production.
|
|
355
364
|
* Always enabled in development.
|