Skip to content

Commit 72f38c8

Browse files
committed
add startOrUpdateSpan function
1 parent 0f8dd5e commit 72f38c8

File tree

5 files changed

+71
-54
lines changed

5 files changed

+71
-54
lines changed

dev-packages/e2e-tests/test-applications/nextjs-14/tests/request-instrumentation.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ test('Should send a transaction with a fetch span', async ({ page }) => {
2424
data: expect.objectContaining({
2525
'http.method': 'GET',
2626
'sentry.op': 'http.client',
27-
'sentry.origin': 'auto.http.otel.http',
27+
// todo: without the HTTP integration in the Next.js SDK, this is set to 'manual' -> we could rename this to be more specific
28+
'sentry.origin': 'manual',
2829
}),
2930
description: 'GET http://example.com/',
3031
}),

packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts

Lines changed: 57 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
23
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
34
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
45
SPAN_STATUS_ERROR,
@@ -18,42 +19,38 @@ import type { RouteHandlerContext } from './types';
1819
import { platformSupportsStreaming } from './utils/platformSupportsStreaming';
1920
import { flushQueue } from './utils/responseEnd';
2021

21-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22-
async function addSpanAttributes<F extends (...args: any[]) => any>(
23-
originalFunction: F,
24-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25-
thisArg: any,
26-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
27-
args: any[],
28-
rootSpan?: Span,
22+
/** As our own HTTP integration is disabled (src/server/index.ts) the rootSpan comes from Next.js.
23+
* In case there is not root span, we start a new span. */
24+
function startOrUpdateSpan(
25+
spanName: string,
26+
handleResponseErrors: (rootSpan: Span) => Promise<Response>,
2927
): Promise<Response> {
30-
const response: Response = await handleCallbackErrors(
31-
() => originalFunction.apply(thisArg, args),
32-
error => {
33-
// Next.js throws errors when calling `redirect()`. We don't wanna report these.
34-
if (isRedirectNavigationError(error)) {
35-
// Don't do anything
36-
} else if (isNotFoundNavigationError(error) && rootSpan) {
37-
rootSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' });
38-
} else {
39-
captureException(error, {
40-
mechanism: {
41-
handled: false,
42-
},
43-
});
44-
}
45-
},
46-
);
28+
const activeSpan = getActiveSpan();
29+
const rootSpan = activeSpan && getRootSpan(activeSpan);
4730

48-
try {
49-
if (rootSpan && response.status) {
50-
setHttpStatus(rootSpan, response.status);
51-
}
52-
} catch {
53-
// best effort - response may be undefined?
54-
}
31+
if (rootSpan) {
32+
rootSpan.updateName(spanName);
33+
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
34+
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
35+
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.function.nextjs');
5536

56-
return response;
37+
return handleResponseErrors(rootSpan);
38+
} else {
39+
return startSpan(
40+
{
41+
op: 'http.server',
42+
name: spanName,
43+
forceTransaction: true,
44+
attributes: {
45+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
46+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs',
47+
},
48+
},
49+
(span: Span) => {
50+
return handleResponseErrors(span);
51+
},
52+
);
53+
}
5754
}
5855

5956
/**
@@ -78,27 +75,35 @@ export function wrapRouteHandlerWithSentry<F extends (...args: any[]) => any>(
7875
});
7976

8077
try {
81-
const activeSpan = getActiveSpan();
82-
const rootSpan = activeSpan && getRootSpan(activeSpan);
83-
84-
if (rootSpan) {
85-
return await addSpanAttributes<F>(originalFunction, thisArg, args, rootSpan);
86-
} else {
87-
/** As our own HTTP integration is disabled (src/server/index.ts) the rootSpan comes from Next.js.
88-
* In case there is not root span, we start a new one. */
89-
return await startSpan(
90-
{
91-
op: 'http.server',
92-
name: `${method} ${parameterizedRoute}`,
93-
forceTransaction: true,
94-
attributes: {
95-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
96-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs',
97-
},
78+
return await startOrUpdateSpan(`${method} ${parameterizedRoute}`, async (rootSpan: Span) => {
79+
const response: Response = await handleCallbackErrors(
80+
() => originalFunction.apply(thisArg, args),
81+
error => {
82+
// Next.js throws errors when calling `redirect()`. We don't wanna report these.
83+
if (isRedirectNavigationError(error)) {
84+
// Don't do anything
85+
} else if (isNotFoundNavigationError(error) && rootSpan) {
86+
rootSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' });
87+
} else {
88+
captureException(error, {
89+
mechanism: {
90+
handled: false,
91+
},
92+
});
93+
}
9894
},
99-
async span => addSpanAttributes(originalFunction, thisArg, args, span),
10095
);
101-
}
96+
97+
try {
98+
if (rootSpan && response.status) {
99+
setHttpStatus(rootSpan, response.status);
100+
}
101+
} catch {
102+
// best effort - response may be undefined?
103+
}
104+
105+
return response;
106+
});
102107
} finally {
103108
if (!platformSupportsStreaming() || process.env.NEXT_RUNTIME === 'edge') {
104109
// 1. Edge transport requires manual flushing

packages/nextjs/src/server/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { onUncaughtExceptionIntegration } from './onUncaughtExceptionIntegration
1212

1313
export * from '@sentry/node';
1414
import type { EventProcessor } from '@sentry/types';
15+
import { requestIsolationScopeIntegration } from './requestIsolationScopeIntegration';
1516

1617
export { captureUnderscoreErrorException } from '../common/_error';
1718
export { onUncaughtExceptionIntegration } from './onUncaughtExceptionIntegration';
@@ -81,6 +82,7 @@ export function init(options: NodeOptions): void {
8182
integration.name !== 'Http',
8283
),
8384
onUncaughtExceptionIntegration(),
85+
requestIsolationScopeIntegration(),
8486
];
8587

8688
// This value is injected at build time, based on the output directory specified in the build config. Though a default

packages/opentelemetry/src/sampler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export class SentrySampler implements Sampler {
7575

7676
const method = `${spanAttributes[SemanticAttributes.HTTP_METHOD]}`.toUpperCase();
7777
if (method === 'OPTIONS' || method === 'HEAD') {
78+
DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`);
7879
return {
7980
decision: SamplingDecision.NOT_RECORD,
8081
attributes,

packages/opentelemetry/src/utils/mapStatus.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const canonicalGrpcErrorCodesMap: Record<string, SpanStatus['message']> = {
2626
'16': 'unauthenticated',
2727
} as const;
2828

29+
const isStatusErrorMessageValid = (message: string): boolean => {
30+
return Object.values(canonicalGrpcErrorCodesMap).includes(message as SpanStatus['message']);
31+
};
32+
2933
/**
3034
* Get a Sentry span status from an otel span.
3135
*/
@@ -39,7 +43,11 @@ export function mapStatus(span: AbstractSpan): SpanStatus {
3943
return { code: SPAN_STATUS_OK };
4044
// If the span is already marked as erroneous we return that exact status
4145
} else if (status.code === SpanStatusCode.ERROR) {
42-
return { code: SPAN_STATUS_ERROR, message: status.message };
46+
if (typeof status.message === 'undefined' || isStatusErrorMessageValid(status.message)) {
47+
return { code: SPAN_STATUS_ERROR, message: status.message };
48+
} else {
49+
return { code: SPAN_STATUS_ERROR, message: 'unknown_error' };
50+
}
4351
}
4452
}
4553

0 commit comments

Comments
 (0)