Skip to content

Commit 2184334

Browse files
committed
Remove Http integration from Next.js
1 parent 214bfb3 commit 2184334

File tree

5 files changed

+131
-76
lines changed

5 files changed

+131
-76
lines changed
Lines changed: 45 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import {
2-
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
3-
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
42
SPAN_STATUS_ERROR,
53
addTracingExtensions,
64
captureException,
7-
continueTrace,
5+
getActiveSpan,
6+
getIsolationScope,
7+
getRootSpan,
88
handleCallbackErrors,
99
setHttpStatus,
10-
startSpan,
11-
withIsolationScope,
1210
} from '@sentry/core';
1311
import { winterCGHeadersToDict } from '@sentry/utils';
14-
1512
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
1613
import type { RouteHandlerContext } from './types';
1714
import { platformSupportsStreaming } from './utils/platformSupportsStreaming';
@@ -26,73 +23,55 @@ export function wrapRouteHandlerWithSentry<F extends (...args: any[]) => any>(
2623
context: RouteHandlerContext,
2724
): (...args: Parameters<F>) => ReturnType<F> extends Promise<unknown> ? ReturnType<F> : Promise<ReturnType<F>> {
2825
addTracingExtensions();
29-
const { method, parameterizedRoute, headers } = context;
26+
27+
const { headers } = context;
28+
3029
return new Proxy(routeHandler, {
31-
apply: (originalFunction, thisArg, args) => {
32-
return withIsolationScope(async isolationScope => {
33-
isolationScope.setSDKProcessingMetadata({
34-
request: {
35-
headers: headers ? winterCGHeadersToDict(headers) : undefined,
36-
},
37-
});
38-
return continueTrace(
39-
{
40-
// TODO(v8): Make it so that continue trace will allow null as sentryTrace value and remove this fallback here
41-
sentryTrace: headers?.get('sentry-trace') ?? undefined,
42-
baggage: headers?.get('baggage'),
43-
},
44-
async () => {
45-
try {
46-
return await startSpan(
47-
{
48-
op: 'http.server',
49-
name: `${method} ${parameterizedRoute}`,
50-
forceTransaction: true,
51-
attributes: {
52-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
53-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs',
54-
},
55-
},
56-
async span => {
57-
const response: Response = await handleCallbackErrors(
58-
() => originalFunction.apply(thisArg, args),
59-
error => {
60-
// Next.js throws errors when calling `redirect()`. We don't wanna report these.
61-
if (isRedirectNavigationError(error)) {
62-
// Don't do anything
63-
} else if (isNotFoundNavigationError(error)) {
64-
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' });
65-
} else {
66-
captureException(error, {
67-
mechanism: {
68-
handled: false,
69-
},
70-
});
71-
}
72-
},
73-
);
30+
apply: async (originalFunction, thisArg, args) => {
31+
getIsolationScope().setSDKProcessingMetadata({
32+
request: {
33+
headers: headers ? winterCGHeadersToDict(headers) : undefined,
34+
},
35+
});
7436

75-
try {
76-
if (span && response.status) {
77-
setHttpStatus(span, response.status);
78-
}
79-
} catch {
80-
// best effort - response may be undefined?
81-
}
37+
try {
38+
const activeSpan = getActiveSpan();
39+
const rootSpan = activeSpan && getRootSpan(activeSpan);
8240

83-
return response;
41+
const response: Response = await handleCallbackErrors(
42+
() => originalFunction.apply(thisArg, args),
43+
error => {
44+
// Next.js throws errors when calling `redirect()`. We don't wanna report these.
45+
if (isRedirectNavigationError(error)) {
46+
// Don't do anything
47+
} else if (isNotFoundNavigationError(error) && rootSpan) {
48+
rootSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' });
49+
} else {
50+
captureException(error, {
51+
mechanism: {
52+
handled: false,
8453
},
85-
);
86-
} finally {
87-
if (!platformSupportsStreaming() || process.env.NEXT_RUNTIME === 'edge') {
88-
// 1. Edge transport requires manual flushing
89-
// 2. Lambdas require manual flushing to prevent execution freeze before the event is sent
90-
await flushQueue();
91-
}
54+
});
9255
}
9356
},
9457
);
95-
});
58+
59+
try {
60+
if (rootSpan && response.status) {
61+
setHttpStatus(rootSpan, response.status);
62+
}
63+
} catch {
64+
// best effort - response may be undefined?
65+
}
66+
67+
return response;
68+
} finally {
69+
if (!platformSupportsStreaming() || process.env.NEXT_RUNTIME === 'edge') {
70+
// 1. Edge transport requires manual flushing
71+
// 2. Lambdas require manual flushing to prevent execution freeze before the event is sent
72+
await flushQueue();
73+
}
74+
}
9675
},
9776
});
9877
}

packages/nextjs/src/server/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ export function init(options: NodeOptions): void {
7575
...getDefaultIntegrations(options).filter(
7676
integration =>
7777
integration.name !== 'OnUncaughtException' &&
78-
// Next.js comes with its own Node-Fetch instrumentation so we shouldn't add ours on-top
79-
integration.name !== 'NodeFetch',
78+
// Next.js comes with its own Node-Fetch and Http instrumentation, so we shouldn't add ours on-top
79+
integration.name !== 'NodeFetch' &&
80+
integration.name !== 'Http',
8081
),
8182
onUncaughtExceptionIntegration(),
8283
];

packages/node-experimental/src/integrations/http.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,6 @@ const _httpIntegration = ((options: HttpOptions = {}) => {
6565
ignoreIncomingRequestHook: request => {
6666
const url = getRequestUrl(request);
6767

68-
const method = request.method?.toUpperCase();
69-
// We do not capture OPTIONS/HEAD requests as transactions
70-
if (method === 'OPTIONS' || method === 'HEAD') {
71-
return true;
72-
}
73-
7468
if (_ignoreIncomingRequests && _ignoreIncomingRequests(url)) {
7569
return true;
7670
}

packages/opentelemetry/src/sampler.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { TraceState } from '@opentelemetry/core';
44
import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base';
55
import { SamplingDecision } from '@opentelemetry/sdk-trace-base';
66
import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, hasTracingEnabled } from '@sentry/core';
7-
import type { Client, ClientOptions, SamplingContext } from '@sentry/types';
7+
import type { Client, ClientOptions, SamplingContext, SpanAttributes } from '@sentry/types';
88
import { isNaN, logger } from '@sentry/utils';
99
import { SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from './constants';
1010

11+
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
1112
import { DEBUG_BUILD } from './debug-build';
1213
import { getPropagationContextFromSpanContext, getSamplingDecision } from './propagator';
1314
import { setIsSetup } from './utils/setupCheck';
@@ -29,7 +30,7 @@ export class SentrySampler implements Sampler {
2930
traceId: string,
3031
spanName: string,
3132
_spanKind: unknown,
32-
spanAttributes: unknown,
33+
spanAttributes: SpanAttributes,
3334
_links: unknown,
3435
): SamplingResult {
3536
const options = this._client.getOptions();
@@ -82,6 +83,15 @@ export class SentrySampler implements Sampler {
8283
};
8384
}
8485

86+
const method = `${spanAttributes[SemanticAttributes.HTTP_METHOD]}`.toUpperCase();
87+
if (method === 'OPTIONS' || method === 'HEAD') {
88+
return {
89+
decision: SamplingDecision.NOT_RECORD,
90+
attributes,
91+
traceState: traceState.set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'),
92+
};
93+
}
94+
8595
// if the function returned 0 (or false), or if `tracesSampleRate` is 0, it's a sign the transaction should be dropped
8696
if (!sampleRate) {
8797
DEBUG_BUILD &&

packages/opentelemetry/test/trace.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import type { Event, Scope } from '@sentry/types';
2121
import { getSamplingDecision, makeTraceState } from '../src/propagator';
2222

23+
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
2324
import { continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../src/trace';
2425
import type { AbstractSpan } from '../src/types';
2526
import { getDynamicSamplingContextFromSpan } from '../src/utils/dynamicSamplingContext';
@@ -1353,6 +1354,76 @@ describe('trace (sampling)', () => {
13531354
});
13541355
});
13551356

1357+
describe('HTTP methods (sampling)', () => {
1358+
beforeEach(() => {
1359+
mockSdkInit({ enableTracing: true });
1360+
});
1361+
1362+
afterEach(() => {
1363+
cleanupOtel();
1364+
});
1365+
1366+
it('does sample when HTTP method is other than OPTIONS or HEAD', () => {
1367+
const spanGET = startSpanManual(
1368+
{ name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'GET' } },
1369+
span => {
1370+
return span;
1371+
},
1372+
);
1373+
expect(spanIsSampled(spanGET)).toBe(true);
1374+
expect(getSamplingDecision(spanGET.spanContext())).toBe(true);
1375+
1376+
const spanPOST = startSpanManual(
1377+
{ name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'POST' } },
1378+
span => {
1379+
return span;
1380+
},
1381+
);
1382+
expect(spanIsSampled(spanPOST)).toBe(true);
1383+
expect(getSamplingDecision(spanPOST.spanContext())).toBe(true);
1384+
1385+
const spanPUT = startSpanManual(
1386+
{ name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'PUT' } },
1387+
span => {
1388+
return span;
1389+
},
1390+
);
1391+
expect(spanIsSampled(spanPUT)).toBe(true);
1392+
expect(getSamplingDecision(spanPUT.spanContext())).toBe(true);
1393+
1394+
const spanDELETE = startSpanManual(
1395+
{ name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'DELETE' } },
1396+
span => {
1397+
return span;
1398+
},
1399+
);
1400+
expect(spanIsSampled(spanDELETE)).toBe(true);
1401+
expect(getSamplingDecision(spanDELETE.spanContext())).toBe(true);
1402+
});
1403+
1404+
it('does not sample when HTTP method is OPTIONS', () => {
1405+
const span = startSpanManual(
1406+
{ name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'OPTIONS' } },
1407+
span => {
1408+
return span;
1409+
},
1410+
);
1411+
expect(spanIsSampled(span)).toBe(false);
1412+
expect(getSamplingDecision(span.spanContext())).toBe(false);
1413+
});
1414+
1415+
it('does not sample when HTTP method is HEAD', () => {
1416+
const span = startSpanManual(
1417+
{ name: 'test span', attributes: { [SemanticAttributes.HTTP_METHOD]: 'HEAD' } },
1418+
span => {
1419+
return span;
1420+
},
1421+
);
1422+
expect(spanIsSampled(span)).toBe(false);
1423+
expect(getSamplingDecision(span.spanContext())).toBe(false);
1424+
});
1425+
});
1426+
13561427
describe('continueTrace', () => {
13571428
beforeEach(() => {
13581429
mockSdkInit({ enableTracing: true });

0 commit comments

Comments
 (0)