From ff7a07dc09730c3ec816d7daea153c4692e1df26 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:49:45 +0200 Subject: [PATCH] feat(nuxt): Add Http `responseHook` with `waitUntil` (#13986) With [waitUntil](https://vercel.com/docs/functions/functions-api-reference#waituntil) the lambda execution continues until all async tasks (like sending data to Sentry) are done. Timing-wise it should work like this: `span.end()` -> `waitUntil()` -> Nitro/Node `response.end()` The problem in [this PR](https://github.com/getsentry/sentry-javascript/pull/13895) was that the Nitro hook `afterResponse` is called to late (after `response.end()`), so `waitUntil()` could not be added to this hook. --- Just for reference how this is done in Nitro (and h3, the underlying http framework): 1. The Nitro `afterResponse` hook is called in `onAfterResponse` https://github.com/unjs/nitro/blob/359af68d2b3d51d740cf869d0f13aec0c5e9f565/src/runtime/internal/app.ts#L71-L77 2. h3 `onAfterResponse` is called after the Node response was sent (and `onBeforeResponse` is called too early for calling `waitUntil`, as the span just starts at this point): https://github.com/unjs/h3/blob/7324eeec854eecc37422074ef9f2aec8a5e4a816/src/adapters/node/index.ts#L38-L47 - `endNodeResponse` calls `response.end()`: https://github.com/unjs/h3/blob/7324eeec854eecc37422074ef9f2aec8a5e4a816/src/adapters/node/internal/utils.ts#L58 --- packages/nuxt/src/server/sdk.ts | 42 +++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index a6599b4ac088..59832bbb2a39 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,7 +1,12 @@ -import { applySdkMetadata, getGlobalScope } from '@sentry/core'; -import { init as initNode } from '@sentry/node'; -import type { Client, EventProcessor } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { applySdkMetadata, flush, getGlobalScope } from '@sentry/core'; +import { + type NodeOptions, + getDefaultIntegrations as getDefaultNodeIntegrations, + httpIntegration, + init as initNode, +} from '@sentry/node'; +import type { Client, EventProcessor, Integration } from '@sentry/types'; +import { logger, vercelWaitUntil } from '@sentry/utils'; import { DEBUG_BUILD } from '../common/debug-build'; import type { SentryNuxtServerOptions } from '../common/types'; @@ -14,6 +19,7 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { const sentryOptions = { ...options, registerEsmLoaderHooks: mergeRegisterEsmLoaderHooks(options), + defaultIntegrations: getNuxtDefaultIntegrations(options), }; applySdkMetadata(sentryOptions, 'nuxt', ['nuxt', 'node']); @@ -46,6 +52,21 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { return client; } +function getNuxtDefaultIntegrations(options: NodeOptions): Integration[] { + return [ + ...getDefaultNodeIntegrations(options).filter(integration => integration.name !== 'Http'), + // The httpIntegration is added as defaultIntegration, so users can still overwrite it + httpIntegration({ + instrumentation: { + responseHook: () => { + // Makes it possible to end the tracing span before closing the Vercel lambda (https://vercel.com/docs/functions/functions-api-reference#waituntil) + vercelWaitUntil(flushSafelyWithTimeout()); + }, + }, + }), + ]; +} + /** * Adds /vue/ to the registerEsmLoaderHooks options and merges it with the old values in the array if one is defined. * If the registerEsmLoaderHooks option is already a boolean, nothing is changed. @@ -64,3 +85,16 @@ export function mergeRegisterEsmLoaderHooks( } return options.registerEsmLoaderHooks ?? { exclude: [/vue/] }; } + +/** + * Flushes pending Sentry events with a 2-second timeout and in a way that cannot create unhandled promise rejections. + */ +export async function flushSafelyWithTimeout(): Promise { + try { + DEBUG_BUILD && logger.log('Flushing events...'); + await flush(2000); + DEBUG_BUILD && logger.log('Done flushing events'); + } catch (e) { + DEBUG_BUILD && logger.log('Error while flushing events:\n', e); + } +}