Skip to content

Commit 4b92c64

Browse files
authored
fix(nextjs): universal random tunnel path support (#18257)
Summary for changelog: The `tunnelRoute: true` option didn't work well with Turbopack due to repeated runs of the config files leading to different tunnel URLs in client, server and edge runtimes, this PR fixes that while also fixing Sentry requests spans not being dropped by the sampler. When using Next.js with Turbopack and the Sentry tunnel route feature (`tunnelRoute: true`), several issues prevented events from being sent properly: ### 1. Tunnel Route Consistency (Turbopack) **Problem**: Random tunnel routes were generated separately for client and server builds in Turbopack. **Solution**: Implemented processs-level caching in `withSentryConfig.ts`: - Extract tunnel route resolution into `resolveTunnelRoute()` function - Use `process.env` to store the random tunnel value across server/client builds. ### 2. Filter Tunnel Request Spans **Problem**: Requests to the tunnel route (before rewrite) and to Sentry ingest URLs (after rewrite) were creating spans that polluted Sentry with internal instrumentation noise, spans were being created by the middleware and OTEL node.js fetch instrumentation. **Solution**: Implemented server-side span filtering: - Created `dropMiddlewareTunnelRequests()` utility to detect and drop tunnel-related spans - Filter spans originating from `Middleware.execute` (Next.js middleware) - Filter spans originating from `auto.http.otel.node_fetch` (Node.js fetch instrumentation) - Check both local tunnel paths and Sentry ingest URLs (using `isSentryRequestSpan` from `@sentry/opentelemetry`) - Mark matching spans with `TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION` to prevent them from being sent - I tried `beforeSampling` hook but it didn't work for some reason, so I stuck with the drop attribute. ---- The final issue was excluding the tunnel requests from the middleware/proxy, but there are many blockers for a solution: 1. The `config` must be statically analyzable, so we cannot expose `withSentryMiddlewareConfig` wrapper of any kind. 2. Warning the user doesn't help much because they can't do anything about it since the tunnel route is random. 3. Tested out writing a loader for turbopack/webpack to inject the tunnel into the matcher as an array but user existing matcher can match still. 4. Only way is to inject an exclusion match into the user existing matcher, if it is an array then we need to inject it into each single entry. I may explore this further later with a loader for both webpack/turbopack, and figure out a reliable way to inject the negative matchers into the user expressions.
1 parent 6240191 commit 4b92c64

29 files changed

+576
-7
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
event-dumps
14+
15+
# testing
16+
/coverage
17+
18+
# next.js
19+
/.next/
20+
/out/
21+
22+
# production
23+
/build
24+
25+
# misc
26+
.DS_Store
27+
*.pem
28+
29+
# debug
30+
npm-debug.log*
31+
yarn-debug.log*
32+
yarn-error.log*
33+
.pnpm-debug.log*
34+
35+
# env files (can opt-in for committing if needed)
36+
.env*
37+
38+
# vercel
39+
.vercel
40+
41+
# typescript
42+
*.tsbuildinfo
43+
next-env.d.ts
44+
45+
# Sentry Config File
46+
.env.sentry-build-plugin
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
3+
public-hoist-pattern[]=*import-in-the-middle*
4+
public-hoist-pattern[]=*require-in-the-middle*
Binary file not shown.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
import NextError from 'next/error';
5+
import { useEffect } from 'react';
6+
7+
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
8+
useEffect(() => {
9+
Sentry.captureException(error);
10+
}, [error]);
11+
12+
return (
13+
<html>
14+
<body>
15+
{/* `NextError` is the default Next.js error page component. Its type
16+
definition requires a `statusCode` prop. However, since the App Router
17+
does not expose status codes for errors, we simply pass 0 to render a
18+
generic error message. */}
19+
<NextError statusCode={0} />
20+
</body>
21+
</html>
22+
);
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Layout({ children }: { children: React.ReactNode }) {
2+
return (
3+
<html lang="en">
4+
<body>{children}</body>
5+
</html>
6+
);
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>Next.js 16 Tunnel Route Test</p>;
3+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { dirname } from 'path';
2+
import { fileURLToPath } from 'url';
3+
import { FlatCompat } from '@eslint/eslintrc';
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = dirname(__filename);
7+
8+
const compat = new FlatCompat({
9+
baseDirectory: __dirname,
10+
});
11+
12+
const eslintConfig = [
13+
...compat.extends('next/core-web-vitals', 'next/typescript'),
14+
{
15+
ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
16+
},
17+
];
18+
19+
export default eslintConfig;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
Sentry.init({
4+
environment: 'qa', // dynamic sampling bias to keep transactions
5+
// Use a fake but properly formatted Sentry SaaS DSN for tunnel route testing
6+
dsn: 'https://public@o12345.ingest.us.sentry.io/67890',
7+
// No tunnel option - using tunnelRoute from withSentryConfig
8+
tracesSampleRate: 1.0,
9+
sendDefaultPii: true,
10+
});
11+
12+
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
export async function register() {
4+
if (process.env.NEXT_RUNTIME === 'nodejs') {
5+
await import('./sentry.server.config');
6+
}
7+
8+
if (process.env.NEXT_RUNTIME === 'edge') {
9+
await import('./sentry.edge.config');
10+
}
11+
}
12+
13+
export const onRequestError = Sentry.captureRequestError;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { withSentryConfig } from '@sentry/nextjs';
2+
import type { NextConfig } from 'next';
3+
4+
const nextConfig: NextConfig = {};
5+
6+
export default withSentryConfig(nextConfig, {
7+
silent: true,
8+
tunnelRoute: true,
9+
});

0 commit comments

Comments
 (0)