@rangojs/router 0.0.0-experimental.126 → 0.0.0-experimental.127

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.
Files changed (44) hide show
  1. package/dist/bin/rango.js +5 -1
  2. package/dist/vite/index.js +55 -40
  3. package/package.json +23 -19
  4. package/skills/observability/SKILL.md +12 -3
  5. package/skills/prerender/SKILL.md +30 -11
  6. package/skills/router-setup/SKILL.md +11 -3
  7. package/src/build/route-types/codegen.ts +12 -1
  8. package/src/cache/cache-scope.ts +20 -0
  9. package/src/cloudflare/index.ts +11 -0
  10. package/src/cloudflare/tracing.ts +109 -0
  11. package/src/index.rsc.ts +19 -2
  12. package/src/index.ts +16 -1
  13. package/src/route-definition/dsl-helpers.ts +19 -0
  14. package/src/router/instrument.ts +230 -0
  15. package/src/router/loader-resolution.ts +15 -10
  16. package/src/router/match-middleware/cache-lookup.ts +9 -14
  17. package/src/router/match-middleware/cache-store.ts +12 -0
  18. package/src/router/middleware.ts +23 -2
  19. package/src/router/prerender-match.ts +5 -2
  20. package/src/router/router-context.ts +2 -1
  21. package/src/router/router-interfaces.ts +8 -0
  22. package/src/router/router-options.ts +58 -4
  23. package/src/router/segment-resolution/fresh.ts +15 -18
  24. package/src/router/segment-resolution/helpers.ts +6 -0
  25. package/src/router/segment-resolution/loader-cache.ts +5 -0
  26. package/src/router/segment-resolution/revalidation.ts +9 -18
  27. package/src/router/segment-wrappers.ts +3 -2
  28. package/src/router/telemetry-otel.ts +161 -179
  29. package/src/router/tracing.ts +198 -0
  30. package/src/router.ts +9 -0
  31. package/src/rsc/handler.ts +132 -134
  32. package/src/rsc/loader-fetch.ts +7 -1
  33. package/src/rsc/progressive-enhancement.ts +9 -2
  34. package/src/rsc/rsc-rendering.ts +38 -14
  35. package/src/rsc/server-action.ts +28 -7
  36. package/src/segment-system.tsx +4 -1
  37. package/src/server/request-context.ts +13 -5
  38. package/src/vite/discovery/prerender-collection.ts +26 -37
  39. package/src/vite/discovery/state.ts +6 -0
  40. package/src/vite/plugin-types.ts +25 -0
  41. package/src/vite/plugins/expose-ids/router-transform.ts +10 -0
  42. package/src/vite/rango.ts +1 -0
  43. package/src/vite/router-discovery.ts +9 -3
  44. package/src/vite/utils/prerender-utils.ts +36 -0
@@ -284,11 +284,17 @@ export async function resolveWithErrorBoundary<TEnv, TResult>(
284
284
  deps: SegmentResolutionDeps<TEnv>,
285
285
  report?: ErrorReportContext,
286
286
  pathname?: string,
287
+ throwOnError?: boolean,
287
288
  ): Promise<TResult> {
288
289
  try {
289
290
  return await resolveFn();
290
291
  } catch (error) {
291
292
  if (error instanceof Response) throw error;
293
+ // Pre-render surfaces render failures to the build instead of baking the
294
+ // error boundary as a frozen 200 (issue #587). A `throw new Skip()` in a
295
+ // render fn also propagates here so the build can skip that URL rather than
296
+ // bake its error page. The live request path leaves throwOnError unset.
297
+ if (throwOnError) throw error;
292
298
  const segment = catchSegmentError(
293
299
  error,
294
300
  entry,
@@ -113,6 +113,11 @@ function getLoaderStore(
113
113
  *
114
114
  * When the LoaderEntry has no cache config, delegates directly to ctx.use(loader).
115
115
  * When cached, checks store first and stores on miss via waitUntil.
116
+ *
117
+ * Loader metering is NOT done here — it lives at the ctx.use execution funnel
118
+ * (observePhase; see instrument.ts). A cache HIT returns without calling ctx.use,
119
+ * so it emits no loader phase (the loader did not execute; the hit is only a
120
+ * LoaderCache debug log).
116
121
  */
117
122
  export function resolveLoaderData<TEnv>(
118
123
  loaderEntry: LoaderEntry,
@@ -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 { resolveSink, safeEmit } from "../telemetry.js";
46
+ import { observeEvent } from "../instrument.js";
47
47
  import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
48
48
  import {
49
49
  track,
@@ -87,23 +87,14 @@ function emitRevalidationDecision(
87
87
  routeKey: string,
88
88
  shouldRevalidate: boolean,
89
89
  ): void {
90
- let routerCtx;
91
- try {
92
- routerCtx = getRouterContext();
93
- } catch {
94
- return;
95
- }
96
- if (routerCtx?.telemetry) {
97
- safeEmit(resolveSink(routerCtx.telemetry), {
98
- type: "revalidation.decision",
99
- timestamp: performance.now(),
100
- requestId: routerCtx.requestId,
101
- segmentId,
102
- pathname,
103
- routeKey,
104
- shouldRevalidate,
105
- });
106
- }
90
+ observeEvent({
91
+ type: "revalidation.decision",
92
+ timestamp: performance.now(),
93
+ segmentId,
94
+ pathname,
95
+ routeKey,
96
+ shouldRevalidate,
97
+ });
107
98
  }
108
99
 
109
100
  // ---------------------------------------------------------------------------
@@ -5,6 +5,7 @@ import type {
5
5
  ShouldRevalidateFn,
6
6
  } from "../types";
7
7
  import type { SegmentResolutionDeps } from "./types.js";
8
+ import type { ResolveSegmentOptions } from "./segment-resolution.js";
8
9
 
9
10
  import {
10
11
  resolveAllSegments as _resolveAllSegments,
@@ -29,7 +30,7 @@ export interface SegmentWrappers<TEnv = any> {
29
30
  params: Record<string, string>,
30
31
  context: HandlerContext<any, TEnv>,
31
32
  loaderPromises: Map<string, Promise<any>>,
32
- options?: { skipLoaders?: boolean },
33
+ options?: ResolveSegmentOptions,
33
34
  ) => Promise<ResolvedSegment[]>;
34
35
  resolveLoadersOnly: (
35
36
  entries: EntryData[],
@@ -123,7 +124,7 @@ export function createSegmentWrappers<TEnv = any>(
123
124
  params: Record<string, string>,
124
125
  context: HandlerContext<any, TEnv>,
125
126
  loaderPromises: Map<string, Promise<any>>,
126
- options?: { skipLoaders?: boolean },
127
+ options?: ResolveSegmentOptions,
127
128
  ): ReturnType<typeof _resolveAllSegments> {
128
129
  return _resolveAllSegments(
129
130
  entries,
@@ -1,20 +1,48 @@
1
1
  /**
2
- * OpenTelemetry Adapter for Router Telemetry
2
+ * OpenTelemetry adapters for the router.
3
3
  *
4
- * Maps internal TelemetrySink events to OTel spans. The core router
5
- * remains OTel-agnostic — this adapter bridges the gap by accepting
6
- * a standard OTel Tracer and producing spans/events from it.
4
+ * Two adapters, two surfaces matching the split in instrument.ts:
5
+ *
6
+ * - createOTelTracing(tracer): the OTel adapter for the `tracing` SLOT — the
7
+ * canonical phase-span layer. It bridges observePhase's callback boundary
8
+ * onto OTel's callback-bound `startActiveSpan`, so the router's phase spans
9
+ * (rango.request/middleware/loader/render/ssr) nest by async context and the
10
+ * loader's own OTel spans (db/fetch) land under rango.loader. This is the
11
+ * OTel equivalent of createCloudflareTracing — pass it to
12
+ * `createRouter({ tracing })`.
13
+ *
14
+ * - createOTelSink(tracer): the OTel adapter for the `telemetry` SLOT — a
15
+ * TelemetrySink for the EVENT-shaped facts (handler errors, cache decisions,
16
+ * revalidation decisions, timeouts, origin rejections). It emits one instant
17
+ * OTel span per fact. It deliberately does NOT emit request/loader phase
18
+ * spans: those are owned by the tracing slot (createOTelTracing) so the two
19
+ * layers don't produce duplicate rango.request / rango.loader spans.
20
+ *
21
+ * The core router stays OTel-agnostic — these adapters accept a standard OTel
22
+ * Tracer (structurally typed, no import needed) and bridge the gap.
7
23
  *
8
24
  * Usage:
9
25
  * import { trace } from "@opentelemetry/api";
10
- * import { createOTelSink } from "@rangojs/router";
26
+ * import { createRouter, createOTelTracing, createOTelSink } from "@rangojs/router";
11
27
  *
28
+ * const tracer = trace.getTracer("my-app");
12
29
  * const router = createRouter({
13
- * telemetry: createOTelSink(trace.getTracer("my-app")),
30
+ * tracing: createOTelTracing(tracer), // phase spans (callback-bound)
31
+ * telemetry: createOTelSink(tracer), // discrete-fact instant spans
14
32
  * });
33
+ *
34
+ * Faithful nesting requires an OTel async context manager
35
+ * (AsyncLocalStorageContextManager) configured in your OTel setup — standard for
36
+ * any startActiveSpan-based instrumentation.
15
37
  */
16
38
 
17
39
  import type { TelemetrySink, TelemetryEvent } from "./telemetry.js";
40
+ import { runThenSettle } from "./tracing.js";
41
+ import type {
42
+ RouterTracingConfig,
43
+ SpanRunner,
44
+ TracePhaseToggles,
45
+ } from "./tracing.js";
18
46
 
19
47
  // ---------------------------------------------------------------------------
20
48
  // Minimal OTel-compatible types (structurally typed, no import needed)
@@ -22,21 +50,21 @@ import type { TelemetrySink, TelemetryEvent } from "./telemetry.js";
22
50
 
23
51
  /**
24
52
  * Minimal Span interface compatible with @opentelemetry/api Span.
25
- * Only the methods used by the adapter are declared.
53
+ * Only the methods used by the adapters are declared.
26
54
  */
27
55
  export interface OTelSpan {
28
56
  setAttribute(key: string, value: string | number | boolean): OTelSpan | void;
29
- addEvent(
30
- name: string,
31
- attributes?: Record<string, string | number | boolean>,
32
- ): OTelSpan | void;
33
57
  setStatus(status: { code: number; message?: string }): OTelSpan | void;
34
58
  recordException(exception: Error): void;
35
59
  end(): void;
36
60
  }
37
61
 
38
62
  /**
39
- * Minimal Tracer interface compatible with @opentelemetry/api Tracer.
63
+ * Minimal Tracer interface for the EVENT sink (createOTelSink): only `startSpan`
64
+ * is used (one instant span per discrete fact). Kept narrow so a custom,
65
+ * event-only tracer that does not implement `startActiveSpan` still satisfies it.
66
+ * The real `@opentelemetry/api` Tracer has both methods, so it satisfies this and
67
+ * `OTelActiveSpanTracer` below.
40
68
  */
41
69
  export interface OTelTracer {
42
70
  startSpan(
@@ -47,182 +75,110 @@ export interface OTelTracer {
47
75
  ): OTelSpan;
48
76
  }
49
77
 
78
+ /**
79
+ * Minimal Tracer interface for the PHASE-SPAN adapter (createOTelTracing): only
80
+ * `startActiveSpan` is used (callback-bound, so the span is active for the work
81
+ * and child spans nest). Declared separately from `OTelTracer` so each factory
82
+ * requires exactly the method it calls.
83
+ */
84
+ export interface OTelActiveSpanTracer {
85
+ startActiveSpan<T>(name: string, fn: (span: OTelSpan) => T): T;
86
+ }
87
+
50
88
  // OTel SpanStatusCode constants (mirrors @opentelemetry/api values)
51
- const STATUS_OK = 1;
52
89
  const STATUS_ERROR = 2;
53
90
 
54
91
  // ---------------------------------------------------------------------------
55
- // Span correlation helpers
92
+ // Tracing adapter: phase spans via startActiveSpan (the `tracing` slot)
56
93
  // ---------------------------------------------------------------------------
57
94
 
58
- // Build correlation keys using requestId.
59
- // getRequestId() always returns a value (generated internally when no
60
- // header is present), so concurrent requests to the same path each get
61
- // their own correlation key and never mis-correlate.
62
-
63
- function requestKey(event: {
64
- requestId?: string;
65
- pathname: string;
66
- transaction: string;
67
- }): string {
68
- return `${event.requestId ?? ""}:${event.pathname}:${event.transaction}`;
69
- }
70
-
71
- function loaderKey(event: {
72
- requestId?: string;
73
- segmentId: string;
74
- loaderName: string;
75
- pathname: string;
76
- }): string {
77
- return `${event.requestId ?? ""}:${event.segmentId}:${event.loaderName}:${event.pathname}`;
95
+ /** Options for createOTelTracing. */
96
+ export interface OTelTracingOptions {
97
+ /** Master switch. Defaults to true. */
98
+ enabled?: boolean;
99
+ /** Per-phase span toggles. Omitted phases default to enabled. */
100
+ spans?: TracePhaseToggles;
78
101
  }
79
102
 
80
- function pushSpan(
81
- map: Map<string, OTelSpan[]>,
82
- key: string,
83
- span: OTelSpan,
84
- ): void {
85
- let stack = map.get(key);
86
- if (!stack) {
87
- stack = [];
88
- map.set(key, stack);
89
- }
90
- stack.push(span);
91
- }
103
+ /**
104
+ * Create the tracing config that maps the router's phases onto OTel spans via
105
+ * `tracer.startActiveSpan`, which runs the work inside the span's active context
106
+ * (so child spans nest) and returns the work's value unchanged. The span ends
107
+ * when that value — or, for async work, the returned promise — settles, matching
108
+ * observePhase's contract. When the work throws or rejects, the exception is
109
+ * recorded and the span status is set to ERROR before it ends, so a failed phase
110
+ * stays visible in the trace (the old createOTelSink error path is preserved here
111
+ * now that phase spans live in this adapter). Pass the result to
112
+ * `createRouter({ tracing })`.
113
+ *
114
+ * @see createCloudflareTracing (`@rangojs/router/cloudflare`) for the same slot
115
+ * using Cloudflare Workers native custom spans.
116
+ */
117
+ export function createOTelTracing(
118
+ tracer: OTelActiveSpanTracer,
119
+ options: OTelTracingOptions = {},
120
+ ): RouterTracingConfig {
121
+ const runner: SpanRunner = <T>(name: string, fn: (span: OTelSpan) => T): T =>
122
+ tracer.startActiveSpan(
123
+ name,
124
+ (span): T =>
125
+ runThenSettle(
126
+ () => fn(span),
127
+ (error) => {
128
+ // On failure record the exception + ERROR status before ending, so a
129
+ // failed phase stays visible in the trace.
130
+ if (error !== undefined) {
131
+ if (error instanceof Error) {
132
+ span.recordException(error);
133
+ span.setStatus({ code: STATUS_ERROR, message: error.message });
134
+ } else {
135
+ span.setStatus({ code: STATUS_ERROR });
136
+ }
137
+ }
138
+ span.end();
139
+ },
140
+ ),
141
+ );
92
142
 
93
- function popSpan(
94
- map: Map<string, OTelSpan[]>,
95
- key: string,
96
- ): OTelSpan | undefined {
97
- const stack = map.get(key);
98
- if (!stack || stack.length === 0) return undefined;
99
- const span = stack.pop()!;
100
- if (stack.length === 0) map.delete(key);
101
- return span;
143
+ return {
144
+ runner,
145
+ enabled: options.enabled ?? true,
146
+ spans: options.spans,
147
+ };
102
148
  }
103
149
 
104
150
  // ---------------------------------------------------------------------------
105
- // Adapter factory
151
+ // Telemetry sink: discrete-fact instant spans (the `telemetry` slot)
106
152
  // ---------------------------------------------------------------------------
107
153
 
108
154
  /**
109
- * Create a TelemetrySink that maps router lifecycle events to OTel spans.
155
+ * Create a TelemetrySink that maps the router's discrete-fact events to instant
156
+ * OTel spans. One span per fact; no duration spans.
157
+ *
158
+ * Fact mapping:
159
+ * - handler.error -> "rango.handler.error" (error)
160
+ * - cache.decision -> "rango.cache.decision"
161
+ * - revalidation.decision -> "rango.revalidation.decision"
162
+ * - request.timeout -> "rango.request.timeout" (error)
163
+ * - request.origin-rejected -> "rango.request.origin-rejected" (error)
110
164
  *
111
- * Span mapping:
112
- * - request.start / request.end / request.error → "rango.request" span
113
- * - loader.start / loader.end / loader.error → "rango.loader" span
114
- * - handler.error → "rango.handler.error" instant span
115
- * - cache.decision → "rango.cache.decision" instant span
116
- * - revalidation.decision → "rango.revalidation.decision" instant span
165
+ * Request and loader PHASE spans are intentionally NOT emitted here — they are
166
+ * owned by the tracing slot (createOTelTracing) so the two layers cannot produce
167
+ * duplicate rango.request / rango.loader spans. request.start/end and
168
+ * loader.start/end events are no-ops for this sink.
117
169
  *
118
170
  * Attributes use the `rango.*` namespace for router-specific data and
119
171
  * `http.method` / `http.route` for HTTP semantics.
120
172
  */
121
173
  export function createOTelSink(tracer: OTelTracer): TelemetrySink {
122
- const requestSpans = new Map<string, OTelSpan[]>();
123
- const loaderSpans = new Map<string, OTelSpan[]>();
174
+ const instant = (
175
+ name: string,
176
+ attributes: Record<string, string | number | boolean>,
177
+ ): OTelSpan => tracer.startSpan(name, { attributes });
124
178
 
125
179
  return {
126
180
  emit(event: TelemetryEvent): void {
127
181
  switch (event.type) {
128
- case "request.start": {
129
- const span = tracer.startSpan("rango.request", {
130
- attributes: {
131
- "http.method": event.method,
132
- "http.route": event.pathname,
133
- "rango.transaction": event.transaction,
134
- "rango.is_partial": event.isPartial,
135
- },
136
- });
137
- pushSpan(requestSpans, requestKey(event), span);
138
- break;
139
- }
140
-
141
- case "request.end": {
142
- const span = popSpan(requestSpans, requestKey(event));
143
- if (span) {
144
- span.setAttribute("rango.duration_ms", event.durationMs);
145
- span.setAttribute("rango.segment_count", event.segmentCount);
146
- span.setAttribute("rango.cache.hit", event.cacheHit);
147
- span.setStatus({ code: STATUS_OK });
148
- span.end();
149
- }
150
- break;
151
- }
152
-
153
- case "request.error": {
154
- const span = popSpan(requestSpans, requestKey(event));
155
- if (span) {
156
- span.setAttribute("rango.duration_ms", event.durationMs);
157
- span.setAttribute("rango.phase", event.phase);
158
- span.recordException(event.error);
159
- span.setStatus({
160
- code: STATUS_ERROR,
161
- message: event.error.message,
162
- });
163
- span.end();
164
- }
165
- break;
166
- }
167
-
168
- case "loader.start": {
169
- const span = tracer.startSpan("rango.loader", {
170
- attributes: {
171
- "rango.segment_id": event.segmentId,
172
- "rango.loader_name": event.loaderName,
173
- "http.route": event.pathname,
174
- },
175
- });
176
- pushSpan(loaderSpans, loaderKey(event), span);
177
- break;
178
- }
179
-
180
- case "loader.end": {
181
- const key = loaderKey(event);
182
- const span = popSpan(loaderSpans, key);
183
- if (span) {
184
- span.setAttribute("rango.duration_ms", event.durationMs);
185
- span.setAttribute("rango.loader.ok", event.ok);
186
- span.setStatus({ code: event.ok ? STATUS_OK : STATUS_ERROR });
187
- span.end();
188
- }
189
- break;
190
- }
191
-
192
- case "loader.error": {
193
- const key = loaderKey(event);
194
- const span = popSpan(loaderSpans, key);
195
- if (span) {
196
- span.setAttribute(
197
- "rango.handled_by_boundary",
198
- event.handledByBoundary,
199
- );
200
- span.recordException(event.error);
201
- span.setStatus({
202
- code: STATUS_ERROR,
203
- message: event.error.message,
204
- });
205
- span.end();
206
- } else {
207
- // No matching start — create a standalone error span
208
- const errorSpan = tracer.startSpan("rango.loader", {
209
- attributes: {
210
- "rango.segment_id": event.segmentId,
211
- "rango.loader_name": event.loaderName,
212
- "http.route": event.pathname,
213
- "rango.handled_by_boundary": event.handledByBoundary,
214
- },
215
- });
216
- errorSpan.recordException(event.error);
217
- errorSpan.setStatus({
218
- code: STATUS_ERROR,
219
- message: event.error.message,
220
- });
221
- errorSpan.end();
222
- }
223
- break;
224
- }
225
-
226
182
  case "handler.error": {
227
183
  const attrs: Record<string, string | number | boolean> = {
228
184
  "rango.handled_by_boundary": event.handledByBoundary,
@@ -232,13 +188,10 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
232
188
  attrs["rango.segment_type"] = event.segmentType;
233
189
  if (event.pathname) attrs["http.route"] = event.pathname;
234
190
  if (event.routeKey) attrs["rango.route_key"] = event.routeKey;
235
- if (event.params) {
191
+ if (event.params)
236
192
  attrs["rango.params"] = JSON.stringify(event.params);
237
- }
238
193
 
239
- const span = tracer.startSpan("rango.handler.error", {
240
- attributes: attrs,
241
- });
194
+ const span = instant("rango.handler.error", attrs);
242
195
  span.recordException(event.error);
243
196
  span.setStatus({ code: STATUS_ERROR, message: event.error.message });
244
197
  span.end();
@@ -253,26 +206,55 @@ export function createOTelSink(tracer: OTelTracer): TelemetrySink {
253
206
  "rango.cache.should_revalidate": event.shouldRevalidate,
254
207
  };
255
208
  if (event.source) attrs["rango.cache.source"] = event.source;
209
+ instant("rango.cache.decision", attrs).end();
210
+ break;
211
+ }
256
212
 
257
- const span = tracer.startSpan("rango.cache.decision", {
258
- attributes: attrs,
213
+ case "revalidation.decision": {
214
+ instant("rango.revalidation.decision", {
215
+ "rango.segment_id": event.segmentId,
216
+ "http.route": event.pathname,
217
+ "rango.route_key": event.routeKey,
218
+ "rango.revalidate": event.shouldRevalidate,
219
+ }).end();
220
+ break;
221
+ }
222
+
223
+ case "request.timeout": {
224
+ const attrs: Record<string, string | number | boolean> = {
225
+ "rango.phase": event.phase,
226
+ "http.route": event.pathname,
227
+ "rango.duration_ms": event.durationMs,
228
+ "rango.timeout.custom_handler": event.customHandler,
229
+ };
230
+ if (event.routeKey) attrs["rango.route_key"] = event.routeKey;
231
+ if (event.actionId) attrs["rango.action_id"] = event.actionId;
232
+ const span = instant("rango.request.timeout", attrs);
233
+ span.setStatus({
234
+ code: STATUS_ERROR,
235
+ message: `timeout: ${event.phase}`,
259
236
  });
260
237
  span.end();
261
238
  break;
262
239
  }
263
240
 
264
- case "revalidation.decision": {
265
- const span = tracer.startSpan("rango.revalidation.decision", {
266
- attributes: {
267
- "rango.segment_id": event.segmentId,
268
- "http.route": event.pathname,
269
- "rango.route_key": event.routeKey,
270
- "rango.revalidate": event.shouldRevalidate,
271
- },
272
- });
241
+ case "request.origin-rejected": {
242
+ const attrs: Record<string, string | number | boolean> = {
243
+ "http.method": event.method,
244
+ "http.route": event.pathname,
245
+ "rango.phase": event.phase,
246
+ };
247
+ if (event.origin) attrs["rango.origin"] = event.origin;
248
+ if (event.host) attrs["http.host"] = event.host;
249
+ const span = instant("rango.request.origin-rejected", attrs);
250
+ span.setStatus({ code: STATUS_ERROR, message: "origin rejected" });
273
251
  span.end();
274
252
  break;
275
253
  }
254
+
255
+ // request.start/end/error and loader.start/end/error are phase events;
256
+ // their spans are owned by the tracing slot (createOTelTracing), so this
257
+ // sink ignores them to avoid duplicate rango.request / rango.loader spans.
276
258
  }
277
259
  },
278
260
  };