diff --git a/packages/next/src/server/api-utils/index.ts b/packages/next/src/server/api-utils/index.ts index b28ac1cc498a4..f5048244f5c03 100644 --- a/packages/next/src/server/api-utils/index.ts +++ b/packages/next/src/server/api-utils/index.ts @@ -1,4 +1,4 @@ -import type { IncomingMessage } from 'http' +import type { IncomingMessage, ServerResponse } from 'http' import type { BaseNextRequest } from '../base-http' import type { CookieSerializeOptions } from 'next/dist/compiled/cookie' import type { NextApiResponse } from '../../shared/lib/utils' @@ -20,10 +20,11 @@ export type __ApiPreviewProps = { previewModeSigningKey: string } -export function wrapApiHandler any>( - page: string, - handler: T -): T { +export function wrapApiHandler< + T extends ( + ...args: [req: IncomingMessage, res: ServerResponse, ...any[]] + ) => any, +>(page: string, handler: T): T { return ((...args) => { getTracer().setRootSpanAttribute('next.route', page) // Call API route method @@ -31,8 +32,21 @@ export function wrapApiHandler any>( NodeSpan.runHandler, { spanName: `executing api route (pages) ${page}`, + manualSpanEnd: true, }, - () => handler(...args) + (span) => { + if (span) { + const res = args[1] + res.end = new Proxy(res.end, { + apply(target, thisArg, argArray) { + span.end() + return target.apply(thisArg, argArray as any) + }, + }) + } + + return handler(...args) + } ) }) as T } diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index afbab780fe370..75108aebf494d 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -897,9 +897,16 @@ export default abstract class Server< 'http.method': method, 'http.target': req.url, }, + manualSpanEnd: true, }, async (span) => this.handleRequestImpl(req, res, parsedUrl).finally(() => { + res.onClose(() => { + if (span) { + span.end() + } + }) + if (!span) return const isRSCRequest = getRequestMeta(req, 'isRSCRequest') ?? false diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index cd2502d9742c7..503af448b3897 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -49,7 +49,7 @@ export function isBubbledError(error: unknown): error is BubbledError { return error instanceof BubbledError } -const closeSpanWithError = (span: Span, error?: Error) => { +const recordErrorOnSpan = (span: Span, error?: Error) => { if (isBubbledError(error) && error.bubble) { span.setAttribute('next.bubble', true) } else { @@ -58,7 +58,6 @@ const closeSpanWithError = (span: Span, error?: Error) => { } span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message }) } - span.end() } type TracerSpanOptions = Omit & { @@ -66,6 +65,7 @@ type TracerSpanOptions = Omit & { spanName?: string attributes?: Partial> hideSpan?: boolean + manualSpanEnd?: boolean } interface NextTracer { @@ -332,6 +332,12 @@ class NextTracerImpl implements NextTracer { } } + const endSpan = () => { + if (!options.manualSpanEnd) { + span.end() + } + } + if (isRootSpan) { rootSpanAttributesStore.set( spanId, @@ -345,7 +351,10 @@ class NextTracerImpl implements NextTracer { } try { if (fn.length > 1) { - return fn(span, (err) => closeSpanWithError(span, err)) + return fn(span, (err) => { + recordErrorOnSpan(span, err) + endSpan() + }) } const result = fn(span) @@ -353,24 +362,25 @@ class NextTracerImpl implements NextTracer { // If there's error make sure it throws return result .then((res) => { - span.end() // Need to pass down the promise result, // it could be react stream response with error { error, stream } return res }) .catch((err) => { - closeSpanWithError(span, err) + recordErrorOnSpan(span, err) + endSpan() throw err }) .finally(onCleanup) } else { - span.end() + endSpan() onCleanup() } return result } catch (err: any) { - closeSpanWithError(span, err) + recordErrorOnSpan(span, err) + endSpan() onCleanup() throw err }