@decocms/nextjs 8.4.0 → 8.5.0

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Deco CMS for any [Next.js](https://nextjs.org) App Router app. It makes no hosting assumptions: no CDN, ISR or platform behaviour is built in. It is a set of small helpers your site calls from its own proxy, layout, pages and `next.config`.
4
4
 
5
- Peers: `@decocms/blocks` (same version), `next` 16, `react` 19.
5
+ Peers: `@decocms/blocks` (same version), `next` 16, `react` 19; optionally `@opentelemetry/api` (see [Request metric](#request-metric)).
6
6
 
7
7
  | Import | What it is for |
8
8
  | --- | --- |
@@ -10,6 +10,7 @@ Peers: `@decocms/blocks` (same version), `next` 16, `react` 19.
10
10
  | `@decocms/nextjs/server` | Draft previews in Server Components: `bindDraft`, `activeDraftPointer`, `contentClient`. |
11
11
  | `@decocms/nextjs` | `DraftPreviewIndicator` (the "Preview mode" badge on a draft page), `StudioBridge` (lets Studio's site editor drive the page) and `StudioPage` (tells it which page is on screen), plus the draft names and Studio's origins (below). |
12
12
  | `@decocms/nextjs/config` | `withDeco(nextConfig)`: the draft header rules and Studio's dev origins. CommonJS, so `next.config.js` can `require()` it. |
13
+ | `@decocms/nextjs/instrumentation` | `register`, the `instrumentation.ts` hook that records `http.server.request.duration` from Next's own request span, and `serverRequestSpanProcessor()` for a site with its own OpenTelemetry SDK. |
13
14
  | `@decocms/nextjs/image-loader` | `createDecoImageLoader(imageUrl)`: a next/image loader over an `ImageUrl` from `@decocms/blocks/image`. |
14
15
 
15
16
  `createCMS` reads `NODE_ENV` to decide dev mode, so `next dev` never swaps the hosted release in over your local content; nothing needs passing. `createCMS({ dev })` still overrides it when a site needs to.
@@ -89,6 +90,27 @@ module.exports = withDeco({ images: { loader: "custom", loaderFile: "./src/image
89
90
 
90
91
  `withDeco` puts v7's two draft header rules before your own (a `?__draft=` query, or the draft cookie: no-store, `Vary: Cookie`, noindex) and adds Studio's origins to `allowedDevOrigins`. Each can be turned off: `withDeco(config, { draftHeaders: false, studioOrigins: false })`. Next sets `Cache-Control` and `Vary` itself on dynamic pages, so only `X-Robots-Tag` is sure to arrive from these rules; the proxy's `applyDraft` sets all three on the responses it makes.
91
92
 
93
+ ## Request metric
94
+
95
+ ```ts
96
+ // src/instrumentation.ts (or instrumentation.ts at the root, next to app/)
97
+ export { register } from "@decocms/nextjs/instrumentation";
98
+ ```
99
+
100
+ Next.js already measures every request it serves: one root span per request (`BaseServer.handleRequest`) that ends with the status, the method and the route's template. `register` gives Next a tracer provider whose only job is that span: when it ends, the request goes to the CMS's telemetry as `http.server.request.duration` (through `recordServerRequest` from `@decocms/blocks/metrics`) (seconds, the same buckets as the upstream clients' `http.client.request.duration`), next to everything else `createCMS` sends. No span is exported anywhere, and every other span Next asks for is a shared non-recording one. With no telemetry destination (no `token` and no `endpoint`) nothing is recorded.
101
+
102
+ | Label | Value |
103
+ | --- | --- |
104
+ | `http.route` | Next's route template: `/[[...path]]`, `/blog`, `/_draft/[[...path]]`. An RSC request counts under its page's route. `unmatched` when Next names none (an Edge route under `next start`) |
105
+ | `http.request.method` | `GET`, `POST`, …; any other is `_OTHER` |
106
+ | `status_class` | `2xx` … `5xx`; a request that threw is `5xx` |
107
+ | `cache` | `DRAFT` for a draft render (the proxy forwarded `x-deco-draft`, or the request was rewritten onto `/_draft`), otherwise `BYPASS`: Next decides its own cache (`x-nextjs-cache`) on the response, which the span never sees |
108
+
109
+ - `@opentelemetry/api` is an optional peer: `register` uses the same copy Next does (yours when installed, else Next's compiled one) and does nothing if neither loads.
110
+ - **Your own OpenTelemetry setup** (`@vercel/otel`, `NodeSDK`): only one tracer provider can be registered, so `register` leaves yours alone and logs how to combine them. Add the processor to yours instead: `registerOTel({ serviceName, spanProcessors: ["auto", serverRequestSpanProcessor()] })`.
111
+ - **The proxy**: under `next start` Next runs the proxy through a request span of its own; that one is not counted, and the request the proxy hands on is. A response the proxy makes itself (a redirect, a 404 for a direct `/_draft` hit) never reaches a page and is not counted.
112
+ - **Edge runtime**: `register` does nothing there. Under `next start` an Edge route is still measured by the Node.js server that serves it (`unmatched`, no template); on a host that runs Edge functions on their own, nothing measures them.
113
+
92
114
  ## Images
93
115
 
94
116
  ```ts
package/dist/draft.d.ts CHANGED
@@ -25,6 +25,8 @@ export declare const DRAFT_COOKIE = "__deco_draft";
25
25
  * client can't send one itself.
26
26
  */
27
27
  export declare const DRAFT_HEADER = "x-deco-draft";
28
+ /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
29
+ export declare const DRAFT_ROUTE_PREFIX = "/_draft";
28
30
  /**
29
31
  * The response headers of a page rendered from a draft: never stored by a
30
32
  * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
@@ -1 +1 @@
1
- {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO/F"}
1
+ {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO/F"}
package/dist/draft.js CHANGED
@@ -25,6 +25,8 @@ export const DRAFT_COOKIE = "__deco_draft";
25
25
  * client can't send one itself.
26
26
  */
27
27
  export const DRAFT_HEADER = "x-deco-draft";
28
+ /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
29
+ export const DRAFT_ROUTE_PREFIX = "/_draft";
28
30
  /**
29
31
  * The response headers of a page rendered from a draft: never stored by a
30
32
  * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
package/dist/draft.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AAErC;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAkC;IACrE,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO;QACL,eAAe,EAAE,mBAAmB;QACpC,IAAI,EAAE,QAAQ;QACd,cAAc,EAAE,mBAAmB;KACpC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AAErC;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C,uFAAuF;AACvF,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAkC;IACrE,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO;QACL,eAAe,EAAE,mBAAmB;QACpC,IAAI,EAAE,QAAQ;QACd,cAAc,EAAE,mBAAmB;KACpC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `@decocms/nextjs/instrumentation`: `http.server.request.duration` for a
3
+ * Next.js site, from the request span Next.js already opens.
4
+ *
5
+ * Next.js measures every request it serves: `BaseServer.handleRequest` opens
6
+ * one root span per request and, when the response is done, sets its status,
7
+ * its method and, when Next routed it, the route's template (`/[slug]/p`,
8
+ * not the path). It creates spans only while a tracer provider is registered
9
+ * through `@opentelemetry/api` (the app's copy, or Next's compiled one when
10
+ * the app has none; both read the same global registration).
11
+ *
12
+ * `register()`, the site's `instrumentation.ts` hook, registers a provider
13
+ * whose only job is that span: when it ends, the request is recorded with
14
+ * `recordServerRequest` from `@decocms/blocks/metrics`, into the CMS's
15
+ * telemetry in the same process. No span is exported anywhere and every other
16
+ * span Next.js asks for is a shared non-recording one. With no
17
+ * `@opentelemetry/api` reachable, on the Edge runtime, or when another
18
+ * provider is already registered, it registers nothing and never throws.
19
+ *
20
+ * A site that registers its own OpenTelemetry SDK (`@vercel/otel`,
21
+ * `NodeSDK`) adds `serverRequestSpanProcessor()` to that SDK's span
22
+ * processors instead.
23
+ *
24
+ * Labels: `http.route` is Next's route template (`unmatched` when Next names
25
+ * none); RSC requests count under their page's route, as the requests they
26
+ * are. `cache` is `DRAFT` for a draft render (the proxy forwarded
27
+ * `x-deco-draft`, or the request was rewritten onto `/_draft`) and otherwise
28
+ * `BYPASS`: Next decides its cache (`x-nextjs-cache`) on the response, which
29
+ * the span never sees.
30
+ */
31
+ type Attributes = Record<string, unknown>;
32
+ /** What `serverRequestSpanProcessor()` reads from an ended span (an SDK's `ReadableSpan`). */
33
+ interface EndedSpan {
34
+ readonly attributes: Attributes;
35
+ /** `[seconds, nanoseconds]`, as OpenTelemetry's `HrTime`. */
36
+ readonly duration: readonly [number, number];
37
+ readonly status?: {
38
+ readonly code: number;
39
+ };
40
+ }
41
+ /** An OpenTelemetry SDK `SpanProcessor` that records nothing but the request metric. */
42
+ export interface ServerRequestSpanProcessor {
43
+ onStart(span: unknown, parentContext: unknown): void;
44
+ onEnd(span: EndedSpan): void;
45
+ forceFlush(): Promise<void>;
46
+ shutdown(): Promise<void>;
47
+ }
48
+ /**
49
+ * For a site with its own OpenTelemetry SDK: a span processor that records
50
+ * `http.server.request.duration` from Next's request span and leaves every
51
+ * span to the SDK's other processors and exporters.
52
+ *
53
+ * ```ts
54
+ * registerOTel({ serviceName: "my-store", spanProcessors: ["auto", serverRequestSpanProcessor()] });
55
+ * ```
56
+ */
57
+ export declare function serverRequestSpanProcessor(): ServerRequestSpanProcessor;
58
+ /**
59
+ * The site's `instrumentation.ts` hook:
60
+ *
61
+ * ```ts
62
+ * // instrumentation.ts
63
+ * export { register } from "@decocms/nextjs/instrumentation";
64
+ * ```
65
+ *
66
+ * Registers the request-metric tracer provider, once per process. Does
67
+ * nothing on the Edge runtime (the Node.js server's request span covers the
68
+ * Edge pages it serves), without `@opentelemetry/api` (the app's or Next's),
69
+ * without `AsyncLocalStorage`, or when another provider is registered (it
70
+ * then says to use `serverRequestSpanProcessor()`). Never throws.
71
+ */
72
+ export declare function register(): Promise<void>;
73
+ export {};
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/instrumentation/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AA4BH,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAQ1C,8FAA8F;AAC9F,UAAU,SAAS;IACjB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,wFAAwF;AACxF,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;IACrD,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAgCD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,IAAI,0BAA0B,CAiBvE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAyB9C"}
@@ -0,0 +1,294 @@
1
+ /**
2
+ * `@decocms/nextjs/instrumentation`: `http.server.request.duration` for a
3
+ * Next.js site, from the request span Next.js already opens.
4
+ *
5
+ * Next.js measures every request it serves: `BaseServer.handleRequest` opens
6
+ * one root span per request and, when the response is done, sets its status,
7
+ * its method and, when Next routed it, the route's template (`/[slug]/p`,
8
+ * not the path). It creates spans only while a tracer provider is registered
9
+ * through `@opentelemetry/api` (the app's copy, or Next's compiled one when
10
+ * the app has none; both read the same global registration).
11
+ *
12
+ * `register()`, the site's `instrumentation.ts` hook, registers a provider
13
+ * whose only job is that span: when it ends, the request is recorded with
14
+ * `recordServerRequest` from `@decocms/blocks/metrics`, into the CMS's
15
+ * telemetry in the same process. No span is exported anywhere and every other
16
+ * span Next.js asks for is a shared non-recording one. With no
17
+ * `@opentelemetry/api` reachable, on the Edge runtime, or when another
18
+ * provider is already registered, it registers nothing and never throws.
19
+ *
20
+ * A site that registers its own OpenTelemetry SDK (`@vercel/otel`,
21
+ * `NodeSDK`) adds `serverRequestSpanProcessor()` to that SDK's span
22
+ * processors instead.
23
+ *
24
+ * Labels: `http.route` is Next's route template (`unmatched` when Next names
25
+ * none); RSC requests count under their page's route, as the requests they
26
+ * are. `cache` is `DRAFT` for a draft render (the proxy forwarded
27
+ * `x-deco-draft`, or the request was rewritten onto `/_draft`) and otherwise
28
+ * `BYPASS`: Next decides its cache (`x-nextjs-cache`) on the response, which
29
+ * the span never sees.
30
+ */
31
+ import { recordServerRequest } from "@decocms/blocks/metrics";
32
+ import { DRAFT_HEADER, DRAFT_ROUTE_PREFIX } from "../draft.js";
33
+ /** The span type Next.js gives its one root span per request. */
34
+ const ROOT_SPAN_TYPE = "BaseServer.handleRequest";
35
+ const REGISTERED = Symbol.for("decocms.nextjs.instrumentation");
36
+ /**
37
+ * Under `next start`, the router runs the proxy (`proxy.ts`/`middleware.ts`)
38
+ * through a `handleRequest` of its own, which ends by "bubbling" the proxy's
39
+ * result back to the router (`next.bubble`). That span is the proxy's, not a
40
+ * request: the request it hands on is measured by its own span.
41
+ */
42
+ const BUBBLE = "next.bubble";
43
+ /** OpenTelemetry's `SpanStatusCode.ERROR`. */
44
+ const STATUS_ERROR = 2;
45
+ /** Records one ended `BaseServer.handleRequest` span. Never throws. */
46
+ function recordRequestSpan(attributes, seconds, errored, draft) {
47
+ const route = typeof attributes["http.route"] === "string" ? attributes["http.route"] : undefined;
48
+ const code = Number(attributes["http.status_code"]);
49
+ // Next sets the status before the span ends; a request that threw is a 5xx whatever it says.
50
+ const status = errored && !(code >= 500) ? 500 : code || 500;
51
+ // Never rejects; the span's route is already a string, so nothing to wait for.
52
+ void recordServerRequest({
53
+ route,
54
+ method: String(attributes["http.method"] ?? ""),
55
+ status,
56
+ cache: draft || isDraftRoute(route) ? "DRAFT" : "BYPASS",
57
+ seconds,
58
+ });
59
+ }
60
+ function isDraftRoute(route) {
61
+ if (route === undefined)
62
+ return false;
63
+ const path = route.replace(/^\/%5Fdraft(?=\/|$)/i, DRAFT_ROUTE_PREFIX);
64
+ return path === DRAFT_ROUTE_PREFIX || path.startsWith(`${DRAFT_ROUTE_PREFIX}/`);
65
+ }
66
+ /**
67
+ * For a site with its own OpenTelemetry SDK: a span processor that records
68
+ * `http.server.request.duration` from Next's request span and leaves every
69
+ * span to the SDK's other processors and exporters.
70
+ *
71
+ * ```ts
72
+ * registerOTel({ serviceName: "my-store", spanProcessors: ["auto", serverRequestSpanProcessor()] });
73
+ * ```
74
+ */
75
+ export function serverRequestSpanProcessor() {
76
+ return {
77
+ onStart() { },
78
+ onEnd(span) {
79
+ try {
80
+ const attributes = span.attributes;
81
+ if (attributes?.["next.span_type"] !== ROOT_SPAN_TYPE || attributes[BUBBLE] === true)
82
+ return;
83
+ const [s, ns] = span.duration;
84
+ recordRequestSpan(attributes, s + ns / 1e9, span.status?.code === STATUS_ERROR, false);
85
+ }
86
+ catch {
87
+ // A metric never breaks a request.
88
+ }
89
+ },
90
+ forceFlush: () => Promise.resolve(),
91
+ shutdown: () => Promise.resolve(),
92
+ };
93
+ }
94
+ /**
95
+ * The site's `instrumentation.ts` hook:
96
+ *
97
+ * ```ts
98
+ * // instrumentation.ts
99
+ * export { register } from "@decocms/nextjs/instrumentation";
100
+ * ```
101
+ *
102
+ * Registers the request-metric tracer provider, once per process. Does
103
+ * nothing on the Edge runtime (the Node.js server's request span covers the
104
+ * Edge pages it serves), without `@opentelemetry/api` (the app's or Next's),
105
+ * without `AsyncLocalStorage`, or when another provider is registered (it
106
+ * then says to use `serverRequestSpanProcessor()`). Never throws.
107
+ */
108
+ export async function register() {
109
+ try {
110
+ const global = globalThis;
111
+ if (global[REGISTERED] === true || typeof global.EdgeRuntime === "string")
112
+ return;
113
+ const Storage = global.AsyncLocalStorage;
114
+ if (typeof Storage !== "function")
115
+ return;
116
+ const api = await loadApi();
117
+ if (api === undefined)
118
+ return;
119
+ global[REGISTERED] = true;
120
+ const draft = api.createContextKey("decocms.nextjs.draft");
121
+ if (!api.trace.setGlobalTracerProvider(requestTracerProvider(api, draft))) {
122
+ console.warn("[@decocms/nextjs] Another OpenTelemetry tracer provider is registered, so register() added nothing. " +
123
+ "To record http.server.request.duration, add serverRequestSpanProcessor() to that provider's span processors.");
124
+ return;
125
+ }
126
+ // Both may already be registered by something else; Next's spans work with either.
127
+ api.context.setGlobalContextManager(storageContextManager(api, new Storage()));
128
+ api.propagation.setGlobalPropagator(draftPropagator(draft));
129
+ }
130
+ catch {
131
+ // Instrumentation never stops a server from starting.
132
+ }
133
+ }
134
+ /**
135
+ * The `@opentelemetry/api` Next.js uses: the app's own when installed (Next's
136
+ * bundlers alias it to Next's compiled copy when it isn't), else Next's
137
+ * compiled copy.
138
+ */
139
+ async function loadApi() {
140
+ try {
141
+ return await import("@opentelemetry/api");
142
+ }
143
+ catch {
144
+ try {
145
+ return (await import("next/dist/compiled/@opentelemetry/api/index.js"));
146
+ }
147
+ catch {
148
+ return undefined;
149
+ }
150
+ }
151
+ }
152
+ function requestTracerProvider(api, DRAFT) {
153
+ const IN_REQUEST = api.createContextKey("decocms.nextjs.inRequest");
154
+ const inert = api.trace.wrapSpanContext(api.INVALID_SPAN_CONTEXT);
155
+ const tracer = {
156
+ startSpan(_name, options, context) {
157
+ const ctx = context ?? api.context.active();
158
+ if (options?.attributes?.["next.span_type"] !== ROOT_SPAN_TYPE)
159
+ return inert;
160
+ // A request Next serves inside another (a nested handleRequest) is already measured.
161
+ if (ctx.getValue(IN_REQUEST) === true)
162
+ return inert;
163
+ return new RequestSpan(api.INVALID_SPAN_CONTEXT, options.attributes, ctx.getValue(DRAFT) === true);
164
+ },
165
+ startActiveSpan(name, ...rest) {
166
+ const fn = rest[rest.length - 1];
167
+ const options = (rest.length > 1 ? rest[0] : undefined);
168
+ const context = (rest.length > 2 ? rest[1] : undefined);
169
+ const span = tracer.startSpan(name, options, context);
170
+ // Only the request span changes the context; every other span costs a call.
171
+ if (span === inert)
172
+ return fn(span);
173
+ const ctx = api.trace
174
+ .setSpan(context ?? api.context.active(), span)
175
+ .setValue(IN_REQUEST, true);
176
+ return api.context.with(ctx, fn, undefined, span);
177
+ },
178
+ };
179
+ // Not named NoopTracerProvider: Next checks that name to decide whether tracing is on.
180
+ return { getTracer: () => tracer };
181
+ }
182
+ /** Marks the request's context when the proxy forwarded an accepted draft. */
183
+ function draftPropagator(DRAFT) {
184
+ return {
185
+ inject() { },
186
+ extract(context, carrier, getter) {
187
+ try {
188
+ const value = getter.get(carrier, DRAFT_HEADER);
189
+ return value !== undefined && value !== "" ? context.setValue(DRAFT, true) : context;
190
+ }
191
+ catch {
192
+ return context;
193
+ }
194
+ },
195
+ fields: () => [],
196
+ };
197
+ }
198
+ /** The request span: keeps only the attributes the metric needs and records on `end()`. */
199
+ class RequestSpan {
200
+ #context;
201
+ #attributes = {};
202
+ #start = performance.now();
203
+ #draft;
204
+ #errored = false;
205
+ #proxy = false;
206
+ #ended = false;
207
+ constructor(context, attributes, draft) {
208
+ this.#context = context;
209
+ this.#draft = draft;
210
+ if (attributes)
211
+ this.setAttributes(attributes);
212
+ }
213
+ spanContext() {
214
+ return this.#context;
215
+ }
216
+ setAttribute(key, value) {
217
+ if (key === "http.route" || key === "http.status_code" || key === "http.method") {
218
+ this.#attributes[key] = value;
219
+ }
220
+ else if (key === BUBBLE && value === true) {
221
+ this.#proxy = true;
222
+ }
223
+ return this;
224
+ }
225
+ setAttributes(attributes) {
226
+ for (const key in attributes)
227
+ this.setAttribute(key, attributes[key]);
228
+ return this;
229
+ }
230
+ setStatus(status) {
231
+ if (status?.code === STATUS_ERROR)
232
+ this.#errored = true;
233
+ return this;
234
+ }
235
+ addEvent() {
236
+ return this;
237
+ }
238
+ addLink() {
239
+ return this;
240
+ }
241
+ addLinks() {
242
+ return this;
243
+ }
244
+ updateName() {
245
+ return this;
246
+ }
247
+ recordException(exception) {
248
+ // The proxy's own invocation ends with an error marked `bubble`, whatever its type.
249
+ if (exception?.bubble === true)
250
+ this.#proxy = true;
251
+ }
252
+ isRecording() {
253
+ return !this.#ended;
254
+ }
255
+ end() {
256
+ if (this.#ended)
257
+ return;
258
+ this.#ended = true;
259
+ if (this.#proxy)
260
+ return;
261
+ try {
262
+ const seconds = (performance.now() - this.#start) / 1000;
263
+ recordRequestSpan(this.#attributes, seconds, this.#errored, this.#draft);
264
+ }
265
+ catch {
266
+ // A metric never breaks a request.
267
+ }
268
+ }
269
+ }
270
+ /** OpenTelemetry's context manager over `AsyncLocalStorage` (what `@opentelemetry/context-async-hooks` does). */
271
+ function storageContextManager(api, storage) {
272
+ const manager = {
273
+ active: () => storage.getStore() ?? api.ROOT_CONTEXT,
274
+ with(context, fn, thisArg, ...args) {
275
+ const call = thisArg === undefined ? fn : fn.bind(thisArg);
276
+ return storage.run(context, call, ...args);
277
+ },
278
+ bind(context, target) {
279
+ if (typeof target !== "function")
280
+ return target;
281
+ const fn = target;
282
+ return function (...args) {
283
+ return manager.with(context, () => fn.apply(this, args));
284
+ };
285
+ },
286
+ enable: () => manager,
287
+ disable() {
288
+ storage.disable();
289
+ return manager;
290
+ },
291
+ };
292
+ return manager;
293
+ }
294
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/instrumentation/index.ts"],"names":[],"mappings":"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"}
@@ -18,6 +18,10 @@
18
18
  * is rewritten onto a dynamic draft route (`/_draft/<path>`, mounted as
19
19
  * `app/%5Fdraft/...`: a literal `_draft` folder is private in Next and never
20
20
  * routes).
21
+ *
22
+ * The proxy measures no requests: it runs before Next routes and renders the
23
+ * page. `http.server.request.duration` comes from Next's own request span
24
+ * (`register` from `@decocms/nextjs/instrumentation`).
21
25
  */
22
26
  import type { CMS } from "@decocms/blocks";
23
27
  import { type NextRequest, NextResponse } from "next/server.js";
@@ -32,8 +36,7 @@ export interface DraftDecision {
32
36
  /** The `Set-Cookie` the response carries: storing a draft link's pointer, or ending a preview. */
33
37
  setCookie: string | null;
34
38
  }
35
- /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
36
- export declare const DRAFT_ROUTE_PREFIX = "/_draft";
39
+ export { DRAFT_ROUTE_PREFIX } from "../draft.ts";
37
40
  /**
38
41
  * The draft a request renders and the cookie its response sets, under
39
42
  * `policy` (the same options the pages pass to `bindDraft`). A request with
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEhE,OAAO,EAML,KAAK,aAAa,EAEnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,oBAAoB,EACpB,KAAK,aAAa,EAClB,mBAAmB,EACnB,WAAW,GACZ,MAAM,cAAc,CAAC;AAEtB,4FAA4F;AAC5F,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,GAAG,YAAY,CAAC,CAAC;AAE9E,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kGAAkG;IAClG,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAI5C;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,WAAW,GAAG,OAAO,EAC9B,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,aAAa,CAAC,CAYxB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,WAAW,GAAG,OAAO,EAC9B,QAAQ,EAAE,aAAa,GACtB,OAAO,CAKT;AAED,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,aAAa,GACtB,YAAY,GAAG,IAAI,CAQrB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,aAAa,GAAG,CAAC,CAMtF;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;CAC7C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,SAAS,EACd,OAAO,GAAE,iBAAsB,GAC9B,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAUjD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,KAAK,WAAW,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEhE,OAAO,EAML,KAAK,aAAa,EAEnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,oBAAoB,EACpB,KAAK,aAAa,EAClB,mBAAmB,EACnB,WAAW,GACZ,MAAM,cAAc,CAAC;AAEtB,4FAA4F;AAC5F,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,GAAG,UAAU,GAAG,YAAY,CAAC,CAAC;AAE9E,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,kGAAkG;IAClG,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAIjD;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,SAAS,EACd,OAAO,EAAE,WAAW,GAAG,OAAO,EAC9B,MAAM,GAAE,aAAkB,GACzB,OAAO,CAAC,aAAa,CAAC,CAYxB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,WAAW,GAAG,OAAO,EAC9B,QAAQ,EAAE,aAAa,GACtB,OAAO,CAKT;AAED,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,aAAa,GACtB,YAAY,GAAG,IAAI,CAQrB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,aAAa,GAAG,CAAC,CAMtF;AAED,MAAM,WAAW,iBAAkB,SAAQ,aAAa;IACtD;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;CAC7C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,SAAS,EACd,OAAO,GAAE,iBAAsB,GAC9B,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAUjD"}
@@ -1,9 +1,8 @@
1
1
  import { NextResponse } from "next/server.js";
2
- import { DRAFT_HEADER, draftResponseHeaders } from "../draft.js";
2
+ import { DRAFT_HEADER, DRAFT_ROUTE_PREFIX, draftResponseHeaders } from "../draft.js";
3
3
  import { allowedDraft, draftCookie, endsPreview, fromDraftLink, hasDraftSignal, requestLike, } from "../drafts.js";
4
4
  export { inferredPreviewHosts, previewHostDecision, requestLike, } from "../drafts.js";
5
- /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
6
- export const DRAFT_ROUTE_PREFIX = "/_draft";
5
+ export { DRAFT_ROUTE_PREFIX } from "../draft.js";
7
6
  const INERT = Object.freeze({ pointer: null, setCookie: null });
8
7
  /**
9
8
  * The draft a request renders and the cookie its response sets, under
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/proxy/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/nextjs",
3
- "version": "8.4.0",
3
+ "version": "8.5.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -38,6 +38,11 @@
38
38
  "types": "./src/config/index.d.cts",
39
39
  "default": "./src/config/index.cjs"
40
40
  },
41
+ "./instrumentation": {
42
+ "types": "./dist/instrumentation/index.d.ts",
43
+ "source": "./src/instrumentation/index.ts",
44
+ "default": "./dist/instrumentation/index.js"
45
+ },
41
46
  "./image-loader": {
42
47
  "types": "./dist/image-loader/index.d.ts",
43
48
  "source": "./src/image-loader/index.ts",
@@ -52,11 +57,18 @@
52
57
  },
53
58
  "peerDependencies": {
54
59
  "@decocms/blocks": "^8.4.0",
60
+ "@opentelemetry/api": "^1.0.0",
55
61
  "next": "^16.0.0",
56
62
  "react": "^19.0.0"
57
63
  },
64
+ "peerDependenciesMeta": {
65
+ "@opentelemetry/api": {
66
+ "optional": true
67
+ }
68
+ },
58
69
  "devDependencies": {
59
- "@decocms/blocks": "8.4.0",
70
+ "@decocms/blocks": "8.5.0",
71
+ "@opentelemetry/api": "^1.9.1",
60
72
  "@types/react": "^19.0.0",
61
73
  "@types/react-dom": "^19.0.0",
62
74
  "next": "16.3.8",
package/src/draft.ts CHANGED
@@ -29,6 +29,9 @@ export const DRAFT_COOKIE = "__deco_draft";
29
29
  */
30
30
  export const DRAFT_HEADER = "x-deco-draft";
31
31
 
32
+ /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
33
+ export const DRAFT_ROUTE_PREFIX = "/_draft";
34
+
32
35
  /**
33
36
  * The response headers of a page rendered from a draft: never stored by a
34
37
  * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
@@ -0,0 +1,337 @@
1
+ /**
2
+ * `@decocms/nextjs/instrumentation`: `http.server.request.duration` for a
3
+ * Next.js site, from the request span Next.js already opens.
4
+ *
5
+ * Next.js measures every request it serves: `BaseServer.handleRequest` opens
6
+ * one root span per request and, when the response is done, sets its status,
7
+ * its method and, when Next routed it, the route's template (`/[slug]/p`,
8
+ * not the path). It creates spans only while a tracer provider is registered
9
+ * through `@opentelemetry/api` (the app's copy, or Next's compiled one when
10
+ * the app has none; both read the same global registration).
11
+ *
12
+ * `register()`, the site's `instrumentation.ts` hook, registers a provider
13
+ * whose only job is that span: when it ends, the request is recorded with
14
+ * `recordServerRequest` from `@decocms/blocks/metrics`, into the CMS's
15
+ * telemetry in the same process. No span is exported anywhere and every other
16
+ * span Next.js asks for is a shared non-recording one. With no
17
+ * `@opentelemetry/api` reachable, on the Edge runtime, or when another
18
+ * provider is already registered, it registers nothing and never throws.
19
+ *
20
+ * A site that registers its own OpenTelemetry SDK (`@vercel/otel`,
21
+ * `NodeSDK`) adds `serverRequestSpanProcessor()` to that SDK's span
22
+ * processors instead.
23
+ *
24
+ * Labels: `http.route` is Next's route template (`unmatched` when Next names
25
+ * none); RSC requests count under their page's route, as the requests they
26
+ * are. `cache` is `DRAFT` for a draft render (the proxy forwarded
27
+ * `x-deco-draft`, or the request was rewritten onto `/_draft`) and otherwise
28
+ * `BYPASS`: Next decides its cache (`x-nextjs-cache`) on the response, which
29
+ * the span never sees.
30
+ */
31
+
32
+ import { recordServerRequest } from "@decocms/blocks/metrics";
33
+ import type {
34
+ Context,
35
+ ContextManager,
36
+ Span,
37
+ SpanContext,
38
+ SpanOptions,
39
+ TextMapPropagator,
40
+ Tracer,
41
+ TracerProvider,
42
+ } from "@opentelemetry/api";
43
+ import { DRAFT_HEADER, DRAFT_ROUTE_PREFIX } from "../draft.ts";
44
+
45
+ type Api = typeof import("@opentelemetry/api");
46
+
47
+ /** The span type Next.js gives its one root span per request. */
48
+ const ROOT_SPAN_TYPE = "BaseServer.handleRequest";
49
+ const REGISTERED = Symbol.for("decocms.nextjs.instrumentation");
50
+ /**
51
+ * Under `next start`, the router runs the proxy (`proxy.ts`/`middleware.ts`)
52
+ * through a `handleRequest` of its own, which ends by "bubbling" the proxy's
53
+ * result back to the router (`next.bubble`). That span is the proxy's, not a
54
+ * request: the request it hands on is measured by its own span.
55
+ */
56
+ const BUBBLE = "next.bubble";
57
+
58
+ type Attributes = Record<string, unknown>;
59
+
60
+ interface AsyncLocalStorageLike<T> {
61
+ getStore(): T | undefined;
62
+ run<R>(store: T, fn: (...args: unknown[]) => R, ...args: unknown[]): R;
63
+ disable(): void;
64
+ }
65
+
66
+ /** What `serverRequestSpanProcessor()` reads from an ended span (an SDK's `ReadableSpan`). */
67
+ interface EndedSpan {
68
+ readonly attributes: Attributes;
69
+ /** `[seconds, nanoseconds]`, as OpenTelemetry's `HrTime`. */
70
+ readonly duration: readonly [number, number];
71
+ readonly status?: { readonly code: number };
72
+ }
73
+
74
+ /** An OpenTelemetry SDK `SpanProcessor` that records nothing but the request metric. */
75
+ export interface ServerRequestSpanProcessor {
76
+ onStart(span: unknown, parentContext: unknown): void;
77
+ onEnd(span: EndedSpan): void;
78
+ forceFlush(): Promise<void>;
79
+ shutdown(): Promise<void>;
80
+ }
81
+
82
+ /** OpenTelemetry's `SpanStatusCode.ERROR`. */
83
+ const STATUS_ERROR = 2;
84
+
85
+ /** Records one ended `BaseServer.handleRequest` span. Never throws. */
86
+ function recordRequestSpan(
87
+ attributes: Attributes,
88
+ seconds: number,
89
+ errored: boolean,
90
+ draft: boolean,
91
+ ): void {
92
+ const route = typeof attributes["http.route"] === "string" ? attributes["http.route"] : undefined;
93
+ const code = Number(attributes["http.status_code"]);
94
+ // Next sets the status before the span ends; a request that threw is a 5xx whatever it says.
95
+ const status = errored && !(code >= 500) ? 500 : code || 500;
96
+ // Never rejects; the span's route is already a string, so nothing to wait for.
97
+ void recordServerRequest({
98
+ route,
99
+ method: String(attributes["http.method"] ?? ""),
100
+ status,
101
+ cache: draft || isDraftRoute(route) ? "DRAFT" : "BYPASS",
102
+ seconds,
103
+ });
104
+ }
105
+
106
+ function isDraftRoute(route: string | undefined): boolean {
107
+ if (route === undefined) return false;
108
+ const path = route.replace(/^\/%5Fdraft(?=\/|$)/i, DRAFT_ROUTE_PREFIX);
109
+ return path === DRAFT_ROUTE_PREFIX || path.startsWith(`${DRAFT_ROUTE_PREFIX}/`);
110
+ }
111
+
112
+ /**
113
+ * For a site with its own OpenTelemetry SDK: a span processor that records
114
+ * `http.server.request.duration` from Next's request span and leaves every
115
+ * span to the SDK's other processors and exporters.
116
+ *
117
+ * ```ts
118
+ * registerOTel({ serviceName: "my-store", spanProcessors: ["auto", serverRequestSpanProcessor()] });
119
+ * ```
120
+ */
121
+ export function serverRequestSpanProcessor(): ServerRequestSpanProcessor {
122
+ return {
123
+ onStart() {},
124
+ onEnd(span) {
125
+ try {
126
+ const attributes = span.attributes;
127
+ if (attributes?.["next.span_type"] !== ROOT_SPAN_TYPE || attributes[BUBBLE] === true)
128
+ return;
129
+ const [s, ns] = span.duration;
130
+ recordRequestSpan(attributes, s + ns / 1e9, span.status?.code === STATUS_ERROR, false);
131
+ } catch {
132
+ // A metric never breaks a request.
133
+ }
134
+ },
135
+ forceFlush: () => Promise.resolve(),
136
+ shutdown: () => Promise.resolve(),
137
+ };
138
+ }
139
+
140
+ /**
141
+ * The site's `instrumentation.ts` hook:
142
+ *
143
+ * ```ts
144
+ * // instrumentation.ts
145
+ * export { register } from "@decocms/nextjs/instrumentation";
146
+ * ```
147
+ *
148
+ * Registers the request-metric tracer provider, once per process. Does
149
+ * nothing on the Edge runtime (the Node.js server's request span covers the
150
+ * Edge pages it serves), without `@opentelemetry/api` (the app's or Next's),
151
+ * without `AsyncLocalStorage`, or when another provider is registered (it
152
+ * then says to use `serverRequestSpanProcessor()`). Never throws.
153
+ */
154
+ export async function register(): Promise<void> {
155
+ try {
156
+ const global = globalThis as Record<PropertyKey, unknown>;
157
+ if (global[REGISTERED] === true || typeof global.EdgeRuntime === "string") return;
158
+ const Storage = global.AsyncLocalStorage as
159
+ | (new () => AsyncLocalStorageLike<Context>)
160
+ | undefined;
161
+ if (typeof Storage !== "function") return;
162
+ const api = await loadApi();
163
+ if (api === undefined) return;
164
+ global[REGISTERED] = true;
165
+ const draft = api.createContextKey("decocms.nextjs.draft");
166
+ if (!api.trace.setGlobalTracerProvider(requestTracerProvider(api, draft))) {
167
+ console.warn(
168
+ "[@decocms/nextjs] Another OpenTelemetry tracer provider is registered, so register() added nothing. " +
169
+ "To record http.server.request.duration, add serverRequestSpanProcessor() to that provider's span processors.",
170
+ );
171
+ return;
172
+ }
173
+ // Both may already be registered by something else; Next's spans work with either.
174
+ api.context.setGlobalContextManager(storageContextManager(api, new Storage()));
175
+ api.propagation.setGlobalPropagator(draftPropagator(draft));
176
+ } catch {
177
+ // Instrumentation never stops a server from starting.
178
+ }
179
+ }
180
+
181
+ /**
182
+ * The `@opentelemetry/api` Next.js uses: the app's own when installed (Next's
183
+ * bundlers alias it to Next's compiled copy when it isn't), else Next's
184
+ * compiled copy.
185
+ */
186
+ async function loadApi(): Promise<Api | undefined> {
187
+ try {
188
+ return await import("@opentelemetry/api");
189
+ } catch {
190
+ try {
191
+ return (await import("next/dist/compiled/@opentelemetry/api/index.js")) as Api;
192
+ } catch {
193
+ return undefined;
194
+ }
195
+ }
196
+ }
197
+
198
+ function requestTracerProvider(api: Api, DRAFT: symbol): TracerProvider {
199
+ const IN_REQUEST = api.createContextKey("decocms.nextjs.inRequest");
200
+ const inert = api.trace.wrapSpanContext(api.INVALID_SPAN_CONTEXT);
201
+
202
+ const tracer: Tracer = {
203
+ startSpan(_name: string, options?: SpanOptions, context?: Context): Span {
204
+ const ctx = context ?? api.context.active();
205
+ if (options?.attributes?.["next.span_type"] !== ROOT_SPAN_TYPE) return inert;
206
+ // A request Next serves inside another (a nested handleRequest) is already measured.
207
+ if (ctx.getValue(IN_REQUEST) === true) return inert;
208
+ return new RequestSpan(
209
+ api.INVALID_SPAN_CONTEXT,
210
+ options.attributes,
211
+ ctx.getValue(DRAFT) === true,
212
+ );
213
+ },
214
+ startActiveSpan(name: string, ...rest: unknown[]) {
215
+ const fn = rest[rest.length - 1] as (span: Span) => unknown;
216
+ const options = (rest.length > 1 ? rest[0] : undefined) as SpanOptions | undefined;
217
+ const context = (rest.length > 2 ? rest[1] : undefined) as Context | undefined;
218
+ const span = tracer.startSpan(name, options, context);
219
+ // Only the request span changes the context; every other span costs a call.
220
+ if (span === inert) return fn(span);
221
+ const ctx = api.trace
222
+ .setSpan(context ?? api.context.active(), span)
223
+ .setValue(IN_REQUEST, true);
224
+ return api.context.with(ctx, fn, undefined, span);
225
+ },
226
+ } as Tracer;
227
+
228
+ // Not named NoopTracerProvider: Next checks that name to decide whether tracing is on.
229
+ return { getTracer: () => tracer };
230
+ }
231
+
232
+ /** Marks the request's context when the proxy forwarded an accepted draft. */
233
+ function draftPropagator(DRAFT: symbol): TextMapPropagator {
234
+ return {
235
+ inject() {},
236
+ extract(context, carrier, getter) {
237
+ try {
238
+ const value = getter.get(carrier, DRAFT_HEADER);
239
+ return value !== undefined && value !== "" ? context.setValue(DRAFT, true) : context;
240
+ } catch {
241
+ return context;
242
+ }
243
+ },
244
+ fields: () => [],
245
+ };
246
+ }
247
+
248
+ /** The request span: keeps only the attributes the metric needs and records on `end()`. */
249
+ class RequestSpan implements Span {
250
+ readonly #context: SpanContext;
251
+ readonly #attributes: Attributes = {};
252
+ readonly #start = performance.now();
253
+ readonly #draft: boolean;
254
+ #errored = false;
255
+ #proxy = false;
256
+ #ended = false;
257
+
258
+ constructor(context: SpanContext, attributes: Attributes | undefined, draft: boolean) {
259
+ this.#context = context;
260
+ this.#draft = draft;
261
+ if (attributes) this.setAttributes(attributes);
262
+ }
263
+
264
+ spanContext(): SpanContext {
265
+ return this.#context;
266
+ }
267
+ setAttribute(key: string, value: unknown): this {
268
+ if (key === "http.route" || key === "http.status_code" || key === "http.method") {
269
+ this.#attributes[key] = value;
270
+ } else if (key === BUBBLE && value === true) {
271
+ this.#proxy = true;
272
+ }
273
+ return this;
274
+ }
275
+ setAttributes(attributes: Attributes): this {
276
+ for (const key in attributes) this.setAttribute(key, attributes[key]);
277
+ return this;
278
+ }
279
+ setStatus(status: { code: number }): this {
280
+ if (status?.code === STATUS_ERROR) this.#errored = true;
281
+ return this;
282
+ }
283
+ addEvent(): this {
284
+ return this;
285
+ }
286
+ addLink(): this {
287
+ return this;
288
+ }
289
+ addLinks(): this {
290
+ return this;
291
+ }
292
+ updateName(): this {
293
+ return this;
294
+ }
295
+ recordException(exception: unknown): void {
296
+ // The proxy's own invocation ends with an error marked `bubble`, whatever its type.
297
+ if ((exception as { bubble?: unknown } | null)?.bubble === true) this.#proxy = true;
298
+ }
299
+ isRecording(): boolean {
300
+ return !this.#ended;
301
+ }
302
+ end(): void {
303
+ if (this.#ended) return;
304
+ this.#ended = true;
305
+ if (this.#proxy) return;
306
+ try {
307
+ const seconds = (performance.now() - this.#start) / 1000;
308
+ recordRequestSpan(this.#attributes, seconds, this.#errored, this.#draft);
309
+ } catch {
310
+ // A metric never breaks a request.
311
+ }
312
+ }
313
+ }
314
+
315
+ /** OpenTelemetry's context manager over `AsyncLocalStorage` (what `@opentelemetry/context-async-hooks` does). */
316
+ function storageContextManager(api: Api, storage: AsyncLocalStorageLike<Context>): ContextManager {
317
+ const manager: ContextManager = {
318
+ active: () => storage.getStore() ?? api.ROOT_CONTEXT,
319
+ with(context, fn, thisArg, ...args) {
320
+ const call = thisArg === undefined ? fn : fn.bind(thisArg);
321
+ return storage.run(context, call as (...a: unknown[]) => ReturnType<typeof fn>, ...args);
322
+ },
323
+ bind<T>(context: Context, target: T): T {
324
+ if (typeof target !== "function") return target;
325
+ const fn = target as unknown as (...a: unknown[]) => unknown;
326
+ return function (this: unknown, ...args: unknown[]) {
327
+ return manager.with(context, () => fn.apply(this, args));
328
+ } as unknown as T;
329
+ },
330
+ enable: () => manager,
331
+ disable() {
332
+ storage.disable();
333
+ return manager;
334
+ },
335
+ };
336
+ return manager;
337
+ }
@@ -18,10 +18,14 @@
18
18
  * is rewritten onto a dynamic draft route (`/_draft/<path>`, mounted as
19
19
  * `app/%5Fdraft/...`: a literal `_draft` folder is private in Next and never
20
20
  * routes).
21
+ *
22
+ * The proxy measures no requests: it runs before Next routes and renders the
23
+ * page. `http.server.request.duration` comes from Next's own request span
24
+ * (`register` from `@decocms/nextjs/instrumentation`).
21
25
  */
22
26
  import type { CMS } from "@decocms/blocks";
23
27
  import { type NextRequest, NextResponse } from "next/server.js";
24
- import { DRAFT_HEADER, draftResponseHeaders } from "../draft.ts";
28
+ import { DRAFT_HEADER, DRAFT_ROUTE_PREFIX, draftResponseHeaders } from "../draft.ts";
25
29
  import {
26
30
  allowedDraft,
27
31
  draftCookie,
@@ -50,8 +54,7 @@ export interface DraftDecision {
50
54
  setCookie: string | null;
51
55
  }
52
56
 
53
- /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
54
- export const DRAFT_ROUTE_PREFIX = "/_draft";
57
+ export { DRAFT_ROUTE_PREFIX } from "../draft.ts";
55
58
 
56
59
  const INERT: DraftDecision = Object.freeze({ pointer: null, setCookie: null });
57
60