From 186a8af9683d91771121a405ae28260e4ccf3396 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 14 Aug 2025 19:01:03 +0200 Subject: [PATCH 1/5] ref(sveltekit): Handle SvelteKit-generated spans in `sentryHandle` --- .../sveltekit/src/server-common/handle.ts | 115 ++++++++++++------ .../test/server-common/handle.test.ts | 28 ++++- 2 files changed, 104 insertions(+), 39 deletions(-) diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 696c3d765c5b..ab0632b715df 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -89,10 +89,18 @@ export function isFetchProxyRequired(version: string): boolean { } async function instrumentHandle( - { event, resolve }: Parameters[0], - options: SentryHandleOptions, + { + event, + resolve, + }: { + event: Parameters[0]['event']; + resolve: Parameters[0]['resolve']; + }, + options: SentryHandleOptions & { svelteKitTracingEnabled: boolean }, ): Promise { - if (!event.route?.id && !options.handleUnknownRoutes) { + const routeId = event.route?.id; + + if (!routeId && !options.handleUnknownRoutes) { return resolve(event); } @@ -108,7 +116,7 @@ async function instrumentHandle( } } - const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`; + const routeName = `${event.request.method} ${routeId || event.url.pathname}`; if (getIsolationScope() !== getDefaultIsolationScope()) { getIsolationScope().setTransactionName(routeName); @@ -116,34 +124,45 @@ async function instrumentHandle( DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName'); } + // We only start a span if SvelteKit's native tracing is not enabled. Two reasons: + // - Used Kit version doesn't yet support tracing + // - Users didn't enable tracing + const shouldStartSpan = !options.svelteKitTracingEnabled; + try { - const resolveResult = await startSpan( - { - op: 'http.server', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url', - 'http.method': event.request.method, - }, - name: routeName, - }, - async (span?: Span) => { - getCurrentScope().setSDKProcessingMetadata({ - // We specifically avoid cloning the request here to avoid double read errors. - // We only read request headers so we're not consuming the body anyway. - // Note to future readers: This sounds counter-intuitive but please read - // https://github.com/getsentry/sentry-javascript/issues/14583 - normalizedRequest: winterCGRequestToRequestData(event.request), - }); - const res = await resolve(event, { - transformPageChunk: addSentryCodeToPage({ injectFetchProxyScript: options.injectFetchProxyScript ?? true }), - }); - if (span) { - setHttpStatus(span, res.status); - } - return res; - }, - ); + const resolveWithSentry: (span?: Span) => Promise = async (span?: Span) => { + getCurrentScope().setSDKProcessingMetadata({ + // We specifically avoid cloning the request here to avoid double read errors. + // We only read request headers so we're not consuming the body anyway. + // Note to future readers: This sounds counter-intuitive but please read + // https://github.com/getsentry/sentry-javascript/issues/14583 + normalizedRequest: winterCGRequestToRequestData(event.request), + }); + const res = await resolve(event, { + transformPageChunk: addSentryCodeToPage({ + injectFetchProxyScript: options.injectFetchProxyScript ?? true, + }), + }); + if (span) { + setHttpStatus(span, res.status); + } + return res; + }; + + const resolveResult = shouldStartSpan + ? await startSpan( + { + op: 'http.server', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', + 'http.method': event.request.method, + }, + name: routeName, + }, + resolveWithSentry, + ) + : await resolveWithSentry(); return resolveResult; } catch (e: unknown) { sendErrorToSentry(e, 'handle'); @@ -153,6 +172,19 @@ async function instrumentHandle( } } +interface BackwardsForwardsCompatibleEvent { + /** + * For now taken from: https://github.com/sveltejs/kit/pull/13899 + * Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing. + * @since 2.30.0 + */ + tracing?: { + /** Whether tracing is enabled. */ + enabled: boolean; + // omitting other properties for now, since we don't use them. + }; +} + /** * A SvelteKit handle function that wraps the request for Sentry error and * performance monitoring. @@ -176,9 +208,14 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { }; const sentryRequestHandler: Handle = input => { + const backwardsForwardsCompatibleEvent = input.event as typeof input.event & BackwardsForwardsCompatibleEvent; + // Escape hatch to suppress request isolation and trace continuation (see initCloudflareSentryHandle) const skipIsolation = - '_sentrySkipRequestIsolation' in input.event.locals && input.event.locals._sentrySkipRequestIsolation; + '_sentrySkipRequestIsolation' in backwardsForwardsCompatibleEvent.locals && + backwardsForwardsCompatibleEvent.locals._sentrySkipRequestIsolation; + + const svelteKitTracingEnabled = !!backwardsForwardsCompatibleEvent.tracing?.enabled; // In case of a same-origin `fetch` call within a server`load` function, // SvelteKit will actually just re-enter the `handle` function and set `isSubRequest` @@ -186,8 +223,11 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { // We want the `http.server` span of that nested call to be a child span of the // currently active span instead of a new root span to correctly reflect this // behavior. - if (skipIsolation || input.event.isSubRequest) { - return instrumentHandle(input, options); + if (skipIsolation || input.event.isSubRequest || svelteKitTracingEnabled) { + return instrumentHandle(input, { + ...options, + svelteKitTracingEnabled, + }); } return withIsolationScope(isolationScope => { @@ -200,7 +240,12 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { // https://github.com/getsentry/sentry-javascript/issues/14583 normalizedRequest: winterCGRequestToRequestData(input.event.request), }); - return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options)); + return continueTrace(getTracePropagationData(input.event), () => + instrumentHandle(input, { + ...options, + svelteKitTracingEnabled, + }), + ); }); }; diff --git a/packages/sveltekit/test/server-common/handle.test.ts b/packages/sveltekit/test/server-common/handle.test.ts index 79c0f88e0b5d..db1e1fe4811f 100644 --- a/packages/sveltekit/test/server-common/handle.test.ts +++ b/packages/sveltekit/test/server-common/handle.test.ts @@ -111,7 +111,7 @@ describe('sentryHandle', () => { [Type.Async, true, undefined], [Type.Async, false, mockResponse], ])('%s resolve with error %s', (type, isError, mockResponse) => { - it('should return a response', async () => { + it('returns a response', async () => { let response: any = undefined; try { response = await sentryHandle()({ event: mockEvent(), resolve: resolve(type, isError) }); @@ -123,7 +123,7 @@ describe('sentryHandle', () => { expect(response).toEqual(mockResponse); }); - it("creates a transaction if there's no active span", async () => { + it("starts a span if there's no active span", async () => { let _span: Span | undefined = undefined; client.on('spanEnd', span => { if (span === getRootSpan(span)) { @@ -150,7 +150,27 @@ describe('sentryHandle', () => { expect(spans).toHaveLength(1); }); - it('creates a child span for nested server calls (i.e. if there is an active span)', async () => { + it("doesn't start a span if sveltekit tracing is enabled", async () => { + let _span: Span | undefined = undefined; + client.on('spanEnd', span => { + if (span === getRootSpan(span)) { + _span = span; + } + }); + + try { + await sentryHandle()({ + event: mockEvent({ tracing: { enabled: true } }), + resolve: resolve(type, isError), + }); + } catch { + // + } + + expect(_span).toBeUndefined(); + }); + + it('starts a child span for nested server calls (i.e. if there is an active span)', async () => { let _span: Span | undefined = undefined; let txnCount = 0; client.on('spanEnd', span => { @@ -197,7 +217,7 @@ describe('sentryHandle', () => { ); }); - it("creates a transaction from sentry-trace header but doesn't populate a new DSC", async () => { + it("starts a span from sentry-trace header but doesn't populate a new DSC", async () => { const event = mockEvent({ request: { headers: { From 7bdbb409229550e9627447f74727e938b8f98c20 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 14 Aug 2025 19:04:32 +0200 Subject: [PATCH 2/5] simplify --- .../sveltekit/src/server-common/handle.ts | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index ab0632b715df..806042a2f074 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -88,15 +88,28 @@ export function isFetchProxyRequired(version: string): boolean { return true; } +interface BackwardsForwardsCompatibleEvent { + /** + * For now taken from: https://github.com/sveltejs/kit/pull/13899 + * Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing. + * @since 2.30.0 + */ + tracing?: { + /** Whether tracing is enabled. */ + enabled: boolean; + // omitting other properties for now, since we don't use them. + }; +} + async function instrumentHandle( { event, resolve, }: { - event: Parameters[0]['event']; + event: Parameters[0]['event'] & BackwardsForwardsCompatibleEvent; resolve: Parameters[0]['resolve']; }, - options: SentryHandleOptions & { svelteKitTracingEnabled: boolean }, + options: SentryHandleOptions, ): Promise { const routeId = event.route?.id; @@ -127,7 +140,7 @@ async function instrumentHandle( // We only start a span if SvelteKit's native tracing is not enabled. Two reasons: // - Used Kit version doesn't yet support tracing // - Users didn't enable tracing - const shouldStartSpan = !options.svelteKitTracingEnabled; + const shouldStartSpan = !event.tracing?.enabled; try { const resolveWithSentry: (span?: Span) => Promise = async (span?: Span) => { @@ -172,19 +185,6 @@ async function instrumentHandle( } } -interface BackwardsForwardsCompatibleEvent { - /** - * For now taken from: https://github.com/sveltejs/kit/pull/13899 - * Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing. - * @since 2.30.0 - */ - tracing?: { - /** Whether tracing is enabled. */ - enabled: boolean; - // omitting other properties for now, since we don't use them. - }; -} - /** * A SvelteKit handle function that wraps the request for Sentry error and * performance monitoring. @@ -215,18 +215,15 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { '_sentrySkipRequestIsolation' in backwardsForwardsCompatibleEvent.locals && backwardsForwardsCompatibleEvent.locals._sentrySkipRequestIsolation; - const svelteKitTracingEnabled = !!backwardsForwardsCompatibleEvent.tracing?.enabled; - // In case of a same-origin `fetch` call within a server`load` function, // SvelteKit will actually just re-enter the `handle` function and set `isSubRequest` // to `true` so that no additional network call is made. // We want the `http.server` span of that nested call to be a child span of the // currently active span instead of a new root span to correctly reflect this // behavior. - if (skipIsolation || input.event.isSubRequest || svelteKitTracingEnabled) { + if (skipIsolation || input.event.isSubRequest) { return instrumentHandle(input, { ...options, - svelteKitTracingEnabled, }); } @@ -243,7 +240,6 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, { ...options, - svelteKitTracingEnabled, }), ); }); From d78638e03e637f7fa6f86af42e4d409d4790f607 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 15 Aug 2025 12:27:12 +0200 Subject: [PATCH 3/5] 2.31.0 --- packages/sveltekit/src/server-common/handle.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 806042a2f074..60e34a3a6a4c 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -92,7 +92,7 @@ interface BackwardsForwardsCompatibleEvent { /** * For now taken from: https://github.com/sveltejs/kit/pull/13899 * Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing. - * @since 2.30.0 + * @since 2.31.0 */ tracing?: { /** Whether tracing is enabled. */ From 2b8d229bf2dc7ecc892e42fe13d2df0beed6a569 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 18 Aug 2025 12:13:35 +0200 Subject: [PATCH 4/5] process kit spans --- .../sveltekit/src/server-common/handle.ts | 61 ++++++-- .../src/server-common/processKitSpans.ts | 72 +++++++++ packages/sveltekit/src/server/sdk.ts | 7 +- packages/sveltekit/src/worker/cloudflare.ts | 7 +- .../server-common/processKitSpans.test.ts | 144 ++++++++++++++++++ 5 files changed, 275 insertions(+), 16 deletions(-) create mode 100644 packages/sveltekit/src/server-common/processKitSpans.ts create mode 100644 packages/sveltekit/test/server-common/processKitSpans.test.ts diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 60e34a3a6a4c..61f68dcb7258 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -7,10 +7,13 @@ import { getDefaultIsolationScope, getIsolationScope, getTraceMetaTags, + SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setHttpStatus, + spanToJSON, startSpan, + updateSpanName, winterCGRequestToRequestData, withIsolationScope, } from '@sentry/core'; @@ -97,7 +100,8 @@ interface BackwardsForwardsCompatibleEvent { tracing?: { /** Whether tracing is enabled. */ enabled: boolean; - // omitting other properties for now, since we don't use them. + current: Span; + root: Span; }; } @@ -140,10 +144,10 @@ async function instrumentHandle( // We only start a span if SvelteKit's native tracing is not enabled. Two reasons: // - Used Kit version doesn't yet support tracing // - Users didn't enable tracing - const shouldStartSpan = !event.tracing?.enabled; + const kitTracingEnabled = event.tracing?.enabled; try { - const resolveWithSentry: (span?: Span) => Promise = async (span?: Span) => { + const resolveWithSentry: (sentrySpan?: Span) => Promise = async (sentrySpan?: Span) => { getCurrentScope().setSDKProcessingMetadata({ // We specifically avoid cloning the request here to avoid double read errors. // We only read request headers so we're not consuming the body anyway. @@ -151,19 +155,45 @@ async function instrumentHandle( // https://github.com/getsentry/sentry-javascript/issues/14583 normalizedRequest: winterCGRequestToRequestData(event.request), }); + const res = await resolve(event, { transformPageChunk: addSentryCodeToPage({ injectFetchProxyScript: options.injectFetchProxyScript ?? true, }), }); - if (span) { - setHttpStatus(span, res.status); + + const kitRootSpan = event.tracing?.root; + + if (sentrySpan) { + setHttpStatus(sentrySpan, res.status); + } else if (kitRootSpan) { + // Update the root span emitted from SvelteKit to resemble a `http.server` span + // We're doing this here instead of an event processor to ensure we update the + // span name as early as possible (for dynamic sampling, et al.) + // Other spans are enhanced in the `processKitSpans` function. + const spanJson = spanToJSON(kitRootSpan); + const kitRootSpanAttributes = spanJson.data; + const originalName = spanJson.description; + + const routeName = kitRootSpanAttributes['http.route']; + if (routeName && typeof routeName === 'string') { + updateSpanName(kitRootSpan, routeName); + } + + kitRootSpan.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltejs.kit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', + 'sveltekit.tracing.original_name': originalName, + }); } + return res; }; - const resolveResult = shouldStartSpan - ? await startSpan( + const resolveResult = kitTracingEnabled + ? await resolveWithSentry() + : await startSpan( { op: 'http.server', attributes: { @@ -174,8 +204,8 @@ async function instrumentHandle( name: routeName, }, resolveWithSentry, - ) - : await resolveWithSentry(); + ); + return resolveResult; } catch (e: unknown) { sendErrorToSentry(e, 'handle'); @@ -237,11 +267,14 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { // https://github.com/getsentry/sentry-javascript/issues/14583 normalizedRequest: winterCGRequestToRequestData(input.event.request), }); - return continueTrace(getTracePropagationData(input.event), () => - instrumentHandle(input, { - ...options, - }), - ); + + if (backwardsForwardsCompatibleEvent.tracing?.enabled) { + // if sveltekit tracing is enabled (since 2.31.0), trace continuation is handled by + // kit before our hook is executed. No noeed to call `continueTrace` from our end + return instrumentHandle(input, options); + } + + return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options)); }); }; diff --git a/packages/sveltekit/src/server-common/processKitSpans.ts b/packages/sveltekit/src/server-common/processKitSpans.ts new file mode 100644 index 000000000000..6894565459aa --- /dev/null +++ b/packages/sveltekit/src/server-common/processKitSpans.ts @@ -0,0 +1,72 @@ +import type { Integration, SpanOrigin } from '@sentry/core'; +import { type SpanJSON, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +/** + * A small integration that preprocesses spans so that SvelteKit-generated spans + * (via Kit's tracing feature since 2.31.0) get the correct Sentry attributes + * and data. + */ +export function svelteKitSpansIntegration(): Integration { + return { + name: 'SvelteKitSpansEnhancment', + preprocessEvent(event) { + if (event.type === 'transaction') { + event.spans?.forEach(_enhanceKitSpan); + } + }, + }; +} + +/** + * Adds sentry-specific attributes and data to a span emitted by SvelteKit's native tracing (since 2.31.0) + * @exported for testing + */ +export function _enhanceKitSpan(span: SpanJSON): void { + let op: string | undefined = undefined; + let origin: SpanOrigin | undefined = undefined; + + const spanName = span.description; + + switch (spanName) { + case 'sveltekit.resolve': + op = 'http.sveltekit.resolve'; + origin = 'auto.http.sveltekit'; + break; + case 'sveltekit.load': + op = 'function.sveltekit.load'; + origin = 'auto.function.sveltekit.load'; + break; + case 'sveltekit.form_action': + op = 'function.sveltekit.form_action'; + origin = 'auto.function.sveltekit.action'; + break; + case 'sveltekit.remote.call': + op = 'function.sveltekit.remote'; + origin = 'auto.rpc.sveltekit.remote'; + break; + case 'sveltekit.handle.root': + // We don't want to overwrite the root handle span at this point since + // we already enhance the root span in our `sentryHandle` hook. + break; + default: { + if (spanName?.startsWith('sveltekit.handle.sequenced.')) { + op = 'function.sveltekit.handle'; + origin = 'auto.function.sveltekit.handle'; + } + break; + } + } + + const previousOp = span.op || span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]; + const previousOrigin = span.origin || span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; + + if (!previousOp && op) { + span.op = op; + span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op; + } + + if (!previousOrigin && origin) { + span.origin = origin; + span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin; + } +} diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 19a0a8f9f5ad..c075cc6f19da 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -1,6 +1,7 @@ import { applySdkMetadata } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node'; +import { svelteKitSpansIntegration } from '../server-common/processKitSpans'; import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration'; /** @@ -9,7 +10,11 @@ import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegrat */ export function init(options: NodeOptions): NodeClient | undefined { const opts = { - defaultIntegrations: [...getDefaultNodeIntegrations(options), rewriteFramesIntegration()], + defaultIntegrations: [ + ...getDefaultNodeIntegrations(options), + rewriteFramesIntegration(), + svelteKitSpansIntegration(), + ], ...options, }; diff --git a/packages/sveltekit/src/worker/cloudflare.ts b/packages/sveltekit/src/worker/cloudflare.ts index b27ceba87780..e27a5aca96ef 100644 --- a/packages/sveltekit/src/worker/cloudflare.ts +++ b/packages/sveltekit/src/worker/cloudflare.ts @@ -6,6 +6,7 @@ import { } from '@sentry/cloudflare'; import { addNonEnumerableProperty } from '@sentry/core'; import type { Handle } from '@sveltejs/kit'; +import { svelteKitSpansIntegration } from '../server-common/processKitSpans'; import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration'; /** @@ -16,7 +17,11 @@ import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegrat */ export function initCloudflareSentryHandle(options: CloudflareOptions): Handle { const opts: CloudflareOptions = { - defaultIntegrations: [...getDefaultCloudflareIntegrations(options), rewriteFramesIntegration()], + defaultIntegrations: [ + ...getDefaultCloudflareIntegrations(options), + rewriteFramesIntegration(), + svelteKitSpansIntegration(), + ], ...options, }; diff --git a/packages/sveltekit/test/server-common/processKitSpans.test.ts b/packages/sveltekit/test/server-common/processKitSpans.test.ts new file mode 100644 index 000000000000..3bcb2be0a5ea --- /dev/null +++ b/packages/sveltekit/test/server-common/processKitSpans.test.ts @@ -0,0 +1,144 @@ +import type { EventType, SpanJSON, TransactionEvent } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { describe, expect, it } from 'vitest'; +import { _enhanceKitSpan, svelteKitSpansIntegration } from '../../src/server-common/processKitSpans'; + +describe('svelteKitSpansIntegration', () => { + it('has a name and a preprocessEventHook', () => { + const integration = svelteKitSpansIntegration(); + + expect(integration.name).toBe('SvelteKitSpansEnhancment'); + expect(typeof integration.preprocessEvent).toBe('function'); + }); + + it('enhances spans from SvelteKit', () => { + const event: TransactionEvent = { + type: 'transaction', + spans: [ + { + description: 'sveltekit.resolve', + data: { + someAttribute: 'someValue', + }, + span_id: '123', + trace_id: 'abc', + start_timestamp: 0, + }, + ], + }; + + // @ts-expect-error -- passing in an empty option for client but it is unused in the integration + svelteKitSpansIntegration().preprocessEvent?.(event, {}, {}); + + expect(event.spans).toHaveLength(1); + expect(event.spans?.[0]?.op).toBe('http.sveltekit.resolve'); + expect(event.spans?.[0]?.origin).toBe('auto.http.sveltekit'); + expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.sveltekit.resolve'); + expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + }); + + describe('_enhanceKitSpan', () => { + it.each([ + ['sveltekit.resolve', 'http.sveltekit.resolve', 'auto.http.sveltekit'], + ['sveltekit.load', 'function.sveltekit.load', 'auto.function.sveltekit.load'], + ['sveltekit.form_action', 'function.sveltekit.form_action', 'auto.function.sveltekit.action'], + ['sveltekit.remote.call', 'function.sveltekit.remote', 'auto.rpc.sveltekit.remote'], + ['sveltekit.handle.sequenced.0', 'function.sveltekit.handle', 'auto.function.sveltekit.handle'], + ['sveltekit.handle.sequenced.myHandler', 'function.sveltekit.handle', 'auto.function.sveltekit.handle'], + ])('enhances %s span with the correct op and origin', (spanName, op, origin) => { + const span = { + description: spanName, + data: { + someAttribute: 'someValue', + }, + span_id: '123', + trace_id: 'abc', + start_timestamp: 0, + } as SpanJSON; + + _enhanceKitSpan(span); + + expect(span.op).toBe(op); + expect(span.origin).toBe(origin); + expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe(op); + expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin); + }); + + it("doesn't change spans from other origins", () => { + const span = { + description: 'someOtherSpan', + data: {}, + } as SpanJSON; + + _enhanceKitSpan(span); + + expect(span.op).toBeUndefined(); + expect(span.origin).toBeUndefined(); + expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); + expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); + }); + + it("doesn't overwrite the sveltekit.handle.root span", () => { + const rootHandleSpan = { + description: 'sveltekit.handle.root', + op: 'http.server', + origin: 'auto.http.sveltekit', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + }, + span_id: '123', + trace_id: 'abc', + start_timestamp: 0, + } as SpanJSON; + + _enhanceKitSpan(rootHandleSpan); + + expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server'); + expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); + expect(rootHandleSpan.op).toBe('http.server'); + expect(rootHandleSpan.origin).toBe('auto.http.sveltekit'); + }); + + it("doesn't enhance unrelated spans", () => { + const span = { + description: 'someOtherSpan', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg', + }, + op: 'db', + origin: 'auto.db.pg', + span_id: '123', + trace_id: 'abc', + start_timestamp: 0, + } as SpanJSON; + + _enhanceKitSpan(span); + + expect(span.op).toBe('db'); + expect(span.origin).toBe('auto.db.pg'); + expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('db'); + expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg'); + }); + + it("doesn't overwrite already set ops or origins on sveltekit spans", () => { + // for example, if users manually set this (for whatever reason) + const span = { + description: 'sveltekit.resolve', + origin: 'auto.custom.origin', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op', + }, + span_id: '123', + trace_id: 'abc', + start_timestamp: 0, + } as SpanJSON; + + _enhanceKitSpan(span); + + expect(span.origin).toBe('auto.custom.origin'); + expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + }); + }); +}); From e0316f733a05d32be2184babd24a9717da224503 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 19 Aug 2025 13:08:42 +0200 Subject: [PATCH 5/5] wip but detect tracing config at SDK setup time --- .../src/server-common/processKitSpans.ts | 2 + packages/sveltekit/src/server-common/utils.ts | 17 +++++++- packages/sveltekit/src/server/sdk.ts | 27 +++++++++--- .../sveltekit/src/vite/injectGlobalValues.ts | 2 + packages/sveltekit/src/vite/sourceMaps.ts | 9 +++- packages/sveltekit/src/vite/svelteConfig.ts | 41 ++++++++++++++++++- 6 files changed, 87 insertions(+), 11 deletions(-) diff --git a/packages/sveltekit/src/server-common/processKitSpans.ts b/packages/sveltekit/src/server-common/processKitSpans.ts index 6894565459aa..078817e5ce27 100644 --- a/packages/sveltekit/src/server-common/processKitSpans.ts +++ b/packages/sveltekit/src/server-common/processKitSpans.ts @@ -9,6 +9,8 @@ import { type SpanJSON, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ export function svelteKitSpansIntegration(): Integration { return { name: 'SvelteKitSpansEnhancment', + // Using preprocessEvent to ensure the processing happens before user-configured + // event processors are executed preprocessEvent(event) { if (event.type === 'transaction') { event.spans?.forEach(_enhanceKitSpan); diff --git a/packages/sveltekit/src/server-common/utils.ts b/packages/sveltekit/src/server-common/utils.ts index b861bf758697..821ce0a66eaf 100644 --- a/packages/sveltekit/src/server-common/utils.ts +++ b/packages/sveltekit/src/server-common/utils.ts @@ -1,6 +1,7 @@ -import { captureException, objectify } from '@sentry/core'; +import { captureException, GLOBAL_OBJ, objectify } from '@sentry/core'; import type { RequestEvent } from '@sveltejs/kit'; import { isHttpError, isRedirect } from '../common/utils'; +import type { GlobalWithSentryValues } from '../vite/injectGlobalValues'; /** * Takes a request event and extracts traceparent and DSC data @@ -52,3 +53,17 @@ export function sendErrorToSentry(e: unknown, handlerFn: 'handle' | 'load' | 'se return objectifiedErr; } + +/** + * During build, we inject the SvelteKit tracing config into the global object of the server. + * @returns tracing config (available since 2.31.0) + */ +export function getKitTracingConfig(): { instrumentation: boolean; tracing: boolean } { + const globalWithSentryValues: GlobalWithSentryValues = GLOBAL_OBJ; + const kitTracingConfig = globalWithSentryValues.__sentry_sveltekit_tracing_config; + + return { + instrumentation: kitTracingConfig?.instrumentation?.server ?? false, + tracing: kitTracingConfig?.tracing?.server ?? false, + }; +} diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index c075cc6f19da..5c84f8cc65f3 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -1,20 +1,35 @@ import { applySdkMetadata } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node'; +import { + getDefaultIntegrations as getDefaultNodeIntegrations, + httpIntegration, + init as initNodeSdk, +} from '@sentry/node'; import { svelteKitSpansIntegration } from '../server-common/processKitSpans'; import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration'; +import { getKitTracingConfig } from '../server-common/utils'; /** * Initialize the Server-side Sentry SDK * @param options */ export function init(options: NodeOptions): NodeClient | undefined { + const defaultIntegrations = [...getDefaultNodeIntegrations(options), rewriteFramesIntegration()]; + + const config = getKitTracingConfig(); + if (config.instrumentation) { + // Whenever `instrumentation` is enabled, we don't need httpIntegration to emit spans + // - if `tracing` is enabled, kit will emit the root span + // - if `tracing` is disabled, our handler will emit the root span + defaultIntegrations.push(httpIntegration({ disableIncomingRequestSpans: true })); + if (config.tracing) { + // If `tracing` is enabled, we need to instrument spans for the server + defaultIntegrations.push(svelteKitSpansIntegration()); + } + } + const opts = { - defaultIntegrations: [ - ...getDefaultNodeIntegrations(options), - rewriteFramesIntegration(), - svelteKitSpansIntegration(), - ], + defaultIntegrations, ...options, }; diff --git a/packages/sveltekit/src/vite/injectGlobalValues.ts b/packages/sveltekit/src/vite/injectGlobalValues.ts index 96ad05123ce6..b6a5b9ca168a 100644 --- a/packages/sveltekit/src/vite/injectGlobalValues.ts +++ b/packages/sveltekit/src/vite/injectGlobalValues.ts @@ -1,7 +1,9 @@ import type { InternalGlobal } from '@sentry/core'; +import type { SvelteKitTracingConfig } from './svelteConfig'; export type GlobalSentryValues = { __sentry_sveltekit_output_dir?: string; + __sentry_sveltekit_tracing_config?: SvelteKitTracingConfig; }; /** diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index eb3b449144f8..8f0e523fd128 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -11,7 +11,7 @@ import type { Plugin, UserConfig } from 'vite'; import { WRAPPED_MODULE_SUFFIX } from '../common/utils'; import type { GlobalSentryValues } from './injectGlobalValues'; import { getGlobalValueInjectionCode, VIRTUAL_GLOBAL_VALUES_FILE } from './injectGlobalValues'; -import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from './svelteConfig'; +import { getAdapterOutputDir, getHooksFileName, getTracingConfig, loadSvelteConfig } from './svelteConfig'; import type { CustomSentryVitePluginOptions } from './types'; // sorcery has no types, so these are some basic type definitions: @@ -153,8 +153,11 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug const globalSentryValues: GlobalSentryValues = { __sentry_sveltekit_output_dir: adapterOutputDir, + __sentry_sveltekit_tracing_config: getTracingConfig(svelteConfig), }; + console.log('xx globalSentryValues', globalSentryValues); + const sourceMapSettingsPlugin: Plugin = { name: 'sentry-sveltekit-update-source-map-setting-plugin', apply: 'build', // only apply this plugin at build time @@ -235,8 +238,10 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug transform: async (code, id) => { // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway const isServerHooksFile = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id); + const isInstrumentationServerFile = /instrumentation\.server\./.test(id); - if (isServerHooksFile) { + if (isServerHooksFile || isInstrumentationServerFile) { + console.log('xx inject global values', id); const ms = new MagicString(code); ms.append(`\n; import "${VIRTUAL_GLOBAL_VALUES_FILE}";\n`); return { diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts index 34874bfd2f97..8f72fe3c6206 100644 --- a/packages/sveltekit/src/vite/svelteConfig.ts +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -4,12 +4,31 @@ import * as path from 'path'; import * as url from 'url'; import type { SupportedSvelteKitAdapters } from './detectAdapter'; +export type SvelteKitTracingConfig = { + tracing?: { + server: boolean; + }; + instrumentation?: { + server: boolean; + }; +}; + +/** + * Experimental tracing and instrumentation config is available + * @since 2.31.0 + */ +type BackwardsForwardsCompatibleKitConfig = Config['kit'] & { experimental?: SvelteKitTracingConfig }; + +interface BackwardsForwardsCompatibleSvelteConfig extends Config { + kit?: BackwardsForwardsCompatibleKitConfig; +} + /** * Imports the svelte.config.js file and returns the config object. * The sveltekit plugins import the config in the same way. * See: https://github.com/sveltejs/kit/blob/master/packages/kit/src/core/config/index.js#L63 */ -export async function loadSvelteConfig(): Promise { +export async function loadSvelteConfig(): Promise { // This can only be .js (see https://github.com/sveltejs/kit/pull/4031#issuecomment-1049475388) const SVELTE_CONFIG_FILE = 'svelte.config.js'; @@ -23,7 +42,7 @@ export async function loadSvelteConfig(): Promise { const svelteConfigModule = await import(`${url.pathToFileURL(configFile).href}`); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return (svelteConfigModule?.default as Config) || {}; + return (svelteConfigModule?.default as BackwardsForwardsCompatibleSvelteConfig) || {}; } catch (e) { // eslint-disable-next-line no-console console.warn("[Source Maps Plugin] Couldn't load svelte.config.js:"); @@ -110,3 +129,21 @@ async function getNodeAdapterOutputDir(svelteConfig: Config): Promise { return outputDir; } + +/** + * Returns the Sveltekit tracing config users can enable in svelte.config.js. + * Available in Kit @since 2.31.0 + */ +export function getTracingConfig( + svelteConfig: BackwardsForwardsCompatibleSvelteConfig, +): BackwardsForwardsCompatibleKitConfig['experimental'] { + const experimentalConfig = svelteConfig.kit?.experimental; + return { + instrumentation: { + server: experimentalConfig?.instrumentation?.server ?? false, + }, + tracing: { + server: experimentalConfig?.tracing?.server ?? false, + }, + }; +}