@rangojs/router 0.0.0-experimental.126 → 0.0.0-experimental.128
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 +39 -4
- package/skills/prerender/SKILL.md +30 -11
- package/skills/router-setup/SKILL.md +23 -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 +112 -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 +440 -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 +203 -0
- package/src/router.ts +9 -0
- package/src/rsc/handler.ts +140 -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 +23 -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
package/src/rsc/handler.ts
CHANGED
|
@@ -46,8 +46,6 @@ import {
|
|
|
46
46
|
createReverseFunction,
|
|
47
47
|
stripInternalParams,
|
|
48
48
|
} from "../router/handler-context.js";
|
|
49
|
-
import { getRouterContext } from "../router/router-context.js";
|
|
50
|
-
import { resolveSink, safeEmit } from "../router/telemetry.js";
|
|
51
49
|
import { contextSet } from "../context-var.js";
|
|
52
50
|
import {
|
|
53
51
|
hasCachedManifest,
|
|
@@ -84,6 +82,12 @@ import {
|
|
|
84
82
|
appendMetric,
|
|
85
83
|
buildMetricsTiming,
|
|
86
84
|
} from "../router/metrics.js";
|
|
85
|
+
import {
|
|
86
|
+
observePhase,
|
|
87
|
+
observeRequestPhase,
|
|
88
|
+
observeEvent,
|
|
89
|
+
PHASES,
|
|
90
|
+
} from "../router/instrument.js";
|
|
87
91
|
import {
|
|
88
92
|
startSSRSetup,
|
|
89
93
|
getSSRSetup,
|
|
@@ -244,24 +248,16 @@ export function createRSCHandler<
|
|
|
244
248
|
metadata: { timeout: true, phase, durationMs },
|
|
245
249
|
});
|
|
246
250
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
actionId,
|
|
258
|
-
durationMs,
|
|
259
|
-
customHandler: !!router.onTimeout,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
} catch {
|
|
263
|
-
// Router context may not be available
|
|
264
|
-
}
|
|
251
|
+
observeEvent({
|
|
252
|
+
type: "request.timeout",
|
|
253
|
+
timestamp: performance.now(),
|
|
254
|
+
phase,
|
|
255
|
+
pathname: url.pathname,
|
|
256
|
+
routeKey,
|
|
257
|
+
actionId,
|
|
258
|
+
durationMs,
|
|
259
|
+
customHandler: !!router.onTimeout,
|
|
260
|
+
});
|
|
265
261
|
|
|
266
262
|
if (router.onTimeout) {
|
|
267
263
|
try {
|
|
@@ -499,86 +495,106 @@ export function createRSCHandler<
|
|
|
499
495
|
// Store basename on request context (scoped per-request via existing ALS)
|
|
500
496
|
requestContext._basename = router.basename;
|
|
501
497
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
)
|
|
498
|
+
// Resolved span tracing for this request (read at each traced phase).
|
|
499
|
+
requestContext._tracing = router.tracing;
|
|
500
|
+
|
|
501
|
+
// The "rango.request" span is opened inside the request context so the
|
|
502
|
+
// Cloudflare runner can read executionContext.tracing, and so every nested
|
|
503
|
+
// phase span (and the platform's automatic KV/D1/fetch spans) nests under
|
|
504
|
+
// it. observeRequestPhase owns the drain barrier: it instruments the final
|
|
505
|
+
// response body so this span (and the streaming inner phases) stay open
|
|
506
|
+
// until the body drains, keeping the tree valid. metric:false — handler:total
|
|
507
|
+
// is metered directly below (a grand total incl. the pre-context bootstrap
|
|
508
|
+
// timings) and stays construction-bound (it ships as a Server-Timing header,
|
|
509
|
+
// flushed before drain). When no surface is active this is a pass-through.
|
|
510
|
+
return runWithRequestContext(requestContext, () =>
|
|
511
|
+
observeRequestPhase(PHASES.request, async (span) => {
|
|
512
|
+
span.setAttribute("http.method", request.method);
|
|
513
|
+
// The matched route template is not known until match() runs later, so
|
|
514
|
+
// emit the concrete path as url.path (low-level), NOT http.route — the
|
|
515
|
+
// latter is reserved for the low-cardinality template (OTel convention).
|
|
516
|
+
span.setAttribute("url.path", url.pathname);
|
|
517
|
+
|
|
518
|
+
// Core handler logic (wrapped by middleware)
|
|
519
|
+
const coreHandler = async (): Promise<Response> => {
|
|
520
|
+
return coreRequestHandler(request, env, url, variables, nonce);
|
|
521
|
+
};
|
|
519
522
|
|
|
520
|
-
if
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
523
|
+
// Execute middleware chain if any, otherwise call core handler directly
|
|
524
|
+
let response: Response;
|
|
525
|
+
if (matchedMiddleware.length > 0) {
|
|
526
|
+
const mwResponse = await executeMiddleware(
|
|
527
|
+
matchedMiddleware,
|
|
528
|
+
request,
|
|
529
|
+
env,
|
|
530
|
+
variables,
|
|
531
|
+
coreHandler,
|
|
532
|
+
createReverseFunction(getRequiredRouteMap()),
|
|
527
533
|
);
|
|
528
|
-
|
|
534
|
+
|
|
535
|
+
if (
|
|
536
|
+
url.searchParams.has("_rsc_partial") ||
|
|
537
|
+
url.searchParams.has("_rsc_action")
|
|
538
|
+
) {
|
|
539
|
+
const intercepted = interceptRedirectForPartial(
|
|
540
|
+
mwResponse,
|
|
541
|
+
createRedirectFlightResponse,
|
|
542
|
+
);
|
|
543
|
+
response = intercepted ?? finalizeResponse(mwResponse);
|
|
544
|
+
} else {
|
|
545
|
+
response = finalizeResponse(mwResponse);
|
|
546
|
+
}
|
|
529
547
|
} else {
|
|
530
|
-
response =
|
|
548
|
+
response = await coreHandler();
|
|
531
549
|
}
|
|
532
|
-
} else {
|
|
533
|
-
response = await coreHandler();
|
|
534
|
-
}
|
|
535
550
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
551
|
+
// Finalize metrics after all middleware (including post-next work)
|
|
552
|
+
// has completed so :post spans are captured in the timeline.
|
|
553
|
+
// Handler timing parts are always emitted (even without debug metrics)
|
|
554
|
+
// so non-debug requests still get bootstrap Server-Timing entries.
|
|
555
|
+
const handlerTimingArr: string[] = variables.__handlerTiming || [];
|
|
556
|
+
// Preserve any existing Server-Timing set by response routes or middleware
|
|
557
|
+
const existingTiming = response.headers.get("Server-Timing");
|
|
558
|
+
const timingParts = existingTiming
|
|
559
|
+
? [existingTiming, ...handlerTimingArr]
|
|
560
|
+
: [...handlerTimingArr];
|
|
561
|
+
|
|
562
|
+
const metricsStore = requestContext._metricsStore;
|
|
563
|
+
if (metricsStore) {
|
|
564
|
+
// When the store was created at handler start (earlyMetricsStore),
|
|
565
|
+
// handler:total covers the full request. When ctx.debugPerformance()
|
|
566
|
+
// created the store mid-request, use its requestStart to avoid a
|
|
567
|
+
// negative startTime offset.
|
|
568
|
+
const totalStart = earlyMetricsStore
|
|
569
|
+
? handlerStart
|
|
570
|
+
: metricsStore.requestStart;
|
|
571
|
+
appendMetric(
|
|
572
|
+
metricsStore,
|
|
573
|
+
"handler:total",
|
|
574
|
+
totalStart,
|
|
575
|
+
performance.now() - totalStart,
|
|
576
|
+
);
|
|
577
|
+
const metricsTiming = buildMetricsTiming(
|
|
578
|
+
request.method,
|
|
579
|
+
url.pathname,
|
|
580
|
+
metricsStore,
|
|
581
|
+
);
|
|
582
|
+
if (metricsTiming) timingParts.push(metricsTiming);
|
|
583
|
+
}
|
|
569
584
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
585
|
+
const fullTiming = timingParts.join(", ");
|
|
586
|
+
if (fullTiming && !isWebSocketUpgradeResponse(response)) {
|
|
587
|
+
response.headers.set("Server-Timing", fullTiming);
|
|
588
|
+
}
|
|
574
589
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
590
|
+
// Single open-redirect chokepoint: every response (PE, full-page,
|
|
591
|
+
// middleware short-circuit, response-route) funnels through here, so
|
|
592
|
+
// guarding browser-followed (3xx) redirects once covers them all and any
|
|
593
|
+
// future redirect exit. Soft SPA/Flight redirects are 200/204 and pass
|
|
594
|
+
// through untouched (validated client-side instead).
|
|
595
|
+
return guardOutgoingRedirect(response, url.origin, router.basename);
|
|
596
|
+
}),
|
|
597
|
+
);
|
|
582
598
|
};
|
|
583
599
|
|
|
584
600
|
// Core request handling logic (separated for middleware wrapping).
|
|
@@ -715,23 +731,15 @@ export function createRSCHandler<
|
|
|
715
731
|
},
|
|
716
732
|
});
|
|
717
733
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
phase: originPhase,
|
|
728
|
-
origin: request.headers.get("origin"),
|
|
729
|
-
host: request.headers.get("host"),
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
} catch {
|
|
733
|
-
// Router context may not be available
|
|
734
|
-
}
|
|
734
|
+
observeEvent({
|
|
735
|
+
type: "request.origin-rejected",
|
|
736
|
+
timestamp: performance.now(),
|
|
737
|
+
method: request.method,
|
|
738
|
+
pathname: url.pathname,
|
|
739
|
+
phase: originPhase,
|
|
740
|
+
origin: request.headers.get("origin"),
|
|
741
|
+
host: request.headers.get("host"),
|
|
742
|
+
});
|
|
735
743
|
|
|
736
744
|
return originResult;
|
|
737
745
|
}
|
|
@@ -773,23 +781,15 @@ export function createRSCHandler<
|
|
|
773
781
|
params: reqCtx.params as Record<string, string>,
|
|
774
782
|
handledByBoundary: true,
|
|
775
783
|
});
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
pathname: url.pathname,
|
|
786
|
-
routeKey: reqCtx._routeName,
|
|
787
|
-
params: reqCtx.params as Record<string, string>,
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
} catch {
|
|
791
|
-
// Router context may not be available (e.g. prerender path)
|
|
792
|
-
}
|
|
784
|
+
observeEvent({
|
|
785
|
+
type: "handler.error",
|
|
786
|
+
timestamp: performance.now(),
|
|
787
|
+
error,
|
|
788
|
+
handledByBoundary: true,
|
|
789
|
+
pathname: url.pathname,
|
|
790
|
+
routeKey: reqCtx._routeName,
|
|
791
|
+
params: reqCtx.params as Record<string, string>,
|
|
792
|
+
});
|
|
793
793
|
};
|
|
794
794
|
|
|
795
795
|
// Set route params early so all execution paths can access ctx.params.
|
|
@@ -894,14 +894,20 @@ export function createRSCHandler<
|
|
|
894
894
|
if (plan.mode === "action") {
|
|
895
895
|
let actionContinuation: ActionContinuation | undefined;
|
|
896
896
|
try {
|
|
897
|
+
// Instrument the action execution as its own phase (action:<actionId> +
|
|
898
|
+
// rango.action), so a POST shows the mutation time AND which action ran,
|
|
899
|
+
// not just the downstream revalidation render. The action's own
|
|
900
|
+
// loaders/fetches nest under rango.action.
|
|
897
901
|
const actionOutcome = await withTimeout(
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
902
|
+
observePhase(PHASES.action(plan.actionId), () =>
|
|
903
|
+
executeServerAction(
|
|
904
|
+
handlerCtx,
|
|
905
|
+
request,
|
|
906
|
+
env,
|
|
907
|
+
url,
|
|
908
|
+
plan.actionId,
|
|
909
|
+
handleStore,
|
|
910
|
+
),
|
|
905
911
|
),
|
|
906
912
|
router.timeouts.actionMs,
|
|
907
913
|
"action",
|
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { getLoaderLazy } from "../server/loader-registry.js";
|
|
15
15
|
import { executeLoaderMiddleware } from "../router/middleware.js";
|
|
16
16
|
import { requireRequestContext } from "../server/request-context.js";
|
|
17
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
17
18
|
import {
|
|
18
19
|
createReverseFunction,
|
|
19
20
|
stripInternalParams,
|
|
@@ -162,7 +163,12 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
162
163
|
...(loaderFormData ? { formData: loaderFormData } : {}),
|
|
163
164
|
};
|
|
164
165
|
|
|
165
|
-
|
|
166
|
+
// Meter the fetchable-loader execution via observePhase, the sole
|
|
167
|
+
// funnel for this path (fn is called directly, not via ctx.use).
|
|
168
|
+
// depth:1 — a fetchable request has no render-phase parent.
|
|
169
|
+
const result = await observePhase(PHASES.loader(loaderId, 1), () =>
|
|
170
|
+
fn(loaderCtx),
|
|
171
|
+
);
|
|
166
172
|
|
|
167
173
|
interface LoaderPayload {
|
|
168
174
|
loaderResult: unknown;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { getSSRSetup } from "./ssr-setup.js";
|
|
14
14
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
15
15
|
import { executeMiddleware } from "../router/middleware.js";
|
|
16
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
16
17
|
import type { RscPayload, ReactFormState } from "./types.js";
|
|
17
18
|
import {
|
|
18
19
|
createResponseWithMergedHeaders,
|
|
@@ -124,7 +125,11 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
124
125
|
const boundAction = await ctx.decodeAction(formData);
|
|
125
126
|
// React's custom .bind() preserves $$id on server references.
|
|
126
127
|
useActionStateId = (boundAction as { $$id?: string }).$$id ?? undefined;
|
|
127
|
-
|
|
128
|
+
// Meter the no-JS form action as the action phase, same as the JS path.
|
|
129
|
+
actionResult = await observePhase(
|
|
130
|
+
PHASES.action(useActionStateId ?? "useActionState"),
|
|
131
|
+
() => boundAction(),
|
|
132
|
+
);
|
|
128
133
|
} catch (error) {
|
|
129
134
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
130
135
|
const redirectResponse = extractRedirectResponse(error);
|
|
@@ -172,7 +177,9 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
172
177
|
|
|
173
178
|
try {
|
|
174
179
|
const loadedAction = await ctx.loadServerAction(directActionId);
|
|
175
|
-
actionResult = await
|
|
180
|
+
actionResult = await observePhase(PHASES.action(directActionId), () =>
|
|
181
|
+
loadedAction.apply(null, args),
|
|
182
|
+
);
|
|
176
183
|
} catch (error) {
|
|
177
184
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
178
185
|
const redirectResponse = extractRedirectResponse(error);
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
setRequestContextParams,
|
|
12
12
|
} from "../server/request-context.js";
|
|
13
13
|
import { appendMetric } from "../router/metrics.js";
|
|
14
|
+
import { observeStreamingPhase, PHASES } from "../router/instrument.js";
|
|
14
15
|
import { getSSRSetup, isRscRequest } from "./ssr-setup.js";
|
|
15
16
|
import type { RscPayload } from "./types.js";
|
|
16
17
|
import type { MatchResult } from "../types.js";
|
|
@@ -21,7 +22,34 @@ import {
|
|
|
21
22
|
} from "./helpers.js";
|
|
22
23
|
import type { HandlerContext } from "./handler-context.js";
|
|
23
24
|
|
|
24
|
-
export
|
|
25
|
+
export function handleRscRendering<TEnv>(
|
|
26
|
+
ctx: HandlerContext<TEnv>,
|
|
27
|
+
request: Request,
|
|
28
|
+
env: TEnv,
|
|
29
|
+
url: URL,
|
|
30
|
+
isPartial: boolean,
|
|
31
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
32
|
+
nonce: string | undefined,
|
|
33
|
+
): Promise<Response> {
|
|
34
|
+
// Instrument the whole render phase once through the unified API: it records
|
|
35
|
+
// the "render:total" perf metric AND opens the "rango.render" span from the
|
|
36
|
+
// same boundary (match -> serialize -> SSR), so the two surfaces agree.
|
|
37
|
+
// Loaders kicked off during matching nest under the span; the SSR HTML pass
|
|
38
|
+
// below opens "rango.ssr" the same way.
|
|
39
|
+
return observeStreamingPhase(PHASES.render, () =>
|
|
40
|
+
handleRscRenderingInner(
|
|
41
|
+
ctx,
|
|
42
|
+
request,
|
|
43
|
+
env,
|
|
44
|
+
url,
|
|
45
|
+
isPartial,
|
|
46
|
+
handleStore,
|
|
47
|
+
nonce,
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function handleRscRenderingInner<TEnv>(
|
|
25
53
|
ctx: HandlerContext<TEnv>,
|
|
26
54
|
request: Request,
|
|
27
55
|
env: TEnv,
|
|
@@ -158,7 +186,6 @@ export async function handleRscRendering<TEnv>(
|
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
const metricsStore = reqCtx._metricsStore;
|
|
161
|
-
const renderStart = performance.now();
|
|
162
189
|
|
|
163
190
|
// Serialize to RSC stream
|
|
164
191
|
const rscSerializeStart = performance.now();
|
|
@@ -177,8 +204,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
177
204
|
);
|
|
178
205
|
|
|
179
206
|
if (isRscRequest(request, url, isPartial)) {
|
|
180
|
-
|
|
181
|
-
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
207
|
+
// render:total is recorded by the observePhase wrapper around this function.
|
|
182
208
|
const rscHeaders: Record<string, string> = {
|
|
183
209
|
"content-type": "text/x-component;charset=utf-8",
|
|
184
210
|
vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
|
|
@@ -220,16 +246,14 @@ export async function handleRscRendering<TEnv>(
|
|
|
220
246
|
metricsStore,
|
|
221
247
|
);
|
|
222
248
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const renderDur = performance.now() - renderStart;
|
|
232
|
-
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
249
|
+
// ssr-render-html metric + rango.ssr span from one boundary. render:total is
|
|
250
|
+
// recorded by the observePhase wrapper around this function.
|
|
251
|
+
const htmlStream = await observeStreamingPhase(PHASES.ssr, () =>
|
|
252
|
+
ssrModule.renderHTML(rscStream, {
|
|
253
|
+
nonce,
|
|
254
|
+
streamMode,
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
233
257
|
|
|
234
258
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
235
259
|
headers: { "content-type": "text/html;charset=utf-8" },
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
setRequestContextParams,
|
|
21
21
|
} from "../server/request-context.js";
|
|
22
22
|
import { appendMetric } from "../router/metrics.js";
|
|
23
|
+
import { observeStreamingPhase, PHASES } from "../router/instrument.js";
|
|
23
24
|
import type { RscPayload } from "./types.js";
|
|
24
25
|
import {
|
|
25
26
|
hasBodyContent,
|
|
@@ -256,7 +257,32 @@ export async function executeServerAction<TEnv>(
|
|
|
256
257
|
* provide. Redirects are the only non-partial outcome and are handled via
|
|
257
258
|
* X-RSC-Redirect headers before Flight deserialization.
|
|
258
259
|
*/
|
|
259
|
-
export
|
|
260
|
+
export function revalidateAfterAction<TEnv>(
|
|
261
|
+
ctx: HandlerContext<TEnv>,
|
|
262
|
+
request: Request,
|
|
263
|
+
env: TEnv,
|
|
264
|
+
url: URL,
|
|
265
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
266
|
+
continuation: ActionContinuation,
|
|
267
|
+
): Promise<Response> {
|
|
268
|
+
// Instrument the action-revalidation render through the unified phase API,
|
|
269
|
+
// exactly like a normal navigation render (handleRscRendering). It records
|
|
270
|
+
// "render:total" AND opens "rango.render" from one boundary covering
|
|
271
|
+
// matchPartial -> serialize, so the revalidation loaders' rango.loader spans
|
|
272
|
+
// nest under a rango.render parent instead of dangling at the request root.
|
|
273
|
+
return observeStreamingPhase(PHASES.render, () =>
|
|
274
|
+
revalidateAfterActionInner(
|
|
275
|
+
ctx,
|
|
276
|
+
request,
|
|
277
|
+
env,
|
|
278
|
+
url,
|
|
279
|
+
handleStore,
|
|
280
|
+
continuation,
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function revalidateAfterActionInner<TEnv>(
|
|
260
286
|
ctx: HandlerContext<TEnv>,
|
|
261
287
|
request: Request,
|
|
262
288
|
env: TEnv,
|
|
@@ -335,13 +361,8 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
335
361
|
});
|
|
336
362
|
const rscSerializeDur = performance.now() - renderStart;
|
|
337
363
|
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
364
|
+
// render:total is recorded by the observePhase wrapper in revalidateAfterAction.
|
|
338
365
|
appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
|
|
339
|
-
appendMetric(
|
|
340
|
-
metricsStore,
|
|
341
|
-
"render:total",
|
|
342
|
-
renderStart,
|
|
343
|
-
performance.now() - renderStart,
|
|
344
|
-
);
|
|
345
366
|
|
|
346
367
|
return createResponseWithMergedHeaders(rscStream, {
|
|
347
368
|
status: actionStatus,
|
package/src/segment-system.tsx
CHANGED
|
@@ -504,7 +504,10 @@ export async function renderSegments(
|
|
|
504
504
|
// slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
|
|
505
505
|
// (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
|
|
506
506
|
// is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
|
|
507
|
-
// dropped the loader's data.
|
|
507
|
+
// dropped the loader's data. The first-`D<index>.` strip is only correct because
|
|
508
|
+
// slot names cannot contain "." -- assertValidSlotName (route-definition/
|
|
509
|
+
// dsl-helpers.ts) rejects a "." at definition time, so a name like "@D3.foo"
|
|
510
|
+
// (which WOULD mis-cut here) can never reach this function.
|
|
508
511
|
function loaderParentId(loaderSegmentId: string): string {
|
|
509
512
|
return loaderSegmentId.replace(/D\d+\..*$/, "");
|
|
510
513
|
}
|
|
@@ -41,11 +41,13 @@ import {
|
|
|
41
41
|
} from "./handle-store.js";
|
|
42
42
|
import { isHandle } from "../handle.js";
|
|
43
43
|
import { withDefer } from "../defer.js";
|
|
44
|
-
import {
|
|
44
|
+
import { type MetricsStore } from "./context.js";
|
|
45
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
45
46
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
46
47
|
import type { SegmentCacheStore } from "../cache/types.js";
|
|
47
48
|
import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
48
49
|
import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
|
|
50
|
+
import type { ResolvedTracing } from "../router/tracing.js";
|
|
49
51
|
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
50
52
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
51
53
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
@@ -363,6 +365,19 @@ export interface RequestContext<
|
|
|
363
365
|
/** @internal Request-scoped performance metrics store */
|
|
364
366
|
_metricsStore?: MetricsStore;
|
|
365
367
|
|
|
368
|
+
/** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
|
|
369
|
+
_tracing?: ResolvedTracing;
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* @internal Drain barrier for streaming phase spans. The request phase
|
|
373
|
+
* (observeRequestPhase) sets this to a promise that resolves when the final
|
|
374
|
+
* response body finishes draining; the streaming inner phases
|
|
375
|
+
* (observeStreamingPhase: middleware/render/ssr) await it so their span AND
|
|
376
|
+
* perf metric end at body-drain rather than at stream construction. Undefined
|
|
377
|
+
* when neither the perf store nor tracing is active (no instrumentation).
|
|
378
|
+
*/
|
|
379
|
+
_finalDrain?: Promise<void>;
|
|
380
|
+
|
|
366
381
|
/** @internal Router basename for this request (used by redirect()) */
|
|
367
382
|
_basename?: string;
|
|
368
383
|
|
|
@@ -1114,10 +1129,13 @@ export function createUseFunction<TEnv>(
|
|
|
1114
1129
|
},
|
|
1115
1130
|
};
|
|
1116
1131
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1132
|
+
// Meter through the same unified phase API as the loader-resolution funnel
|
|
1133
|
+
// (observePhase), so a loader resolved via this base request-context ctx.use
|
|
1134
|
+
// co-emits the "loader:<id>" perf metric AND the "rango.loader" span — no
|
|
1135
|
+
// drift between the two ctx.use implementations.
|
|
1136
|
+
const promise = observePhase(PHASES.loader(loader.$$id), () =>
|
|
1137
|
+
Promise.resolve(loaderFn(loaderCtx)),
|
|
1138
|
+
);
|
|
1121
1139
|
|
|
1122
1140
|
loaderPromises.set(loader.$$id, promise);
|
|
1123
1141
|
|