Skip to content

Commit f787df1

Browse files
committed
feat: drop tunnel requests from middleware execution
1 parent 2feb13e commit f787df1

File tree

3 files changed

+89
-0
lines changed

3 files changed

+89
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { ATTR_URL_QUERY, SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions';
2+
import { type Span, type SpanAttributes, GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
3+
import { isSentryRequestSpan } from '@sentry/opentelemetry';
4+
import { ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes';
5+
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached';
6+
7+
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
8+
_sentryRewritesTunnelPath?: string;
9+
};
10+
11+
/**
12+
* Drops spans for tunnel requests from middleware or fetch instrumentation.
13+
* This catches both:
14+
* 1. Requests to the local tunnel route (before rewrite)
15+
* 2. Requests to Sentry ingest (after rewrite)
16+
*/
17+
export function dropMiddlewareTunnelRequests(span: Span, attrs: SpanAttributes | undefined): void {
18+
// Only filter middleware spans or HTTP fetch spans
19+
const isMiddleware = attrs?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute';
20+
// The fetch span could be originating from rewrites re-writing a tunnel request
21+
// So we want to filter it out
22+
const isFetchSpan = attrs?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.otel.node_fetch';
23+
24+
// If the span is not a middleware span or a fetch span, return
25+
if (!isMiddleware && !isFetchSpan) {
26+
return;
27+
}
28+
29+
// Check if this is either a tunnel route request or a Sentry ingest request
30+
const isTunnel = isTunnelRouteSpan(attrs || {});
31+
const isSentry = isSentryRequestSpan(span);
32+
33+
if (isTunnel || isSentry) {
34+
// Mark the span to be dropped
35+
span.setAttribute(TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, true);
36+
}
37+
}
38+
39+
/**
40+
* Checks if a span's HTTP target matches the tunnel route.
41+
*/
42+
function isTunnelRouteSpan(spanAttributes: Record<string, unknown>): boolean {
43+
// Don't use process.env here because it will have a different value in the build and runtime
44+
// We want to use the one in build
45+
const tunnelPath = globalWithInjectedValues._sentryRewritesTunnelPath || process.env._sentryRewritesTunnelPath;
46+
if (!tunnelPath) {
47+
return false;
48+
}
49+
50+
// Check both http.target (older) and url.query (newer) attributes
51+
// eslint-disable-next-line deprecation/deprecation
52+
const httpTarget = spanAttributes[SEMATTRS_HTTP_TARGET] || spanAttributes[ATTR_URL_QUERY];
53+
54+
if (typeof httpTarget === 'string') {
55+
// Extract pathname from the target (e.g., "/tunnel?o=123&p=456" -> "/tunnel")
56+
const pathname = httpTarget.split('?')[0] || '';
57+
58+
return pathname.startsWith(tunnelPath);
59+
}
60+
61+
return false;
62+
}

packages/nextjs/src/edge/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { context } from '@opentelemetry/api';
22
import {
3+
type EventProcessor,
34
applySdkMetadata,
45
getCapturedScopesOnSpan,
56
getCurrentScope,
@@ -19,7 +20,9 @@ import {
1920
import { getScopesFromContext } from '@sentry/opentelemetry';
2021
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
2122
import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge';
23+
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../common/span-attributes-with-logic-attached';
2224
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
25+
import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests';
2326
import { isBuild } from '../common/utils/isBuild';
2427
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
2528
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
@@ -35,6 +38,7 @@ export type EdgeOptions = VercelEdgeOptions;
3538
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
3639
_sentryRewriteFramesDistDir?: string;
3740
_sentryRelease?: string;
41+
_sentryRewritesTunnelPath?: string;
3842
};
3943

4044
/** Inits the Sentry NextJS SDK on the Edge Runtime. */
@@ -70,6 +74,8 @@ export function init(options: VercelEdgeOptions = {}): void {
7074
const rootSpan = getRootSpan(span);
7175
const isRootSpan = span === rootSpan;
7276

77+
dropMiddlewareTunnelRequests(span, spanAttributes);
78+
7379
// Mark all spans generated by Next.js as 'auto'
7480
if (spanAttributes?.['next.span_type'] !== undefined) {
7581
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto');
@@ -137,6 +143,24 @@ export function init(options: VercelEdgeOptions = {}): void {
137143
}
138144
});
139145

146+
getGlobalScope().addEventProcessor(
147+
Object.assign(
148+
(event => {
149+
// Filter transactions that we explicitly want to drop.
150+
if (event.type === 'transaction') {
151+
if (event.contexts?.trace?.data?.[TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]) {
152+
return null;
153+
}
154+
155+
return event;
156+
} else {
157+
return event;
158+
}
159+
}) satisfies EventProcessor,
160+
{ id: 'NextLowQualityTransactionsFilter' },
161+
),
162+
);
163+
140164
try {
141165
// @ts-expect-error `process.turbopack` is a magic string that will be replaced by Next.js
142166
if (process.turbopack) {

packages/nextjs/src/server/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION,
3939
} from '../common/span-attributes-with-logic-attached';
4040
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
41+
import { dropMiddlewareTunnelRequests } from '../common/utils/dropMiddlewareTunnelRequests';
4142
import { isBuild } from '../common/utils/isBuild';
4243
import { setUrlProcessingMetadata } from '../common/utils/setUrlProcessingMetadata';
4344
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
@@ -169,6 +170,8 @@ export function init(options: NodeOptions): NodeClient | undefined {
169170
const rootSpan = getRootSpan(span);
170171
const isRootSpan = span === rootSpan;
171172

173+
dropMiddlewareTunnelRequests(span, spanAttributes);
174+
172175
// What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted
173176
// by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute.
174177
if (typeof spanAttributes?.[ATTR_NEXT_ROUTE] === 'string') {

0 commit comments

Comments
 (0)