@vigilon/nextjs 0.0.0-stage → 0.2.15

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.
@@ -0,0 +1,116 @@
1
+ import { VigilonRegisterParams, register as register$1 } from '@vigilon/node';
2
+ export * from '@vigilon/node';
3
+ import { Context } from '@opentelemetry/api';
4
+ import { SpanProcessor, Span, ReadableSpan } from '@opentelemetry/sdk-trace-node';
5
+
6
+ type VigilonNextjsRegisterParams = Omit<VigilonRegisterParams, "extendSpanProcessors">;
7
+ /**
8
+ * Starts Vigilon in the Next.js Node.js runtime: @vigilon/node's register()
9
+ * plus route attribution from Next's own spans and, on Vercel, the
10
+ * end-of-request flush and real response status. Call it from
11
+ * instrumentation.ts. Every parameter is optional, as with @vigilon/node.
12
+ */
13
+ declare function register(params?: VigilonNextjsRegisterParams): ReturnType<typeof register$1>;
14
+
15
+ declare const NEXT_ROUTE_ATTRIBUTE = "next.route";
16
+ declare const HTTP_ROUTE_ATTRIBUTE = "http.route";
17
+ /**
18
+ * Propagates the route Next.js resolves to the two places the rest of the
19
+ * pipeline reads it from, neither of which Next.js reliably fills itself:
20
+ *
21
+ * 1. The incoming request's RPC metadata — where the HTTP instrumentation
22
+ * reads `http.route` when labelling its server span. Express and Fastify
23
+ * instrumentations write the matched route there; Next.js never does.
24
+ *
25
+ * 2. Next's own `BaseServer.handleRequest` entry span. Next normally copies
26
+ * `next.route` onto it via a tracer-internal root-span-attribute store,
27
+ * but bundled runtimes (Vercel's serverless build) load private copies of
28
+ * Next's tracer per route chunk, so the route module writes to one store
29
+ * while the base server reads another and the entry span exports without
30
+ * a route. Endpoint metrics are collector-derived from entry spans, so
31
+ * every Vercel request was bucketed as "unknown".
32
+ *
33
+ * A span processor is used instead of patching Next's internals because the
34
+ * global tracer provider is the one channel every bundled copy of Next's
35
+ * tracer shares — proven by child spans parenting correctly even where the
36
+ * attribute store is split.
37
+ */
38
+ declare class NextRouteSpanProcessor implements SpanProcessor {
39
+ onStart(span: Span, parentContext: Context): void;
40
+ onEnd(): void;
41
+ forceFlush(): Promise<void>;
42
+ shutdown(): Promise<void>;
43
+ }
44
+
45
+ type Flushable = {
46
+ forceFlush(): Promise<void>;
47
+ };
48
+ /**
49
+ * Exports telemetry at the end of every request instead of trusting timers.
50
+ *
51
+ * Vercel suspends the function instance the moment a response is sent, so the
52
+ * BatchSpanProcessor's export timer rarely gets to run, and even an export
53
+ * started at span end freezes mid-POST: it only completes if another request
54
+ * thaws the instance before the connection dies.
55
+ * Observed in production as "N consecutive requests record N-1", "spaced-out
56
+ * requests record nothing", and stale spans surfacing alongside later
57
+ * traffic.
58
+ *
59
+ * The keep-alive is therefore registered with Vercel's waitUntil when the
60
+ * entry span STARTS — registrations made after the response settles are
61
+ * silently dropped, and the entry span only ends after that point. The
62
+ * deferred handed to waitUntil at start is resolved once the end-of-request
63
+ * flush completes, so the instance stays alive exactly long enough for the
64
+ * export POST to land.
65
+ *
66
+ * Must be registered AFTER the BatchSpanProcessor: processors run in order,
67
+ * and the entry span must already be enqueued when the flush starts.
68
+ */
69
+ declare class VercelFlushSpanProcessor implements SpanProcessor {
70
+ private readonly flushables;
71
+ private readonly keepAlives;
72
+ constructor(flushables: Flushable[]);
73
+ onStart(span: Span, _parentContext: Context): void;
74
+ onEnd(span: ReadableSpan): void;
75
+ forceFlush(): Promise<void>;
76
+ shutdown(): Promise<void>;
77
+ }
78
+
79
+ declare const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = "http.response.status_code";
80
+ /**
81
+ * Records the real HTTP response status onto the service-entry span on Vercel.
82
+ *
83
+ * Vercel's bundled Next.js runtime stamps the entry span's status from the
84
+ * response object before the handler's response is written, so the span always
85
+ * reports the default 200 — a 429 or 500 to the client still exports as 200,
86
+ * and warn/error rates flatline. The truth passes through
87
+ * `ServerResponse.prototype.writeHead` when the response is actually written,
88
+ * so this processor tracks live entry spans by trace id and a prototype patch
89
+ * (see `patchServerResponseStatus`) reports the written status back to the
90
+ * entry span of whichever trace is active.
91
+ *
92
+ * The stable `http.response.status_code` key is set deliberately: the bundled
93
+ * runtime writes only the old `http.status_code` key, and the collector only
94
+ * normalizes old → stable when the stable key is absent, so the value recorded
95
+ * here survives regardless of what the runtime writes afterwards.
96
+ */
97
+ declare class VercelResponseStatusProcessor implements SpanProcessor {
98
+ private readonly entrySpans;
99
+ onStart(span: Span, _parentContext: Context): void;
100
+ onEnd(span: ReadableSpan): void;
101
+ /** Called from the ServerResponse patch when a response's head is written. */
102
+ recordStatus(statusCode: number): void;
103
+ forceFlush(): Promise<void>;
104
+ shutdown(): Promise<void>;
105
+ }
106
+ /**
107
+ * Patches `ServerResponse.prototype.writeHead` to report the written status to
108
+ * the processor. Prototype patching works regardless of when the server was
109
+ * created, which is what makes this viable on Vercel where the runtime's
110
+ * server exists before the SDK starts (the reason HttpInstrumentation never
111
+ * engages there). Node's implicit header write on `res.end()` also goes
112
+ * through `writeHead`, so unwritten-head responses are covered too.
113
+ */
114
+ declare function patchServerResponseStatus(processor: VercelResponseStatusProcessor): void;
115
+
116
+ export { type Flushable, HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, HTTP_ROUTE_ATTRIBUTE, NEXT_ROUTE_ATTRIBUTE, NextRouteSpanProcessor, VercelFlushSpanProcessor, VercelResponseStatusProcessor, type VigilonNextjsRegisterParams, patchServerResponseStatus, register };
@@ -0,0 +1,180 @@
1
+ // src/server/index.ts
2
+ export * from "@vigilon/node";
3
+
4
+ // src/server/register.ts
5
+ import { register as registerNode } from "@vigilon/node";
6
+
7
+ // src/server/nextRouteSpanProcessor.ts
8
+ import { trace } from "@opentelemetry/api";
9
+ import { getRPCMetadata, RPCType } from "@opentelemetry/core";
10
+ var NEXT_ROUTE_ATTRIBUTE = "next.route";
11
+ var HTTP_ROUTE_ATTRIBUTE = "http.route";
12
+ var NEXT_SPAN_TYPE_ATTRIBUTE = "next.span_type";
13
+ var NEXT_ENTRY_SPAN_TYPE = "BaseServer.handleRequest";
14
+ var NextRouteSpanProcessor = class {
15
+ onStart(span, parentContext) {
16
+ const route = span.attributes[NEXT_ROUTE_ATTRIBUTE];
17
+ if (typeof route !== "string" || route.length === 0) return;
18
+ const rpcMetadata = getRPCMetadata(parentContext);
19
+ if (rpcMetadata?.type === RPCType.HTTP) {
20
+ rpcMetadata.route = route;
21
+ }
22
+ const parentSpan = trace.getSpan(parentContext);
23
+ if (!parentSpan?.isRecording()) return;
24
+ const parentAttributes = parentSpan.attributes;
25
+ if (parentAttributes?.[NEXT_SPAN_TYPE_ATTRIBUTE] === NEXT_ENTRY_SPAN_TYPE && parentAttributes[HTTP_ROUTE_ATTRIBUTE] === void 0) {
26
+ parentSpan.setAttribute(HTTP_ROUTE_ATTRIBUTE, route);
27
+ parentSpan.setAttribute(NEXT_ROUTE_ATTRIBUTE, route);
28
+ }
29
+ }
30
+ onEnd() {
31
+ }
32
+ forceFlush() {
33
+ return Promise.resolve();
34
+ }
35
+ shutdown() {
36
+ return Promise.resolve();
37
+ }
38
+ };
39
+
40
+ // src/server/entrySpan.ts
41
+ import { SpanKind } from "@opentelemetry/api";
42
+ function isEntrySpan(span) {
43
+ if (span.kind !== SpanKind.SERVER) return false;
44
+ const parent = span.parentSpanContext;
45
+ return !parent || parent.isRemote === true;
46
+ }
47
+
48
+ // src/server/vercelFlushSpanProcessor.ts
49
+ var VERCEL_REQUEST_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context");
50
+ var KEEP_ALIVE_TIMEOUT_MS = 15e3;
51
+ function vercelWaitUntil(promise) {
52
+ const requestContext = globalThis[VERCEL_REQUEST_CONTEXT];
53
+ requestContext?.get?.()?.waitUntil?.(promise);
54
+ }
55
+ var VercelFlushSpanProcessor = class {
56
+ constructor(flushables) {
57
+ this.flushables = flushables;
58
+ this.keepAlives = /* @__PURE__ */ new Map();
59
+ }
60
+ onStart(span, _parentContext) {
61
+ if (!isEntrySpan(span)) return;
62
+ let resolveKeepAlive;
63
+ const keepAlive = new Promise((resolve) => {
64
+ resolveKeepAlive = resolve;
65
+ });
66
+ vercelWaitUntil(keepAlive);
67
+ const backstop = setTimeout(resolveKeepAlive, KEEP_ALIVE_TIMEOUT_MS);
68
+ backstop.unref?.();
69
+ this.keepAlives.set(span.spanContext().spanId, () => {
70
+ clearTimeout(backstop);
71
+ resolveKeepAlive();
72
+ });
73
+ }
74
+ onEnd(span) {
75
+ if (!isEntrySpan(span)) return;
76
+ const spanId = span.spanContext().spanId;
77
+ const release = this.keepAlives.get(spanId);
78
+ this.keepAlives.delete(spanId);
79
+ const exportDone = Promise.allSettled(
80
+ this.flushables.map((flushable) => flushable.forceFlush())
81
+ ).then(() => void 0);
82
+ if (release) void exportDone.finally(release);
83
+ }
84
+ forceFlush() {
85
+ return Promise.resolve();
86
+ }
87
+ shutdown() {
88
+ for (const release of this.keepAlives.values()) release();
89
+ this.keepAlives.clear();
90
+ return Promise.resolve();
91
+ }
92
+ };
93
+
94
+ // src/server/vercelResponseStatusProcessor.ts
95
+ import * as http from "http";
96
+ import { trace as trace2 } from "@opentelemetry/api";
97
+ var HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = "http.response.status_code";
98
+ var VercelResponseStatusProcessor = class {
99
+ constructor() {
100
+ this.entrySpans = /* @__PURE__ */ new Map();
101
+ }
102
+ onStart(span, _parentContext) {
103
+ if (!isEntrySpan(span)) return;
104
+ this.entrySpans.set(span.spanContext().traceId, span);
105
+ }
106
+ onEnd(span) {
107
+ if (!isEntrySpan(span)) return;
108
+ this.entrySpans.delete(span.spanContext().traceId);
109
+ }
110
+ /** Called from the ServerResponse patch when a response's head is written. */
111
+ recordStatus(statusCode) {
112
+ const traceId = trace2.getActiveSpan()?.spanContext().traceId;
113
+ if (!traceId) return;
114
+ const entrySpan = this.entrySpans.get(traceId);
115
+ if (entrySpan?.isRecording()) {
116
+ entrySpan.setAttribute(HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, statusCode);
117
+ }
118
+ }
119
+ forceFlush() {
120
+ return Promise.resolve();
121
+ }
122
+ shutdown() {
123
+ this.entrySpans.clear();
124
+ return Promise.resolve();
125
+ }
126
+ };
127
+ var PATCHED_FLAG = /* @__PURE__ */ Symbol.for("vigilon.writeHead.patched");
128
+ function patchServerResponseStatus(processor) {
129
+ const prototype = http.ServerResponse.prototype;
130
+ if (prototype[PATCHED_FLAG]) return;
131
+ prototype[PATCHED_FLAG] = true;
132
+ const originalWriteHead = prototype.writeHead;
133
+ prototype.writeHead = function patchedWriteHead(...args) {
134
+ const statusCode = typeof args[0] === "number" ? args[0] : this.statusCode;
135
+ try {
136
+ processor.recordStatus(statusCode);
137
+ } catch {
138
+ }
139
+ return originalWriteHead.apply(this, args);
140
+ };
141
+ }
142
+
143
+ // src/server/register.ts
144
+ var NODE_REGISTER_STATE_KEY = /* @__PURE__ */ Symbol.for("vigilon.register.started");
145
+ var NEXTJS_REGISTER_STATE_KEY = /* @__PURE__ */ Symbol.for("vigilon.register.nextjs");
146
+ function register(params = {}) {
147
+ const globalState = globalThis;
148
+ if (globalState[NODE_REGISTER_STATE_KEY] && !globalState[NEXTJS_REGISTER_STATE_KEY]) {
149
+ console.warn(
150
+ "Vigilon is already registered by plain @vigilon/node (most likely a NODE_OPTIONS preload of @vigilon/node/register), so the Next.js extensions (route attribution and the Vercel runtime fixes) are not active. Preload @vigilon/nextjs/register instead, or remove the preload and register from instrumentation.ts."
151
+ );
152
+ return void 0;
153
+ }
154
+ const sdk = registerNode({
155
+ ...params,
156
+ extendSpanProcessors: ({ batchSpanProcessor }) => {
157
+ const append = [];
158
+ if (process.env.VERCEL) {
159
+ append.push(new VercelFlushSpanProcessor([batchSpanProcessor]));
160
+ const responseStatusProcessor = new VercelResponseStatusProcessor();
161
+ append.push(responseStatusProcessor);
162
+ patchServerResponseStatus(responseStatusProcessor);
163
+ }
164
+ return { prepend: [new NextRouteSpanProcessor()], append };
165
+ }
166
+ });
167
+ globalState[NEXTJS_REGISTER_STATE_KEY] = true;
168
+ return sdk;
169
+ }
170
+ export {
171
+ HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE,
172
+ HTTP_ROUTE_ATTRIBUTE,
173
+ NEXT_ROUTE_ATTRIBUTE,
174
+ NextRouteSpanProcessor,
175
+ VercelFlushSpanProcessor,
176
+ VercelResponseStatusProcessor,
177
+ patchServerResponseStatus,
178
+ register
179
+ };
180
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/index.ts","../src/server/register.ts","../src/server/nextRouteSpanProcessor.ts","../src/server/entrySpan.ts","../src/server/vercelFlushSpanProcessor.ts","../src/server/vercelResponseStatusProcessor.ts"],"sourcesContent":["/**\n * @vigilon/nextjs/server: the Node.js runtime half of @vigilon/nextjs, for\n * instrumentation.ts. Re-exports @vigilon/node (recordException,\n * withJobMonitor, and the rest), with a Next.js-aware register().\n */\nexport * from \"@vigilon/node\";\nexport { register, type VigilonNextjsRegisterParams } from \"./register\";\nexport {\n HTTP_ROUTE_ATTRIBUTE,\n NEXT_ROUTE_ATTRIBUTE,\n NextRouteSpanProcessor,\n} from \"./nextRouteSpanProcessor\";\nexport { type Flushable, VercelFlushSpanProcessor } from \"./vercelFlushSpanProcessor\";\nexport {\n HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE,\n patchServerResponseStatus,\n VercelResponseStatusProcessor,\n} from \"./vercelResponseStatusProcessor\";\n","import type { SpanProcessor } from \"@opentelemetry/sdk-trace-node\";\nimport { register as registerNode, type VigilonRegisterParams } from \"@vigilon/node\";\nimport { NextRouteSpanProcessor } from \"./nextRouteSpanProcessor\";\nimport { VercelFlushSpanProcessor } from \"./vercelFlushSpanProcessor\";\nimport {\n patchServerResponseStatus,\n VercelResponseStatusProcessor,\n} from \"./vercelResponseStatusProcessor\";\n\nexport type VigilonNextjsRegisterParams = Omit<VigilonRegisterParams, \"extendSpanProcessors\">;\n\n// Shared with @vigilon/node: set once any register() variant has started an\n// SDK in this process.\nconst NODE_REGISTER_STATE_KEY = Symbol.for(\"vigilon.register.started\");\n// Set only by this register(), so a plain @vigilon/node start followed by\n// this call is told apart from a harmless second call of this one.\nconst NEXTJS_REGISTER_STATE_KEY = Symbol.for(\"vigilon.register.nextjs\");\n\n/**\n * Starts Vigilon in the Next.js Node.js runtime: @vigilon/node's register()\n * plus route attribution from Next's own spans and, on Vercel, the\n * end-of-request flush and real response status. Call it from\n * instrumentation.ts. Every parameter is optional, as with @vigilon/node.\n */\nexport function register(\n params: VigilonNextjsRegisterParams = {},\n): ReturnType<typeof registerNode> {\n const globalState = globalThis as Record<symbol, unknown>;\n if (globalState[NODE_REGISTER_STATE_KEY] && !globalState[NEXTJS_REGISTER_STATE_KEY]) {\n console.warn(\n \"Vigilon is already registered by plain @vigilon/node (most likely a NODE_OPTIONS preload of @vigilon/node/register), so the Next.js extensions (route attribution and the Vercel runtime fixes) are not active. Preload @vigilon/nextjs/register instead, or remove the preload and register from instrumentation.ts.\",\n );\n return undefined;\n }\n const sdk = registerNode({\n ...params,\n extendSpanProcessors: ({ batchSpanProcessor }) => {\n const append: SpanProcessor[] = [];\n // Vercel suspends the instance as soon as the response is sent, which\n // stops the batch and export timers, so telemetry would leave late or\n // not at all. Flush when each entry span ends instead. Must come after\n // the batch processor so the ending span is already enqueued.\n if (process.env.VERCEL) {\n append.push(new VercelFlushSpanProcessor([batchSpanProcessor]));\n // Vercel's bundled Next.js runtime stamps the entry span's status\n // before the response is written (always 200), so the real status is\n // recorded from ServerResponse.writeHead instead.\n const responseStatusProcessor = new VercelResponseStatusProcessor();\n append.push(responseStatusProcessor);\n patchServerResponseStatus(responseStatusProcessor);\n }\n return { prepend: [new NextRouteSpanProcessor()], append };\n },\n });\n globalState[NEXTJS_REGISTER_STATE_KEY] = true;\n return sdk;\n}\n","import { Context, trace } from \"@opentelemetry/api\";\nimport { getRPCMetadata, RPCType } from \"@opentelemetry/core\";\nimport {\n SpanProcessor,\n Span,\n ReadableSpan,\n} from \"@opentelemetry/sdk-trace-node\";\n\nexport const NEXT_ROUTE_ATTRIBUTE = \"next.route\";\nexport const HTTP_ROUTE_ATTRIBUTE = \"http.route\";\n\nconst NEXT_SPAN_TYPE_ATTRIBUTE = \"next.span_type\";\nconst NEXT_ENTRY_SPAN_TYPE = \"BaseServer.handleRequest\";\n\n/**\n * Propagates the route Next.js resolves to the two places the rest of the\n * pipeline reads it from, neither of which Next.js reliably fills itself:\n *\n * 1. The incoming request's RPC metadata — where the HTTP instrumentation\n * reads `http.route` when labelling its server span. Express and Fastify\n * instrumentations write the matched route there; Next.js never does.\n *\n * 2. Next's own `BaseServer.handleRequest` entry span. Next normally copies\n * `next.route` onto it via a tracer-internal root-span-attribute store,\n * but bundled runtimes (Vercel's serverless build) load private copies of\n * Next's tracer per route chunk, so the route module writes to one store\n * while the base server reads another and the entry span exports without\n * a route. Endpoint metrics are collector-derived from entry spans, so\n * every Vercel request was bucketed as \"unknown\".\n *\n * A span processor is used instead of patching Next's internals because the\n * global tracer provider is the one channel every bundled copy of Next's\n * tracer shares — proven by child spans parenting correctly even where the\n * attribute store is split.\n */\nexport class NextRouteSpanProcessor implements SpanProcessor {\n onStart(span: Span, parentContext: Context): void {\n const route = span.attributes[NEXT_ROUTE_ATTRIBUTE];\n if (typeof route !== \"string\" || route.length === 0) return;\n\n const rpcMetadata = getRPCMetadata(parentContext);\n if (rpcMetadata?.type === RPCType.HTTP) {\n rpcMetadata.route = route;\n }\n\n const parentSpan = trace.getSpan(parentContext);\n if (!parentSpan?.isRecording()) return;\n // Only SDK spans expose attributes; a NonRecordingSpan (remote or no-op\n // parent) has none and is skipped by the checks below.\n const parentAttributes = (parentSpan as unknown as ReadableSpan).attributes;\n if (\n parentAttributes?.[NEXT_SPAN_TYPE_ATTRIBUTE] === NEXT_ENTRY_SPAN_TYPE &&\n parentAttributes[HTTP_ROUTE_ATTRIBUTE] === undefined\n ) {\n parentSpan.setAttribute(HTTP_ROUTE_ATTRIBUTE, route);\n parentSpan.setAttribute(NEXT_ROUTE_ATTRIBUTE, route);\n }\n }\n\n onEnd(): void {}\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n\n shutdown(): Promise<void> {\n return Promise.resolve();\n }\n}\n","import { SpanKind } from \"@opentelemetry/api\";\nimport { ReadableSpan, Span } from \"@opentelemetry/sdk-trace-node\";\n\n/**\n * A service-entry span: the one SERVER span that marks a request entering\n * this service — its parent is either absent or remote. Matches the\n * definition the collector counts for endpoint metrics.\n */\nexport function isEntrySpan(span: Span | ReadableSpan): boolean {\n if (span.kind !== SpanKind.SERVER) return false;\n const parent = (span as ReadableSpan).parentSpanContext;\n return !parent || parent.isRemote === true;\n}\n","import { Context } from \"@opentelemetry/api\";\nimport { SpanProcessor, Span, ReadableSpan } from \"@opentelemetry/sdk-trace-node\";\nimport { isEntrySpan } from \"./entrySpan\";\n\nexport type Flushable = { forceFlush(): Promise<void> };\n\n// The request-context global that @vercel/functions uses to reach waitUntil.\n// Reading it directly keeps the SDK dependency-free; it is simply absent\n// outside Vercel's runtime.\nconst VERCEL_REQUEST_CONTEXT = Symbol.for(\"@vercel/request-context\");\n\n// Backstop so a request that never ends its entry span (crash, client\n// disconnect edge cases) cannot pin the instance — and its billing — open\n// indefinitely via the keep-alive registered at span start.\nconst KEEP_ALIVE_TIMEOUT_MS = 15_000;\n\nfunction vercelWaitUntil(promise: Promise<unknown>): void {\n const requestContext = (\n globalThis as Record<symbol, unknown> as {\n [VERCEL_REQUEST_CONTEXT]?: {\n get?: () => { waitUntil?: (p: Promise<unknown>) => void } | undefined;\n };\n }\n )[VERCEL_REQUEST_CONTEXT];\n requestContext?.get?.()?.waitUntil?.(promise);\n}\n\n/**\n * Exports telemetry at the end of every request instead of trusting timers.\n *\n * Vercel suspends the function instance the moment a response is sent, so the\n * BatchSpanProcessor's export timer rarely gets to run, and even an export\n * started at span end freezes mid-POST: it only completes if another request\n * thaws the instance before the connection dies.\n * Observed in production as \"N consecutive requests record N-1\", \"spaced-out\n * requests record nothing\", and stale spans surfacing alongside later\n * traffic.\n *\n * The keep-alive is therefore registered with Vercel's waitUntil when the\n * entry span STARTS — registrations made after the response settles are\n * silently dropped, and the entry span only ends after that point. The\n * deferred handed to waitUntil at start is resolved once the end-of-request\n * flush completes, so the instance stays alive exactly long enough for the\n * export POST to land.\n *\n * Must be registered AFTER the BatchSpanProcessor: processors run in order,\n * and the entry span must already be enqueued when the flush starts.\n */\nexport class VercelFlushSpanProcessor implements SpanProcessor {\n private readonly keepAlives = new Map<string, () => void>();\n\n constructor(private readonly flushables: Flushable[]) {}\n\n onStart(span: Span, _parentContext: Context): void {\n if (!isEntrySpan(span)) return;\n\n let resolveKeepAlive!: () => void;\n const keepAlive = new Promise<void>((resolve) => {\n resolveKeepAlive = resolve;\n });\n vercelWaitUntil(keepAlive);\n\n const backstop = setTimeout(resolveKeepAlive, KEEP_ALIVE_TIMEOUT_MS);\n backstop.unref?.();\n this.keepAlives.set(span.spanContext().spanId, () => {\n clearTimeout(backstop);\n resolveKeepAlive();\n });\n }\n\n onEnd(span: ReadableSpan): void {\n if (!isEntrySpan(span)) return;\n\n const spanId = span.spanContext().spanId;\n const release = this.keepAlives.get(spanId);\n this.keepAlives.delete(spanId);\n\n const exportDone = Promise.allSettled(\n this.flushables.map((flushable) => flushable.forceFlush()),\n ).then(() => undefined);\n if (release) void exportDone.finally(release);\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n\n shutdown(): Promise<void> {\n for (const release of this.keepAlives.values()) release();\n this.keepAlives.clear();\n return Promise.resolve();\n }\n}\n","import * as http from \"node:http\";\nimport { Context, trace } from \"@opentelemetry/api\";\nimport {\n SpanProcessor,\n Span,\n ReadableSpan,\n} from \"@opentelemetry/sdk-trace-node\";\nimport { isEntrySpan } from \"./entrySpan\";\n\nexport const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = \"http.response.status_code\";\n\n/**\n * Records the real HTTP response status onto the service-entry span on Vercel.\n *\n * Vercel's bundled Next.js runtime stamps the entry span's status from the\n * response object before the handler's response is written, so the span always\n * reports the default 200 — a 429 or 500 to the client still exports as 200,\n * and warn/error rates flatline. The truth passes through\n * `ServerResponse.prototype.writeHead` when the response is actually written,\n * so this processor tracks live entry spans by trace id and a prototype patch\n * (see `patchServerResponseStatus`) reports the written status back to the\n * entry span of whichever trace is active.\n *\n * The stable `http.response.status_code` key is set deliberately: the bundled\n * runtime writes only the old `http.status_code` key, and the collector only\n * normalizes old → stable when the stable key is absent, so the value recorded\n * here survives regardless of what the runtime writes afterwards.\n */\nexport class VercelResponseStatusProcessor implements SpanProcessor {\n private readonly entrySpans = new Map<string, Span>();\n\n onStart(span: Span, _parentContext: Context): void {\n if (!isEntrySpan(span)) return;\n this.entrySpans.set(span.spanContext().traceId, span);\n }\n\n onEnd(span: ReadableSpan): void {\n if (!isEntrySpan(span)) return;\n this.entrySpans.delete(span.spanContext().traceId);\n }\n\n /** Called from the ServerResponse patch when a response's head is written. */\n recordStatus(statusCode: number): void {\n const traceId = trace.getActiveSpan()?.spanContext().traceId;\n if (!traceId) return;\n const entrySpan = this.entrySpans.get(traceId);\n if (entrySpan?.isRecording()) {\n entrySpan.setAttribute(HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE, statusCode);\n }\n }\n\n forceFlush(): Promise<void> {\n return Promise.resolve();\n }\n\n shutdown(): Promise<void> {\n this.entrySpans.clear();\n return Promise.resolve();\n }\n}\n\nconst PATCHED_FLAG = Symbol.for(\"vigilon.writeHead.patched\");\n\n/**\n * Patches `ServerResponse.prototype.writeHead` to report the written status to\n * the processor. Prototype patching works regardless of when the server was\n * created, which is what makes this viable on Vercel where the runtime's\n * server exists before the SDK starts (the reason HttpInstrumentation never\n * engages there). Node's implicit header write on `res.end()` also goes\n * through `writeHead`, so unwritten-head responses are covered too.\n */\nexport function patchServerResponseStatus(\n processor: VercelResponseStatusProcessor,\n): void {\n const prototype = http.ServerResponse.prototype as typeof http.ServerResponse.prototype & {\n [PATCHED_FLAG]?: boolean;\n };\n if (prototype[PATCHED_FLAG]) return;\n prototype[PATCHED_FLAG] = true;\n\n const originalWriteHead = prototype.writeHead;\n prototype.writeHead = function patchedWriteHead(\n this: http.ServerResponse,\n ...args: Parameters<typeof originalWriteHead>\n ) {\n const statusCode = typeof args[0] === \"number\" ? args[0] : this.statusCode;\n try {\n processor.recordStatus(statusCode);\n } catch {\n // Never let telemetry break the response path.\n }\n return originalWriteHead.apply(this, args);\n } as typeof originalWriteHead;\n}\n"],"mappings":"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","names":["trace","trace"]}
package/package.json CHANGED
@@ -1,6 +1,104 @@
1
1
  {
2
2
  "name": "@vigilon/nextjs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.15",
4
+ "description": "Vigilon monitoring for Next.js applications: server traces and browser errors",
5
+ "license": "ISC",
6
+ "author": "Vigilon",
7
+ "homepage": "https://vigilon.io",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vigilonio/js.git",
11
+ "directory": "packages/nextjs"
12
+ },
13
+ "keywords": [
14
+ "vigilon",
15
+ "nextjs",
16
+ "error-monitoring",
17
+ "opentelemetry",
18
+ "observability"
19
+ ],
20
+ "sideEffects": false,
21
+ "main": "./dist/index.cjs",
22
+ "module": "./dist/index.mjs",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "import": {
27
+ "types": "./dist/index.d.mts",
28
+ "default": "./dist/index.mjs"
29
+ },
30
+ "require": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.cjs"
33
+ }
34
+ },
35
+ "./server": {
36
+ "import": {
37
+ "types": "./dist/server.d.mts",
38
+ "default": "./dist/server.mjs"
39
+ },
40
+ "require": {
41
+ "types": "./dist/server.d.ts",
42
+ "default": "./dist/server.cjs"
43
+ }
44
+ },
45
+ "./config": {
46
+ "import": {
47
+ "types": "./dist/config.d.mts",
48
+ "default": "./dist/config.mjs"
49
+ },
50
+ "require": {
51
+ "types": "./dist/config.d.ts",
52
+ "default": "./dist/config.cjs"
53
+ }
54
+ },
55
+ "./register": {
56
+ "import": "./register.mjs",
57
+ "require": "./register.cjs",
58
+ "default": "./register.cjs"
59
+ },
60
+ "./package.json": "./package.json"
61
+ },
62
+ "files": [
63
+ "dist",
64
+ "register.mjs",
65
+ "register.cjs",
66
+ "README.md",
67
+ "LICENSE"
68
+ ],
69
+ "publishConfig": {
70
+ "access": "public"
71
+ },
72
+ "engines": {
73
+ "node": ">=22.12.0"
74
+ },
75
+ "dependencies": {
76
+ "@opentelemetry/api": "^1.9.1",
77
+ "@opentelemetry/core": "^2.11.0",
78
+ "@opentelemetry/sdk-trace-node": "^2.11.0",
79
+ "@vigilon/cli": "0.2.15",
80
+ "@vigilon/node": "0.2.15",
81
+ "@vigilon/react": "0.2.15"
82
+ },
83
+ "peerDependencies": {
84
+ "react": ">=18.0.0"
85
+ },
86
+ "devDependencies": {
87
+ "@types/node": "^24.19.2",
88
+ "@types/react": "19.3.0",
89
+ "@types/react-dom": "19.3.0",
90
+ "happy-dom": "^20.0.0",
91
+ "react": "19.3.0",
92
+ "react-dom": "19.3.0",
93
+ "tsup": "^8.5.0",
94
+ "typescript": "^6.0.3",
95
+ "vitest": "^4.1.10"
96
+ },
97
+ "scripts": {
98
+ "clean": "rm -rf dist",
99
+ "build": "rm -rf dist && tsup",
100
+ "test": "vitest run",
101
+ "test:watch": "vitest",
102
+ "typecheck": "tsc --noEmit"
103
+ }
6
104
  }
package/register.cjs ADDED
@@ -0,0 +1,5 @@
1
+ // CommonJS preload entrypoint for self-hosted Next.js:
2
+ // `NODE_OPTIONS='--require @vigilon/nextjs/register' next start`.
3
+ // register() is idempotent, so this preload plus a register() call from
4
+ // instrumentation.ts cannot start two SDKs.
5
+ require("./dist/server.cjs").register();
package/register.mjs ADDED
@@ -0,0 +1,12 @@
1
+ // Preload entrypoint for self-hosted Next.js:
2
+ // `NODE_OPTIONS='--import @vigilon/nextjs/register' next start`.
3
+ import { register as registerLoaderHook } from "node:module";
4
+
5
+ // Lets OpenTelemetry's instrumentations intercept ESM imports as well as
6
+ // require(), as @vigilon/node/register does.
7
+ registerLoaderHook("@opentelemetry/instrumentation/hook.mjs", import.meta.url);
8
+
9
+ // register() is idempotent, so this preload plus a register() call from
10
+ // instrumentation.ts cannot start two SDKs.
11
+ const { register } = await import("./dist/server.mjs");
12
+ register();