Skip to content

Commit 7c2f2b4

Browse files
authored
fix(sveltekit): Ensure sub requests are recorded as child spans of parent request (#11130)
When switching the SvelteKit server side SDK to `@sentry/node` powered by Otel, the semantics behind `continueTrace` changed as outlined in #11199. TLDR: We previously called `continueTrace` in a nested way when dealing with sub-requests`*` in SvelteKit. In our old Node SDK, this did nothing; in the new SDK, this currently causes a new root span/transaction to be created for the sub request. This patch now ensures that we continue to send sub request spans as child spans of the top-level/parent request.
1 parent f05b4a1 commit 7c2f2b4

File tree

13 files changed

+217
-214
lines changed

13 files changed

+217
-214
lines changed

dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,7 @@
2626
<li>
2727
<a id="redirectLink" href="/redirect1">Redirect</a>
2828
</li>
29+
<li>
30+
<a href="/server-load-fetch">Route with nested fetch in server load</a>
31+
</li>
2932
</ul>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const load = async ({ fetch }) => {
2+
const res = await fetch('/api/users');
3+
const data = await res.json();
4+
return { data };
5+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
export let data;
3+
</script>
4+
5+
<main>
6+
<h1>Server Load Fetch</h1>
7+
<p>{JSON.stringify(data, null, 2)}</p>
8+
</main>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '../event-proxy-server';
3+
4+
test('server pageload request span has nested request span for sub request', async ({ page }) => {
5+
const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => {
6+
return txnEvent?.transaction === 'GET /server-load-fetch';
7+
});
8+
9+
await page.goto('/server-load-fetch');
10+
11+
const serverTxnEvent = await serverTxnEventPromise;
12+
const spans = serverTxnEvent.spans;
13+
14+
expect(serverTxnEvent).toMatchObject({
15+
transaction: 'GET /server-load-fetch',
16+
tags: { runtime: 'node' },
17+
transaction_info: { source: 'route' },
18+
type: 'transaction',
19+
contexts: {
20+
trace: {
21+
op: 'http.server',
22+
origin: 'auto.http.sveltekit',
23+
},
24+
},
25+
});
26+
27+
expect(spans).toEqual(
28+
expect.arrayContaining([
29+
// load span where the server load function initiates the sub request:
30+
expect.objectContaining({ op: 'function.sveltekit.server.load', description: '/server-load-fetch' }),
31+
// sub request span:
32+
expect.objectContaining({ op: 'http.server', description: 'GET /api/users' }),
33+
]),
34+
);
35+
});

dev-packages/e2e-tests/test-applications/sveltekit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"@sentry/utils": "latest || *",
2323
"@sveltejs/adapter-auto": "^2.0.0",
2424
"@sveltejs/adapter-node": "^1.2.4",
25-
"@sveltejs/kit": "^1.30.3",
25+
"@sveltejs/kit": "1.20.5",
2626
"svelte": "^3.54.0",
2727
"svelte-check": "^3.0.1",
2828
"ts-node": "10.9.1",

dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@
2323
<li>
2424
<a href="/universal-load-fetch">Route with fetch in universal load</a>
2525
</li>
26+
<li>
27+
<a href="/server-load-fetch">Route with nested fetch in server load</a>
28+
</li>
2629
</ul>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const load = async ({ fetch }) => {
2+
const res = await fetch('/api/users');
3+
const data = await res.json();
4+
return { data };
5+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
export let data;
3+
</script>
4+
5+
<main>
6+
<h1>Server Load Fetch</h1>
7+
<p>{JSON.stringify(data, null, 2)}</p>
8+
</main>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForTransaction } from '../event-proxy-server';
3+
4+
test('server pageload request span has nested request span for sub request', async ({ page }) => {
5+
const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => {
6+
return txnEvent?.transaction === 'GET /server-load-fetch';
7+
});
8+
9+
await page.goto('/server-load-fetch');
10+
11+
const serverTxnEvent = await serverTxnEventPromise;
12+
const spans = serverTxnEvent.spans;
13+
14+
expect(serverTxnEvent).toMatchObject({
15+
transaction: 'GET /server-load-fetch',
16+
tags: { runtime: 'node' },
17+
transaction_info: { source: 'route' },
18+
type: 'transaction',
19+
contexts: {
20+
trace: {
21+
op: 'http.server',
22+
origin: 'auto.http.sveltekit',
23+
},
24+
},
25+
});
26+
27+
expect(spans).toEqual(
28+
expect.arrayContaining([
29+
// load span where the server load function initiates the sub request:
30+
expect.objectContaining({ op: 'function.sveltekit.server.load', description: '/server-load-fetch' }),
31+
// sub request span:
32+
expect.objectContaining({ op: 'http.server', description: 'GET /api/users' }),
33+
]),
34+
);
35+
});

packages/sveltekit/src/server/handle.ts

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,26 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
149149
};
150150

151151
const sentryRequestHandler: Handle = input => {
152-
// if there is an active span, we know that this handle call is nested and hence
153-
// we don't create a new execution context for it.
154-
// If we created one, nested server calls would create new root span instead
155-
// of adding a child span to the currently active span.
156-
if (getActiveSpan()) {
152+
// event.isSubRequest was added in SvelteKit 1.21.0 and we can use it to check
153+
// if we should create a new execution context or not.
154+
// In case of a same-origin `fetch` call within a server`load` function,
155+
// SvelteKit will actually just re-enter the `handle` function and set `isSubRequest`
156+
// to `true` so that no additional network call is made.
157+
// We want the `http.server` span of that nested call to be a child span of the
158+
// currently active span instead of a new root span to correctly reflect this
159+
// behavior.
160+
// As a fallback for Kit < 1.21.0, we check if there is an active span only if there's none,
161+
// we create a new execution context.
162+
const isSubRequest = typeof input.event.isSubRequest === 'boolean' ? input.event.isSubRequest : !!getActiveSpan();
163+
164+
if (isSubRequest) {
157165
return instrumentHandle(input, options);
158166
}
167+
159168
return withIsolationScope(() => {
160-
return instrumentHandle(input, options);
169+
// We only call continueTrace in the initial top level request to avoid
170+
// creating a new root span for the sub request.
171+
return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options));
161172
});
162173
};
163174

@@ -172,36 +183,32 @@ async function instrumentHandle(
172183
return resolve(event);
173184
}
174185

175-
const { sentryTrace, baggage } = getTracePropagationData(event);
176-
177-
return continueTrace({ sentryTrace, baggage }, async () => {
178-
try {
179-
const resolveResult = await startSpan(
180-
{
181-
op: 'http.server',
182-
attributes: {
183-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
184-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url',
185-
'http.method': event.request.method,
186-
},
187-
name: `${event.request.method} ${event.route?.id || event.url.pathname}`,
188-
},
189-
async (span?: Span) => {
190-
const res = await resolve(event, {
191-
transformPageChunk: addSentryCodeToPage(options),
192-
});
193-
if (span) {
194-
setHttpStatus(span, res.status);
195-
}
196-
return res;
186+
try {
187+
const resolveResult = await startSpan(
188+
{
189+
op: 'http.server',
190+
attributes: {
191+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
192+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url',
193+
'http.method': event.request.method,
197194
},
198-
);
199-
return resolveResult;
200-
} catch (e: unknown) {
201-
sendErrorToSentry(e);
202-
throw e;
203-
} finally {
204-
await flushIfServerless();
205-
}
206-
});
195+
name: `${event.request.method} ${event.route?.id || event.url.pathname}`,
196+
},
197+
async (span?: Span) => {
198+
const res = await resolve(event, {
199+
transformPageChunk: addSentryCodeToPage(options),
200+
});
201+
if (span) {
202+
setHttpStatus(span, res.status);
203+
}
204+
return res;
205+
},
206+
);
207+
return resolveResult;
208+
} catch (e: unknown) {
209+
sendErrorToSentry(e);
210+
throw e;
211+
} finally {
212+
await flushIfServerless();
213+
}
207214
}

0 commit comments

Comments
 (0)