@rangojs/router 0.0.0-experimental.130 → 0.0.0-experimental.131

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.130",
2136
+ version: "0.0.0-experimental.131",
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.130",
3
+ "version": "0.0.0-experimental.131",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -231,26 +231,46 @@ export function observePhase<T>(
231
231
  // Neither surface active: direct call, zero overhead.
232
232
  if (!store && !tracing) return fn(NOOP_TRACE_SPAN);
233
233
 
234
- // Attributes only land on a real span, so skip the wrapper when only the perf
235
- // surface is active (traceSpan would apply them to NOOP_TRACE_SPAN for nothing).
236
- // `lazyAttributes` resolve AFTER fn runs (e.g. rango.route, known post-match).
234
+ // Attributes only land on a real span. Build the attribute/lazy wrapper only
235
+ // when this phase's span is actually enabled (not toggled off via `spans`), and
236
+ // short-circuit inside when the runner hands back the no-op span (tracing
237
+ // configured but off at runtime — e.g. no executionContext.tracing). That keeps
238
+ // the "configured but effectively off" path free of per-call attribute loops
239
+ // and lazy `.then()` allocations. `lazyAttributes` resolve AFTER fn runs (e.g.
240
+ // rango.route, known post-match) and apply on BOTH success and failure so an
241
+ // errored phase span is still tagged.
237
242
  const attributes = spec.attributes;
238
243
  const lazy = spec.lazyAttributes;
244
+ const spanEnabled =
245
+ tracing !== undefined && tracing.phases[spec.tracePhase] !== false;
239
246
  const wrapped: (span: TraceSpan) => T =
240
- (attributes || lazy) && tracing
247
+ (attributes || lazy) && spanEnabled
241
248
  ? (span) => {
249
+ if (span === NOOP_TRACE_SPAN) return fn(span);
242
250
  if (attributes) applyAttributes(span, attributes);
251
+ // A SYNCHRONOUS throw from fn skips applyLate — fine by design: the only
252
+ // lazyAttributes phase (render) is always async, so any internal throw
253
+ // surfaces as a rejection that the onReject branch below DOES tag. If a
254
+ // sync lazyAttributes phase is ever added, wrap this in try/catch.
243
255
  const out = fn(span);
244
256
  if (!lazy) return out;
257
+ const applyLate = () => {
258
+ const late = lazy();
259
+ if (late) applyAttributes(span, late);
260
+ };
245
261
  if (out instanceof Promise) {
246
- return out.then((value) => {
247
- const late = lazy();
248
- if (late) applyAttributes(span, late);
249
- return value;
250
- }) as T;
262
+ return out.then(
263
+ (value) => {
264
+ applyLate();
265
+ return value;
266
+ },
267
+ (error) => {
268
+ applyLate();
269
+ throw error;
270
+ },
271
+ ) as T;
251
272
  }
252
- const late = lazy();
253
- if (late) applyAttributes(span, late);
273
+ applyLate();
254
274
  return out;
255
275
  }
256
276
  : fn;
@@ -269,6 +289,24 @@ export function observePhase<T>(
269
289
  return runThenSettle(runSpan, () => recordPhaseMetric(store, metric, start));
270
290
  }
271
291
 
292
+ /**
293
+ * Open a rango.handler span around one segment route/layout handler call. The
294
+ * segment-resolution hot path runs this PER SEGMENT, so it checks the
295
+ * perf/tracing surface FIRST and calls the handler directly when neither is
296
+ * active — building neither the PhaseSpec (PHASES.handler allocates) nor the
297
+ * wrapper closure on the off path. The handler:<id> perf metric is owned by the
298
+ * track() at the call site, so this is span-only (metric:false).
299
+ */
300
+ export function observeHandler<C, R>(
301
+ id: string,
302
+ handler: (ctx: C) => R,
303
+ ctx: C,
304
+ ): R {
305
+ const reqCtx = _getRequestContext();
306
+ if (!reqCtx?._metricsStore && !reqCtx?._tracing) return handler(ctx);
307
+ return observePhase(PHASES.handler(id), () => handler(ctx));
308
+ }
309
+
272
310
  /**
273
311
  * Emit one discrete telemetry event (the event-shaped counterpart to
274
312
  * observePhase). Resolves the sink from the active router context and stamps the
@@ -30,7 +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
+ import { observeHandler } from "../instrument.js";
34
34
  import {
35
35
  track,
36
36
  RangoContext,
@@ -260,7 +260,7 @@ export async function resolveSegment<TEnv>(
260
260
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
261
261
  if (entry.loading) {
262
262
  const result = handleHandlerResult(
263
- observePhase(PHASES.handler(entry.id), () => handler(context)),
263
+ observeHandler(entry.id, handler, context),
264
264
  );
265
265
  if (result instanceof Promise) {
266
266
  warnOnStreamedResponse(result, entry.id);
@@ -284,7 +284,7 @@ export async function resolveSegment<TEnv>(
284
284
  }
285
285
  } else {
286
286
  component = handleHandlerResult(
287
- await observePhase(PHASES.handler(entry.id), () => handler(context)),
287
+ await observeHandler(entry.id, handler, context),
288
288
  );
289
289
  doneRouteHandler();
290
290
  }
@@ -511,9 +511,7 @@ export async function resolveParallelEntry<TEnv>(
511
511
  if (hasLoadingFallback) {
512
512
  const result =
513
513
  typeof handler === "function"
514
- ? observePhase(PHASES.handler(`${parallelEntry.id}.${slot}`), () =>
515
- handler(context),
516
- )
514
+ ? observeHandler(`${parallelEntry.id}.${slot}`, handler, context)
517
515
  : handler;
518
516
  if (result instanceof Promise) {
519
517
  result.finally(doneParallelHandler).catch(() => {});
@@ -537,9 +535,10 @@ export async function resolveParallelEntry<TEnv>(
537
535
  } else {
538
536
  component =
539
537
  typeof handler === "function"
540
- ? await observePhase(
541
- PHASES.handler(`${parallelEntry.id}.${slot}`),
542
- () => handler(context),
538
+ ? await observeHandler(
539
+ `${parallelEntry.id}.${slot}`,
540
+ handler,
541
+ context,
543
542
  )
544
543
  : handler;
545
544
  doneParallelHandler();
@@ -23,7 +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
+ import { observeHandler } from "../instrument.js";
27
27
  import type { TelemetrySink } from "../telemetry.js";
28
28
  import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
29
29
 
@@ -131,15 +131,16 @@ export async function resolveLayoutComponent<TEnv>(
131
131
  entry: EntryData,
132
132
  context: HandlerContext<any, TEnv>,
133
133
  ): Promise<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
- });
134
+ // Static/prerender hit: no handler runs, so emit no rango.handler span.
135
+ const staticComponent = await tryStaticHandler(entry, entry.shortCode);
136
+ if (staticComponent !== undefined) return staticComponent;
137
+ const handler = entry.handler;
138
+ if (typeof handler !== "function") return handler as ReactNode;
139
+ // Wrap ONLY the handler call in the rango.handler span (the perf metric is owned
140
+ // by track("handler:<id>") at the call site). handleHandlerResult stays OUTSIDE
141
+ // the span so a handler that returns a Response (redirect control flow, which it
142
+ // rethrows) is not recorded as a span error — mirrors the route-handler sites.
143
+ return handleHandlerResult(await observeHandler(entry.id, handler, context));
143
144
  }
144
145
 
145
146
  // ---------------------------------------------------------------------------
@@ -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, observePhase, PHASES } from "../instrument.js";
46
+ import { observeEvent, observeHandler } from "../instrument.js";
47
47
  import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
48
48
  import {
49
49
  track,
@@ -794,14 +794,14 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
794
794
  : routeEntry.handler;
795
795
  if (!routeEntry.loading) {
796
796
  const result = handleHandlerResult(
797
- await observePhase(PHASES.handler(entry.id), () => handler(context)),
797
+ await observeHandler(entry.id, handler, context),
798
798
  );
799
799
  doneHandler();
800
800
  return result;
801
801
  }
802
802
  if (!actionContext) {
803
803
  const result = handleHandlerResult(
804
- observePhase(PHASES.handler(entry.id), () => handler(context)),
804
+ observeHandler(entry.id, handler, context),
805
805
  );
806
806
  if (result instanceof Promise) {
807
807
  warnOnStreamedResponse(result, routeEntry.id);
@@ -827,7 +827,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
827
827
  entryId: entry.id,
828
828
  });
829
829
  const actionResult = handleHandlerResult(
830
- await observePhase(PHASES.handler(entry.id), () => handler(context)),
830
+ await observeHandler(entry.id, handler, context),
831
831
  );
832
832
  doneHandler();
833
833
  return {