diff --git a/packages/nitro-utils/src/index.ts b/packages/nitro-utils/src/index.ts index 9aa98faf5d62..92a212e16f1d 100644 --- a/packages/nitro-utils/src/index.ts +++ b/packages/nitro-utils/src/index.ts @@ -1,3 +1,5 @@ +export { patchEventHandler } from './nitro/patchEventHandler'; + export { wrapServerEntryWithDynamicImport, type WrapServerEntryPluginOptions, diff --git a/packages/nitro-utils/src/nitro/patchEventHandler.ts b/packages/nitro-utils/src/nitro/patchEventHandler.ts new file mode 100644 index 000000000000..d15da9f0db27 --- /dev/null +++ b/packages/nitro-utils/src/nitro/patchEventHandler.ts @@ -0,0 +1,39 @@ +import { getDefaultIsolationScope, getIsolationScope, logger, withIsolationScope } from '@sentry/core'; +import type { EventHandler } from 'h3'; +import { flushIfServerless } from '../util/flush'; + +/** + * A helper to patch a given `h3` event handler, ensuring that + * requests are properly isolated and data is flushed to Sentry. + */ +export function patchEventHandler(handler: EventHandler): EventHandler { + return new Proxy(handler, { + async apply(handlerTarget, handlerThisArg, handlerArgs: Parameters) { + // In environments where we cannot make use of the OTel + // http instrumentation (e.g. when using top level import + // of the server instrumentation file instead of + // `--import` or dynamic import, like on vercel) + // we still need to ensure requests are properly isolated + // by comparing the current isolation scope to the default + // one. + // Requests are properly isolated if they differ. + // If that's not the case, we fork the isolation scope here. + const isolationScope = getIsolationScope(); + const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + + logger.log( + `Patched h3 event handler. ${ + isolationScope === newIsolationScope ? 'Using existing' : 'Created new' + } isolation scope.`, + ); + + return withIsolationScope(newIsolationScope, async () => { + try { + return await handlerTarget.apply(handlerThisArg, handlerArgs); + } finally { + await flushIfServerless(); + } + }); + }, + }); +} diff --git a/packages/nitro-utils/src/util/flush.ts b/packages/nitro-utils/src/util/flush.ts new file mode 100644 index 000000000000..8e8f08cf1a3b --- /dev/null +++ b/packages/nitro-utils/src/util/flush.ts @@ -0,0 +1,30 @@ +import { GLOBAL_OBJ, flush, getClient, logger, vercelWaitUntil } from '@sentry/core'; + +/** + * Flushes Sentry for serverless environments. + */ +export async function flushIfServerless(): Promise { + const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!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(); + } +} + +/** + * Flushes Sentry. + */ +export async function flushWithTimeout(): Promise { + const isDebug = getClient()?.getOptions()?.debug; + + try { + isDebug && logger.log('Flushing events...'); + await flush(2000); + isDebug && logger.log('Done flushing events'); + } catch (e) { + isDebug && logger.log('Error while flushing events:\n', e); + } +}