Skip to content

Commit 51e015c

Browse files
authored
feat(nitro-utils): Use patchEventHandler from nitro-utils (#14612)
1 parent a985d64 commit 51e015c

File tree

6 files changed

+72
-35
lines changed

6 files changed

+72
-35
lines changed

dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/sentry.server.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ Sentry.init({
55
environment: 'qa', // dynamic sampling bias to keep transactions
66
tracesSampleRate: 1.0, // Capture 100% of the transactions
77
tunnel: 'http://localhost:3031/', // proxy server
8+
debug: !!process.env.DEBUG,
89
});
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { getDefaultIsolationScope } from '@sentry/core';
2+
import * as Sentry from '@sentry/nuxt';
13
import { defineEventHandler } from '#imports';
24

35
export default defineEventHandler(event => {
6+
Sentry.setTag('my-isolated-tag', true);
7+
Sentry.setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); // We set this tag to be able to assert that the previously set tag has not leaked into the global isolation scope
8+
49
throw new Error('Nuxt 3 Server error');
510
});

dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError } from '@sentry-internal/test-utils';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
33

44
test.describe('server-side errors', async () => {
55
test('captures api fetch error (fetched on click)', async ({ page }) => {
6+
const transactionEventPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => {
7+
return transactionEvent?.transaction === 'GET /api/server-error';
8+
});
9+
610
const errorPromise = waitForError('nuxt-3-top-level-import', async errorEvent => {
711
return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Server error';
812
});
913

1014
await page.goto(`/fetch-server-error`);
1115
await page.getByText('Fetch Server Data', { exact: true }).click();
1216

17+
const transactionEvent = await transactionEventPromise;
1318
const error = await errorPromise;
1419

1520
expect(error.transaction).toEqual('GET /api/server-error');
@@ -18,6 +23,32 @@ test.describe('server-side errors', async () => {
1823
expect(exception.type).toEqual('Error');
1924
expect(exception.value).toEqual('Nuxt 3 Server error');
2025
expect(exception.mechanism.handled).toBe(false);
26+
27+
expect(error.tags?.['my-isolated-tag']).toBe(true);
28+
expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
29+
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
30+
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
31+
});
32+
33+
test('isolates requests', async ({ page }) => {
34+
const transactionEventPromise = waitForTransaction('nuxt-3-top-level-import', async transactionEvent => {
35+
return transactionEvent?.transaction === 'GET /api/server-error';
36+
});
37+
38+
const errorPromise = waitForError('nuxt-3-top-level-import', async errorEvent => {
39+
return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Server error';
40+
});
41+
42+
await page.goto(`/fetch-server-error`);
43+
await page.getByText('Fetch Server Data', { exact: true }).click();
44+
45+
const transactionEvent = await transactionEventPromise;
46+
const error = await errorPromise;
47+
48+
expect(error.tags?.['my-isolated-tag']).toBe(true);
49+
expect(error.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
50+
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
51+
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
2152
});
2253

2354
test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => {
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { GLOBAL_OBJ, flush, getClient, logger, vercelWaitUntil } from '@sentry/core';
2+
3+
/**
4+
* Flushes Sentry for serverless environments.
5+
*/
6+
export async function flushIfServerless(): Promise<void> {
7+
const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL || !!process.env.NETLIFY;
8+
9+
// @ts-expect-error - this is not typed
10+
if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) {
11+
vercelWaitUntil(flushWithTimeout());
12+
} else if (isServerless) {
13+
await flushWithTimeout();
14+
}
15+
}
16+
17+
/**
18+
* Flushes Sentry.
19+
*/
20+
export async function flushWithTimeout(): Promise<void> {
21+
const isDebug = getClient()?.getOptions()?.debug;
22+
23+
try {
24+
isDebug && logger.log('Flushing events...');
25+
await flush(2000);
26+
isDebug && logger.log('Done flushing events');
27+
} catch (e) {
28+
isDebug && logger.log('Error while flushing events:\n', e);
29+
}
30+
}

packages/nuxt/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
},
5454
"devDependencies": {
5555
"@nuxt/module-builder": "^0.8.4",
56+
"@sentry-internal/nitro-utils": "8.42.0",
5657
"nuxt": "^3.13.2"
5758
},
5859
"scripts": {

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

+3-34
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,13 @@
1-
import {
2-
GLOBAL_OBJ,
3-
flush,
4-
getClient,
5-
getDefaultIsolationScope,
6-
getIsolationScope,
7-
logger,
8-
vercelWaitUntil,
9-
withIsolationScope,
10-
} from '@sentry/core';
1+
import { patchEventHandler } from '@sentry-internal/nitro-utils';
2+
import { GLOBAL_OBJ, flush, getClient, logger, vercelWaitUntil } from '@sentry/core';
113
import * as Sentry from '@sentry/node';
124
import { H3Error } from 'h3';
135
import { defineNitroPlugin } from 'nitropack/runtime';
146
import type { NuxtRenderHTMLContext } from 'nuxt/app';
157
import { addSentryTracingMetaTags, extractErrorContext } from '../utils';
168

179
export default defineNitroPlugin(nitroApp => {
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-
});
10+
nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler);
4211

4312
nitroApp.hooks.hook('error', async (error, errorContext) => {
4413
// Do not handle 404 and 422

0 commit comments

Comments
 (0)