@rangojs/router 0.0.0-experimental.142 → 0.0.0-experimental.143
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 +3 -1
- package/package.json +3 -1
- package/skills/caching/SKILL.md +18 -0
- package/skills/composability/SKILL.md +32 -0
- package/skills/observability/SKILL.md +8 -0
- package/skills/parallel/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +76 -10
- 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/src/cloudflare/tracing.ts +7 -8
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -8
- package/src/route-definition/helpers-types.ts +5 -4
- package/src/route-map-builder.ts +24 -1
- package/src/router/find-match.ts +15 -1
- package/src/router/instrument.ts +9 -4
- package/src/router/match-handlers.ts +170 -133
- package/src/router/middleware.ts +36 -29
- 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/tracing.ts +14 -5
- package/src/router.ts +15 -6
- package/src/rsc/handler.ts +46 -30
- package/src/rsc/shell-capture.ts +5 -0
- package/src/testing/dispatch.ts +142 -37
- package/src/urls/path-helper-types.ts +9 -4
- package/src/vercel/tracing.ts +7 -7
|
@@ -41,7 +41,7 @@ import { runThenSettle } from "./tracing.js";
|
|
|
41
41
|
import type {
|
|
42
42
|
RouterTracingConfig,
|
|
43
43
|
SpanRunner,
|
|
44
|
-
|
|
44
|
+
TracingToggleOptions,
|
|
45
45
|
} from "./tracing.js";
|
|
46
46
|
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
@@ -92,13 +92,11 @@ const STATUS_ERROR = 2;
|
|
|
92
92
|
// Tracing adapter: phase spans via startActiveSpan (the `tracing` slot)
|
|
93
93
|
// ---------------------------------------------------------------------------
|
|
94
94
|
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
spans?: TracePhaseToggles;
|
|
101
|
-
}
|
|
95
|
+
/**
|
|
96
|
+
* Options for createOTelTracing. Alias of the shared {@link TracingToggleOptions}
|
|
97
|
+
* (`enabled` master switch + per-phase `spans` toggles); the name is public API.
|
|
98
|
+
*/
|
|
99
|
+
export type OTelTracingOptions = TracingToggleOptions;
|
|
102
100
|
|
|
103
101
|
/**
|
|
104
102
|
* Create the tracing config that maps the router's phases onto OTel spans via
|
package/src/router/tracing.ts
CHANGED
|
@@ -81,17 +81,26 @@ export interface TracePhaseToggles {
|
|
|
81
81
|
ssr?: boolean;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The option pair shared by every tracing factory (enabled master switch +
|
|
86
|
+
* per-phase span toggles). Extended by OTelTracingOptions,
|
|
87
|
+
* CloudflareTracingOptions, VercelTracingOptions, and RouterTracingConfig so
|
|
88
|
+
* a phase added to TracePhaseToggles propagates everywhere from one place.
|
|
89
|
+
*/
|
|
90
|
+
export interface TracingToggleOptions {
|
|
91
|
+
/** Master switch. Defaults to true. */
|
|
92
|
+
enabled?: boolean;
|
|
93
|
+
/** Per-phase span toggles. Omitted phases default to enabled. */
|
|
94
|
+
spans?: TracePhaseToggles;
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
/**
|
|
85
98
|
* Value passed to `createRouter({ tracing })`. Produced by a platform factory
|
|
86
99
|
* such as `createCloudflareTracing()`.
|
|
87
100
|
*/
|
|
88
|
-
export interface RouterTracingConfig {
|
|
101
|
+
export interface RouterTracingConfig extends TracingToggleOptions {
|
|
89
102
|
/** Platform span runner. */
|
|
90
103
|
runner: SpanRunner;
|
|
91
|
-
/** Master switch. Defaults to true when a config object is provided. */
|
|
92
|
-
enabled?: boolean;
|
|
93
|
-
/** Per-phase span toggles. */
|
|
94
|
-
spans?: TracePhaseToggles;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
/**
|
package/src/router.ts
CHANGED
|
@@ -420,13 +420,13 @@ export function createRouter<TEnv = any>(
|
|
|
420
420
|
|
|
421
421
|
// Wrapper to pass debugPerformance to external createMetricsStore.
|
|
422
422
|
// Also checks per-request flag set by ctx.debugPerformance() in middleware.
|
|
423
|
+
// With no active request context there is nowhere to hang the store, so return
|
|
424
|
+
// undefined: an orphan store would collect metrics no reader can reach (nothing
|
|
425
|
+
// holds it, and appendMetric(undefined, ...) is already a no-op).
|
|
423
426
|
const getMetricsStore = () => {
|
|
424
427
|
const reqCtx = _getRequestContext();
|
|
425
428
|
const enabled = debugPerformance || !!reqCtx?._debugPerformance;
|
|
426
|
-
if (!enabled) return undefined;
|
|
427
|
-
if (!reqCtx) {
|
|
428
|
-
return createMetricsStore(true);
|
|
429
|
-
}
|
|
429
|
+
if (!enabled || !reqCtx) return undefined;
|
|
430
430
|
reqCtx._metricsStore ??= createMetricsStore(true);
|
|
431
431
|
return reqCtx._metricsStore;
|
|
432
432
|
};
|
|
@@ -501,8 +501,12 @@ export function createRouter<TEnv = any>(
|
|
|
501
501
|
? getRequestId(errorContext.request)
|
|
502
502
|
: undefined
|
|
503
503
|
: undefined;
|
|
504
|
+
// Derived once here for both the loader.start and loader.end emits (the
|
|
505
|
+
// loader.error emit uses ctx.loaderName from wrapLoaderWithErrorHandling).
|
|
506
|
+
const loaderName = telemetrySink
|
|
507
|
+
? segmentId.split(".").pop() || "unknown"
|
|
508
|
+
: "";
|
|
504
509
|
if (telemetrySink) {
|
|
505
|
-
const loaderName = segmentId.split(".").pop() || "unknown";
|
|
506
510
|
safeEmit(telemetry, {
|
|
507
511
|
type: "loader.start",
|
|
508
512
|
timestamp: loaderStart,
|
|
@@ -556,7 +560,6 @@ export function createRouter<TEnv = any>(
|
|
|
556
560
|
|
|
557
561
|
// Emit loader.end after the promise settles (fire-and-forget)
|
|
558
562
|
if (telemetrySink) {
|
|
559
|
-
const loaderName = segmentId.split(".").pop() || "unknown";
|
|
560
563
|
result.then((r) => {
|
|
561
564
|
safeEmit(telemetry, {
|
|
562
565
|
type: "loader.end",
|
|
@@ -1008,6 +1011,12 @@ export function createRouter<TEnv = any>(
|
|
|
1008
1011
|
// Expose resolved span tracing for the handler (Cloudflare custom spans)
|
|
1009
1012
|
tracing: resolvedTracing,
|
|
1010
1013
|
|
|
1014
|
+
// Expose the raw telemetry sink so handler-level emitters (timeout, origin
|
|
1015
|
+
// rejection, late-handle handler.error) can emit outside the match ALS.
|
|
1016
|
+
// Raw (not the resolveSink no-op wrapper) so router.telemetry stays
|
|
1017
|
+
// undefined when unconfigured and call sites gate on truthiness.
|
|
1018
|
+
telemetry: telemetrySink,
|
|
1019
|
+
|
|
1011
1020
|
// Expose debug manifest flag for handler
|
|
1012
1021
|
allowDebugManifest: allowDebugManifestOption,
|
|
1013
1022
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -85,7 +85,8 @@ import {
|
|
|
85
85
|
appendMetric,
|
|
86
86
|
buildMetricsTiming,
|
|
87
87
|
} from "../router/metrics.js";
|
|
88
|
-
import { observePhase,
|
|
88
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
89
|
+
import { safeEmit, resolveSink, getRequestId } from "../router/telemetry.js";
|
|
89
90
|
import {
|
|
90
91
|
startSSRSetup,
|
|
91
92
|
getSSRSetup,
|
|
@@ -246,16 +247,19 @@ export function createRSCHandler<
|
|
|
246
247
|
metadata: { timeout: true, phase, durationMs },
|
|
247
248
|
});
|
|
248
249
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
if (router.telemetry) {
|
|
251
|
+
safeEmit(resolveSink(router.telemetry), {
|
|
252
|
+
type: "request.timeout",
|
|
253
|
+
timestamp: performance.now(),
|
|
254
|
+
requestId: getRequestId(request),
|
|
255
|
+
phase,
|
|
256
|
+
pathname: url.pathname,
|
|
257
|
+
routeKey,
|
|
258
|
+
actionId,
|
|
259
|
+
durationMs,
|
|
260
|
+
customHandler: !!router.onTimeout,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
259
263
|
|
|
260
264
|
if (router.onTimeout) {
|
|
261
265
|
try {
|
|
@@ -590,7 +594,13 @@ export function createRSCHandler<
|
|
|
590
594
|
|
|
591
595
|
const fullTiming = timingParts.join(", ");
|
|
592
596
|
if (fullTiming && !isWebSocketUpgradeResponse(response)) {
|
|
593
|
-
|
|
597
|
+
try {
|
|
598
|
+
response.headers.set("Server-Timing", fullTiming);
|
|
599
|
+
} catch {
|
|
600
|
+
// Immutable headers (e.g. a passed-through platform Response) — drop
|
|
601
|
+
// the timing header, never the response. Instrumentation must not
|
|
602
|
+
// 500 a request.
|
|
603
|
+
}
|
|
594
604
|
}
|
|
595
605
|
|
|
596
606
|
// Single open-redirect chokepoint: every response (PE, full-page,
|
|
@@ -737,15 +747,18 @@ export function createRSCHandler<
|
|
|
737
747
|
},
|
|
738
748
|
});
|
|
739
749
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
750
|
+
if (router.telemetry) {
|
|
751
|
+
safeEmit(resolveSink(router.telemetry), {
|
|
752
|
+
type: "request.origin-rejected",
|
|
753
|
+
timestamp: performance.now(),
|
|
754
|
+
requestId: getRequestId(request),
|
|
755
|
+
method: request.method,
|
|
756
|
+
pathname: url.pathname,
|
|
757
|
+
phase: originPhase,
|
|
758
|
+
origin: request.headers.get("origin"),
|
|
759
|
+
host: request.headers.get("host"),
|
|
760
|
+
});
|
|
761
|
+
}
|
|
749
762
|
|
|
750
763
|
return originResult;
|
|
751
764
|
}
|
|
@@ -787,15 +800,18 @@ export function createRSCHandler<
|
|
|
787
800
|
params: reqCtx.params as Record<string, string>,
|
|
788
801
|
handledByBoundary: true,
|
|
789
802
|
});
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
803
|
+
if (router.telemetry) {
|
|
804
|
+
safeEmit(resolveSink(router.telemetry), {
|
|
805
|
+
type: "handler.error",
|
|
806
|
+
timestamp: performance.now(),
|
|
807
|
+
requestId: getRequestId(request),
|
|
808
|
+
error,
|
|
809
|
+
handledByBoundary: true,
|
|
810
|
+
pathname: url.pathname,
|
|
811
|
+
routeKey: reqCtx._routeName,
|
|
812
|
+
params: reqCtx.params as Record<string, string>,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
799
815
|
};
|
|
800
816
|
|
|
801
817
|
// Set route params early so all execution paths can access ctx.params.
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -715,6 +715,11 @@ async function attemptCapture(
|
|
|
715
715
|
derivedCtx._transitionWhen = [];
|
|
716
716
|
derivedCtx._shellCaptureRun = true;
|
|
717
717
|
derivedCtx._metricsStore = undefined;
|
|
718
|
+
// Spans, like perf metrics above, are a FOREGROUND surface: the capture
|
|
719
|
+
// re-render must not emit a second rango.render/loader/ssr set after the
|
|
720
|
+
// foreground rango.request span ended (orphan spans in the trace).
|
|
721
|
+
// _tracing is otherwise inherited through Object.create(reqCtx).
|
|
722
|
+
derivedCtx._tracing = undefined;
|
|
718
723
|
// Bake-lane loader containers (loaders on entries with no renderable
|
|
719
724
|
// loading() execute during capture — docs/design/loader-container-bake.md).
|
|
720
725
|
// resolveLoaderData registers each container promise here; the drain in
|
package/src/testing/dispatch.ts
CHANGED
|
@@ -55,6 +55,14 @@
|
|
|
55
55
|
* metadata.category, while the request still degrades-to-miss exactly as before.
|
|
56
56
|
* (A thrown response-route HANDLER error is the one onError path NOT covered —
|
|
57
57
|
* see "DOES NOT support" below.)
|
|
58
|
+
* - createRouter({ telemetry }) match-transaction lifecycle: request.start opens
|
|
59
|
+
* the transaction before the global middleware chain, request.end closes it
|
|
60
|
+
* after finalizeResponse (segmentCount 0 / cacheHit false — dispatch renders no
|
|
61
|
+
* RSC segments and holds no match-cache state), and a thrown non-Response error
|
|
62
|
+
* emits request.error with phase "routing". All three carry the same requestId
|
|
63
|
+
* (getRequestId). Emission is gated entirely on a configured sink; with none,
|
|
64
|
+
* dispatch does zero new work and stays byte-identical for existing callers.
|
|
65
|
+
* Lets a consumer unit-test their sink wiring in-process instead of only at e2e.
|
|
58
66
|
*
|
|
59
67
|
* What dispatch DOES NOT support (and why):
|
|
60
68
|
* - RSC component routes — rendering requires the Flight serializer + React
|
|
@@ -76,6 +84,13 @@
|
|
|
76
84
|
* merged cookies/headers all match production; only the Flight-embedded
|
|
77
85
|
* location-state entries are absent. Cover location-state restoration across a
|
|
78
86
|
* partial redirect with an e2e test.
|
|
87
|
+
* - Telemetry cache.decision / loader.* / handler.error events: these fire from
|
|
88
|
+
* the real match()/matchPartial() + RSC render + loader pipeline (match-
|
|
89
|
+
* handlers.ts, loader-resolution.ts, segment-resolution), none of which
|
|
90
|
+
* dispatch runs. Synthesizing them here would be faking (the events would not
|
|
91
|
+
* reflect a real cache lookup or loader run), so dispatch emits only the
|
|
92
|
+
* request.start/end/error lifecycle above; cover cache/loader telemetry with an
|
|
93
|
+
* e2e test driving a real RSC request.
|
|
79
94
|
*
|
|
80
95
|
* dispatch reuses router.previewMatch(), which itself runs content negotiation
|
|
81
96
|
* and resolves route middleware from the matched entry tree, so dispatch's
|
|
@@ -127,6 +142,8 @@ import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
|
127
142
|
import { invokeOnError } from "../router/error-handling.js";
|
|
128
143
|
import type { OnErrorCallback } from "../types/error-types.js";
|
|
129
144
|
import type { Rango } from "../router/router-interfaces.js";
|
|
145
|
+
import { getRequestId, resolveSink, safeEmit } from "../router/telemetry.js";
|
|
146
|
+
import type { TelemetrySink } from "../router/telemetry.js";
|
|
130
147
|
|
|
131
148
|
/**
|
|
132
149
|
* The internal subset of the router surface dispatch depends on. The public
|
|
@@ -140,6 +157,13 @@ interface DispatchableRouter<TEnv> {
|
|
|
140
157
|
routeMap: Record<string, unknown>;
|
|
141
158
|
middleware: MiddlewareEntry<TEnv>[];
|
|
142
159
|
onError?: OnErrorCallback<TEnv>;
|
|
160
|
+
/**
|
|
161
|
+
* Optional telemetry sink from createRouter({ telemetry }) (RangoInternal
|
|
162
|
+
* field). dispatch emits the match-transaction lifecycle events onto it so a
|
|
163
|
+
* consumer can unit-test their sink wiring in-process; undefined keeps dispatch
|
|
164
|
+
* byte-identical for every existing caller.
|
|
165
|
+
*/
|
|
166
|
+
telemetry?: TelemetrySink;
|
|
143
167
|
findMatch(pathname: string): Promise<{
|
|
144
168
|
redirectTo?: string;
|
|
145
169
|
routeKey?: string;
|
|
@@ -444,6 +468,19 @@ export async function dispatch<TEnv = any>(
|
|
|
444
468
|
const isPartial = url.searchParams.has("_rsc_partial");
|
|
445
469
|
const isAction = url.searchParams.has("_rsc_action");
|
|
446
470
|
|
|
471
|
+
// Telemetry: mirror the router's match-transaction lifecycle onto the
|
|
472
|
+
// configured sink so a consumer can unit-test createRouter({ telemetry })
|
|
473
|
+
// wiring in-process (the dogfood gap the RSC-free dispatch left — see
|
|
474
|
+
// tests/cloudflare-basic/test/cache-status.test.ts). Every emit is gated on
|
|
475
|
+
// `sink` truthiness: with no sink configured dispatch does zero new work and
|
|
476
|
+
// stays byte-identical for existing callers. Only request.start/end/error are
|
|
477
|
+
// reachable here — cache.decision and loader.* originate in the real match()/
|
|
478
|
+
// matchPartial() + RSC render pipeline dispatch deliberately does not run
|
|
479
|
+
// (module header), so fabricating them would violate the no-fake rule.
|
|
480
|
+
const sink = router.telemetry;
|
|
481
|
+
const telemetryRequestId = sink ? getRequestId(req) : undefined;
|
|
482
|
+
const telemetryStart = sink ? performance.now() : 0;
|
|
483
|
+
|
|
447
484
|
return runWithRequestContext(requestContext, async () => {
|
|
448
485
|
// Set params before middleware/handler run, so global middleware sees
|
|
449
486
|
// ctx.params (production sets them during matching, before middleware).
|
|
@@ -658,44 +695,112 @@ export async function dispatch<TEnv = any>(
|
|
|
658
695
|
return callResponseRoute();
|
|
659
696
|
};
|
|
660
697
|
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
:
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
// Match production's global-chain exit (handler.ts): on a partial/action
|
|
677
|
-
// request a middleware 3xx redirect is converted to a Flight-safe response
|
|
678
|
-
// so fetch() does not auto-follow it; every path then drains onResponse
|
|
679
|
-
// callbacks via finalizeResponse. dispatch is RSC-free, so the
|
|
680
|
-
// createRedirectFlightResponse stand-in falls back to the no-state
|
|
681
|
-
// 204 + X-RSC-Redirect (see the location-state divergence in the header).
|
|
682
|
-
let finalResponse: Response;
|
|
683
|
-
if (isPartial || isAction) {
|
|
684
|
-
const intercepted = interceptRedirectForPartial(
|
|
685
|
-
mwResponse,
|
|
686
|
-
(redirectUrl) => createSimpleRedirectResponse(redirectUrl),
|
|
687
|
-
);
|
|
688
|
-
finalResponse = finalizeResponse(intercepted ?? mwResponse);
|
|
689
|
-
} else {
|
|
690
|
-
finalResponse = finalizeResponse(mwResponse);
|
|
698
|
+
// request.start opens the match transaction, mirroring match-handlers.ts.
|
|
699
|
+
// transaction is always "match" (dispatch has no matchPartial split);
|
|
700
|
+
// isPartial carries the ?_rsc_partial signal the same way production does.
|
|
701
|
+
if (sink) {
|
|
702
|
+
safeEmit(resolveSink(sink), {
|
|
703
|
+
type: "request.start",
|
|
704
|
+
timestamp: telemetryStart,
|
|
705
|
+
requestId: telemetryRequestId,
|
|
706
|
+
method: req.method,
|
|
707
|
+
pathname: url.pathname,
|
|
708
|
+
transaction: "match",
|
|
709
|
+
isPartial,
|
|
710
|
+
});
|
|
691
711
|
}
|
|
692
712
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
713
|
+
try {
|
|
714
|
+
// Global (pattern-matched) middleware wraps coreHandler, exactly as
|
|
715
|
+
// production wraps coreHandler with executeMiddleware (handler.ts).
|
|
716
|
+
const globalMatches = matchMiddleware(url.pathname, router.middleware);
|
|
717
|
+
const mwResponse =
|
|
718
|
+
globalMatches.length === 0
|
|
719
|
+
? await coreHandler()
|
|
720
|
+
: await executeMiddleware<TEnv>(
|
|
721
|
+
globalMatches,
|
|
722
|
+
req,
|
|
723
|
+
env,
|
|
724
|
+
variables,
|
|
725
|
+
coreHandler,
|
|
726
|
+
reverse,
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// Match production's global-chain exit (handler.ts): on a partial/action
|
|
730
|
+
// request a middleware 3xx redirect is converted to a Flight-safe response
|
|
731
|
+
// so fetch() does not auto-follow it; every path then drains onResponse
|
|
732
|
+
// callbacks via finalizeResponse. dispatch is RSC-free, so the
|
|
733
|
+
// createRedirectFlightResponse stand-in falls back to the no-state
|
|
734
|
+
// 204 + X-RSC-Redirect (see the location-state divergence in the header).
|
|
735
|
+
let finalResponse: Response;
|
|
736
|
+
if (isPartial || isAction) {
|
|
737
|
+
const intercepted = interceptRedirectForPartial(
|
|
738
|
+
mwResponse,
|
|
739
|
+
(redirectUrl) => createSimpleRedirectResponse(redirectUrl),
|
|
740
|
+
);
|
|
741
|
+
finalResponse = finalizeResponse(intercepted ?? mwResponse);
|
|
742
|
+
} else {
|
|
743
|
+
finalResponse = finalizeResponse(mwResponse);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// request.end closes the transaction. dispatch produces no RSC segments and
|
|
747
|
+
// holds no match-cache state, so segmentCount/cacheHit are 0/false — the
|
|
748
|
+
// same shape production emits for its own redirect (segment-less) result.
|
|
749
|
+
if (sink) {
|
|
750
|
+
safeEmit(resolveSink(sink), {
|
|
751
|
+
type: "request.end",
|
|
752
|
+
timestamp: performance.now(),
|
|
753
|
+
requestId: telemetryRequestId,
|
|
754
|
+
method: req.method,
|
|
755
|
+
pathname: url.pathname,
|
|
756
|
+
transaction: "match",
|
|
757
|
+
durationMs: performance.now() - telemetryStart,
|
|
758
|
+
segmentCount: 0,
|
|
759
|
+
cacheHit: false,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Mirror production's single open-redirect chokepoint (handler.ts): every
|
|
764
|
+
// browser-followed (3xx + Location) redirect is same-origin guarded before
|
|
765
|
+
// it leaves -- a cross-origin Location is rewritten to the basename root
|
|
766
|
+
// unless redirect(url, { external: true }) opted out. Soft partial/action
|
|
767
|
+
// redirects are 204 + X-RSC-Redirect and pass through untouched (the client
|
|
768
|
+
// validates them), so this is a no-op for them.
|
|
769
|
+
return guardOutgoingRedirect(finalResponse, url.origin, router.basename);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (sink) {
|
|
772
|
+
if (error instanceof Response) {
|
|
773
|
+
// executeMiddleware absorbs a middleware-thrown Response and returns it
|
|
774
|
+
// (middleware.ts:566), so a thrown Response never actually reaches this
|
|
775
|
+
// level in dispatch. Defensive: if one ever does it is a completed
|
|
776
|
+
// request from the consumer's seat (a short-circuit redirect), so mirror
|
|
777
|
+
// plan 002 / match-handlers.ts and emit request.end, not request.error.
|
|
778
|
+
safeEmit(resolveSink(sink), {
|
|
779
|
+
type: "request.end",
|
|
780
|
+
timestamp: performance.now(),
|
|
781
|
+
requestId: telemetryRequestId,
|
|
782
|
+
method: req.method,
|
|
783
|
+
pathname: url.pathname,
|
|
784
|
+
transaction: "match",
|
|
785
|
+
durationMs: performance.now() - telemetryStart,
|
|
786
|
+
segmentCount: 0,
|
|
787
|
+
cacheHit: false,
|
|
788
|
+
});
|
|
789
|
+
} else {
|
|
790
|
+
safeEmit(resolveSink(sink), {
|
|
791
|
+
type: "request.error",
|
|
792
|
+
timestamp: performance.now(),
|
|
793
|
+
requestId: telemetryRequestId,
|
|
794
|
+
method: req.method,
|
|
795
|
+
pathname: url.pathname,
|
|
796
|
+
transaction: "match",
|
|
797
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
798
|
+
phase: "routing",
|
|
799
|
+
durationMs: performance.now() - telemetryStart,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
700
805
|
});
|
|
701
806
|
}
|
|
@@ -268,9 +268,16 @@ export type PathHelpers<TEnv> = {
|
|
|
268
268
|
* `{ handler, use? }` whose `use` is scoped to that slot only. Per-slot
|
|
269
269
|
* merge order is `handler.use` → shared `use` → slot-local `use`, with
|
|
270
270
|
* narrowest scope winning for last-write-wins items like `loading()`.
|
|
271
|
+
*
|
|
272
|
+
* Not generic over the slots record: an inferred type parameter makes the
|
|
273
|
+
* object literal an inference site, which suppresses contextual typing of
|
|
274
|
+
* arrow slot handlers (`(ctx) => ...` was implicit any). Bare handlers infer
|
|
275
|
+
* now; a descriptor's `handler:` arrow still needs an explicit ctx annotation
|
|
276
|
+
* because StaticHandlerDefinition's own `.handler` joins the contextual union
|
|
277
|
+
* (two callables — see parallel-slot-handler-types.test.ts).
|
|
271
278
|
*/
|
|
272
|
-
parallel:
|
|
273
|
-
|
|
279
|
+
parallel: (
|
|
280
|
+
slots: Record<
|
|
274
281
|
`@${string}`,
|
|
275
282
|
| Handler<any, any, TEnv>
|
|
276
283
|
| ReactNode
|
|
@@ -283,8 +290,6 @@ export type PathHelpers<TEnv> = {
|
|
|
283
290
|
use?: () => ParallelUseItem[];
|
|
284
291
|
}
|
|
285
292
|
>,
|
|
286
|
-
>(
|
|
287
|
-
slots: TSlots,
|
|
288
293
|
use?: () => ParallelUseItem[],
|
|
289
294
|
) => ParallelItem;
|
|
290
295
|
|
package/src/vercel/tracing.ts
CHANGED
|
@@ -44,15 +44,15 @@ import {
|
|
|
44
44
|
} from "../router/telemetry-otel.js";
|
|
45
45
|
import type {
|
|
46
46
|
RouterTracingConfig,
|
|
47
|
-
|
|
47
|
+
TracingToggleOptions,
|
|
48
48
|
} from "../router/tracing.js";
|
|
49
49
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Options for createVercelTracing. Extends the shared
|
|
52
|
+
* {@link TracingToggleOptions} (`enabled` + per-phase `spans`) with the
|
|
53
|
+
* Vercel-specific tracer selectors.
|
|
54
|
+
*/
|
|
55
|
+
export interface VercelTracingOptions extends TracingToggleOptions {
|
|
56
56
|
/**
|
|
57
57
|
* OTel instrumentation-scope name passed to `trace.getTracer()`. Defaults to
|
|
58
58
|
* `"rango"`. Ignored when `tracer` is provided.
|