From e1dae9daf2a0b56e347de260345b37a7e9a6c176 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 6 Dec 2024 10:48:26 +0100 Subject: [PATCH 1/4] fix(nuxt): Flush serverless to send events --- .../nuxt/src/runtime/plugins/sentry.server.ts | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 1159a6d427ff..596c5899d10c 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -1,3 +1,14 @@ +import { + GLOBAL_OBJ, + consoleSandbox, + flush, + getClient, + getDefaultIsolationScope, + getIsolationScope, + logger, + vercelWaitUntil, + withIsolationScope, +} from '@sentry/core'; import * as Sentry from '@sentry/node'; import { H3Error } from 'h3'; import { defineNitroPlugin } from 'nitropack/runtime'; @@ -5,7 +16,34 @@ import type { NuxtRenderHTMLContext } from 'nuxt/app'; import { addSentryTracingMetaTags, extractErrorContext } from '../utils'; export default defineNitroPlugin(nitroApp => { - nitroApp.hooks.hook('error', (error, errorContext) => { + nitroApp.h3App.handler = new Proxy(nitroApp.h3App.handler, { + async apply(handlerTarget, handlerThisArg, handlerArgs: Parameters) { + // In environments where we cannot make use of OTel httpInstrumentation, 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 + const isolationScope = getIsolationScope(); + const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; + + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + `[Sentry] Patched event handler. Using ${ + isolationScope === newIsolationScope ? 'existing' : 'new' + } isolationscope.`, + ); + }); + + return withIsolationScope(newIsolationScope, async () => { + try { + return await handlerTarget.apply(handlerThisArg, handlerArgs); + } finally { + await flushIfServerless(); + } + }); + }, + }); + + nitroApp.hooks.hook('error', async (error, errorContext) => { // Do not handle 404 and 422 if (error instanceof H3Error) { // Do not report if status code is 3xx or 4xx @@ -29,6 +67,8 @@ export default defineNitroPlugin(nitroApp => { captureContext: { contexts: { nuxt: structuredContext } }, mechanism: { handled: false }, }); + + await flushIfServerless(); }); // @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context @@ -36,3 +76,27 @@ export default defineNitroPlugin(nitroApp => { addSentryTracingMetaTags(html.head); }); }); + +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(); + } +} + +async function flushWithTimeout(): Promise { + const sentryClient = getClient(); + const isDebug = sentryClient ? sentryClient.getOptions().debug : false; + + 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); + } +} From 245864f82fd68e598da960f58676df0c9b806b59 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 6 Dec 2024 10:51:08 +0100 Subject: [PATCH 2/4] rewrite comment --- packages/nuxt/src/runtime/plugins/sentry.server.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 596c5899d10c..8e98e5e0c3d8 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -18,9 +18,8 @@ import { addSentryTracingMetaTags, extractErrorContext } from '../utils'; export default defineNitroPlugin(nitroApp => { nitroApp.h3App.handler = new Proxy(nitroApp.h3App.handler, { async apply(handlerTarget, handlerThisArg, handlerArgs: Parameters) { - // In environments where we cannot make use of OTel httpInstrumentation, 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 + // In environments where we cannot make use of OTel httpInstrumentation, e.g. when just importing the Sentry server config at + // the top level instead of `--import` or dynamic import like on Vercel, we still need to ensure requests are properly isolated const isolationScope = getIsolationScope(); const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; From 3a914147ccca2aaf40b8db4cad3a0684d8e6ba5b Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 6 Dec 2024 10:52:40 +0100 Subject: [PATCH 3/4] change log message --- packages/nuxt/src/runtime/plugins/sentry.server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 8e98e5e0c3d8..58333e18d2c1 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -26,9 +26,9 @@ export default defineNitroPlugin(nitroApp => { consoleSandbox(() => { // eslint-disable-next-line no-console console.log( - `[Sentry] Patched event handler. Using ${ - isolationScope === newIsolationScope ? 'existing' : 'new' - } isolationscope.`, + `[Sentry] Patched h3 event handler. ${ + isolationScope === newIsolationScope ? 'Using existing' : 'Created new' + } isolation scope.`, ); }); From 790f7654d20d5bd6c0baf1d2f28daf64cd257e43 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Fri, 6 Dec 2024 11:36:21 +0100 Subject: [PATCH 4/4] review suggestions --- .../nuxt/src/runtime/plugins/sentry.server.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 58333e18d2c1..15dd2ea27e61 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -1,6 +1,5 @@ import { GLOBAL_OBJ, - consoleSandbox, flush, getClient, getDefaultIsolationScope, @@ -18,19 +17,18 @@ import { addSentryTracingMetaTags, extractErrorContext } from '../utils'; export default defineNitroPlugin(nitroApp => { nitroApp.h3App.handler = new Proxy(nitroApp.h3App.handler, { async apply(handlerTarget, handlerThisArg, handlerArgs: Parameters) { - // In environments where we cannot make use of OTel httpInstrumentation, e.g. when just importing the Sentry server config at - // the top level instead of `--import` or dynamic import like on Vercel, we still need to ensure requests are properly isolated + // In environments where we cannot make use of OTel httpInstrumentation, we still need to ensure requests are properly isolated (e.g. when just importing the Sentry server config at the top level instead of `--import`). + // If OTel httpInstrumentation works, requests will be already isolated by the SentryHttpInstrumentation. + // We can identify this by comparing the current isolation scope to the default one. The requests are properly isolated if + // the current isolation scope is different from the default one. If that is not the case, we fork the isolation scope here. const isolationScope = getIsolationScope(); const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope; - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.log( - `[Sentry] Patched h3 event handler. ${ - isolationScope === newIsolationScope ? 'Using existing' : 'Created new' - } isolation scope.`, - ); - }); + logger.log( + `[Sentry] Patched h3 event handler. ${ + isolationScope === newIsolationScope ? 'Using existing' : 'Created new' + } isolation scope.`, + ); return withIsolationScope(newIsolationScope, async () => { try {