diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts index 880b43061b93..8f6ef4516fab 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts @@ -1,4 +1,4 @@ -import { Context, GLOBAL_OBJ, flush, debug, vercelWaitUntil } from '@sentry/core'; +import { Context, flushIfServerless } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import { H3Error } from 'h3'; import type { CapturedErrorContext } from 'nitropack'; @@ -53,31 +53,3 @@ function extractErrorContext(errorContext: CapturedErrorContext): Context { return ctx; } - -async function flushIfServerless(): Promise { - const isServerless = - !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions - !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda - !!process.env.VERCEL || - !!process.env.NETLIFY; - - // @ts-expect-error This is not typed - if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { - vercelWaitUntil(flushWithTimeout()); - } else if (isServerless) { - await flushWithTimeout(); - } -} - -async function flushWithTimeout(): Promise { - const sentryClient = SentryNode.getClient(); - const isDebug = sentryClient ? sentryClient.getOptions().debug : false; - - try { - isDebug && debug.log('Flushing events...'); - await flush(2000); - isDebug && debug.log('Done flushing events'); - } catch (e) { - isDebug && debug.log('Error while flushing events:\n', e); - } -} diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 9f04d5427fcf..3f5d81383ee9 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,17 +1,15 @@ import type { RequestEventData, Scope, SpanAttributes } from '@sentry/core'; import { addNonEnumerableProperty, - debug, extractQueryParamsFromUrl, + flushIfServerless, objectify, stripUrlQueryAndFragment, - vercelWaitUntil, winterCGRequestToRequestData, } from '@sentry/core'; import { captureException, continueTrace, - flush, getActiveSpan, getClient, getCurrentScope, @@ -233,16 +231,7 @@ async function instrumentRequest( ); return res; } finally { - vercelWaitUntil( - (async () => { - // Flushes pending Sentry events with a 2-second timeout and in a way that cannot create unhandled promise rejections. - try { - await flush(2000); - } catch (e) { - debug.log('Error while flushing events:\n', e); - } - })(), - ); + await flushIfServerless(); } // TODO: flush if serverless (first extract function) }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ace67c267fd4..ad566343b0b6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -269,6 +269,7 @@ export { callFrameToStackFrame, watchdogTimer } from './utils/anr'; export { LRUMap } from './utils/lru'; export { generateTraceId, generateSpanId } from './utils/propagationContext'; export { vercelWaitUntil } from './utils/vercelWaitUntil'; +export { flushIfServerless } from './utils/flushIfServerless'; export { SDK_VERSION } from './utils/version'; export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils/debug-ids'; export { escapeStringForRegex } from './vendor/escapeStringForRegex'; diff --git a/packages/core/src/utils/flushIfServerless.ts b/packages/core/src/utils/flushIfServerless.ts new file mode 100644 index 000000000000..2f8d387990c9 --- /dev/null +++ b/packages/core/src/utils/flushIfServerless.ts @@ -0,0 +1,77 @@ +import { flush } from '../exports'; +import { debug } from './debug-logger'; +import { vercelWaitUntil } from './vercelWaitUntil'; +import { GLOBAL_OBJ } from './worldwide'; + +type MinimalCloudflareContext = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + waitUntil(promise: Promise): void; +}; + +async function flushWithTimeout(timeout: number): Promise { + try { + debug.log('Flushing events...'); + await flush(timeout); + debug.log('Done flushing events'); + } catch (e) { + debug.log('Error while flushing events:\n', e); + } +} + +/** + * Flushes the event queue with a timeout in serverless environments to ensure that events are sent to Sentry before the + * serverless function execution ends. + * + * The function is async, but in environments that support a `waitUntil` mechanism, it will run synchronously. + * + * This function is aware of the following serverless platforms: + * - Cloudflare: If a Cloudflare context is provided, it will use `ctx.waitUntil()` to flush events (keeps the `this` context of `ctx`). + * If a `cloudflareWaitUntil` function is provided, it will use that to flush events (looses the `this` context of `ctx`). + * - Vercel: It detects the Vercel environment and uses Vercel's `waitUntil` function. + * - Other Serverless (AWS Lambda, Google Cloud, etc.): It detects the environment via environment variables + * and uses a regular `await flush()`. + * + * @internal This function is supposed for internal Sentry SDK usage only. + * @hidden + */ +export async function flushIfServerless( + params: // eslint-disable-next-line @typescript-eslint/no-explicit-any + | { timeout?: number; cloudflareWaitUntil?: (task: Promise) => void } + | { timeout?: number; cloudflareCtx?: MinimalCloudflareContext } = {}, +): Promise { + const { timeout = 2000 } = params; + + if ('cloudflareWaitUntil' in params && typeof params?.cloudflareWaitUntil === 'function') { + params.cloudflareWaitUntil(flushWithTimeout(timeout)); + return; + } + + if ('cloudflareCtx' in params && typeof params.cloudflareCtx?.waitUntil === 'function') { + params.cloudflareCtx.waitUntil(flushWithTimeout(timeout)); + return; + } + + // @ts-expect-error This is not typed + if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { + // Vercel has a waitUntil equivalent that works without execution context + vercelWaitUntil(flushWithTimeout(timeout)); + return; + } + + if (typeof process === 'undefined') { + return; + } + + const isServerless = + !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions + !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda + !!process.env.K_SERVICE || // Google Cloud Run + !!process.env.CF_PAGES || // Cloudflare Pages + !!process.env.VERCEL || + !!process.env.NETLIFY; + + if (isServerless) { + // Use regular flush for environments without a generic waitUntil mechanism + await flushWithTimeout(timeout); + } +} diff --git a/packages/core/test/lib/utils/flushIfServerless.test.ts b/packages/core/test/lib/utils/flushIfServerless.test.ts new file mode 100644 index 000000000000..aa0314f183dc --- /dev/null +++ b/packages/core/test/lib/utils/flushIfServerless.test.ts @@ -0,0 +1,128 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import * as flushModule from '../../../src/exports'; +import { flushIfServerless } from '../../../src/utils/flushIfServerless'; +import * as vercelWaitUntilModule from '../../../src/utils/vercelWaitUntil'; +import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; + +describe('flushIfServerless', () => { + let originalProcess: typeof process; + + beforeEach(() => { + vi.resetAllMocks(); + originalProcess = global.process; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('should bind context (preserve `this`) when calling waitUntil from the Cloudflare execution context', async () => { + const flushMock = vi.spyOn(flushModule, 'flush').mockResolvedValue(true); + + // Mock Cloudflare context with `waitUntil` (which should be called if `this` is bound correctly) + const mockCloudflareCtx = { + contextData: 'test-data', + waitUntil: function (promise: Promise) { + // This will fail if 'this' is not bound correctly + expect(this.contextData).toBe('test-data'); + return promise; + }, + }; + + const waitUntilSpy = vi.spyOn(mockCloudflareCtx, 'waitUntil'); + + await flushIfServerless({ cloudflareCtx: mockCloudflareCtx }); + + expect(waitUntilSpy).toHaveBeenCalledTimes(1); + expect(flushMock).toHaveBeenCalledWith(2000); + }); + + test('should use cloudflare waitUntil when valid cloudflare context is provided', async () => { + const flushMock = vi.spyOn(flushModule, 'flush').mockResolvedValue(true); + const mockCloudflareCtx = { + waitUntil: vi.fn(), + }; + + await flushIfServerless({ cloudflareCtx: mockCloudflareCtx, timeout: 5000 }); + + expect(mockCloudflareCtx.waitUntil).toHaveBeenCalledTimes(1); + expect(flushMock).toHaveBeenCalledWith(5000); + }); + + test('should use cloudflare waitUntil when Cloudflare `waitUntil` is provided', async () => { + const flushMock = vi.spyOn(flushModule, 'flush').mockResolvedValue(true); + const mockCloudflareCtx = { + waitUntil: vi.fn(), + }; + + await flushIfServerless({ cloudflareWaitUntil: mockCloudflareCtx.waitUntil, timeout: 5000 }); + + expect(mockCloudflareCtx.waitUntil).toHaveBeenCalledTimes(1); + expect(flushMock).toHaveBeenCalledWith(5000); + }); + + test('should ignore cloudflare context when waitUntil is not a function (and use Vercel waitUntil instead)', async () => { + const flushMock = vi.spyOn(flushModule, 'flush').mockResolvedValue(true); + const vercelWaitUntilSpy = vi.spyOn(vercelWaitUntilModule, 'vercelWaitUntil').mockImplementation(() => {}); + + // Mock Vercel environment + // @ts-expect-error This is not typed + GLOBAL_OBJ[Symbol.for('@vercel/request-context')] = { get: () => ({ waitUntil: vi.fn() }) }; + + const mockCloudflareCtx = { + waitUntil: 'not-a-function', // Invalid waitUntil + }; + + // @ts-expect-error Using the wrong type here on purpose + await flushIfServerless({ cloudflareCtx: mockCloudflareCtx }); + + expect(vercelWaitUntilSpy).toHaveBeenCalledTimes(1); + expect(flushMock).toHaveBeenCalledWith(2000); + }); + + test('should handle multiple serverless environment variables simultaneously', async () => { + const flushMock = vi.spyOn(flushModule, 'flush').mockResolvedValue(true); + + global.process = { + ...originalProcess, + env: { + ...originalProcess.env, + LAMBDA_TASK_ROOT: '/var/task', + VERCEL: '1', + NETLIFY: 'true', + CF_PAGES: '1', + }, + }; + + await flushIfServerless({ timeout: 4000 }); + + expect(flushMock).toHaveBeenCalledWith(4000); + }); + + test('should use default timeout when not specified', async () => { + const flushMock = vi.spyOn(flushModule, 'flush').mockResolvedValue(true); + const mockCloudflareCtx = { + waitUntil: vi.fn(), + }; + + await flushIfServerless({ cloudflareCtx: mockCloudflareCtx }); + + expect(flushMock).toHaveBeenCalledWith(2000); + }); + + test('should handle zero timeout value', async () => { + const flushMock = vi.spyOn(flushModule, 'flush').mockResolvedValue(true); + + global.process = { + ...originalProcess, + env: { + ...originalProcess.env, + LAMBDA_TASK_ROOT: '/var/task', + }, + }; + + await flushIfServerless({ timeout: 0 }); + + expect(flushMock).toHaveBeenCalledWith(0); + }); +}); diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 9f8673a2fab8..e3cc2831d5e4 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -13,9 +13,9 @@ import { vercelWaitUntil, withIsolationScope, } from '@sentry/core'; +import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import { DEBUG_BUILD } from './debug-build'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; -import { flushSafelyWithTimeout } from './utils/responseEnd'; interface Options { formData?: FormData; diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index 66e598b5c10f..3a9ca786d697 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -13,8 +13,8 @@ import { winterCGRequestToRequestData, withIsolationScope, } from '@sentry/core'; +import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import type { EdgeRouteHandler } from '../edge/types'; -import { flushSafelyWithTimeout } from './utils/responseEnd'; /** * Wraps Next.js middleware with Sentry error and performance instrumentation. diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 16f6728deda1..d4ac11f3e848 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -22,8 +22,8 @@ import { } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; +import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; -import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; import { getSanitizedRequestUrl } from './utils/urls'; diff --git a/packages/nuxt/src/runtime/hooks/captureErrorHook.ts b/packages/nuxt/src/runtime/hooks/captureErrorHook.ts index 8f38bd11061c..7a27b7e6e4c6 100644 --- a/packages/nuxt/src/runtime/hooks/captureErrorHook.ts +++ b/packages/nuxt/src/runtime/hooks/captureErrorHook.ts @@ -1,8 +1,8 @@ -import { captureException, getClient, getCurrentScope } from '@sentry/core'; +import { captureException, flushIfServerless, getClient, getCurrentScope } from '@sentry/core'; // eslint-disable-next-line import/no-extraneous-dependencies import { H3Error } from 'h3'; import type { CapturedErrorContext } from 'nitropack/types'; -import { extractErrorContext, flushIfServerless } from '../utils'; +import { extractErrorContext } from '../utils'; /** * Hook that can be added in a Nitro plugin. It captures an error and sends it to Sentry. diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 543a8a78ebe1..c76f7ffce5bf 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -1,4 +1,10 @@ -import { debug, getDefaultIsolationScope, getIsolationScope, withIsolationScope } from '@sentry/core'; +import { + debug, + flushIfServerless, + getDefaultIsolationScope, + getIsolationScope, + withIsolationScope, +} from '@sentry/core'; // eslint-disable-next-line import/no-extraneous-dependencies import { type EventHandler } from 'h3'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -6,7 +12,7 @@ import { defineNitroPlugin } from 'nitropack/runtime'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; import { sentryCaptureErrorHook } from '../hooks/captureErrorHook'; import { updateRouteBeforeResponse } from '../hooks/updateRouteBeforeResponse'; -import { addSentryTracingMetaTags, flushIfServerless } from '../utils'; +import { addSentryTracingMetaTags } from '../utils'; export default defineNitroPlugin(nitroApp => { nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler); diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/runtime/utils.ts index 7c9b49612525..29abbe23ec62 100644 --- a/packages/nuxt/src/runtime/utils.ts +++ b/packages/nuxt/src/runtime/utils.ts @@ -1,5 +1,5 @@ import type { ClientOptions, Context, SerializedTraceData } from '@sentry/core'; -import { captureException, debug, flush, getClient, getTraceMetaTags, GLOBAL_OBJ, vercelWaitUntil } from '@sentry/core'; +import { captureException, debug, getClient, getTraceMetaTags } from '@sentry/core'; import type { VueOptions } from '@sentry/vue/src/types'; import type { CapturedErrorContext } from 'nitropack/types'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; @@ -85,33 +85,3 @@ export function reportNuxtError(options: { }); }); } - -async function flushWithTimeout(): Promise { - try { - debug.log('Flushing events...'); - await flush(2000); - debug.log('Done flushing events'); - } catch (e) { - debug.log('Error while flushing events:\n', e); - } -} - -/** - * Flushes if in a serverless environment - */ -export async function flushIfServerless(): Promise { - const isServerless = - !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions - !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda - !!process.env.K_SERVICE || // Google Cloud Run - !!process.env.CF_PAGES || // Cloudflare - !!process.env.VERCEL || - !!process.env.NETLIFY; - - // @ts-expect-error This is not typed - if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { - vercelWaitUntil(flushWithTimeout()); - } else if (isServerless) { - await flushWithTimeout(); - } -} diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 0a1ede6b83a1..ed04267a2536 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -94,7 +94,7 @@ function getNuxtDefaultIntegrations(options: NodeOptions): Integration[] { /** * Flushes pending Sentry events with a 2-second timeout and in a way that cannot create unhandled promise rejections. */ -export async function flushSafelyWithTimeout(): Promise { +async function flushSafelyWithTimeout(): Promise { try { DEBUG_BUILD && debug.log('Flushing events...'); await flush(2000); diff --git a/packages/solidstart/src/server/utils.ts b/packages/solidstart/src/server/utils.ts index fc7beea9daa0..1560b254bd22 100644 --- a/packages/solidstart/src/server/utils.ts +++ b/packages/solidstart/src/server/utils.ts @@ -1,28 +1,6 @@ import type { EventProcessor, Options } from '@sentry/core'; import { debug } from '@sentry/core'; -import { flush, getGlobalScope } from '@sentry/node'; -import { DEBUG_BUILD } from '../common/debug-build'; - -/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */ -export async function flushIfServerless(): Promise { - const isServerless = - !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions - !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda - !!process.env.K_SERVICE || // Google Cloud Run - !!process.env.CF_PAGES || // Cloudflare - !!process.env.VERCEL || - !!process.env.NETLIFY; - - if (isServerless) { - try { - DEBUG_BUILD && debug.log('Flushing events...'); - await flush(2000); - DEBUG_BUILD && debug.log('Done flushing events'); - } catch (e) { - DEBUG_BUILD && debug.log('Error while flushing events:\n', e); - } - } -} +import { getGlobalScope } from '@sentry/node'; /** * Determines if a thrown "error" is a redirect Response which Solid Start users can throw to redirect to another route. diff --git a/packages/solidstart/src/server/withServerActionInstrumentation.ts b/packages/solidstart/src/server/withServerActionInstrumentation.ts index a894837c3947..c5c726614279 100644 --- a/packages/solidstart/src/server/withServerActionInstrumentation.ts +++ b/packages/solidstart/src/server/withServerActionInstrumentation.ts @@ -1,6 +1,11 @@ -import { handleCallbackErrors, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR } from '@sentry/core'; +import { + flushIfServerless, + handleCallbackErrors, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_STATUS_ERROR, +} from '@sentry/core'; import { captureException, getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON, startSpan } from '@sentry/node'; -import { flushIfServerless, isRedirect } from './utils'; +import { isRedirect } from './utils'; /** * Wraps a server action (functions that use the 'use server' directive) diff --git a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts index 76acc1e46b12..d2bc90259942 100644 --- a/packages/solidstart/test/server/withServerActionInstrumentation.test.ts +++ b/packages/solidstart/test/server/withServerActionInstrumentation.test.ts @@ -1,4 +1,4 @@ -import { SentrySpan } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import * as SentryNode from '@sentry/node'; import { createTransport, @@ -16,7 +16,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { withServerActionInstrumentation } from '../../src/server'; const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => ''); -const mockFlush = vi.spyOn(SentryNode, 'flush').mockImplementation(async () => true); +const mockFlush = vi.spyOn(SentryCore, 'flushIfServerless').mockImplementation(async () => {}); const mockGetActiveSpan = vi.spyOn(SentryNode, 'getActiveSpan'); const mockGetRequestEvent = vi.fn(); @@ -126,7 +126,7 @@ describe('withServerActionInstrumentation', () => { }); it('sets a server action name on the active span', async () => { - const span = new SentrySpan(); + const span = new SentryCore.SentrySpan(); span.setAttribute('http.target', '/_server'); mockGetActiveSpan.mockReturnValue(span); const mockSpanSetAttribute = vi.spyOn(span, 'setAttribute'); @@ -145,7 +145,7 @@ describe('withServerActionInstrumentation', () => { }); it('does not set a server action name if the active span had a non `/_server` target', async () => { - const span = new SentrySpan(); + const span = new SentryCore.SentrySpan(); span.setAttribute('http.target', '/users/5'); mockGetActiveSpan.mockReturnValue(span); const mockSpanSetAttribute = vi.spyOn(span, 'setAttribute'); diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index aa2649a28a3a..696c3d765c5b 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -2,6 +2,7 @@ import type { Span } from '@sentry/core'; import { continueTrace, debug, + flushIfServerless, getCurrentScope, getDefaultIsolationScope, getIsolationScope, @@ -15,7 +16,7 @@ import { } from '@sentry/core'; import type { Handle, ResolveOptions } from '@sveltejs/kit'; import { DEBUG_BUILD } from '../common/debug-build'; -import { flushIfServerless, getTracePropagationData, sendErrorToSentry } from './utils'; +import { getTracePropagationData, sendErrorToSentry } from './utils'; export type SentryHandleOptions = { /** diff --git a/packages/sveltekit/src/server-common/handleError.ts b/packages/sveltekit/src/server-common/handleError.ts index 046e4201c3cb..0ca6597ea864 100644 --- a/packages/sveltekit/src/server-common/handleError.ts +++ b/packages/sveltekit/src/server-common/handleError.ts @@ -1,6 +1,5 @@ -import { captureException, consoleSandbox, flush } from '@sentry/core'; +import { captureException, consoleSandbox, flushIfServerless } from '@sentry/core'; import type { HandleServerError } from '@sveltejs/kit'; -import { flushIfServerless } from '../server-common/utils'; // The SvelteKit default error handler just logs the error's stack trace to the console // see: https://github.com/sveltejs/kit/blob/369e7d6851f543a40c947e033bfc4a9506fdc0a8/packages/kit/src/runtime/server/index.js#L43 @@ -48,14 +47,12 @@ export function handleErrorWithSentry(handleError?: HandleServerError): HandleSe }; }; - // Cloudflare workers have a `waitUntil` method that we can use to flush the event queue + // Cloudflare workers have a `waitUntil` method on `ctx` that we can use to flush the event queue // We already call this in `wrapRequestHandler` from `sentryHandleInitCloudflare` // However, `handleError` can be invoked when wrapRequestHandler already finished // (e.g. when responses are streamed / returning promises from load functions) - const cloudflareWaitUntil = platform?.context?.waitUntil; - if (typeof cloudflareWaitUntil === 'function') { - const waitUntil = cloudflareWaitUntil.bind(platform.context); - waitUntil(flush(2000)); + if (typeof platform?.context?.waitUntil === 'function') { + await flushIfServerless({ cloudflareCtx: platform.context as { waitUntil(promise: Promise): void } }); } else { await flushIfServerless(); } diff --git a/packages/sveltekit/src/server-common/load.ts b/packages/sveltekit/src/server-common/load.ts index ede0991d29c4..8b9cfca7de9b 100644 --- a/packages/sveltekit/src/server-common/load.ts +++ b/packages/sveltekit/src/server-common/load.ts @@ -1,12 +1,13 @@ import { addNonEnumerableProperty, + flushIfServerless, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startSpan, } from '@sentry/core'; import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit'; import type { SentryWrappedFlag } from '../common/utils'; -import { flushIfServerless, sendErrorToSentry } from './utils'; +import { sendErrorToSentry } from './utils'; type PatchedLoadEvent = LoadEvent & SentryWrappedFlag; type PatchedServerLoadEvent = ServerLoadEvent & SentryWrappedFlag; diff --git a/packages/sveltekit/src/server-common/serverRoute.ts b/packages/sveltekit/src/server-common/serverRoute.ts index 72607318ecb3..d09233cb3633 100644 --- a/packages/sveltekit/src/server-common/serverRoute.ts +++ b/packages/sveltekit/src/server-common/serverRoute.ts @@ -1,11 +1,12 @@ import { addNonEnumerableProperty, + flushIfServerless, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startSpan, } from '@sentry/core'; import type { RequestEvent } from '@sveltejs/kit'; -import { flushIfServerless, sendErrorToSentry } from './utils'; +import { sendErrorToSentry } from './utils'; type PatchedServerRouteEvent = RequestEvent & { __sentry_wrapped__?: boolean }; diff --git a/packages/sveltekit/src/server-common/utils.ts b/packages/sveltekit/src/server-common/utils.ts index 03601cb3bbb5..b861bf758697 100644 --- a/packages/sveltekit/src/server-common/utils.ts +++ b/packages/sveltekit/src/server-common/utils.ts @@ -1,6 +1,5 @@ -import { captureException, debug, flush, objectify } from '@sentry/core'; +import { captureException, objectify } from '@sentry/core'; import type { RequestEvent } from '@sveltejs/kit'; -import { DEBUG_BUILD } from '../common/debug-build'; import { isHttpError, isRedirect } from '../common/utils'; /** @@ -16,31 +15,6 @@ export function getTracePropagationData(event: RequestEvent): { sentryTrace: str return { sentryTrace, baggage }; } -/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */ -export async function flushIfServerless(): Promise { - if (typeof process === 'undefined') { - return; - } - - const isServerless = - !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions - !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda - !!process.env.K_SERVICE || // Google Cloud Run - !!process.env.CF_PAGES || // Cloudflare - !!process.env.VERCEL || - !!process.env.NETLIFY; - - if (isServerless) { - try { - DEBUG_BUILD && debug.log('Flushing events...'); - await flush(2000); - DEBUG_BUILD && debug.log('Done flushing events'); - } catch (e) { - DEBUG_BUILD && debug.log('Error while flushing events:\n', e); - } - } -} - /** * Extracts a server-side sveltekit error, filters a couple of known errors we don't want to capture * and captures the error via `captureException`.