Skip to content

Commit 7fe783d

Browse files
committed
feat(nuxt, nitro): Serve the managed tunnel before the app's server middleware
On Nitro v3, Nuxt and `@sentry/nitro` put the tunnel check first in h3's middleware list, since Nitro runs the app's `server/middleware` before anything a module registers and has no public way to go first. On Nitro v2, Nuxt answers from the `request` hook.
1 parent de28310 commit 7fe783d

23 files changed

Lines changed: 241 additions & 23 deletions

File tree

‎dev-packages/e2e-tests/test-applications/nitro-3/instrument.mjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/nitro';
22

33
Sentry.init({
44
environment: 'qa', // dynamic sampling bias to keep transactions
5-
dsn: process.env.E2E_TEST_DSN,
6-
tunnel: `http://localhost:3031/`, // proxy server
5+
dsn: process.env.E2E_TEST_TUNNEL ? 'http://public@localhost:3031/1337' : process.env.E2E_TEST_DSN,
6+
tunnel: process.env.E2E_TEST_TUNNEL ? true : `http://localhost:3031/`, // proxy server
77
tracesSampleRate: 1,
88
});

‎dev-packages/e2e-tests/test-applications/nitro-3/package.json‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"clean": "npx rimraf node_modules pnpm-lock.yaml .output",
1010
"test": "playwright test",
1111
"test:build": "pnpm install && pnpm build",
12-
"test:assert": "pnpm test"
12+
"test:assert": "pnpm test",
13+
"test:build:tunnel": "pnpm install && E2E_TEST_TUNNEL=1 pnpm build",
14+
"test:assert:tunnel": "E2E_TEST_TUNNEL=1 playwright test tunnel"
1315
},
1416
"dependencies": {
1517
"@sentry/browser": "latest || *",
@@ -25,5 +27,14 @@
2527
},
2628
"volta": {
2729
"extends": "../../package.json"
30+
},
31+
"sentryTest": {
32+
"variants": [
33+
{
34+
"build-command": "pnpm test:build:tunnel",
35+
"assert-command": "pnpm test:assert:tunnel",
36+
"label": "nitro-3 (tunnel)"
37+
}
38+
]
2839
}
2940
}

‎dev-packages/e2e-tests/test-applications/nitro-3/server/middleware/test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { defineHandler, getQuery, setResponseHeader } from 'nitro/h3';
1+
import { defineHandler, getQuery, HTTPError, setResponseHeader } from 'nitro/h3';
22

33
export default defineHandler(event => {
4+
// The tunnel variant checks that the managed tunnel answers before an auth middleware like this one.
5+
if (process.env.E2E_TEST_TUNNEL && event.req.method === 'POST' && event.req.headers.get('x-auth') !== 'ok') {
6+
throw new HTTPError({ status: 401, statusText: 'blocked by user auth' });
7+
}
8+
49
setResponseHeader(event, 'x-sentry-test-middleware', 'executed');
510

611
const query = getQuery(event);
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import * as Sentry from '@sentry/browser';
22

3+
declare const __E2E_TEST_TUNNEL__: boolean;
4+
35
// Let's us test trace propagation
46
Sentry.init({
57
environment: 'qa',
6-
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7-
tunnel: 'http://localhost:3031/', // proxy server
8+
dsn: __E2E_TEST_TUNNEL__ ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
9+
tunnel: __E2E_TEST_TUNNEL__ ? true : 'http://localhost:3031/', // proxy server
810
integrations: [Sentry.browserTracingIntegration()],
911
tracesSampleRate: 1.0,
1012
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect, test } from '@playwright/test';
2+
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
3+
4+
test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant');
5+
6+
const TUNNEL_PATH = /^\/[a-z0-9]{1,7}$/;
7+
8+
test('answers the managed tunnel before the app auth middleware', async ({ page }) => {
9+
// The variant's auth middleware rejects every other POST.
10+
expect((await page.request.post('/api/anything', { data: 'x' })).status()).toBe(401);
11+
12+
const tunnelServerSpanPromise = waitForStreamedSpan(
13+
'nitro-3',
14+
span => getSpanOp(span) === 'http.server' && TUNNEL_PATH.test(String(span.attributes['url.path']?.value ?? '')),
15+
);
16+
const tunnelResponsePromise = page.waitForResponse(
17+
response => response.request().method() === 'POST' && TUNNEL_PATH.test(new URL(response.url()).pathname),
18+
);
19+
20+
await page.goto('/');
21+
const tunnelResponse = await tunnelResponsePromise;
22+
23+
expect(tunnelResponse.status()).toBe(200);
24+
expect(new URL(tunnelResponse.url()).origin).toBe(new URL(page.url()).origin);
25+
26+
// Anchor on a regular server span requested after the tunnel traffic: spans flush in order, so a tunnel span
27+
// would arrive first.
28+
const anchorSpanPromise = waitForStreamedSpan(
29+
'nitro-3',
30+
span =>
31+
getSpanOp(span) === 'http.server' && String(span.attributes['url.path']?.value ?? '') === '/api/test-transaction',
32+
);
33+
await page.request.get('/api/test-transaction');
34+
35+
const winner = await Promise.race([
36+
tunnelServerSpanPromise.then(() => 'tunnel' as const),
37+
anchorSpanPromise.then(() => 'anchor' as const),
38+
]);
39+
expect(winner).toBe('anchor');
40+
});

‎dev-packages/e2e-tests/test-applications/nitro-3/vite.config.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { nitro } from 'nitro/vite';
33
import { defineConfig } from 'vite';
44

55
export default defineConfig({
6+
define: {
7+
__E2E_TEST_TUNNEL__: JSON.stringify(!!process.env.E2E_TEST_TUNNEL),
8+
},
69
plugins: [
710
nitro(
811
// FIXME: Nitro plugin has a type issue

‎dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// https://nuxt.com/docs/api/configuration/nuxt-config
2+
const tunnelVariant = !!process.env.E2E_TEST_TUNNEL;
3+
24
export default defineNuxtConfig({
35
compatibilityDate: '2025-06-06',
46
imports: { autoImport: false },
@@ -16,8 +18,10 @@ export default defineNuxtConfig({
1618
runtimeConfig: {
1719
public: {
1820
sentry: {
19-
dsn: 'https://public@dsn.ingest.sentry.io/1337',
21+
// The tunnel variant forwards to the test proxy, so the DSN has to point there.
22+
dsn: tunnelVariant ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
2023
},
24+
e2eTunnel: tunnelVariant,
2125
},
2226
},
2327
nitro: {

‎dev-packages/e2e-tests/test-applications/nuxt-4/package.json‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
"test:build": "pnpm install && node ./scripts/build-with-prerender-event-sink.mjs",
1717
"test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@latest && pnpm add nitropack@npm:nitropack-nightly@latest && pnpm install --force && node ./scripts/build-with-prerender-event-sink.mjs",
1818
"test:assert": "pnpm test:prod && pnpm test:dev",
19-
"test:prod:import": "TEST_ENV=production-import playwright test"
19+
"test:prod:import": "TEST_ENV=production-import playwright test",
20+
"test:build:tunnel": "pnpm install && E2E_TEST_TUNNEL=1 node ./scripts/build-with-prerender-event-sink.mjs",
21+
"test:assert:tunnel": "E2E_TEST_TUNNEL=1 TEST_ENV=production playwright test tunnel"
2022
},
2123
"//": "Pin ioredis to 5.10.1 because that's the last version before it publishes its own diagnostics channels",
2224
"dependencies": {
@@ -45,6 +47,11 @@
4547
"build-command": "pnpm test:build",
4648
"assert-command": "pnpm test:prod:import",
4749
"label": "nuxt-4 (--import compat)"
50+
},
51+
{
52+
"build-command": "pnpm test:build:tunnel",
53+
"assert-command": "pnpm test:assert:tunnel",
54+
"label": "nuxt-4 (tunnel)"
4855
}
4956
],
5057
"optionalVariants": [

‎dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { usePinia, useRuntimeConfig } from '#imports';
33

44
Sentry.init({
55
dsn: useRuntimeConfig().public.sentry.dsn,
6-
tunnel: `http://localhost:3031/`, // proxy server
6+
tunnel: useRuntimeConfig().public.e2eTunnel ? true : `http://localhost:3031/`, // proxy server
77
tracesSampleRate: 1.0,
88
integrations: [
99
Sentry.piniaIntegration(usePinia(), {

‎dev-packages/e2e-tests/test-applications/nuxt-4/sentry.server.config.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as Sentry from '@sentry/nuxt';
22

33
Sentry.init({
4-
dsn: 'https://public@dsn.ingest.sentry.io/1337',
4+
dsn: process.env.E2E_TEST_TUNNEL ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
55
tracesSampleRate: 1.0, // Capture 100% of the transactions
6-
tunnel: 'http://localhost:3031/', // proxy server
6+
tunnel: process.env.E2E_TEST_TUNNEL ? true : 'http://localhost:3031/', // proxy server
77
// Opt into the Sentry OpenTelemetry tracer provider in the "(tracer provider)" e2e variant.
88
// Leaving it `undefined` otherwise keeps the SDK's default (no provider).
99
enableOpenTelemetrySetup: process.env.E2E_TEST_OTEL_SETUP === 'true' ? true : undefined,

0 commit comments

Comments
 (0)