Repository navigation
feat(nextjs): Add a tunnel route handler that sends only the event payload to Sentry #25456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,9 @@ | |
|
|
||
| event-dumps | ||
|
|
||
| # Copied by `test:build-handler*` | ||
| /app/sentry-tunnel/ | ||
|
|
||
| # testing | ||
| /coverage | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| // The handler variants send the server events to the event proxy too, so the tests also see server spans. | ||
| export const SENTRY_DSN = | ||
| process.env.NEXT_PUBLIC_E2E_TEST_TUNNEL_MODE === 'handler' | ||
| ? 'http://public@localhost:3031/1337' | ||
| : // A fake but properly formatted Sentry SaaS DSN, because the rewrite mode tunnels only SaaS DSNs | ||
| 'https://public@o12345.ingest.us.sentry.io/67890'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; | ||
| import type { SerializedStreamedSpan } from '@sentry/core'; | ||
| import { getSpanOp, waitForRequest, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; | ||
| import type { Event, SerializedStreamedSpan } from '@sentry/core'; | ||
|
|
||
| function getStringAttribute(span: SerializedStreamedSpan, key: string): string | undefined { | ||
| const attribute = span.attributes[key]; | ||
|
|
@@ -120,3 +120,68 @@ test('Tunnel requests should not create middleware or fetch spans', async ({ pag | |
|
|
||
| expect(sentryFetchSpans).toHaveLength(0); | ||
| }); | ||
|
|
||
| test.describe('Tunnel route handler', () => { | ||
| test.skip(process.env.NEXT_PUBLIC_E2E_TEST_TUNNEL_MODE !== 'handler', 'Only the handler variants use the handler'); | ||
|
|
||
| test('forwards events without the cookies of the browser', async ({ page, context, baseURL }) => { | ||
| await context.addCookies([{ name: 'session', value: '8f2c1d', url: baseURL }]); | ||
| const proxyRequestPromise = waitForRequest('nextjs-16-tunnel', ({ envelope }) => | ||
| envelope[1].some( | ||
| ([itemHeader, item]) => | ||
| itemHeader.type === 'event' && | ||
| (item as Event).exception?.values?.[0]?.value === 'Tunnel route handler cookie test', | ||
| ), | ||
| ); | ||
|
|
||
| await page.goto('/'); | ||
| await page.waitForFunction(() => typeof (window as { __SENTRY__?: unknown }).__SENTRY__ === 'object'); | ||
| const tunnelRequestPromise = page.waitForRequest( | ||
| request => | ||
| request.method() === 'POST' && | ||
| new URL(request.url()).origin === baseURL && | ||
| (request.postData() ?? '').includes('Tunnel route handler cookie test'), | ||
| ); | ||
| await page.evaluate(() => { | ||
| setTimeout(() => { | ||
| throw new Error('Tunnel route handler cookie test'); | ||
| }); | ||
| }); | ||
|
|
||
| const tunnelRequest = await tunnelRequestPromise; | ||
| const proxyRequest = await proxyRequestPromise; | ||
|
|
||
| expect(new URL(tunnelRequest.url()).search).toBe(''); | ||
| expect((await tunnelRequest.allHeaders()).cookie).toBe('session=8f2c1d'); | ||
| expect(proxyRequest.rawProxyRequestHeaders.cookie).toBeUndefined(); | ||
| expect(proxyRequest.rawProxyRequestHeaders['content-type']).toBe('application/x-sentry-envelope'); | ||
| }); | ||
|
|
||
| test('does not create server spans for tunnel requests', async ({ page }) => { | ||
| // The callback never returns true, so this promise is deliberately left unsettled. | ||
| const allSpans: SerializedStreamedSpan[] = []; | ||
| void waitForStreamedSpans('nextjs-16-tunnel', spans => { | ||
| allSpans.push(...spans); | ||
| return false; | ||
| }); | ||
| const pageloadSpanPromise = waitForStreamedSpan('nextjs-16-tunnel', span => { | ||
| return getSpanOp(span) === 'pageload' && span.is_segment; | ||
| }); | ||
|
|
||
| await page.goto('/'); | ||
| await pageloadSpanPromise; | ||
| // Wait for the server to send the spans that the tunnel request would wrongly produce | ||
| await page.waitForTimeout(3000); | ||
|
|
||
| const postSpans = allSpans.filter( | ||
| span => span.name.startsWith('POST') || getStringAttribute(span, 'http.request.method') === 'POST', | ||
| ); | ||
| const ingestSpans = allSpans.filter( | ||
| span => | ||
| getSpanOp(span) === 'http.client' && | ||
| !!getStringAttribute(span, 'url.full')?.startsWith('http://localhost:3031/'), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tunnel tests hardcode span attributesLow Severity New span assertions compare Additional Locations (1)Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit de8fd3d. Configure here. |
||
| ); | ||
| expect(postSpans).toEqual([]); | ||
| expect(ingestSpans).toEqual([]); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { createSentryTunnelRoute } from '@sentry/nextjs/tunnel'; | ||
| import { SENTRY_DSN } from '@/sentry-dsn'; | ||
|
|
||
| export const { POST } = createSentryTunnelRoute({ allowedDsns: [SENTRY_DSN] }); | ||
|
|
||
| export const runtime = 'edge'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { POST } from '@sentry/nextjs/tunnel'; |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tunnel test sleeps to assert absence
Low Severity
The new handler test asserts that tunnel requests create no server spans by sleeping for three seconds after pageload. Absence checks that wait on a timeout stay racy under load and can pass while a late span still arrives.
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit de8fd3d. Configure here.