@rangojs/router 0.0.0-experimental.128 → 0.0.0-experimental.129

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.
@@ -2133,7 +2133,7 @@ import { resolve } from "node:path";
2133
2133
  // package.json
2134
2134
  var package_default = {
2135
2135
  name: "@rangojs/router",
2136
- version: "0.0.0-experimental.128",
2136
+ version: "0.0.0-experimental.129",
2137
2137
  description: "Django-inspired RSC router with composable URL patterns",
2138
2138
  keywords: [
2139
2139
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.128",
3
+ "version": "0.0.0-experimental.129",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -2,9 +2,9 @@
2
2
  * Cloudflare custom-spans integration.
3
3
  *
4
4
  * Bridges the router's performance phases (request, middleware, action,
5
- * loaders, render, ssr) onto Cloudflare Workers custom spans so they show up in the
6
- * trace waterfall and OpenTelemetry exports next to the platform's automatic
7
- * spans (KV reads, D1 queries, fetch calls), with correct parent-child nesting.
5
+ * loaders, handler, render, ssr) onto Cloudflare Workers custom spans so they show
6
+ * up in the trace waterfall and OpenTelemetry exports next to the platform's
7
+ * automatic spans (KV reads, D1 queries, fetch calls), with correct nesting.
8
8
  *
9
9
  * Usage (Cloudflare preset only):
10
10
  *
@@ -95,8 +95,8 @@ const cloudflareSpanRunner: SpanRunner = (name, fn) => {
95
95
  /**
96
96
  * Create the tracing config for a Cloudflare router. Pass the result to
97
97
  * `createRouter({ tracing })`. Spans are emitted for the request, middleware,
98
- * action, loaders, render, and ssr phases; pass `spans` to turn individual
99
- * phases off.
98
+ * action, loaders, handler, render, and ssr phases; pass `spans` to turn
99
+ * individual phases off.
100
100
  *
101
101
  * @see createOTelTracing (`@rangojs/router`) for the same slot on any platform
102
102
  * with an OpenTelemetry SDK.
@@ -66,6 +66,25 @@ export interface PhaseSpec {
66
66
  spanName: string;
67
67
  /** Span attributes set automatically when the span opens. */
68
68
  attributes?: Record<string, string | number | boolean>;
69
+ /**
70
+ * Span attributes resolved AFTER the wrapped work runs (so they can read state
71
+ * that only exists once the work is underway, e.g. the matched route name).
72
+ * Applied for streaming phases once fn has constructed its value. Return
73
+ * undefined to add nothing.
74
+ */
75
+ lazyAttributes?: () => Record<string, string | number | boolean> | undefined;
76
+ }
77
+
78
+ /**
79
+ * The matched route name for the current request, or undefined when there is no
80
+ * named route (unmatched / auto-generated). Shared by the render phase's metric
81
+ * label and its rango.route span attribute so the two can't disagree.
82
+ */
83
+ function currentRouteName(): string | undefined {
84
+ const routeName = _getRequestContext()?._routeName;
85
+ return routeName && !isAutoGeneratedRouteName(routeName)
86
+ ? routeName
87
+ : undefined;
69
88
  }
70
89
 
71
90
  /**
@@ -115,6 +134,18 @@ export const PHASES = {
115
134
  attributes: { "rango.loader_id": id },
116
135
  }),
117
136
 
137
+ /** One segment route/layout handler execution (the component/handler that
138
+ * produces a segment). Span only — the perf metric (handler:<id>) is owned by
139
+ * the legacy track() at the same call site, so observePhase here adds the
140
+ * rango.handler span without double-recording. `id` is the segment id, carried
141
+ * as the rango.segment_id attribute to match the handler:<id> perf row. */
142
+ handler: (id: string): PhaseSpec => ({
143
+ metric: false,
144
+ tracePhase: "handler",
145
+ spanName: "rango.handler",
146
+ attributes: { "rango.segment_id": id },
147
+ }),
148
+
118
149
  /** Whole render phase: match + serialize + SSR. The metric label is resolved
119
150
  * lazily at record time (after match has set the route name) so the perf
120
151
  * timeline shows WHICH route rendered: `render:total:<routeName>`, falling back
@@ -122,14 +153,20 @@ export const PHASES = {
122
153
  render: {
123
154
  metric: {
124
155
  label: () => {
125
- const routeName = _getRequestContext()?._routeName;
126
- return routeName && !isAutoGeneratedRouteName(routeName)
127
- ? `render:total:${routeName}`
128
- : "render:total";
156
+ const routeName = currentRouteName();
157
+ return routeName ? `render:total:${routeName}` : "render:total";
129
158
  },
130
159
  },
131
160
  tracePhase: "render",
132
161
  spanName: "rango.render",
162
+ // Tag the render span with the matched route so the Cloudflare/OTel waterfall
163
+ // shows WHICH route rendered (rango.render + rango.route=index), resolved
164
+ // after match has run. Kept an attribute (not baked into the span name) so the
165
+ // span name stays low-cardinality and aggregatable across routes.
166
+ lazyAttributes: () => {
167
+ const routeName = currentRouteName();
168
+ return routeName ? { "rango.route": routeName } : undefined;
169
+ },
133
170
  } as PhaseSpec,
134
171
 
135
172
  /** SSR HTML render from the RSC stream. Colon-delimited like the other ssr:*
@@ -313,6 +350,11 @@ function runDrainBoundPhase<R>(
313
350
  reject(error);
314
351
  throw error; // settle the span with the error, at construction
315
352
  }
353
+ // Late attributes (e.g. rango.route) — resolved now that the work has run,
354
+ // so they can read state like the matched route name that match sets midway.
355
+ const lazy =
356
+ tracing && spec.lazyAttributes ? spec.lazyAttributes() : undefined;
357
+ if (lazy) applyAttributes(span, lazy);
316
358
  record(); // construction-bound metric, before the response/header is built
317
359
  deliver(onDeliver(value));
318
360
  await drain; // hold the span open until the response body drains
@@ -30,6 +30,7 @@ import {
30
30
  import { applyViewTransitionDefault } from "./view-transition-default.js";
31
31
  import { getRouterContext } from "../router-context.js";
32
32
  import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
33
+ import { observePhase, PHASES } from "../instrument.js";
33
34
  import {
34
35
  track,
35
36
  RangoContext,
@@ -258,7 +259,9 @@ export async function resolveSegment<TEnv>(
258
259
  !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
259
260
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
260
261
  if (entry.loading) {
261
- const result = handleHandlerResult(handler(context));
262
+ const result = handleHandlerResult(
263
+ observePhase(PHASES.handler(entry.id), () => handler(context)),
264
+ );
262
265
  if (result instanceof Promise) {
263
266
  warnOnStreamedResponse(result, entry.id);
264
267
  result.finally(doneRouteHandler).catch(() => {});
@@ -280,7 +283,9 @@ export async function resolveSegment<TEnv>(
280
283
  component = result;
281
284
  }
282
285
  } else {
283
- component = handleHandlerResult(await handler(context));
286
+ component = handleHandlerResult(
287
+ await observePhase(PHASES.handler(entry.id), () => handler(context)),
288
+ );
284
289
  doneRouteHandler();
285
290
  }
286
291
  }
@@ -505,7 +510,11 @@ export async function resolveParallelEntry<TEnv>(
505
510
  parallelEntry.loading !== undefined && parallelEntry.loading !== false;
506
511
  if (hasLoadingFallback) {
507
512
  const result =
508
- typeof handler === "function" ? handler(context) : handler;
513
+ typeof handler === "function"
514
+ ? observePhase(PHASES.handler(`${parallelEntry.id}.${slot}`), () =>
515
+ handler(context),
516
+ )
517
+ : handler;
509
518
  if (result instanceof Promise) {
510
519
  result.finally(doneParallelHandler).catch(() => {});
511
520
  const tracked = deps.trackHandler(result, {
@@ -527,7 +536,12 @@ export async function resolveParallelEntry<TEnv>(
527
536
  }
528
537
  } else {
529
538
  component =
530
- typeof handler === "function" ? await handler(context) : handler;
539
+ typeof handler === "function"
540
+ ? await observePhase(
541
+ PHASES.handler(`${parallelEntry.id}.${slot}`),
542
+ () => handler(context),
543
+ )
544
+ : handler;
531
545
  doneParallelHandler();
532
546
  }
533
547
  }
@@ -23,6 +23,7 @@ import type { ResolvedSegment, ErrorInfo, HandlerContext } from "../../types";
23
23
  import type { SegmentResolutionDeps } from "../types.js";
24
24
  import { debugLog } from "../logging.js";
25
25
  import { tryStaticLookup } from "./static-store.js";
26
+ import { observePhase, PHASES } from "../instrument.js";
26
27
  import type { TelemetrySink } from "../telemetry.js";
27
28
  import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
28
29
 
@@ -130,11 +131,15 @@ export async function resolveLayoutComponent<TEnv>(
130
131
  entry: EntryData,
131
132
  context: HandlerContext<any, TEnv>,
132
133
  ): Promise<ReactNode> {
133
- const component = await tryStaticHandler(entry, entry.shortCode);
134
- if (component !== undefined) return component;
135
- return typeof entry.handler === "function"
136
- ? handleHandlerResult(await entry.handler(context))
137
- : (entry.handler as ReactNode);
134
+ // rango.handler span for this layout/cache handler (the perf metric is owned
135
+ // by the track("handler:<id>") at the call site; this adds the span only).
136
+ return observePhase(PHASES.handler(entry.id), async () => {
137
+ const component = await tryStaticHandler(entry, entry.shortCode);
138
+ if (component !== undefined) return component;
139
+ return typeof entry.handler === "function"
140
+ ? handleHandlerResult(await entry.handler(context))
141
+ : (entry.handler as ReactNode);
142
+ });
138
143
  }
139
144
 
140
145
  // ---------------------------------------------------------------------------
@@ -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 { observeEvent } from "../instrument.js";
46
+ import { observeEvent, observePhase, PHASES } from "../instrument.js";
47
47
  import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
48
48
  import {
49
49
  track,
@@ -793,12 +793,16 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
793
793
  ? routeEntry.liveHandler
794
794
  : routeEntry.handler;
795
795
  if (!routeEntry.loading) {
796
- const result = handleHandlerResult(await handler(context));
796
+ const result = handleHandlerResult(
797
+ await observePhase(PHASES.handler(entry.id), () => handler(context)),
798
+ );
797
799
  doneHandler();
798
800
  return result;
799
801
  }
800
802
  if (!actionContext) {
801
- const result = handleHandlerResult(handler(context));
803
+ const result = handleHandlerResult(
804
+ observePhase(PHASES.handler(entry.id), () => handler(context)),
805
+ );
802
806
  if (result instanceof Promise) {
803
807
  warnOnStreamedResponse(result, routeEntry.id);
804
808
  result.finally(doneHandler).catch(() => {});
@@ -822,7 +826,9 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
822
826
  debugLog("segment.action", "resolving action route with awaited value", {
823
827
  entryId: entry.id,
824
828
  });
825
- const actionResult = handleHandlerResult(await handler(context));
829
+ const actionResult = handleHandlerResult(
830
+ await observePhase(PHASES.handler(entry.id), () => handler(context)),
831
+ );
826
832
  doneHandler();
827
833
  return {
828
834
  content: Promise.resolve(actionResult),
@@ -26,7 +26,9 @@
26
26
  * metered directly), rango.middleware (span-only incl. intercept middleware;
27
27
  * pre/post metered directly), rango.action (action:<id>; server-action
28
28
  * execution, JS + no-JS/PE), rango.loader (loader:<id>; single metering site at
29
- * useLoader, plus the fetchable path), rango.render (render:total:<route>; normal AND
29
+ * useLoader, plus the fetchable path), rango.handler (span-only, one per segment
30
+ * route/layout handler execution; the handler:<id> perf metric is owned by the
31
+ * track() at the call site), rango.render (render:total:<route>; normal AND
30
32
  * action-revalidation renders), rango.ssr (ssr:render-html).
31
33
  *
32
34
  * Streaming-phase span lifetime: a span ends when its callback's value (or
@@ -67,6 +69,7 @@ export type TracePhase =
67
69
  | "middleware"
68
70
  | "action"
69
71
  | "loader"
72
+ | "handler"
70
73
  | "render"
71
74
  | "ssr";
72
75
 
@@ -76,6 +79,7 @@ export interface TracePhaseToggles {
76
79
  middleware?: boolean;
77
80
  action?: boolean;
78
81
  loader?: boolean;
82
+ handler?: boolean;
79
83
  render?: boolean;
80
84
  ssr?: boolean;
81
85
  }
@@ -112,6 +116,7 @@ const ALL_PHASES_ON: Record<TracePhase, boolean> = {
112
116
  middleware: true,
113
117
  action: true,
114
118
  loader: true,
119
+ handler: true,
115
120
  render: true,
116
121
  ssr: true,
117
122
  };
@@ -139,6 +144,7 @@ export function resolveTracing(
139
144
  middleware: spans.middleware ?? true,
140
145
  action: spans.action ?? true,
141
146
  loader: spans.loader ?? true,
147
+ handler: spans.handler ?? true,
142
148
  render: spans.render ?? true,
143
149
  ssr: spans.ssr ?? true,
144
150
  }