Skip to content

Commit 773f180

Browse files
authored
fix(sveltekit): Handle nested server calls in sentryHandle (#7598)
Previously, we always created a new domain when entering the `sentryHandle` function. This caused "nested" SvelteKit Server calls to create new transaction whenever a new domain was created in the `sentryHandle` function because the active span is bound to the domain. This fix introduces a check if there is an active transaction, in which case we just execute the handler and don't create a new domain, as we are already in this active domain. As a result, nested SK server calls become a span of the parent server call instead of a new transaction.
1 parent b068c68 commit 773f180

File tree

2 files changed

+82
-28
lines changed

2 files changed

+82
-28
lines changed

packages/sveltekit/src/server/handle.ts

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
22
import type { Span } from '@sentry/core';
3-
import { getActiveTransaction, trace } from '@sentry/core';
3+
import { getActiveTransaction, getCurrentHub, trace } from '@sentry/core';
44
import { captureException } from '@sentry/node';
55
import {
66
addExceptionMechanism,
@@ -64,32 +64,42 @@ export const transformPageChunk: NonNullable<ResolveOptions['transformPageChunk'
6464
* // export const handle = sequence(sentryHandle, yourCustomHandle);
6565
* ```
6666
*/
67-
export const sentryHandle: Handle = ({ event, resolve }) => {
67+
export const sentryHandle: Handle = input => {
68+
// if there is an active transaction, we know that this handle call is nested and hence
69+
// we don't create a new domain for it. If we created one, nested server calls would
70+
// create new transactions instead of adding a child span to the currently active span.
71+
if (getCurrentHub().getScope().getSpan()) {
72+
return instrumentHandle(input);
73+
}
6874
return domain.create().bind(() => {
69-
const sentryTraceHeader = event.request.headers.get('sentry-trace');
70-
const baggageHeader = event.request.headers.get('baggage');
71-
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
72-
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
73-
74-
return trace(
75-
{
76-
op: 'http.server',
77-
name: `${event.request.method} ${event.route.id}`,
78-
status: 'ok',
79-
...traceparentData,
80-
metadata: {
81-
source: 'route',
82-
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
83-
},
84-
},
85-
async (span?: Span) => {
86-
const res = await resolve(event, { transformPageChunk });
87-
if (span) {
88-
span.setHttpStatus(res.status);
89-
}
90-
return res;
91-
},
92-
sendErrorToSentry,
93-
);
75+
return instrumentHandle(input);
9476
})();
9577
};
78+
79+
function instrumentHandle({ event, resolve }: Parameters<Handle>[0]): ReturnType<Handle> {
80+
const sentryTraceHeader = event.request.headers.get('sentry-trace');
81+
const baggageHeader = event.request.headers.get('baggage');
82+
const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined;
83+
const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader);
84+
85+
return trace(
86+
{
87+
op: 'http.server',
88+
name: `${event.request.method} ${event.route.id}`,
89+
status: 'ok',
90+
...traceparentData,
91+
metadata: {
92+
source: 'route',
93+
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
94+
},
95+
},
96+
async (span?: Span) => {
97+
const res = await resolve(event, { transformPageChunk });
98+
if (span) {
99+
span.setHttpStatus(res.status);
100+
}
101+
return res;
102+
},
103+
sendErrorToSentry,
104+
);
105+
}

packages/sveltekit/test/server/handle.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ describe('handleSentry', () => {
129129
expect(response).toEqual(mockResponse);
130130
});
131131

132-
it('creates a transaction', async () => {
132+
it("creates a transaction if there's no active span", async () => {
133133
let ref: any = undefined;
134134
client.on('finishTransaction', (transaction: Transaction) => {
135135
ref = transaction;
@@ -149,6 +149,50 @@ describe('handleSentry', () => {
149149
expect(ref.metadata.source).toEqual('route');
150150

151151
expect(ref.endTimestamp).toBeDefined();
152+
expect(ref.spanRecorder.spans).toHaveLength(1);
153+
});
154+
155+
it('creates a child span for nested server calls (i.e. if there is an active span)', async () => {
156+
let ref: any = undefined;
157+
let txnCount = 0;
158+
client.on('finishTransaction', (transaction: Transaction) => {
159+
ref = transaction;
160+
++txnCount;
161+
});
162+
163+
try {
164+
await sentryHandle({
165+
event: mockEvent(),
166+
resolve: async _ => {
167+
// simulateing a nested load call:
168+
await sentryHandle({
169+
event: mockEvent({ route: { id: 'api/users/details/[id]' } }),
170+
resolve: resolve(type, isError),
171+
});
172+
return mockResponse;
173+
},
174+
});
175+
} catch (e) {
176+
//
177+
}
178+
179+
expect(txnCount).toEqual(1);
180+
expect(ref).toBeDefined();
181+
182+
expect(ref.name).toEqual('GET /users/[id]');
183+
expect(ref.op).toEqual('http.server');
184+
expect(ref.status).toEqual(isError ? 'internal_error' : 'ok');
185+
expect(ref.metadata.source).toEqual('route');
186+
187+
expect(ref.endTimestamp).toBeDefined();
188+
189+
expect(ref.spanRecorder.spans).toHaveLength(2);
190+
expect(ref.spanRecorder.spans).toEqual(
191+
expect.arrayContaining([
192+
expect.objectContaining({ op: 'http.server', description: 'GET /users/[id]' }),
193+
expect.objectContaining({ op: 'http.server', description: 'GET api/users/details/[id]' }),
194+
]),
195+
);
152196
});
153197

154198
it('creates a transaction from sentry-trace header', async () => {

0 commit comments

Comments
 (0)