Skip to content

Commit 2b8d229

Browse files
committed
process kit spans
1 parent d78638e commit 2b8d229

File tree

5 files changed

+275
-16
lines changed

5 files changed

+275
-16
lines changed

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

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import {
77
getDefaultIsolationScope,
88
getIsolationScope,
99
getTraceMetaTags,
10+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1011
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1112
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
1213
setHttpStatus,
14+
spanToJSON,
1315
startSpan,
16+
updateSpanName,
1417
winterCGRequestToRequestData,
1518
withIsolationScope,
1619
} from '@sentry/core';
@@ -97,7 +100,8 @@ interface BackwardsForwardsCompatibleEvent {
97100
tracing?: {
98101
/** Whether tracing is enabled. */
99102
enabled: boolean;
100-
// omitting other properties for now, since we don't use them.
103+
current: Span;
104+
root: Span;
101105
};
102106
}
103107

@@ -140,30 +144,56 @@ async function instrumentHandle(
140144
// We only start a span if SvelteKit's native tracing is not enabled. Two reasons:
141145
// - Used Kit version doesn't yet support tracing
142146
// - Users didn't enable tracing
143-
const shouldStartSpan = !event.tracing?.enabled;
147+
const kitTracingEnabled = event.tracing?.enabled;
144148

145149
try {
146-
const resolveWithSentry: (span?: Span) => Promise<Response> = async (span?: Span) => {
150+
const resolveWithSentry: (sentrySpan?: Span) => Promise<Response> = async (sentrySpan?: Span) => {
147151
getCurrentScope().setSDKProcessingMetadata({
148152
// We specifically avoid cloning the request here to avoid double read errors.
149153
// We only read request headers so we're not consuming the body anyway.
150154
// Note to future readers: This sounds counter-intuitive but please read
151155
// https://github.com/getsentry/sentry-javascript/issues/14583
152156
normalizedRequest: winterCGRequestToRequestData(event.request),
153157
});
158+
154159
const res = await resolve(event, {
155160
transformPageChunk: addSentryCodeToPage({
156161
injectFetchProxyScript: options.injectFetchProxyScript ?? true,
157162
}),
158163
});
159-
if (span) {
160-
setHttpStatus(span, res.status);
164+
165+
const kitRootSpan = event.tracing?.root;
166+
167+
if (sentrySpan) {
168+
setHttpStatus(sentrySpan, res.status);
169+
} else if (kitRootSpan) {
170+
// Update the root span emitted from SvelteKit to resemble a `http.server` span
171+
// We're doing this here instead of an event processor to ensure we update the
172+
// span name as early as possible (for dynamic sampling, et al.)
173+
// Other spans are enhanced in the `processKitSpans` function.
174+
const spanJson = spanToJSON(kitRootSpan);
175+
const kitRootSpanAttributes = spanJson.data;
176+
const originalName = spanJson.description;
177+
178+
const routeName = kitRootSpanAttributes['http.route'];
179+
if (routeName && typeof routeName === 'string') {
180+
updateSpanName(kitRootSpan, routeName);
181+
}
182+
183+
kitRootSpan.setAttributes({
184+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
185+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltejs.kit',
186+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url',
187+
'sveltekit.tracing.original_name': originalName,
188+
});
161189
}
190+
162191
return res;
163192
};
164193

165-
const resolveResult = shouldStartSpan
166-
? await startSpan(
194+
const resolveResult = kitTracingEnabled
195+
? await resolveWithSentry()
196+
: await startSpan(
167197
{
168198
op: 'http.server',
169199
attributes: {
@@ -174,8 +204,8 @@ async function instrumentHandle(
174204
name: routeName,
175205
},
176206
resolveWithSentry,
177-
)
178-
: await resolveWithSentry();
207+
);
208+
179209
return resolveResult;
180210
} catch (e: unknown) {
181211
sendErrorToSentry(e, 'handle');
@@ -237,11 +267,14 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
237267
// https://github.com/getsentry/sentry-javascript/issues/14583
238268
normalizedRequest: winterCGRequestToRequestData(input.event.request),
239269
});
240-
return continueTrace(getTracePropagationData(input.event), () =>
241-
instrumentHandle(input, {
242-
...options,
243-
}),
244-
);
270+
271+
if (backwardsForwardsCompatibleEvent.tracing?.enabled) {
272+
// if sveltekit tracing is enabled (since 2.31.0), trace continuation is handled by
273+
// kit before our hook is executed. No noeed to call `continueTrace` from our end
274+
return instrumentHandle(input, options);
275+
}
276+
277+
return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options));
245278
});
246279
};
247280

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Integration, SpanOrigin } from '@sentry/core';
2+
import { type SpanJSON, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
3+
4+
/**
5+
* A small integration that preprocesses spans so that SvelteKit-generated spans
6+
* (via Kit's tracing feature since 2.31.0) get the correct Sentry attributes
7+
* and data.
8+
*/
9+
export function svelteKitSpansIntegration(): Integration {
10+
return {
11+
name: 'SvelteKitSpansEnhancment',
12+
preprocessEvent(event) {
13+
if (event.type === 'transaction') {
14+
event.spans?.forEach(_enhanceKitSpan);
15+
}
16+
},
17+
};
18+
}
19+
20+
/**
21+
* Adds sentry-specific attributes and data to a span emitted by SvelteKit's native tracing (since 2.31.0)
22+
* @exported for testing
23+
*/
24+
export function _enhanceKitSpan(span: SpanJSON): void {
25+
let op: string | undefined = undefined;
26+
let origin: SpanOrigin | undefined = undefined;
27+
28+
const spanName = span.description;
29+
30+
switch (spanName) {
31+
case 'sveltekit.resolve':
32+
op = 'http.sveltekit.resolve';
33+
origin = 'auto.http.sveltekit';
34+
break;
35+
case 'sveltekit.load':
36+
op = 'function.sveltekit.load';
37+
origin = 'auto.function.sveltekit.load';
38+
break;
39+
case 'sveltekit.form_action':
40+
op = 'function.sveltekit.form_action';
41+
origin = 'auto.function.sveltekit.action';
42+
break;
43+
case 'sveltekit.remote.call':
44+
op = 'function.sveltekit.remote';
45+
origin = 'auto.rpc.sveltekit.remote';
46+
break;
47+
case 'sveltekit.handle.root':
48+
// We don't want to overwrite the root handle span at this point since
49+
// we already enhance the root span in our `sentryHandle` hook.
50+
break;
51+
default: {
52+
if (spanName?.startsWith('sveltekit.handle.sequenced.')) {
53+
op = 'function.sveltekit.handle';
54+
origin = 'auto.function.sveltekit.handle';
55+
}
56+
break;
57+
}
58+
}
59+
60+
const previousOp = span.op || span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP];
61+
const previousOrigin = span.origin || span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN];
62+
63+
if (!previousOp && op) {
64+
span.op = op;
65+
span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op;
66+
}
67+
68+
if (!previousOrigin && origin) {
69+
span.origin = origin;
70+
span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin;
71+
}
72+
}

packages/sveltekit/src/server/sdk.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { applySdkMetadata } from '@sentry/core';
22
import type { NodeClient, NodeOptions } from '@sentry/node';
33
import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node';
4+
import { svelteKitSpansIntegration } from '../server-common/processKitSpans';
45
import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration';
56

67
/**
@@ -9,7 +10,11 @@ import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegrat
910
*/
1011
export function init(options: NodeOptions): NodeClient | undefined {
1112
const opts = {
12-
defaultIntegrations: [...getDefaultNodeIntegrations(options), rewriteFramesIntegration()],
13+
defaultIntegrations: [
14+
...getDefaultNodeIntegrations(options),
15+
rewriteFramesIntegration(),
16+
svelteKitSpansIntegration(),
17+
],
1318
...options,
1419
};
1520

packages/sveltekit/src/worker/cloudflare.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from '@sentry/cloudflare';
77
import { addNonEnumerableProperty } from '@sentry/core';
88
import type { Handle } from '@sveltejs/kit';
9+
import { svelteKitSpansIntegration } from '../server-common/processKitSpans';
910
import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration';
1011

1112
/**
@@ -16,7 +17,11 @@ import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegrat
1617
*/
1718
export function initCloudflareSentryHandle(options: CloudflareOptions): Handle {
1819
const opts: CloudflareOptions = {
19-
defaultIntegrations: [...getDefaultCloudflareIntegrations(options), rewriteFramesIntegration()],
20+
defaultIntegrations: [
21+
...getDefaultCloudflareIntegrations(options),
22+
rewriteFramesIntegration(),
23+
svelteKitSpansIntegration(),
24+
],
2025
...options,
2126
};
2227

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import type { EventType, SpanJSON, TransactionEvent } from '@sentry/core';
2+
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
3+
import { describe, expect, it } from 'vitest';
4+
import { _enhanceKitSpan, svelteKitSpansIntegration } from '../../src/server-common/processKitSpans';
5+
6+
describe('svelteKitSpansIntegration', () => {
7+
it('has a name and a preprocessEventHook', () => {
8+
const integration = svelteKitSpansIntegration();
9+
10+
expect(integration.name).toBe('SvelteKitSpansEnhancment');
11+
expect(typeof integration.preprocessEvent).toBe('function');
12+
});
13+
14+
it('enhances spans from SvelteKit', () => {
15+
const event: TransactionEvent = {
16+
type: 'transaction',
17+
spans: [
18+
{
19+
description: 'sveltekit.resolve',
20+
data: {
21+
someAttribute: 'someValue',
22+
},
23+
span_id: '123',
24+
trace_id: 'abc',
25+
start_timestamp: 0,
26+
},
27+
],
28+
};
29+
30+
// @ts-expect-error -- passing in an empty option for client but it is unused in the integration
31+
svelteKitSpansIntegration().preprocessEvent?.(event, {}, {});
32+
33+
expect(event.spans).toHaveLength(1);
34+
expect(event.spans?.[0]?.op).toBe('http.sveltekit.resolve');
35+
expect(event.spans?.[0]?.origin).toBe('auto.http.sveltekit');
36+
expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.sveltekit.resolve');
37+
expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit');
38+
});
39+
40+
describe('_enhanceKitSpan', () => {
41+
it.each([
42+
['sveltekit.resolve', 'http.sveltekit.resolve', 'auto.http.sveltekit'],
43+
['sveltekit.load', 'function.sveltekit.load', 'auto.function.sveltekit.load'],
44+
['sveltekit.form_action', 'function.sveltekit.form_action', 'auto.function.sveltekit.action'],
45+
['sveltekit.remote.call', 'function.sveltekit.remote', 'auto.rpc.sveltekit.remote'],
46+
['sveltekit.handle.sequenced.0', 'function.sveltekit.handle', 'auto.function.sveltekit.handle'],
47+
['sveltekit.handle.sequenced.myHandler', 'function.sveltekit.handle', 'auto.function.sveltekit.handle'],
48+
])('enhances %s span with the correct op and origin', (spanName, op, origin) => {
49+
const span = {
50+
description: spanName,
51+
data: {
52+
someAttribute: 'someValue',
53+
},
54+
span_id: '123',
55+
trace_id: 'abc',
56+
start_timestamp: 0,
57+
} as SpanJSON;
58+
59+
_enhanceKitSpan(span);
60+
61+
expect(span.op).toBe(op);
62+
expect(span.origin).toBe(origin);
63+
expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe(op);
64+
expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin);
65+
});
66+
67+
it("doesn't change spans from other origins", () => {
68+
const span = {
69+
description: 'someOtherSpan',
70+
data: {},
71+
} as SpanJSON;
72+
73+
_enhanceKitSpan(span);
74+
75+
expect(span.op).toBeUndefined();
76+
expect(span.origin).toBeUndefined();
77+
expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined();
78+
expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined();
79+
});
80+
81+
it("doesn't overwrite the sveltekit.handle.root span", () => {
82+
const rootHandleSpan = {
83+
description: 'sveltekit.handle.root',
84+
op: 'http.server',
85+
origin: 'auto.http.sveltekit',
86+
data: {
87+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
88+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
89+
},
90+
span_id: '123',
91+
trace_id: 'abc',
92+
start_timestamp: 0,
93+
} as SpanJSON;
94+
95+
_enhanceKitSpan(rootHandleSpan);
96+
97+
expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server');
98+
expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit');
99+
expect(rootHandleSpan.op).toBe('http.server');
100+
expect(rootHandleSpan.origin).toBe('auto.http.sveltekit');
101+
});
102+
103+
it("doesn't enhance unrelated spans", () => {
104+
const span = {
105+
description: 'someOtherSpan',
106+
data: {
107+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
108+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg',
109+
},
110+
op: 'db',
111+
origin: 'auto.db.pg',
112+
span_id: '123',
113+
trace_id: 'abc',
114+
start_timestamp: 0,
115+
} as SpanJSON;
116+
117+
_enhanceKitSpan(span);
118+
119+
expect(span.op).toBe('db');
120+
expect(span.origin).toBe('auto.db.pg');
121+
expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('db');
122+
expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg');
123+
});
124+
125+
it("doesn't overwrite already set ops or origins on sveltekit spans", () => {
126+
// for example, if users manually set this (for whatever reason)
127+
const span = {
128+
description: 'sveltekit.resolve',
129+
origin: 'auto.custom.origin',
130+
data: {
131+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op',
132+
},
133+
span_id: '123',
134+
trace_id: 'abc',
135+
start_timestamp: 0,
136+
} as SpanJSON;
137+
138+
_enhanceKitSpan(span);
139+
140+
expect(span.origin).toBe('auto.custom.origin');
141+
expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op');
142+
});
143+
});
144+
});

0 commit comments

Comments
 (0)