Skip to content

Commit ddd753f

Browse files
authored
feat(nuxt): Improve serverless event flushing and scope isolation (#14605)
Adds serverless (e.g. Vercel, Netlify) improvements: - Cloining/forking the isolation context when needed - flushing at the end of the h3 event handler and when an error happens - using Vercel's `waitUntil` for waiting on Sentry events to send before shutting down the function
1 parent 121ae07 commit ddd753f

File tree

1 file changed

+62
-1
lines changed

1 file changed

+62
-1
lines changed

packages/nuxt/src/runtime/plugins/sentry.server.ts

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,46 @@
1+
import {
2+
GLOBAL_OBJ,
3+
flush,
4+
getClient,
5+
getDefaultIsolationScope,
6+
getIsolationScope,
7+
logger,
8+
vercelWaitUntil,
9+
withIsolationScope,
10+
} from '@sentry/core';
111
import * as Sentry from '@sentry/node';
212
import { H3Error } from 'h3';
313
import { defineNitroPlugin } from 'nitropack/runtime';
414
import type { NuxtRenderHTMLContext } from 'nuxt/app';
515
import { addSentryTracingMetaTags, extractErrorContext } from '../utils';
616

717
export default defineNitroPlugin(nitroApp => {
8-
nitroApp.hooks.hook('error', (error, errorContext) => {
18+
nitroApp.h3App.handler = new Proxy(nitroApp.h3App.handler, {
19+
async apply(handlerTarget, handlerThisArg, handlerArgs: Parameters<typeof nitroApp.h3App.handler>) {
20+
// 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`).
21+
// If OTel httpInstrumentation works, requests will be already isolated by the SentryHttpInstrumentation.
22+
// We can identify this by comparing the current isolation scope to the default one. The requests are properly isolated if
23+
// the current isolation scope is different from the default one. If that is not the case, we fork the isolation scope here.
24+
const isolationScope = getIsolationScope();
25+
const newIsolationScope = isolationScope === getDefaultIsolationScope() ? isolationScope.clone() : isolationScope;
26+
27+
logger.log(
28+
`[Sentry] Patched h3 event handler. ${
29+
isolationScope === newIsolationScope ? 'Using existing' : 'Created new'
30+
} isolation scope.`,
31+
);
32+
33+
return withIsolationScope(newIsolationScope, async () => {
34+
try {
35+
return await handlerTarget.apply(handlerThisArg, handlerArgs);
36+
} finally {
37+
await flushIfServerless();
38+
}
39+
});
40+
},
41+
});
42+
43+
nitroApp.hooks.hook('error', async (error, errorContext) => {
944
// Do not handle 404 and 422
1045
if (error instanceof H3Error) {
1146
// Do not report if status code is 3xx or 4xx
@@ -29,10 +64,36 @@ export default defineNitroPlugin(nitroApp => {
2964
captureContext: { contexts: { nuxt: structuredContext } },
3065
mechanism: { handled: false },
3166
});
67+
68+
await flushIfServerless();
3269
});
3370

3471
// @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context
3572
nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext) => {
3673
addSentryTracingMetaTags(html.head);
3774
});
3875
});
76+
77+
async function flushIfServerless(): Promise<void> {
78+
const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL || !!process.env.NETLIFY;
79+
80+
// @ts-expect-error This is not typed
81+
if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) {
82+
vercelWaitUntil(flushWithTimeout());
83+
} else if (isServerless) {
84+
await flushWithTimeout();
85+
}
86+
}
87+
88+
async function flushWithTimeout(): Promise<void> {
89+
const sentryClient = getClient();
90+
const isDebug = sentryClient ? sentryClient.getOptions().debug : false;
91+
92+
try {
93+
isDebug && logger.log('Flushing events...');
94+
await flush(2000);
95+
isDebug && logger.log('Done flushing events');
96+
} catch (e) {
97+
isDebug && logger.log('Error while flushing events:\n', e);
98+
}
99+
}

0 commit comments

Comments
 (0)