Skip to content

Commit 3881ffb

Browse files
committed
.
1 parent cffb093 commit 3881ffb

File tree

8 files changed

+102
-21
lines changed

8 files changed

+102
-21
lines changed

packages/nextjs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
},
6767
"dependencies": {
6868
"@opentelemetry/instrumentation-http": "0.51.1",
69+
"@opentelemetry/api": "^1.8.0",
6970
"@rollup/plugin-commonjs": "24.0.0",
7071
"@sentry/core": "8.2.1",
7172
"@sentry/node": "8.2.1",

packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,39 @@ import {
55
} from '@sentry/core';
66
import { WINDOW, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from '@sentry/react';
77
import type { Client } from '@sentry/types';
8-
import { addFetchInstrumentationHandler, browserPerformanceTimeOrigin } from '@sentry/utils';
8+
import { addFetchInstrumentationHandler, browserPerformanceTimeOrigin, parseBaggageHeader } from '@sentry/utils';
99

1010
/** Instruments the Next.js app router for pageloads. */
1111
export function appRouterInstrumentPageLoad(client: Client): void {
12+
// We use an event processor to override the automatically collected Request Browser metric span ID with the span ID
13+
// hint from the server so that the SSR spans are properly attached to the request span.
14+
client.addEventProcessor(event => {
15+
if (event.type !== 'transaction' || event.contexts?.trace?.op !== 'pageload') {
16+
return event;
17+
}
18+
19+
const baggage = WINDOW.document.querySelector('meta[name=baggage]')?.getAttribute('content');
20+
if (baggage) {
21+
const parsedBaggage = parseBaggageHeader(baggage);
22+
if (parsedBaggage && parsedBaggage['sentry-request-span-id-suggestion']) {
23+
const spanIdSuggestion = parsedBaggage['sentry-request-span-id-suggestion'];
24+
event.spans?.forEach(span => {
25+
if (span.description === 'request' && span.op === 'browser') {
26+
// Replace request span ID
27+
span.span_id = spanIdSuggestion;
28+
29+
if (event.contexts?.trace) {
30+
// Unset the parent span of the pageload transaction - it is now the root of the trace
31+
event.contexts.trace.parent_span_id = undefined;
32+
}
33+
}
34+
});
35+
}
36+
}
37+
38+
return event;
39+
});
40+
1241
startBrowserTracingPageLoadSpan(client, {
1342
name: WINDOW.location.pathname,
1443
// pageload should always start at timeOrigin (and needs to be in s, not ms)

packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@ import {
33
SPAN_STATUS_ERROR,
44
SPAN_STATUS_OK,
55
captureException,
6+
getActiveSpan,
67
getClient,
78
handleCallbackErrors,
89
startSpanManual,
910
withIsolationScope,
1011
withScope,
1112
} from '@sentry/core';
1213
import type { WebFetchHeaders } from '@sentry/types';
13-
import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils';
14+
import { propagationContextFromHeaders, uuid4, winterCGHeadersToDict } from '@sentry/utils';
1415

16+
import { context as otelContext } from '@opentelemetry/api';
1517
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
18+
import { EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY } from '@sentry/opentelemetry';
1619
import type { GenerationFunctionContext } from '../common/types';
1720
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
1821
import {
@@ -32,6 +35,7 @@ export function wrapGenerationFunctionWithSentry<F extends (...args: any[]) => a
3235
const { requestAsyncStorage, componentRoute, componentType, generationFunctionIdentifier } = context;
3336
return new Proxy(generationFunction, {
3437
apply: (originalFunction, thisArg, args) => {
38+
const requestTraceId = getActiveSpan()?.spanContext().traceId;
3539
return escapeNextjsTracing(() => {
3640
let headers: WebFetchHeaders | undefined = undefined;
3741
// We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API
@@ -50,22 +54,32 @@ export function wrapGenerationFunctionWithSentry<F extends (...args: any[]) => a
5054
data = { params, searchParams };
5155
}
5256

53-
const incomingPropagationContext = propagationContextFromHeaders(
54-
headers?.get('sentry-trace') ?? undefined,
55-
headers?.get('baggage'),
56-
);
57+
const completeHeadersDict: Record<string, string> = headers ? winterCGHeadersToDict(headers) : {};
5758

5859
const isolationScope = commonObjectToIsolationScope(headers);
59-
const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext);
60+
61+
isolationScope.setSDKProcessingMetadata({
62+
request: {
63+
headers: completeHeadersDict,
64+
},
65+
});
6066

6167
return withIsolationScope(isolationScope, () => {
6268
return withScope(scope => {
6369
scope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${componentRoute})`);
64-
isolationScope.setSDKProcessingMetadata({
65-
request: {
66-
headers: headers ? winterCGHeadersToDict(headers) : undefined,
67-
},
68-
});
70+
71+
const propagationContext = commonObjectToPropagationContext(
72+
headers,
73+
completeHeadersDict['sentry-trace']
74+
? propagationContextFromHeaders(completeHeadersDict['sentry-trace'], completeHeadersDict['baggage'])
75+
: {
76+
traceId: requestTraceId || uuid4(),
77+
spanId: uuid4().substring(16),
78+
parentSpanId: otelContext
79+
.active()
80+
.getValue(EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY) as string | undefined,
81+
},
82+
);
6983

7084
scope.setExtra('route_data', data);
7185
scope.setPropagationContext(propagationContext);

packages/nextjs/src/common/wrapServerComponentWithSentry.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ import {
33
SPAN_STATUS_ERROR,
44
SPAN_STATUS_OK,
55
captureException,
6+
getActiveSpan,
67
handleCallbackErrors,
78
startSpanManual,
89
withIsolationScope,
910
withScope,
1011
} from '@sentry/core';
11-
import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils';
12+
import { propagationContextFromHeaders, uuid4, winterCGHeadersToDict } from '@sentry/utils';
1213

14+
import { context as otelContext } from '@opentelemetry/api';
1315
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
16+
import { EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY } from '@sentry/opentelemetry';
1417
import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils';
1518
import type { ServerComponentContext } from '../common/types';
1619
import { flushQueue } from './utils/responseEnd';
@@ -34,6 +37,7 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>
3437
// hook. 🤯
3538
return new Proxy(appDirComponent, {
3639
apply: (originalFunction, thisArg, args) => {
40+
const requestTraceId = getActiveSpan()?.spanContext().traceId;
3741
return escapeNextjsTracing(() => {
3842
const isolationScope = commonObjectToIsolationScope(context.headers);
3943

@@ -47,17 +51,23 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>
4751
},
4852
});
4953

50-
const incomingPropagationContext = propagationContextFromHeaders(
51-
completeHeadersDict['sentry-trace'],
52-
completeHeadersDict['baggage'],
53-
);
54-
55-
const propagationContext = commonObjectToPropagationContext(context.headers, incomingPropagationContext);
56-
5754
return withIsolationScope(isolationScope, () => {
5855
return withScope(scope => {
5956
scope.setTransactionName(`${componentType} Server Component (${componentRoute})`);
6057

58+
const propagationContext = commonObjectToPropagationContext(
59+
context.headers,
60+
completeHeadersDict['sentry-trace']
61+
? propagationContextFromHeaders(completeHeadersDict['sentry-trace'], completeHeadersDict['baggage'])
62+
: {
63+
traceId: requestTraceId || uuid4(),
64+
spanId: uuid4().substring(16),
65+
parentSpanId: otelContext
66+
.active()
67+
.getValue(EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY) as string | undefined,
68+
},
69+
);
70+
6171
scope.setPropagationContext(propagationContext);
6272
return startSpanManual(
6373
{

packages/nextjs/src/server/httpIntegration.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { context } from '@opentelemetry/api';
12
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
23
import { httpIntegration as originalHttpIntegration } from '@sentry/node';
4+
import { EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY } from '@sentry/opentelemetry';
35
import type { IntegrationFn } from '@sentry/types';
6+
import { uuid4 } from '@sentry/utils';
47

58
/**
69
* Next.js handles incoming requests itself,
@@ -16,7 +19,15 @@ class CustomNextjsHttpIntegration extends HttpInstrumentation {
1619
original: (event: string, ...args: unknown[]) => boolean,
1720
): ((this: unknown, event: string, ...args: unknown[]) => boolean) => {
1821
return function incomingRequest(this: unknown, event: string, ...args: unknown[]): boolean {
19-
return original.apply(this, [event, ...args]);
22+
const requestSpanIdSuggestion = uuid4().substring(16);
23+
return context.with(
24+
context
25+
.active()
26+
.setValue(EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY, requestSpanIdSuggestion),
27+
() => {
28+
return original.apply(this, [event, ...args]);
29+
},
30+
);
2031
};
2132
};
2233
}

packages/opentelemetry/src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ export const SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY = createContextKey('sentry_
1515
export const SENTRY_FORK_SET_SCOPE_CONTEXT_KEY = createContextKey('sentry_fork_set_scope');
1616

1717
export const SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY = createContextKey('sentry_fork_set_isolation_scope');
18+
19+
export const EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY = createContextKey(
20+
'sentry_request_span_id_suggestion',
21+
);

packages/opentelemetry/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,7 @@ export { openTelemetrySetupCheck } from './utils/setupCheck';
3939

4040
export { addOpenTelemetryInstrumentation } from './instrumentation';
4141

42+
export { EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY } from './constants';
43+
4244
// Legacy
4345
export { getClient } from '@sentry/core';

packages/opentelemetry/src/propagator.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from '@sentry/utils';
2323

2424
import {
25+
EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY,
2526
SENTRY_BAGGAGE_HEADER,
2627
SENTRY_TRACE_HEADER,
2728
SENTRY_TRACE_STATE_DSC,
@@ -131,6 +132,15 @@ export class SentryPropagator extends W3CBaggagePropagator {
131132
setter.set(carrier, SENTRY_TRACE_HEADER, generateSentryTraceHeader(traceId, spanId, sampled));
132133
}
133134

135+
const requestSpanIdSuggestion = context.getValue(EXPERIMENTAL_SENTRY_REQUEST_SPAN_ID_SUGGESTION_CONTEXT_KEY) as
136+
| string
137+
| undefined;
138+
if (requestSpanIdSuggestion) {
139+
baggage = baggage.setEntry(`${SENTRY_BAGGAGE_KEY_PREFIX}request-span-id-suggestion`, {
140+
value: requestSpanIdSuggestion,
141+
});
142+
}
143+
134144
super.inject(propagation.setBaggage(context, baggage), carrier, setter);
135145
}
136146

0 commit comments

Comments
 (0)