diff --git a/packages/nextjs/src/common/_error.ts b/packages/nextjs/src/common/_error.ts index f3a198919a72..385df8244a17 100644 --- a/packages/nextjs/src/common/_error.ts +++ b/packages/nextjs/src/common/_error.ts @@ -1,7 +1,7 @@ import { captureException, withScope } from '@sentry/core'; +import { vercelWaitUntil } from '@sentry/utils'; import type { NextPageContext } from 'next'; import { flushSafelyWithTimeout } from './utils/responseEnd'; -import { vercelWaitUntil } from './utils/vercelWaitUntil'; type ContextOrProps = { req?: NextPageContext['req']; diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 65bdabc93dda..5eed59aca0a3 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -9,12 +9,11 @@ import { startSpan, withIsolationScope, } from '@sentry/core'; -import { winterCGRequestToRequestData } from '@sentry/utils'; +import { vercelWaitUntil, winterCGRequestToRequestData } from '@sentry/utils'; import type { EdgeRouteHandler } from '../../edge/types'; import { flushSafelyWithTimeout } from './responseEnd'; import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils'; -import { vercelWaitUntil } from './vercelWaitUntil'; /** * Wraps a function on the edge runtime with error and performance monitoring. diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index f07970e4db3b..ff04aebbd3ed 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -14,11 +14,10 @@ import { withIsolationScope, } from '@sentry/core'; import type { Span } from '@sentry/types'; -import { isString } from '@sentry/utils'; +import { isString, vercelWaitUntil } from '@sentry/utils'; import { autoEndSpanOnResponseEnd, flushSafelyWithTimeout } from './responseEnd'; import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils'; -import { vercelWaitUntil } from './vercelWaitUntil'; declare module 'http' { interface IncomingMessage { diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index c1633d8fab1b..0b8d3b6d7c60 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -5,13 +5,12 @@ import { withIsolationScope, } from '@sentry/core'; import { captureException, continueTrace, getClient, handleCallbackErrors, startSpan } from '@sentry/core'; -import { logger } from '@sentry/utils'; +import { logger, vercelWaitUntil } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import { flushSafelyWithTimeout } from './utils/responseEnd'; import { escapeNextjsTracing } from './utils/tracingUtils'; -import { vercelWaitUntil } from './utils/vercelWaitUntil'; interface Options { formData?: FormData; diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index 09bca8d23d78..f658a4736857 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -6,13 +6,12 @@ import { startSpanManual, withIsolationScope, } from '@sentry/core'; -import { consoleSandbox, isString, logger, objectify } from '@sentry/utils'; +import { consoleSandbox, isString, logger, objectify, vercelWaitUntil } from '@sentry/utils'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler } from './types'; import { flushSafelyWithTimeout } from './utils/responseEnd'; import { escapeNextjsTracing } from './utils/tracingUtils'; -import { vercelWaitUntil } from './utils/vercelWaitUntil'; /** * Wrap the given API route handler for tracing and error capturing. Thin wrapper around `withSentry`, which only diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index bf0d475603f2..71061e913dac 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -10,7 +10,7 @@ import { withIsolationScope, withScope, } from '@sentry/core'; -import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; +import { propagationContextFromHeaders, vercelWaitUntil, winterCGHeadersToDict } from '@sentry/utils'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import type { RouteHandlerContext } from './types'; import { flushSafelyWithTimeout } from './utils/responseEnd'; @@ -19,7 +19,6 @@ import { commonObjectToPropagationContext, escapeNextjsTracing, } from './utils/tracingUtils'; -import { vercelWaitUntil } from './utils/vercelWaitUntil'; /** * Wraps a Next.js App Router Route handler with Sentry error and performance instrumentation. diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index e8d734a90ff3..079722dad76d 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -13,14 +13,13 @@ import { withIsolationScope, withScope, } from '@sentry/core'; -import { propagationContextFromHeaders, uuid4, winterCGHeadersToDict } from '@sentry/utils'; +import { propagationContextFromHeaders, uuid4, vercelWaitUntil, winterCGHeadersToDict } from '@sentry/utils'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; -import { vercelWaitUntil } from './utils/vercelWaitUntil'; /** * Wraps an `app` directory server component with Sentry error instrumentation. diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 245751b3e72c..4a2d68ca0d8b 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -38,4 +38,5 @@ export * from './anr'; export * from './lru'; export * from './buildPolyfills'; export * from './propagationContext'; +export * from './vercelWaitUntil'; export * from './version'; diff --git a/packages/nextjs/src/common/utils/vercelWaitUntil.ts b/packages/utils/src/vercelWaitUntil.ts similarity index 65% rename from packages/nextjs/src/common/utils/vercelWaitUntil.ts rename to packages/utils/src/vercelWaitUntil.ts index 15c6015fe4c9..280130b766b7 100644 --- a/packages/nextjs/src/common/utils/vercelWaitUntil.ts +++ b/packages/utils/src/vercelWaitUntil.ts @@ -1,4 +1,4 @@ -import { GLOBAL_OBJ } from '@sentry/utils'; +import { GLOBAL_OBJ } from './worldwide'; interface VercelRequestContextGlobal { get?(): { @@ -16,6 +16,12 @@ export function vercelWaitUntil(task: Promise): void { // @ts-expect-error This is not typed GLOBAL_OBJ[Symbol.for('@vercel/request-context')]; - const ctx = vercelRequestContextGlobal?.get?.() ?? {}; - ctx.waitUntil?.(task); + const ctx = + vercelRequestContextGlobal && vercelRequestContextGlobal.get && vercelRequestContextGlobal.get() + ? vercelRequestContextGlobal.get() + : {}; + + if (ctx && ctx.waitUntil) { + ctx.waitUntil(task); + } } diff --git a/packages/utils/test/vercelWaitUntil.test.ts b/packages/utils/test/vercelWaitUntil.test.ts new file mode 100644 index 000000000000..881bf265607b --- /dev/null +++ b/packages/utils/test/vercelWaitUntil.test.ts @@ -0,0 +1,39 @@ +import { vercelWaitUntil } from '../src/vercelWaitUntil'; +import { GLOBAL_OBJ } from '../src/worldwide'; + +describe('vercelWaitUntil', () => { + it('should do nothing if GLOBAL_OBJ does not have the @vercel/request-context symbol', () => { + const task = Promise.resolve(); + vercelWaitUntil(task); + // No assertions needed, just ensuring no errors are thrown + }); + + it('should do nothing if get method is not defined', () => { + // @ts-expect-error - Not typed + GLOBAL_OBJ[Symbol.for('@vercel/request-context')] = {}; + const task = Promise.resolve(); + vercelWaitUntil(task); + // No assertions needed, just ensuring no errors are thrown + }); + + it('should do nothing if waitUntil method is not defined', () => { + // @ts-expect-error - Not typed + GLOBAL_OBJ[Symbol.for('@vercel/request-context')] = { + get: () => ({}), + }; + const task = Promise.resolve(); + vercelWaitUntil(task); + // No assertions needed, just ensuring no errors are thrown + }); + + it('should call waitUntil method if it is defined', () => { + const waitUntilMock = jest.fn(); + // @ts-expect-error - Not typed + GLOBAL_OBJ[Symbol.for('@vercel/request-context')] = { + get: () => ({ waitUntil: waitUntilMock }), + }; + const task = Promise.resolve(); + vercelWaitUntil(task); + expect(waitUntilMock).toHaveBeenCalledWith(task); + }); +});