Skip to content

Commit 186a8af

Browse files
committed
ref(sveltekit): Handle SvelteKit-generated spans in sentryHandle
1 parent f25664b commit 186a8af

File tree

2 files changed

+104
-39
lines changed

2 files changed

+104
-39
lines changed

packages/sveltekit/src/server-common/handle.ts

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,18 @@ export function isFetchProxyRequired(version: string): boolean {
8989
}
9090

9191
async function instrumentHandle(
92-
{ event, resolve }: Parameters<Handle>[0],
93-
options: SentryHandleOptions,
92+
{
93+
event,
94+
resolve,
95+
}: {
96+
event: Parameters<Handle>[0]['event'];
97+
resolve: Parameters<Handle>[0]['resolve'];
98+
},
99+
options: SentryHandleOptions & { svelteKitTracingEnabled: boolean },
94100
): Promise<Response> {
95-
if (!event.route?.id && !options.handleUnknownRoutes) {
101+
const routeId = event.route?.id;
102+
103+
if (!routeId && !options.handleUnknownRoutes) {
96104
return resolve(event);
97105
}
98106

@@ -108,42 +116,53 @@ async function instrumentHandle(
108116
}
109117
}
110118

111-
const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`;
119+
const routeName = `${event.request.method} ${routeId || event.url.pathname}`;
112120

113121
if (getIsolationScope() !== getDefaultIsolationScope()) {
114122
getIsolationScope().setTransactionName(routeName);
115123
} else {
116124
DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName');
117125
}
118126

127+
// We only start a span if SvelteKit's native tracing is not enabled. Two reasons:
128+
// - Used Kit version doesn't yet support tracing
129+
// - Users didn't enable tracing
130+
const shouldStartSpan = !options.svelteKitTracingEnabled;
131+
119132
try {
120-
const resolveResult = await startSpan(
121-
{
122-
op: 'http.server',
123-
attributes: {
124-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
125-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url',
126-
'http.method': event.request.method,
127-
},
128-
name: routeName,
129-
},
130-
async (span?: Span) => {
131-
getCurrentScope().setSDKProcessingMetadata({
132-
// We specifically avoid cloning the request here to avoid double read errors.
133-
// We only read request headers so we're not consuming the body anyway.
134-
// Note to future readers: This sounds counter-intuitive but please read
135-
// https://github.com/getsentry/sentry-javascript/issues/14583
136-
normalizedRequest: winterCGRequestToRequestData(event.request),
137-
});
138-
const res = await resolve(event, {
139-
transformPageChunk: addSentryCodeToPage({ injectFetchProxyScript: options.injectFetchProxyScript ?? true }),
140-
});
141-
if (span) {
142-
setHttpStatus(span, res.status);
143-
}
144-
return res;
145-
},
146-
);
133+
const resolveWithSentry: (span?: Span) => Promise<Response> = async (span?: Span) => {
134+
getCurrentScope().setSDKProcessingMetadata({
135+
// We specifically avoid cloning the request here to avoid double read errors.
136+
// We only read request headers so we're not consuming the body anyway.
137+
// Note to future readers: This sounds counter-intuitive but please read
138+
// https://github.com/getsentry/sentry-javascript/issues/14583
139+
normalizedRequest: winterCGRequestToRequestData(event.request),
140+
});
141+
const res = await resolve(event, {
142+
transformPageChunk: addSentryCodeToPage({
143+
injectFetchProxyScript: options.injectFetchProxyScript ?? true,
144+
}),
145+
});
146+
if (span) {
147+
setHttpStatus(span, res.status);
148+
}
149+
return res;
150+
};
151+
152+
const resolveResult = shouldStartSpan
153+
? await startSpan(
154+
{
155+
op: 'http.server',
156+
attributes: {
157+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
158+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url',
159+
'http.method': event.request.method,
160+
},
161+
name: routeName,
162+
},
163+
resolveWithSentry,
164+
)
165+
: await resolveWithSentry();
147166
return resolveResult;
148167
} catch (e: unknown) {
149168
sendErrorToSentry(e, 'handle');
@@ -153,6 +172,19 @@ async function instrumentHandle(
153172
}
154173
}
155174

175+
interface BackwardsForwardsCompatibleEvent {
176+
/**
177+
* For now taken from: https://github.com/sveltejs/kit/pull/13899
178+
* Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing.
179+
* @since 2.30.0
180+
*/
181+
tracing?: {
182+
/** Whether tracing is enabled. */
183+
enabled: boolean;
184+
// omitting other properties for now, since we don't use them.
185+
};
186+
}
187+
156188
/**
157189
* A SvelteKit handle function that wraps the request for Sentry error and
158190
* performance monitoring.
@@ -176,18 +208,26 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
176208
};
177209

178210
const sentryRequestHandler: Handle = input => {
211+
const backwardsForwardsCompatibleEvent = input.event as typeof input.event & BackwardsForwardsCompatibleEvent;
212+
179213
// Escape hatch to suppress request isolation and trace continuation (see initCloudflareSentryHandle)
180214
const skipIsolation =
181-
'_sentrySkipRequestIsolation' in input.event.locals && input.event.locals._sentrySkipRequestIsolation;
215+
'_sentrySkipRequestIsolation' in backwardsForwardsCompatibleEvent.locals &&
216+
backwardsForwardsCompatibleEvent.locals._sentrySkipRequestIsolation;
217+
218+
const svelteKitTracingEnabled = !!backwardsForwardsCompatibleEvent.tracing?.enabled;
182219

183220
// In case of a same-origin `fetch` call within a server`load` function,
184221
// SvelteKit will actually just re-enter the `handle` function and set `isSubRequest`
185222
// to `true` so that no additional network call is made.
186223
// We want the `http.server` span of that nested call to be a child span of the
187224
// currently active span instead of a new root span to correctly reflect this
188225
// behavior.
189-
if (skipIsolation || input.event.isSubRequest) {
190-
return instrumentHandle(input, options);
226+
if (skipIsolation || input.event.isSubRequest || svelteKitTracingEnabled) {
227+
return instrumentHandle(input, {
228+
...options,
229+
svelteKitTracingEnabled,
230+
});
191231
}
192232

193233
return withIsolationScope(isolationScope => {
@@ -200,7 +240,12 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
200240
// https://github.com/getsentry/sentry-javascript/issues/14583
201241
normalizedRequest: winterCGRequestToRequestData(input.event.request),
202242
});
203-
return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options));
243+
return continueTrace(getTracePropagationData(input.event), () =>
244+
instrumentHandle(input, {
245+
...options,
246+
svelteKitTracingEnabled,
247+
}),
248+
);
204249
});
205250
};
206251

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ describe('sentryHandle', () => {
111111
[Type.Async, true, undefined],
112112
[Type.Async, false, mockResponse],
113113
])('%s resolve with error %s', (type, isError, mockResponse) => {
114-
it('should return a response', async () => {
114+
it('returns a response', async () => {
115115
let response: any = undefined;
116116
try {
117117
response = await sentryHandle()({ event: mockEvent(), resolve: resolve(type, isError) });
@@ -123,7 +123,7 @@ describe('sentryHandle', () => {
123123
expect(response).toEqual(mockResponse);
124124
});
125125

126-
it("creates a transaction if there's no active span", async () => {
126+
it("starts a span if there's no active span", async () => {
127127
let _span: Span | undefined = undefined;
128128
client.on('spanEnd', span => {
129129
if (span === getRootSpan(span)) {
@@ -150,7 +150,27 @@ describe('sentryHandle', () => {
150150
expect(spans).toHaveLength(1);
151151
});
152152

153-
it('creates a child span for nested server calls (i.e. if there is an active span)', async () => {
153+
it("doesn't start a span if sveltekit tracing is enabled", async () => {
154+
let _span: Span | undefined = undefined;
155+
client.on('spanEnd', span => {
156+
if (span === getRootSpan(span)) {
157+
_span = span;
158+
}
159+
});
160+
161+
try {
162+
await sentryHandle()({
163+
event: mockEvent({ tracing: { enabled: true } }),
164+
resolve: resolve(type, isError),
165+
});
166+
} catch {
167+
//
168+
}
169+
170+
expect(_span).toBeUndefined();
171+
});
172+
173+
it('starts a child span for nested server calls (i.e. if there is an active span)', async () => {
154174
let _span: Span | undefined = undefined;
155175
let txnCount = 0;
156176
client.on('spanEnd', span => {
@@ -197,7 +217,7 @@ describe('sentryHandle', () => {
197217
);
198218
});
199219

200-
it("creates a transaction from sentry-trace header but doesn't populate a new DSC", async () => {
220+
it("starts a span from sentry-trace header but doesn't populate a new DSC", async () => {
201221
const event = mockEvent({
202222
request: {
203223
headers: {

0 commit comments

Comments
 (0)