Repository navigation
feat(nextjs): Serve the tunnel from the app with a DSN-derived path #25424
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
Draft
Draft
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8f944c8
feat(nextjs): Serve the tunnel from the app with a DSN-derived path
logaretm 988fb97
fix(nextjs): Read the injected tunnel options when the client has none
logaretm f10a7fe
test(nextjs): Wait for a later span instead of a timeout in the tunne…
logaretm ed111d1
fix(nextjs): Write the tunnel route into the root app dir when the ro…
logaretm aa83fbf
test(nextjs): Wait for a later span instead of a timeout in the pages…
logaretm 11b9c12
fix(nextjs): Point middleware at the tunnel path instead of the envel…
logaretm 5658032
feat(nextjs): Accept allowed DSNs in `tunnelRoute` for a browser DSN …
logaretm 30bf482
feat(nextjs): Export `isTunnelRequest` so middleware can let the tunn…
logaretm f6df1c6
fix(nextjs): Serve the server's own DSN path alongside `allowedDsns`
logaretm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 7 additions & 1 deletion
8
dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/proxy.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
dev-packages/e2e-tests/test-applications/nextjs-16-tunnel/tests/tunnel-dsn-path.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { getSpanOp, waitForError, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; | ||
| import type { SerializedStreamedSpan } from '@sentry/core'; | ||
| import { getTunnelPath } from '@sentry/core'; | ||
|
|
||
| const TUNNEL_PATH = getTunnelPath('http://public@localhost:3031/1337'); | ||
|
|
||
| test('sends events to the DSN-derived path, served by the app', async ({ page }) => { | ||
| const tunnelRequests: string[] = []; | ||
| page.on('request', request => { | ||
| if (request.method() === 'POST') { | ||
| tunnelRequests.push(new URL(request.url()).pathname); | ||
| } | ||
| }); | ||
|
|
||
| const errorPromise = waitForError('nextjs-16-tunnel', event => { | ||
| return event.exception?.values?.[0]?.value === 'Managed tunnel error'; | ||
| }); | ||
|
|
||
| await page.goto('/'); | ||
| await page.evaluate(() => { | ||
| setTimeout(() => { | ||
| throw new Error('Managed tunnel error'); | ||
| }); | ||
| }); | ||
|
|
||
| expect(await errorPromise).toBeDefined(); | ||
| expect(tunnelRequests).toContain(TUNNEL_PATH); | ||
| }); | ||
|
|
||
| test('does not create server spans for tunnel requests', async ({ page }) => { | ||
| const serverSpans: SerializedStreamedSpan[] = []; | ||
| void waitForStreamedSpans('nextjs-16-tunnel', spans => { | ||
| serverSpans.push(...spans.filter(span => span.is_segment)); | ||
| return false; | ||
| }); | ||
|
|
||
| const errorPromise = waitForError('nextjs-16-tunnel', event => { | ||
| return event.exception?.values?.[0]?.value === 'Managed tunnel span error'; | ||
| }); | ||
|
|
||
| await page.goto('/'); | ||
| await page.evaluate(() => { | ||
| setTimeout(() => { | ||
| throw new Error('Managed tunnel span error'); | ||
| }); | ||
| }); | ||
| await errorPromise; | ||
|
|
||
| // Spans flush in order, so a span for the tunnel request would arrive before the one for this request. | ||
| const anchorSpanPromise = waitForStreamedSpan( | ||
| 'nextjs-16-tunnel', | ||
| span => span.is_segment && getSpanOp(span) === 'http.server' && span.name === 'GET /', | ||
| ); | ||
| await page.request.get('/'); | ||
| await anchorSpanPromise; | ||
|
|
||
| const tunnelSpans = serverSpans.filter( | ||
| span => span.name.includes(TUNNEL_PATH!) || span.name.includes('sentry-tunnel'), | ||
| ); | ||
| expect(tunnelSpans).toEqual([]); | ||
|
logaretm marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| test('lets tunnel requests through auth middleware with `isTunnelRequest`', async ({ request }) => { | ||
| const headers = { 'content-type': 'application/x-sentry-envelope', 'x-e2e-require-auth': '1' }; | ||
| const data = `${JSON.stringify({ dsn: 'http://public@localhost:3031/1337' })}\n`; | ||
|
|
||
| expect((await request.post(TUNNEL_PATH!, { headers, data })).status()).toBe(200); | ||
| expect((await request.post('/notatunnel', { headers, data })).status()).toBe(401); | ||
| }); | ||
|
|
||
| test('answers 404 to an envelope POST on any other path', async ({ request }) => { | ||
| const response = await request.post('/notatunnel', { | ||
| headers: { 'content-type': 'application/x-sentry-envelope' }, | ||
| data: `${JSON.stringify({ dsn: 'http://public@localhost:3031/1337' })}\n`, | ||
| }); | ||
|
|
||
| expect(response.status()).toBe(404); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/tunnel-dsn-path.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { getSpanOp, waitForError, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; | ||
| import type { SerializedStreamedSpan } from '@sentry/core'; | ||
| import { getTunnelPath } from '@sentry/core'; | ||
|
|
||
| const TUNNEL_PATH = getTunnelPath('http://public@localhost:3031/1337'); | ||
|
|
||
| test.skip(!process.env.NEXT_PUBLIC_E2E_TUNNEL, 'Only runs with `tunnelRoute`'); | ||
|
|
||
| test('sends events to the DSN-derived path, served by the app', async ({ page }) => { | ||
| const tunnelRequests: string[] = []; | ||
| page.on('request', request => { | ||
| if (request.method() === 'POST') { | ||
| tunnelRequests.push(new URL(request.url()).pathname); | ||
| } | ||
| }); | ||
|
|
||
| const errorPromise = waitForError('nextjs-pages-dir', event => { | ||
| return event.exception?.values?.[0]?.value === 'Managed tunnel error'; | ||
| }); | ||
|
|
||
| await page.goto('/'); | ||
| await page.evaluate(() => { | ||
| setTimeout(() => { | ||
| throw new Error('Managed tunnel error'); | ||
| }); | ||
| }); | ||
|
|
||
| expect(await errorPromise).toBeDefined(); | ||
| expect(tunnelRequests).toContain(TUNNEL_PATH); | ||
| }); | ||
|
|
||
| test('does not create server spans for tunnel requests', async ({ page }) => { | ||
| const serverSpans: SerializedStreamedSpan[] = []; | ||
| void waitForStreamedSpans('nextjs-pages-dir', spans => { | ||
| serverSpans.push(...spans.filter(span => span.is_segment)); | ||
| return false; | ||
| }); | ||
|
|
||
| const errorPromise = waitForError('nextjs-pages-dir', event => { | ||
| return event.exception?.values?.[0]?.value === 'Managed tunnel span error'; | ||
| }); | ||
|
|
||
| await page.goto('/'); | ||
| await page.evaluate(() => { | ||
| setTimeout(() => { | ||
| throw new Error('Managed tunnel span error'); | ||
| }); | ||
| }); | ||
| await errorPromise; | ||
|
|
||
| // Spans flush in order, so a span for the tunnel request would arrive before the one for this request. | ||
| const anchorSpanPromise = waitForStreamedSpan( | ||
| 'nextjs-pages-dir', | ||
| span => span.is_segment && getSpanOp(span) === 'http.server' && span.name === 'GET /', | ||
| ); | ||
| await page.request.get('/'); | ||
| await anchorSpanPromise; | ||
|
|
||
| const tunnelSpans = serverSpans.filter( | ||
| span => span.name.includes(TUNNEL_PATH!) || span.name.includes('sentry-tunnel'), | ||
| ); | ||
| expect(tunnelSpans).toEqual([]); | ||
| }); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| test('lets tunnel requests through auth middleware with `isTunnelRequest`', async ({ request }) => { | ||
| const headers = { 'content-type': 'application/x-sentry-envelope', 'x-e2e-require-auth': '1' }; | ||
| const data = `${JSON.stringify({ dsn: 'http://public@localhost:3031/1337' })}\n`; | ||
|
|
||
| expect((await request.post(TUNNEL_PATH!, { headers, data })).status()).toBe(200); | ||
| expect((await request.post('/notatunnel', { headers, data })).status()).toBe(401); | ||
| }); | ||
|
|
||
| test('answers 404 to an envelope POST on any other path', async ({ request }) => { | ||
| const response = await request.post('/notatunnel', { | ||
| headers: { 'content-type': 'application/x-sentry-envelope' }, | ||
| data: `${JSON.stringify({ dsn: 'http://public@localhost:3031/1337' })}\n`, | ||
| }); | ||
|
|
||
| expect(response.status()).toBe(404); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,33 +1,12 @@ | ||
| import { debug, dsnFromString, GLOBAL_OBJ } from '@sentry/core'; | ||
| import type { BrowserOptions } from '@sentry/react'; | ||
| import { DEBUG_BUILD } from '../common/debug-build'; | ||
|
|
||
| const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { | ||
| _sentryRewritesTunnelPath?: string; | ||
| }; | ||
| import { getInjectedManagedTunnelOptions } from '../common/managedTunnelRoute'; | ||
|
|
||
| /** | ||
| * Applies the `tunnel` option to the Next.js SDK options based on `withSentryConfig`'s `tunnelRoute` option. | ||
| */ | ||
| export function applyTunnelRouteOption(options: BrowserOptions): void { | ||
| const tunnelRouteOption = process.env._sentryRewritesTunnelPath || globalWithInjectedValues._sentryRewritesTunnelPath; | ||
| if (tunnelRouteOption && options.dsn) { | ||
| const dsnComponents = dsnFromString(options.dsn); | ||
| if (!dsnComponents) { | ||
| return; | ||
| } | ||
| const sentrySaasDsnMatch = dsnComponents.host.match(/^o(\d+)\.ingest(?:\.([a-z]{2}))?\.sentry\.io$/); | ||
| if (sentrySaasDsnMatch) { | ||
| const orgId = sentrySaasDsnMatch[1]; | ||
| const regionCode = sentrySaasDsnMatch[2]; | ||
| let tunnelPath = `${tunnelRouteOption}?o=${orgId}&p=${dsnComponents.projectId}`; | ||
| if (regionCode) { | ||
| tunnelPath += `&r=${regionCode}`; | ||
| } | ||
| options.tunnel = tunnelPath; | ||
| DEBUG_BUILD && debug.log(`Tunneling events to "${tunnelPath}"`); | ||
| } else { | ||
| DEBUG_BUILD && debug.warn('Provided DSN is not a Sentry SaaS DSN. Will not tunnel events.'); | ||
| } | ||
| const tunnelOptions = options.tunnel === undefined && getInjectedManagedTunnelOptions(); | ||
| if (tunnelOptions) { | ||
| options.tunnel = tunnelOptions; | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.