Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/nitro';

Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.E2E_TEST_DSN,
tunnel: `http://localhost:3031/`, // proxy server
dsn: process.env.E2E_TEST_TUNNEL ? 'http://public@localhost:3031/1337' : process.env.E2E_TEST_DSN,
tunnel: process.env.E2E_TEST_TUNNEL ? true : `http://localhost:3031/`, // proxy server
tracesSampleRate: 1,
});
19 changes: 18 additions & 1 deletion dev-packages/e2e-tests/test-applications/nitro-3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
"clean": "npx rimraf node_modules pnpm-lock.yaml .output",
"test": "playwright test",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test"
"test:assert": "pnpm test",
"test:build:tunnel-basepath": "pnpm install && E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app pnpm build",
"test:assert:tunnel-basepath": "E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app playwright test tunnel",
"test:assert:tunnel-basepath-dev": "TEST_ENV=development E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app playwright test tunnel"
},
"dependencies": {
"@sentry/browser": "latest || *",
Expand All @@ -26,5 +29,19 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"variants": [
{
"build-command": "pnpm test:build:tunnel-basepath",
"assert-command": "pnpm test:assert:tunnel-basepath",
"label": "nitro-3 (tunnel-basepath)"
},
{
"build-command": "pnpm install",
"assert-command": "pnpm test:assert:tunnel-basepath-dev",
"label": "nitro-3 (tunnel-basepath-dev)"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { getPlaywrightConfig } from '@sentry-internal/test-utils';

const config = getPlaywrightConfig({
startCommand: `pnpm start`,
startCommand:
process.env.TEST_ENV === 'development'
? `PORT=3030 NODE_OPTIONS='--import ./instrument.mjs' pnpm exec vite dev --port 3030`
: `pnpm start`,
});

export default config;
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { defineHandler, getQuery, setResponseHeader } from 'nitro/h3';
import { defineHandler, getQuery, HTTPError, setResponseHeader } from 'nitro/h3';

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

setResponseHeader(event, 'x-sentry-test-middleware', 'executed');

const query = getQuery(event);
Expand Down
12 changes: 10 additions & 2 deletions dev-packages/e2e-tests/test-applications/nitro-3/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import * as Sentry from '@sentry/browser';

declare const __E2E_TEST_TUNNEL__: boolean;
declare const __E2E_TEST_BASE_PATH__: string;

// Let's us test trace propagation
Sentry.init({
environment: 'qa',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
tunnel: 'http://localhost:3031/', // proxy server
dsn: __E2E_TEST_TUNNEL__ ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
// Plain Nitro has no client SDK to pass `baseURL` on, so the browser names it. The server picks it up itself.
tunnel: __E2E_TEST_TUNNEL__
? __E2E_TEST_BASE_PATH__
? { basePath: __E2E_TEST_BASE_PATH__ }
: true
: 'http://localhost:3031/', // proxy server
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1.0,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { expect, test } from '@playwright/test';
import {
isServerSpanAt,
makeClientReportEnvelope,
waitForError,
watchForTunnelSpan,
} from '@sentry-internal/test-utils';
import { getTunnelPath } from '@sentry/core';
import { connect } from 'node:net';

test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant');

const BASE_PATH = process.env.E2E_TEST_BASE_PATH ?? '';
const TUNNEL_PATH = new RegExp(`^${BASE_PATH}/[a-z0-9]{1,7}$`);

test('answers the managed tunnel before the app auth middleware', async ({ page }) => {
// The variant's auth middleware rejects every other POST.
expect((await page.request.post(`${BASE_PATH}/api/anything`, { data: 'x' })).status()).toBe(401);

const raceTunnelSpan = watchForTunnelSpan('nitro-3', isServerSpanAt(TUNNEL_PATH));
const tunnelResponsePromise = page.waitForResponse(
response => response.request().method() === 'POST' && TUNNEL_PATH.test(new URL(response.url()).pathname),
);

await page.goto(`${BASE_PATH}/`);

expect((await tunnelResponsePromise).status()).toBe(200);
expect(
await raceTunnelSpan(isServerSpanAt(`${BASE_PATH}/api/test-transaction`), () =>
page.request.get(`${BASE_PATH}/api/test-transaction`),
),
).toBe('anchor');
});

test('answers a request to the managed tunnel itself', async ({ request }) => {
const dsn = 'http://public@localhost:3031/1337';
const response = await request.post(`${BASE_PATH}${getTunnelPath(dsn)}`, {
data: makeClientReportEnvelope(dsn),
headers: { 'Content-Type': 'application/x-sentry-envelope' },
});

expect(response.headers()['x-sentry-tunnel']).toBe('1');
expect(response.status()).toBe(200);
});

test('does not report a client that drops the connection before the envelope header as an error', async ({
request,
}) => {
const dsn = 'http://public@localhost:3031/1337';
const firstErrorPromise = waitForError('nitro-3', () => true);

// Part of the envelope header line, without its newline, then the connection drops.
await new Promise<void>(resolve => {
const socket = connect(3030, 'localhost', () => {
socket.write(
`POST ${BASE_PATH}${getTunnelPath(dsn)} HTTP/1.1\r\nHost: localhost:3030\r\nContent-Type: application/x-sentry-envelope\r\nContent-Length: 1000000\r\n\r\n`,
);
socket.write(JSON.stringify({ dsn, padding: 'x'.repeat(1000) }).slice(0, 500));
setTimeout(() => {
socket.destroy();
resolve();
}, 300);
});
});

// Errors are sent in order, so one from the dropped request would arrive before this one.
await request.get(`${BASE_PATH}/api/test-error`);

const firstError = await firstErrorPromise;
expect(firstError.exception?.values?.[0]?.value).toBe('This is a test error');
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import { nitro } from 'nitro/vite';
import { defineConfig } from 'vite';

export default defineConfig({
define: {
__E2E_TEST_TUNNEL__: JSON.stringify(!!process.env.E2E_TEST_TUNNEL),
__E2E_TEST_BASE_PATH__: JSON.stringify(process.env.E2E_TEST_BASE_PATH ?? ''),
},
plugins: [
nitro(
// FIXME: Nitro plugin has a type issue
// @ts-expect-error
withSentryConfig({
serverDir: './server',
...(process.env.E2E_TEST_BASE_PATH && { baseURL: `${process.env.E2E_TEST_BASE_PATH}/` }),
}),
),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
export default defineNuxtConfig({
compatibilityDate: '2025-06-06',
modules: ['@sentry/nuxt/module'],
// The tunnel variant serves the app, and so the tunnel, under a base URL.
...(process.env.E2E_TEST_TUNNEL && { app: { baseURL: '/app/' } }),
nitro: {
preset: 'cloudflare_module',
cloudflare: {
Expand All @@ -14,5 +16,9 @@ export default defineNuxtConfig({
alias: {
'readable-stream': 'node:stream',
},
// The tunnel variant serves the managed tunnel.
replace: {
__E2E_TEST_TUNNEL__: JSON.stringify(!!process.env.E2E_TEST_TUNNEL),
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"preview": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --port 3030 --log-level=$(test $CI && echo 'none' || echo 'log')",
"test": "playwright test",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test"
"test:assert": "pnpm test",
"test:build:tunnel": "pnpm install && E2E_TEST_TUNNEL=1 pnpm build",
"test:assert:tunnel": "E2E_TEST_TUNNEL=1 pnpm test tunnel"
},
"dependencies": {
"@sentry/nuxt": "file:../../packed/sentry-nuxt-packed.tgz",
Expand All @@ -23,5 +25,14 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"variants": [
{
"build-command": "pnpm test:build:tunnel",
"assert-command": "pnpm test:assert:tunnel",
"label": "nuxt-4-cloudflare (tunnel)"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineEventHandler } from '#imports';

// A plain route for tests to request after the tunnel traffic.
export default defineEventHandler(() => 'ok');
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { defineNitroPlugin } from '#imports';
import { sentryCloudflareNitroPlugin } from '@sentry/nuxt/module/plugins';

declare const __E2E_TEST_TUNNEL__: boolean;

export default defineNitroPlugin(
sentryCloudflareNitroPlugin({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
// The tunnel forwards to the DSN host, which is the event proxy in the tunnel variant.
dsn: __E2E_TEST_TUNNEL__ ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0,
tunnel: 'http://localhost:3031/', // proxy server
tunnel: __E2E_TEST_TUNNEL__ ? true : 'http://localhost:3031/', // proxy server
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { expect, test } from '@playwright/test';
import { isServerSpanAt, makeClientReportEnvelope, watchForTunnelSpan } from '@sentry-internal/test-utils';

test.skip(!process.env.E2E_TEST_TUNNEL, 'Only runs in the tunnel variant');

// The isolate's first tunnel request is the one a late span filter would let through, so this stays the only test.
test('answers the managed tunnel set up by `sentryCloudflareNitroPlugin`, without a span of it', async ({
request,
}) => {
const dsn = 'http://public@localhost:3031/1337';
const raceTunnelSpan = watchForTunnelSpan('nuxt-4-cloudflare', isServerSpanAt(/^\/app\/qpo967/));

// The path the SDK derives from `dsn`.
const response = await request.post('/app/qpo967', {
data: makeClientReportEnvelope(dsn),
headers: { 'Content-Type': 'application/x-sentry-envelope' },
});

expect(response.headers()['x-sentry-tunnel']).toBe('1');
expect(response.status()).toBe(200);
expect(await raceTunnelSpan(isServerSpanAt('/app/api/anchor'), () => request.get('/app/api/anchor'))).toBe('anchor');
});
11 changes: 10 additions & 1 deletion dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
const tunnelVariant = !!process.env.E2E_TEST_TUNNEL;

export default defineNuxtConfig({
compatibilityDate: '2025-06-06',
imports: { autoImport: false },
Expand All @@ -10,14 +12,21 @@ export default defineNuxtConfig({
'/rendering-modes/swr-cached-page': { swr: true },
'/rendering-modes/swr-1h-cached-page': { swr: 3600 },
'/rendering-modes/pre-rendered-page': { prerender: true },
// A headers rule on every path, like a security headers setup, also covers the tunnel's path.
...(tunnelVariant && { '/**': { headers: { 'x-e2e-route-rule': '1' } } }),
},

// The base-path variant serves the app, and so the tunnel, under `/app`.
...(process.env.E2E_TEST_BASE_PATH && { app: { baseURL: `${process.env.E2E_TEST_BASE_PATH}/` } }),

modules: ['@pinia/nuxt', '@sentry/nuxt/module'],
runtimeConfig: {
public: {
sentry: {
dsn: 'https://public@dsn.ingest.sentry.io/1337',
// The tunnel variant forwards to the test proxy, so the DSN has to point there.
dsn: tunnelVariant ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
},
e2eTunnel: tunnelVariant,
},
},
nitro: {
Expand Down
15 changes: 14 additions & 1 deletion dev-packages/e2e-tests/test-applications/nuxt-4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
"test:build": "pnpm install && node ./scripts/build-with-prerender-event-sink.mjs",
"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",
"test:assert": "pnpm test:prod && pnpm test:dev",
"test:prod:import": "TEST_ENV=production-import playwright test"
"test:prod:import": "TEST_ENV=production-import playwright test",
"test:build:tunnel-basepath": "pnpm install && E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app node ./scripts/build-with-prerender-event-sink.mjs",
"test:assert:tunnel-basepath": "E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app TEST_ENV=production playwright test tunnel",
"test:assert:tunnel-basepath-dev": "E2E_TEST_TUNNEL=1 E2E_TEST_BASE_PATH=/app TEST_ENV=development playwright test tunnel"
},
"//": "Pin ioredis to 5.10.1 because that's the last version before it publishes its own diagnostics channels",
"dependencies": {
Expand Down Expand Up @@ -45,6 +48,16 @@
"build-command": "pnpm test:build",
"assert-command": "pnpm test:prod:import",
"label": "nuxt-4 (--import compat)"
},
{
"build-command": "pnpm test:build:tunnel-basepath",
"assert-command": "pnpm test:assert:tunnel-basepath",
"label": "nuxt-4 (tunnel-basepath)"
},
{
"build-command": "pnpm install",
"assert-command": "pnpm test:assert:tunnel-basepath-dev",
"label": "nuxt-4 (tunnel-basepath-dev)"
}
],
"optionalVariants": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { usePinia, useRuntimeConfig } from '#imports';

Sentry.init({
dsn: useRuntimeConfig().public.sentry.dsn,
tunnel: `http://localhost:3031/`, // proxy server
tunnel: useRuntimeConfig().public.e2eTunnel ? true : `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
integrations: [
Sentry.piniaIntegration(usePinia(), {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as Sentry from '@sentry/nuxt';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
dsn: process.env.E2E_TEST_TUNNEL ? 'http://public@localhost:3031/1337' : 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0, // Capture 100% of the transactions
tunnel: 'http://localhost:3031/', // proxy server
tunnel: process.env.E2E_TEST_TUNNEL ? true : 'http://localhost:3031/', // proxy server
// Opt into the Sentry OpenTelemetry tracer provider in the "(tracer provider)" e2e variant.
// Leaving it `undefined` otherwise keeps the SDK's default (no provider).
enableOpenTelemetrySetup: process.env.E2E_TEST_OTEL_SETUP === 'true' ? true : undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { defineEventHandler, setHeader, getQuery } from '#imports';
import { createError, defineEventHandler, getHeader, setHeader, getQuery } from '#imports';

export default defineEventHandler(async event => {
// The tunnel variant checks that the managed tunnel answers before an auth middleware like this one.
if (process.env.E2E_TEST_TUNNEL && event.method === 'POST' && getHeader(event, 'x-auth') !== 'ok') {
throw createError({ statusCode: 401, statusMessage: 'blocked by user auth' });
}

// Check if we should throw an error
const query = getQuery(event);
if (query.throwError === 'true') {
Expand Down
Loading
Loading