From 399373713c3af402ab08ae3af9d2264aff86cea5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 14 Aug 2025 19:01:03 +0200 Subject: [PATCH 01/16] 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 371fe0c2ff0b14082a145f4140b097d8047aef69 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 14 Aug 2025 19:04:32 +0200 Subject: [PATCH 02/16] 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 9c7f66dd155a243aff775882d079de936f4db4d4 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 15 Aug 2025 12:27:12 +0200 Subject: [PATCH 03/16] 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 bb028e1588c349c2927086442150d63c48b29060 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 18 Aug 2025 12:13:35 +0200 Subject: [PATCH 04/16] 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 ea951259e6bd8d38638b8e6140643717203f4a8e Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 19 Aug 2025 13:08:42 +0200 Subject: [PATCH 05/16] 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, + }, + }; +} From c389a1f76432883c8baa8493060a42a31a307fcc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 20 Aug 2025 10:02:48 +0200 Subject: [PATCH 06/16] comment --- packages/sveltekit/src/server/sdk.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 5c84f8cc65f3..6e7dfa478024 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -21,6 +21,9 @@ export function init(options: NodeOptions): NodeClient | undefined { // 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 + // In old (hooks.server.ts) based SDK setups, adding the default version of the integration does nothing + // for incoming requests. We still add it in default config for the undocumented case that anyone is + // using the SDK by `--import`ing the SDK setup directly (e.g. with adapter-node). defaultIntegrations.push(httpIntegration({ disableIncomingRequestSpans: true })); if (config.tracing) { // If `tracing` is enabled, we need to instrument spans for the server From e728084cc9d498751c4afef4cf04540c2ad05e53 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 26 Aug 2025 10:47:29 +0200 Subject: [PATCH 07/16] global values injection plugin, add tests, adjust existing tests --- .../sveltekit/src/vite/injectGlobalValues.ts | 82 ++++++++++++- .../sveltekit/src/vite/sentryVitePlugins.ts | 12 +- packages/sveltekit/src/vite/sourceMaps.ts | 61 +-------- packages/sveltekit/src/vite/svelteConfig.ts | 2 +- packages/sveltekit/src/vite/types.ts | 14 +++ .../test/server-common/utils.test.ts | 36 +++++- .../test/vite/injectGlobalValues.test.ts | 31 ++++- .../test/vite/sentrySvelteKitPlugins.test.ts | 116 ++++++++++++------ .../sveltekit/test/vite/sourceMaps.test.ts | 67 +++++----- 9 files changed, 281 insertions(+), 140 deletions(-) diff --git a/packages/sveltekit/src/vite/injectGlobalValues.ts b/packages/sveltekit/src/vite/injectGlobalValues.ts index b6a5b9ca168a..f7fcc516f3de 100644 --- a/packages/sveltekit/src/vite/injectGlobalValues.ts +++ b/packages/sveltekit/src/vite/injectGlobalValues.ts @@ -1,5 +1,14 @@ -import type { InternalGlobal } from '@sentry/core'; -import type { SvelteKitTracingConfig } from './svelteConfig'; +import { type InternalGlobal, escapeStringForRegex } from '@sentry/core'; +import MagicString from 'magic-string'; +import type { Plugin } from 'vite'; +import { + type BackwardsForwardsCompatibleSvelteConfig, + type SvelteKitTracingConfig, + getAdapterOutputDir, + getHooksFileName, + getTracingConfig, +} from './svelteConfig'; +import type { SentrySvelteKitPluginOptions } from './types'; export type GlobalSentryValues = { __sentry_sveltekit_output_dir?: string; @@ -29,3 +38,72 @@ export function getGlobalValueInjectionCode(globalSentryValues: GlobalSentryValu return `${injectedValuesCode}\n`; } + +/** + * Injects SvelteKit app configuration values the svelte.config.js into the + * server's global object so that the SDK can pick up the information at runtime + */ +export async function makeGlobalValuesInjectionPlugin( + svelteConfig: BackwardsForwardsCompatibleSvelteConfig, + options: Pick, +): Promise { + const { adapter = 'other', debug = false } = options; + + const serverHooksFile = getHooksFileName(svelteConfig, 'server'); + const adapterOutputDir = await getAdapterOutputDir(svelteConfig, adapter); + + const globalSentryValues: GlobalSentryValues = { + __sentry_sveltekit_output_dir: adapterOutputDir, + __sentry_sveltekit_tracing_config: getTracingConfig(svelteConfig), + }; + + if (debug) { + // eslint-disable-next-line no-console + console.log('[Sentry SvelteKit] Global values:', globalSentryValues); + } + + return { + name: 'sentry-sveltekit-global-values-injection-plugin', + resolveId: (id, _importer, _ref) => { + if (id === VIRTUAL_GLOBAL_VALUES_FILE) { + return { + id: VIRTUAL_GLOBAL_VALUES_FILE, + external: false, + moduleSideEffects: true, + }; + } + return null; + }, + + load: id => { + if (id === VIRTUAL_GLOBAL_VALUES_FILE) { + return { + code: getGlobalValueInjectionCode(globalSentryValues), + }; + } + return null; + }, + + transform: async (code, id) => { + const isServerEntryFile = + /instrumentation\.server\./.test(id) || + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway + new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id); + + if (isServerEntryFile) { + if (debug) { + // eslint-disable-next-line no-console + console.log('[Global Values Plugin] Injecting global values into', id); + } + const ms = new MagicString(code); + ms.append(`\n; import "${VIRTUAL_GLOBAL_VALUES_FILE}";\n`); + return { + code: ms.toString(), + map: ms.generateMap({ hires: true }), + }; + } + + return null; + }, + }; +} diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts index 4444ba9a6ab7..6b10e157ea1e 100644 --- a/packages/sveltekit/src/vite/sentryVitePlugins.ts +++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts @@ -2,12 +2,15 @@ import type { Plugin } from 'vite'; import type { AutoInstrumentSelection } from './autoInstrument'; import { makeAutoInstrumentationPlugin } from './autoInstrument'; import { detectAdapter } from './detectAdapter'; +import { makeGlobalValuesInjectionPlugin } from './injectGlobalValues'; import { makeCustomSentryVitePlugins } from './sourceMaps'; +import { loadSvelteConfig } from './svelteConfig'; import type { CustomSentryVitePluginOptions, SentrySvelteKitPluginOptions } from './types'; const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = { autoUploadSourceMaps: true, autoInstrument: true, + injectGlobalValues: true, debug: false, }; @@ -44,9 +47,14 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} const sentryVitePluginsOptions = generateVitePluginOptions(mergedOptions); - if (sentryVitePluginsOptions) { - const sentryVitePlugins = await makeCustomSentryVitePlugins(sentryVitePluginsOptions); + const svelteConfig = await loadSvelteConfig(); + + if (mergedOptions.injectGlobalValues) { + sentryPlugins.push(await makeGlobalValuesInjectionPlugin(svelteConfig, mergedOptions)); + } + if (sentryVitePluginsOptions) { + const sentryVitePlugins = await makeCustomSentryVitePlugins(sentryVitePluginsOptions, svelteConfig); sentryPlugins.push(...sentryVitePlugins); } diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 8f0e523fd128..57bf21055bf1 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -1,17 +1,14 @@ -/* eslint-disable max-lines */ import { escapeStringForRegex, uuid4 } from '@sentry/core'; import { getSentryRelease } from '@sentry/node'; import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; import { sentryVitePlugin } from '@sentry/vite-plugin'; import * as child_process from 'child_process'; import * as fs from 'fs'; -import MagicString from 'magic-string'; import * as path from 'path'; 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, getTracingConfig, loadSvelteConfig } from './svelteConfig'; +import type { BackwardsForwardsCompatibleSvelteConfig } from './svelteConfig'; +import { getAdapterOutputDir } from './svelteConfig'; import type { CustomSentryVitePluginOptions } from './types'; // sorcery has no types, so these are some basic type definitions: @@ -45,9 +42,10 @@ type FilesToDeleteAfterUpload = string | string[] | undefined; * * @returns the custom Sentry Vite plugin */ -export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePluginOptions): Promise { - const svelteConfig = await loadSvelteConfig(); - +export async function makeCustomSentryVitePlugins( + options: CustomSentryVitePluginOptions, + svelteConfig: BackwardsForwardsCompatibleSvelteConfig, +): Promise { const usedAdapter = options?.adapter || 'other'; const adapterOutputDir = await getAdapterOutputDir(svelteConfig, usedAdapter); @@ -149,15 +147,6 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug let isSSRBuild = true; - const serverHooksFile = getHooksFileName(svelteConfig, 'server'); - - 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 @@ -205,26 +194,6 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug name: 'sentry-sveltekit-debug-id-upload-plugin', apply: 'build', // only apply this plugin at build time enforce: 'post', // this needs to be set to post, otherwise we don't pick up the output from the SvelteKit adapter - resolveId: (id, _importer, _ref) => { - if (id === VIRTUAL_GLOBAL_VALUES_FILE) { - return { - id: VIRTUAL_GLOBAL_VALUES_FILE, - external: false, - moduleSideEffects: true, - }; - } - return null; - }, - - load: id => { - if (id === VIRTUAL_GLOBAL_VALUES_FILE) { - return { - code: getGlobalValueInjectionCode(globalSentryValues), - }; - } - return null; - }, - configResolved: config => { // The SvelteKit plugins trigger additional builds within the main (SSR) build. // We just need a mechanism to upload source maps only once. @@ -235,24 +204,6 @@ 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 || isInstrumentationServerFile) { - console.log('xx inject global values', id); - const ms = new MagicString(code); - ms.append(`\n; import "${VIRTUAL_GLOBAL_VALUES_FILE}";\n`); - return { - code: ms.toString(), - map: ms.generateMap({ hires: true }), - }; - } - - return null; - }, - // We need to start uploading source maps later than in the original plugin // because SvelteKit is invoking the adapter at closeBundle. // This means that we need to wait until the adapter is done before we start uploading. diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts index 8f72fe3c6206..6ccaab87e6de 100644 --- a/packages/sveltekit/src/vite/svelteConfig.ts +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -19,7 +19,7 @@ export type SvelteKitTracingConfig = { */ type BackwardsForwardsCompatibleKitConfig = Config['kit'] & { experimental?: SvelteKitTracingConfig }; -interface BackwardsForwardsCompatibleSvelteConfig extends Config { +export interface BackwardsForwardsCompatibleSvelteConfig extends Config { kit?: BackwardsForwardsCompatibleKitConfig; } diff --git a/packages/sveltekit/src/vite/types.ts b/packages/sveltekit/src/vite/types.ts index 0f6717a2c7e9..e7b4fb5273a3 100644 --- a/packages/sveltekit/src/vite/types.ts +++ b/packages/sveltekit/src/vite/types.ts @@ -219,8 +219,22 @@ export type SentrySvelteKitPluginOptions = { * @default true`. */ autoUploadSourceMaps?: boolean; + /** * Options related to source maps upload to Sentry */ sourceMapsUploadOptions?: SourceMapsUploadOptions; + + /** + * Controls whether application config values should be injected into the server's global object at runtime. + * + * The Sentry SDK needs some information about your application config at runtime, like for example + * the used build output directory or your Sveltekit Tracing config. + * + * Important: You most likely don't want to disable this! Only disable injection if you're suspecting any + * kinds of build problems to be related to Sentry. + * + * @default true + */ + injectGlobalValues?: boolean; }; diff --git a/packages/sveltekit/test/server-common/utils.test.ts b/packages/sveltekit/test/server-common/utils.test.ts index df0858ed4654..bb002da927ad 100644 --- a/packages/sveltekit/test/server-common/utils.test.ts +++ b/packages/sveltekit/test/server-common/utils.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest'; -import { getTracePropagationData } from '../../src/server-common/utils'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { getKitTracingConfig, getTracePropagationData } from '../../src/server-common/utils'; const MOCK_REQUEST_EVENT: any = { request: { @@ -48,3 +48,35 @@ describe('getTracePropagationData', () => { expect(baggage).toBeUndefined(); }); }); + +describe('getKitTracingConfig', () => { + beforeEach(() => { + delete globalThis.__sentry_sveltekit_tracing_config; + }); + + it('returns the tracing config from the global object', () => { + globalThis.__sentry_sveltekit_tracing_config = { + instrumentation: { + server: true, + }, + tracing: { + server: false, + }, + }; + + const config = getKitTracingConfig(); + + expect(config).toEqual({ + instrumentation: true, + tracing: false, + }); + }); + + it('returns instrumentation and tracing being disabled by default', () => { + const config = getKitTracingConfig(); + expect(config).toEqual({ + instrumentation: false, + tracing: false, + }); + }); +}); diff --git a/packages/sveltekit/test/vite/injectGlobalValues.test.ts b/packages/sveltekit/test/vite/injectGlobalValues.test.ts index 3e07bf7e26a7..01075fb33ade 100644 --- a/packages/sveltekit/test/vite/injectGlobalValues.test.ts +++ b/packages/sveltekit/test/vite/injectGlobalValues.test.ts @@ -4,19 +4,38 @@ import { getGlobalValueInjectionCode } from '../../src/vite/injectGlobalValues'; describe('getGlobalValueInjectionCode', () => { it('returns code that injects values into the global object', () => { const injectionCode = getGlobalValueInjectionCode({ - // @ts-expect-error - just want to test this with multiple values - something: 'else', __sentry_sveltekit_output_dir: '.svelte-kit/output', + __sentry_sveltekit_tracing_config: { + tracing: { + server: true, + }, + instrumentation: { + server: true, + }, + }, }); - expect(injectionCode).toEqual(`globalThis["something"] = "else"; -globalThis["__sentry_sveltekit_output_dir"] = ".svelte-kit/output"; -`); + + expect(injectionCode).toMatchInlineSnapshot(` + "globalThis["__sentry_sveltekit_output_dir"] = ".svelte-kit/output"; + globalThis["__sentry_sveltekit_tracing_config"] = {"tracing":{"server":true},"instrumentation":{"server":true}}; + " + `); // Check that the code above is in fact valid and works as expected // The return value of eval here is the value of the last expression in the code - expect(eval(`${injectionCode}`)).toEqual('.svelte-kit/output'); + eval(injectionCode); + expect(globalThis.__sentry_sveltekit_output_dir).toEqual('.svelte-kit/output'); + expect(globalThis.__sentry_sveltekit_tracing_config).toEqual({ + tracing: { + server: true, + }, + instrumentation: { + server: true, + }, + }); delete globalThis.__sentry_sveltekit_output_dir; + delete globalThis.__sentry_sveltekit_tracing_config; }); it('returns empty string if no values are passed', () => { diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts index d21bcf0d8d04..68bb1a8b1bfa 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -41,8 +41,8 @@ describe('sentrySvelteKit()', () => { const plugins = await getSentrySvelteKitPlugins(); expect(plugins).toBeInstanceOf(Array); - // 1 auto instrument plugin + 5 source maps plugins - expect(plugins).toHaveLength(9); + // 1 auto instrument plugin + 1 global values injection plugin + 5 source maps plugins + expect(plugins).toHaveLength(10); }); it('returns the custom sentry source maps upload plugin, unmodified sourcemaps plugins and the auto-instrument plugin by default', async () => { @@ -51,6 +51,8 @@ describe('sentrySvelteKit()', () => { expect(pluginNames).toEqual([ // auto instrument plugin: 'sentry-auto-instrumentation', + // global values injection plugin: + 'sentry-sveltekit-global-values-injection-plugin', // default source maps plugins: 'sentry-telemetry-plugin', 'sentry-vite-release-injection-plugin', @@ -68,7 +70,7 @@ describe('sentrySvelteKit()', () => { it("doesn't return the sentry source maps plugins if autoUploadSourcemaps is `false`", async () => { const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: false }); - expect(plugins).toHaveLength(1); + expect(plugins).toHaveLength(2); // auto instrument + global values injection }); it("doesn't return the sentry source maps plugins if `NODE_ENV` is development", async () => { @@ -78,7 +80,7 @@ describe('sentrySvelteKit()', () => { const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: true, autoInstrument: true }); const instrumentPlugin = plugins[0]; - expect(plugins).toHaveLength(1); + expect(plugins).toHaveLength(2); // auto instrument + global values injection expect(instrumentPlugin?.name).toEqual('sentry-auto-instrumentation'); process.env.NODE_ENV = previousEnv; @@ -87,8 +89,8 @@ describe('sentrySvelteKit()', () => { it("doesn't return the auto instrument plugin if autoInstrument is `false`", async () => { const plugins = await getSentrySvelteKitPlugins({ autoInstrument: false }); const pluginNames = plugins.map(plugin => plugin.name); - expect(plugins).toHaveLength(8); - expect(pluginNames).not.toContain('sentry-upload-source-maps'); + expect(plugins).toHaveLength(9); // global values injection + 5 source maps plugins + 3 default plugins + expect(pluginNames).not.toContain('sentry-auto-instrumentation'); }); it('passes user-specified vite plugin options to the custom sentry source maps plugin', async () => { @@ -106,15 +108,18 @@ describe('sentrySvelteKit()', () => { adapter: 'vercel', }); - expect(makePluginSpy).toHaveBeenCalledWith({ - debug: true, - sourcemaps: { - assets: ['foo/*.js'], - ignore: ['bar/*.js'], - filesToDeleteAfterUpload: ['baz/*.js'], + expect(makePluginSpy).toHaveBeenCalledWith( + { + debug: true, + sourcemaps: { + assets: ['foo/*.js'], + ignore: ['bar/*.js'], + filesToDeleteAfterUpload: ['baz/*.js'], + }, + adapter: 'vercel', }, - adapter: 'vercel', - }); + {}, + ); }); it('passes user-specified vite plugin options to the custom sentry source maps plugin', async () => { @@ -152,26 +157,29 @@ describe('sentrySvelteKit()', () => { adapter: 'vercel', }); - expect(makePluginSpy).toHaveBeenCalledWith({ - debug: true, - org: 'other-org', - sourcemaps: { - assets: ['foo/*.js'], - ignore: ['bar/*.js'], - filesToDeleteAfterUpload: ['baz/*.js'], - }, - release: { - inject: false, - name: '3.0.0', - setCommits: { - auto: true, + expect(makePluginSpy).toHaveBeenCalledWith( + { + debug: true, + org: 'other-org', + sourcemaps: { + assets: ['foo/*.js'], + ignore: ['bar/*.js'], + filesToDeleteAfterUpload: ['baz/*.js'], }, + release: { + inject: false, + name: '3.0.0', + setCommits: { + auto: true, + }, + }, + headers: { + 'X-My-Header': 'foo', + }, + adapter: 'vercel', }, - headers: { - 'X-My-Header': 'foo', - }, - adapter: 'vercel', - }); + {}, + ); }); it('passes user-specified options to the auto instrument plugin', async () => { @@ -197,22 +205,30 @@ describe('sentrySvelteKit()', () => { }); describe('generateVitePluginOptions', () => { - it('should return null if no relevant options are provided', () => { + it('returns null if no relevant options are provided', () => { const options: SentrySvelteKitPluginOptions = {}; const result = generateVitePluginOptions(options); expect(result).toBeNull(); }); - it('should use default `debug` value if only default options are provided', () => { + it('uses default `debug` value if only default options are provided', () => { + const originalEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; // Ensure we're not in development mode + const options: SentrySvelteKitPluginOptions = { autoUploadSourceMaps: true, autoInstrument: true, debug: false }; const expected: CustomSentryVitePluginOptions = { debug: false, }; const result = generateVitePluginOptions(options); expect(result).toEqual(expected); + + process.env.NODE_ENV = originalEnv; }); - it('should apply user-defined sourceMapsUploadOptions', () => { + it('applies user-defined sourceMapsUploadOptions', () => { + const originalEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; // Ensure we're not in development mode + const options: SentrySvelteKitPluginOptions = { autoUploadSourceMaps: true, sourceMapsUploadOptions: { @@ -234,9 +250,14 @@ describe('generateVitePluginOptions', () => { }; const result = generateVitePluginOptions(options); expect(result).toEqual(expected); + + process.env.NODE_ENV = originalEnv; }); - it('should override options with unstable_sentryVitePluginOptions', () => { + it('overrides options with unstable_sentryVitePluginOptions', () => { + const originalEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; // Ensure we're not in development mode + const options: SentrySvelteKitPluginOptions = { autoUploadSourceMaps: true, sourceMapsUploadOptions: { @@ -264,9 +285,14 @@ describe('generateVitePluginOptions', () => { }; const result = generateVitePluginOptions(options); expect(result).toEqual(expected); + + process.env.NODE_ENV = originalEnv; }); - it('should merge release options correctly', () => { + it('merges release options correctly', () => { + const originalEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; // Ensure we're not in development mode + const options: SentrySvelteKitPluginOptions = { autoUploadSourceMaps: true, sourceMapsUploadOptions: { @@ -293,9 +319,14 @@ describe('generateVitePluginOptions', () => { }; const result = generateVitePluginOptions(options); expect(result).toEqual(expected); + + process.env.NODE_ENV = originalEnv; }); - it('should handle adapter and debug options correctly', () => { + it('handles adapter and debug options correctly', () => { + const originalEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; // Ensure we're not in development mode + const options: SentrySvelteKitPluginOptions = { autoUploadSourceMaps: true, adapter: 'vercel', @@ -315,9 +346,14 @@ describe('generateVitePluginOptions', () => { }; const result = generateVitePluginOptions(options); expect(result).toEqual(expected); + + process.env.NODE_ENV = originalEnv; }); - it('should apply bundleSizeOptimizations AND sourceMapsUploadOptions when both are set', () => { + it('applies bundleSizeOptimizations AND sourceMapsUploadOptions when both are set', () => { + const originalEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; // Ensure we're not in development mode + const options: SentrySvelteKitPluginOptions = { bundleSizeOptimizations: { excludeTracing: true, @@ -349,5 +385,7 @@ describe('generateVitePluginOptions', () => { }; const result = generateVitePluginOptions(options); expect(result).toEqual(expected); + + process.env.NODE_ENV = originalEnv; }); }); diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index 97d223dc309b..45d0b74ad6ff 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -52,12 +52,15 @@ beforeEach(() => { }); async function getSentryViteSubPlugin(name: string): Promise { - const plugins = await makeCustomSentryVitePlugins({ - authToken: 'token', - org: 'org', - project: 'project', - adapter: 'other', - }); + const plugins = await makeCustomSentryVitePlugins( + { + authToken: 'token', + org: 'org', + project: 'project', + adapter: 'other', + }, + { kit: {} }, + ); return plugins.find(plugin => plugin.name === name); } @@ -79,8 +82,8 @@ describe('makeCustomSentryVitePlugins()', () => { expect(plugin?.apply).toEqual('build'); expect(plugin?.enforce).toEqual('post'); - expect(plugin?.resolveId).toBeInstanceOf(Function); - expect(plugin?.transform).toBeInstanceOf(Function); + expect(plugin?.resolveId).toBeUndefined(); + expect(plugin?.transform).toBeUndefined(); expect(plugin?.configResolved).toBeInstanceOf(Function); @@ -178,18 +181,10 @@ describe('makeCustomSentryVitePlugins()', () => { }); }); - describe('Custom debug id source maps plugin plugin', () => { - it('injects the output dir into the server hooks file', async () => { - const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin'); - // @ts-expect-error this function exists! - const transformOutput = await plugin.transform('foo', '/src/hooks.server.ts'); - const transformedCode = transformOutput.code; - const transformedSourcemap = transformOutput.map; - const expectedTransformedCode = 'foo\n; import "\0sentry-inject-global-values-file";\n'; - expect(transformedCode).toEqual(expectedTransformedCode); - expect(transformedSourcemap).toBeDefined(); - }); + // Note: The global values injection plugin tests are now in a separate test file + // since the plugin was moved to injectGlobalValues.ts + describe('Custom debug id source maps plugin plugin', () => { it('uploads source maps during the SSR build', async () => { const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin'); // @ts-expect-error this function exists! @@ -423,12 +418,15 @@ describe('deleteFilesAfterUpload', () => { }; }); - const plugins = await makeCustomSentryVitePlugins({ - authToken: 'token', - org: 'org', - project: 'project', - adapter: 'other', - }); + const plugins = await makeCustomSentryVitePlugins( + { + authToken: 'token', + org: 'org', + project: 'project', + adapter: 'other', + }, + { kit: {} }, + ); // @ts-expect-error this function exists! const mergedOptions = sentryVitePlugin.mock.calls[0][0]; @@ -498,15 +496,18 @@ describe('deleteFilesAfterUpload', () => { }; }); - const plugins = await makeCustomSentryVitePlugins({ - authToken: 'token', - org: 'org', - project: 'project', - adapter: 'other', - sourcemaps: { - filesToDeleteAfterUpload, + const plugins = await makeCustomSentryVitePlugins( + { + authToken: 'token', + org: 'org', + project: 'project', + adapter: 'other', + sourcemaps: { + filesToDeleteAfterUpload, + }, }, - }); + { kit: {} }, + ); // @ts-expect-error this function exists! const mergedOptions = sentryVitePlugin.mock.calls[0][0]; From 72cc4a9dd7edc7866866699e95efee36ee4ef1e6 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 26 Aug 2025 11:30:59 +0200 Subject: [PATCH 08/16] don't auto-wrap load functions if kit-tracing is enabled --- packages/sveltekit/src/vite/autoInstrument.ts | 21 ++++ .../sveltekit/src/vite/sentryVitePlugins.ts | 8 +- .../test/vite/autoInstrument.test.ts | 111 +++++++++++++++++- .../test/vite/sentrySvelteKitPlugins.test.ts | 1 + 4 files changed, 136 insertions(+), 5 deletions(-) diff --git a/packages/sveltekit/src/vite/autoInstrument.ts b/packages/sveltekit/src/vite/autoInstrument.ts index 63f4888257de..58862e452ddc 100644 --- a/packages/sveltekit/src/vite/autoInstrument.ts +++ b/packages/sveltekit/src/vite/autoInstrument.ts @@ -27,6 +27,7 @@ export type AutoInstrumentSelection = { type AutoInstrumentPluginOptions = AutoInstrumentSelection & { debug: boolean; + onlyInstrumentClient: boolean; }; /** @@ -41,12 +42,26 @@ type AutoInstrumentPluginOptions = AutoInstrumentSelection & { export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptions): Plugin { const { load: wrapLoadEnabled, serverLoad: wrapServerLoadEnabled, debug } = options; + let isServerBuild: boolean | undefined = undefined; + return { name: 'sentry-auto-instrumentation', // This plugin needs to run as early as possible, before the SvelteKit plugin virtualizes all paths and ids enforce: 'pre', + configResolved: config => { + // The SvelteKit plugins trigger additional builds within the main (SSR) build. + // We just need a mechanism to upload source maps only once. + // `config.build.ssr` is `true` for that first build and `false` in the other ones. + // Hence we can use it as a switch to upload source maps only once in main build. + isServerBuild = !!config.build.ssr; + }, + async load(id) { + if (options.onlyInstrumentClient && isServerBuild) { + return null; + } + const applyUniversalLoadWrapper = wrapLoadEnabled && /^\+(page|layout)\.(js|ts|mjs|mts)$/.test(path.basename(id)) && @@ -58,6 +73,12 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio return getWrapperCode('wrapLoadWithSentry', `${id}${WRAPPED_MODULE_SUFFIX}`); } + if (options.onlyInstrumentClient) { + // Now that we've checked universal files, we can early return and avoid further + // regexp checks below for server-only files, in case `onlyInstrumentClient` is `true`. + return null; + } + const applyServerLoadWrapper = wrapServerLoadEnabled && /^\+(page|layout)\.server\.(js|ts|mjs|mts)$/.test(path.basename(id)) && diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts index 6b10e157ea1e..628c2ae524e5 100644 --- a/packages/sveltekit/src/vite/sentryVitePlugins.ts +++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts @@ -28,9 +28,13 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} adapter: options.adapter || (await detectAdapter(options.debug)), }; + const svelteConfig = await loadSvelteConfig(); + const sentryPlugins: Plugin[] = []; if (mergedOptions.autoInstrument) { + const kitTracingEnabled = !!svelteConfig.kit?.experimental?.tracing?.server; + const pluginOptions: AutoInstrumentSelection = { load: true, serverLoad: true, @@ -41,14 +45,14 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} makeAutoInstrumentationPlugin({ ...pluginOptions, debug: options.debug || false, + // if kit-internal tracing is enabled, we only want to instrument client-side code. + onlyInstrumentClient: kitTracingEnabled, }), ); } const sentryVitePluginsOptions = generateVitePluginOptions(mergedOptions); - const svelteConfig = await loadSvelteConfig(); - if (mergedOptions.injectGlobalValues) { sentryPlugins.push(await makeGlobalValuesInjectionPlugin(svelteConfig, mergedOptions)); } diff --git a/packages/sveltekit/test/vite/autoInstrument.test.ts b/packages/sveltekit/test/vite/autoInstrument.test.ts index 364680e31bf3..b58f0280cdea 100644 --- a/packages/sveltekit/test/vite/autoInstrument.test.ts +++ b/packages/sveltekit/test/vite/autoInstrument.test.ts @@ -41,7 +41,12 @@ describe('makeAutoInstrumentationPlugin()', () => { }); it('returns the auto instrumentation plugin', async () => { - const plugin = makeAutoInstrumentationPlugin({ debug: true, load: true, serverLoad: true }); + const plugin = makeAutoInstrumentationPlugin({ + debug: true, + load: true, + serverLoad: true, + onlyInstrumentClient: false, + }); expect(plugin.name).toEqual('sentry-auto-instrumentation'); expect(plugin.enforce).toEqual('pre'); expect(plugin.load).toEqual(expect.any(Function)); @@ -58,7 +63,12 @@ describe('makeAutoInstrumentationPlugin()', () => { 'path/to/+layout.mjs', ])('transform %s files', (path: string) => { it('wraps universal load if `load` option is `true`', async () => { - const plugin = makeAutoInstrumentationPlugin({ debug: false, load: true, serverLoad: true }); + const plugin = makeAutoInstrumentationPlugin({ + debug: false, + load: true, + serverLoad: true, + onlyInstrumentClient: false, + }); // @ts-expect-error this exists const loadResult = await plugin.load(path); expect(loadResult).toEqual( @@ -74,6 +84,7 @@ describe('makeAutoInstrumentationPlugin()', () => { debug: false, load: false, serverLoad: false, + onlyInstrumentClient: false, }); // @ts-expect-error this exists const loadResult = await plugin.load(path); @@ -92,7 +103,12 @@ describe('makeAutoInstrumentationPlugin()', () => { 'path/to/+layout.server.mjs', ])('transform %s files', (path: string) => { it('wraps universal load if `load` option is `true`', async () => { - const plugin = makeAutoInstrumentationPlugin({ debug: false, load: false, serverLoad: true }); + const plugin = makeAutoInstrumentationPlugin({ + debug: false, + load: false, + serverLoad: true, + onlyInstrumentClient: false, + }); // @ts-expect-error this exists const loadResult = await plugin.load(path); expect(loadResult).toEqual( @@ -108,12 +124,101 @@ describe('makeAutoInstrumentationPlugin()', () => { debug: false, load: false, serverLoad: false, + onlyInstrumentClient: false, }); // @ts-expect-error this exists const loadResult = await plugin.load(path); expect(loadResult).toEqual(null); }); }); + + describe('when `onlyInstrumentClient` is `true`', () => { + it.each([ + // server-only files + 'path/to/+page.server.ts', + 'path/to/+layout.server.js', + // universal files + 'path/to/+page.mts', + 'path/to/+layout.mjs', + ])("doesn't wrap code in SSR build in %s", async (path: string) => { + const plugin = makeAutoInstrumentationPlugin({ + debug: false, + load: true, + serverLoad: true, + onlyInstrumentClient: true, + }); + + // @ts-expect-error this exists and is callable + plugin.configResolved({ + build: { + ssr: true, + }, + }); + + // @ts-expect-error this exists + const loadResult = await plugin.load(path); + + expect(loadResult).toEqual(null); + }); + + it.each(['path/to/+page.ts', 'path/to/+layout.js'])( + 'wraps client-side code in universal files in %s', + async (path: string) => { + const plugin = makeAutoInstrumentationPlugin({ + debug: false, + load: true, + serverLoad: true, + onlyInstrumentClient: true, + }); + + // @ts-expect-error this exists and is callable + plugin.configResolved({ + build: { + ssr: false, + }, + }); + + // @ts-expect-error this exists and is callable + const loadResult = await plugin.load(path); + + expect(loadResult).toBe( + 'import { wrapLoadWithSentry } from "@sentry/sveltekit";' + + `import * as userModule from "${path}?sentry-auto-wrap";` + + 'export const load = userModule.load ? wrapLoadWithSentry(userModule.load) : undefined;' + + `export * from "${path}?sentry-auto-wrap";`, + ); + }, + ); + + /** + * This is a bit of a constructed case because in a client build, server-only files + * shouldn't even be passed into the load hook. But just to be extra careful, let's + * make sure we don't wrap server-only files in a client build. + */ + it.each(['path/to/+page.server.ts', 'path/to/+layout.server.js'])( + "doesn't wrap client-side code in server-only files in %s", + async (path: string) => { + const plugin = makeAutoInstrumentationPlugin({ + debug: false, + load: true, + serverLoad: true, + onlyInstrumentClient: true, + }); + + // @ts-expect-error this exists and is callable + plugin.configResolved({ + build: { + ssr: false, + }, + }); + + // @ts-expect-error this exists and is callable + const loadResult = await plugin.load(path); + + expect(loadResult).toBe(null); + }, + ); + }); }); describe('canWrapLoad', () => { diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts index 68bb1a8b1bfa..020206b4790b 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -200,6 +200,7 @@ describe('sentrySvelteKit()', () => { debug: true, load: true, serverLoad: false, + onlyInstrumentClient: false, }); }); }); From 529d718ab0a15657b677d3b44b038954877ae7b3 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 26 Aug 2025 18:28:46 +0200 Subject: [PATCH 09/16] add e2e test app with kit-tracing enabled --- .../sveltekit-2-kit-tracing/.gitignore | 15 + .../sveltekit-2-kit-tracing/.npmrc | 2 + .../sveltekit-2-kit-tracing/README.md | 41 ++ .../sveltekit-2-kit-tracing/package.json | 37 ++ .../playwright.config.mjs | 8 + .../sveltekit-2-kit-tracing/src/app.d.ts | 13 + .../sveltekit-2-kit-tracing/src/app.html | 12 + .../src/hooks.client.ts | 23 + .../src/hooks.server.ts | 12 + .../src/instrumentation.server.ts | 11 + .../src/routes/+layout.svelte | 15 + .../src/routes/+page.svelte | 47 ++ .../src/routes/api/users/+server.ts | 3 + .../src/routes/building/+page.server.ts | 5 + .../src/routes/building/+page.svelte | 21 + .../src/routes/building/+page.ts | 5 + .../src/routes/client-error/+page.svelte | 9 + .../src/routes/components/+page.svelte | 15 + .../src/routes/components/Component1.svelte | 10 + .../src/routes/components/Component2.svelte | 9 + .../src/routes/components/Component3.svelte | 6 + .../src/routes/form-action/+page.server.ts | 7 + .../src/routes/form-action/+page.svelte | 15 + .../src/routes/nav1/+page.svelte | 1 + .../src/routes/nav2/+page.svelte | 1 + .../src/routes/redirect1/+page.ts | 5 + .../src/routes/redirect2/+page.ts | 5 + .../routes/server-load-error/+page.server.ts | 6 + .../src/routes/server-load-error/+page.svelte | 9 + .../routes/server-load-fetch/+page.server.ts | 5 + .../src/routes/server-load-fetch/+page.svelte | 8 + .../routes/server-route-error/+page.svelte | 9 + .../src/routes/server-route-error/+page.ts | 7 + .../src/routes/server-route-error/+server.ts | 6 + .../routes/universal-load-error/+page.svelte | 17 + .../src/routes/universal-load-error/+page.ts | 8 + .../routes/universal-load-fetch/+page.svelte | 14 + .../src/routes/universal-load-fetch/+page.ts | 5 + .../src/routes/users/+page.server.ts | 5 + .../src/routes/users/+page.svelte | 10 + .../src/routes/users/[id]/+page.server.ts | 5 + .../src/routes/users/[id]/+page.svelte | 14 + .../start-event-proxy.mjs | 6 + .../static/favicon.png | Bin 0 -> 1571 bytes .../sveltekit-2-kit-tracing/svelte.config.js | 32 ++ .../tests/errors.client.test.ts | 56 +++ .../tests/errors.server.test.ts | 91 ++++ .../sveltekit-2-kit-tracing/tests/test.json | 49 ++ .../tests/tracing.client.test.ts | 115 +++++ .../tests/tracing.server.test.ts | 190 ++++++++ .../tests/tracing.test.ts | 424 ++++++++++++++++++ .../sveltekit-2-kit-tracing/tests/utils.ts | 49 ++ .../sveltekit-2-kit-tracing/tsconfig.json | 19 + .../sveltekit-2-kit-tracing/vite.config.ts | 12 + 54 files changed, 1524 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/README.md create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.html create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.client.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+layout.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/api/users/+server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/client-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component1.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component2.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component3.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav1/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav2/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect1/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect2/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-error/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-error/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-error/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-fetch/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-fetch/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/[id]/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/[id]/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/static/favicon.png create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/svelte.config.js create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/test.json create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/utils.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/vite.config.ts diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.gitignore b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.gitignore new file mode 100644 index 000000000000..87c54ab857fc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +#temporarily excluding my remote function routes. To be removed with the next PR: +./src/routes/remote-functions/data.remote.ts +./src/routes/remote-functions/+page.svelte +./tests/tracing.remote-functions.ts diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/README.md b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/README.md new file mode 100644 index 000000000000..684cabccfe02 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/README.md @@ -0,0 +1,41 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by +[`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npm create svelte@latest + +# create a new project in my-app +npm create svelte@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a +development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target +> environment. diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json new file mode 100644 index 000000000000..03e5441733da --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json @@ -0,0 +1,37 @@ +{ + "name": "sveltekit-2-kit-tracing", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "proxy": "node start-event-proxy.mjs", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:prod" + }, + "dependencies": { + "@sentry/sveltekit": "latest || *", + "@spotlightjs/spotlight": "2.0.0-alpha.1" + }, + "devDependencies": { + "@playwright/test": "~1.53.2", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sveltejs/adapter-node": "^5.3.1", + "@sveltejs/kit": "^2.31.0", + "@sveltejs/vite-plugin-svelte": "^6.1.3", + "svelte": "^5.38.3", + "svelte-check": "^4.3.1", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^7.1.3" + }, + "type": "module", + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/playwright.config.mjs new file mode 100644 index 000000000000..6ae8142df247 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: 'ORIGIN=http://localhost:3030 node ./build/index.js', + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.d.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.d.ts new file mode 100644 index 000000000000..ede601ab93e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.html new file mode 100644 index 000000000000..77a5ff52c923 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.client.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.client.ts new file mode 100644 index 000000000000..91592e7ab932 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.client.ts @@ -0,0 +1,23 @@ +import { env } from '$env/dynamic/public'; +import * as Sentry from '@sentry/sveltekit'; +import * as Spotlight from '@spotlightjs/spotlight'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: env.PUBLIC_E2E_TEST_DSN, + debug: !!env.PUBLIC_DEBUG, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, +}); + +const myErrorHandler = ({ error, event }: any) => { + console.error('An error occurred on the client side:', error, event); +}; + +export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); + +if (import.meta.env.DEV) { + Spotlight.init({ + injectImmediately: true, + }); +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.server.ts new file mode 100644 index 000000000000..de32b0f9901b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.server.ts @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/sveltekit'; +import { setupSidecar } from '@spotlightjs/spotlight/sidecar'; +import { sequence } from '@sveltejs/kit/hooks'; + +// not logging anything to console to avoid noise in the test output +export const handleError = Sentry.handleErrorWithSentry(() => {}); + +export const handle = sequence(Sentry.sentryHandle()); + +if (import.meta.env.DEV) { + setupSidecar(); +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts new file mode 100644 index 000000000000..136b51a44dee --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/sveltekit'; +import { E2E_TEST_DSN } from '$env/static/private'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: E2E_TEST_DSN, + debug: !!process.env.DEBUG, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + spotlight: import.meta.env.DEV, +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+layout.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+layout.svelte new file mode 100644 index 000000000000..d1fadd2ea5a3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+layout.svelte @@ -0,0 +1,15 @@ + + +

Sveltekit E2E Test app

+
+ +
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+page.svelte new file mode 100644 index 000000000000..95b18ea87844 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+page.svelte @@ -0,0 +1,47 @@ +

Welcome to SvelteKit 2 with Svelte 5!

+

Visit kit.svelte.dev to read the documentation

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/api/users/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/api/users/+server.ts new file mode 100644 index 000000000000..d0e4371c594b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/api/users/+server.ts @@ -0,0 +1,3 @@ +export const GET = () => { + return new Response(JSON.stringify({ users: ['alice', 'bob', 'carol'] })); +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.server.ts new file mode 100644 index 000000000000..b07376ba97c9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.server.ts @@ -0,0 +1,5 @@ +import type { PageServerLoad } from './$types'; + +export const load = (async _event => { + return { name: 'building (server)' }; +}) satisfies PageServerLoad; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.svelte new file mode 100644 index 000000000000..b27edb70053d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.svelte @@ -0,0 +1,21 @@ + + +

Check Build

+ +

+ This route only exists to check that Typescript definitions + and auto instrumentation are working when the project is built. +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.ts new file mode 100644 index 000000000000..049acdc1fafa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.ts @@ -0,0 +1,5 @@ +import type { PageLoad } from './$types'; + +export const load = (async _event => { + return { name: 'building' }; +}) satisfies PageLoad; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/client-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/client-error/+page.svelte new file mode 100644 index 000000000000..ba6b464e9324 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/client-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Client error

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/+page.svelte new file mode 100644 index 000000000000..eff3fa3f2e8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/+page.svelte @@ -0,0 +1,15 @@ + +

Demonstrating Component Tracking

+ + + + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component1.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component1.svelte new file mode 100644 index 000000000000..a675711e4b68 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component1.svelte @@ -0,0 +1,10 @@ + +

Howdy, I'm component 1

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component2.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component2.svelte new file mode 100644 index 000000000000..2b2f38308077 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component2.svelte @@ -0,0 +1,9 @@ + +

Howdy, I'm component 2

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component3.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component3.svelte new file mode 100644 index 000000000000..9b4e028f78e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component3.svelte @@ -0,0 +1,6 @@ + + +

Howdy, I'm component 3

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.server.ts new file mode 100644 index 000000000000..5efea8345851 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.server.ts @@ -0,0 +1,7 @@ +export const actions = { + default: async ({ request }) => { + const formData = await request.formData(); + const name = formData.get('name'); + return { name }; + }, +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.svelte new file mode 100644 index 000000000000..25b71a1c9fc7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.svelte @@ -0,0 +1,15 @@ + + +
+ + + +
+ +{#if form?.name} +

Hello {form.name}

+{/if} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav1/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav1/+page.svelte new file mode 100644 index 000000000000..31abffc512a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav1/+page.svelte @@ -0,0 +1 @@ +

Navigation 1

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav2/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav2/+page.svelte new file mode 100644 index 000000000000..20b44bb32da9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav2/+page.svelte @@ -0,0 +1 @@ +

Navigation 2

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect1/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect1/+page.ts new file mode 100644 index 000000000000..3f462bf810fd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect1/+page.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export const load = async () => { + redirect(301, '/redirect2'); +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect2/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect2/+page.ts new file mode 100644 index 000000000000..99a810761d18 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect2/+page.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export const load = async () => { + redirect(301, '/users/789'); +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-error/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-error/+page.server.ts new file mode 100644 index 000000000000..17dd53fb5bbb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-error/+page.server.ts @@ -0,0 +1,6 @@ +export const load = async () => { + throw new Error('Server Load Error'); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-error/+page.svelte new file mode 100644 index 000000000000..3a0942971d06 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Server load error

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.server.ts new file mode 100644 index 000000000000..709e52bcf351 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.server.ts @@ -0,0 +1,5 @@ +export const load = async ({ fetch }) => { + const res = await fetch('/api/users'); + const data = await res.json(); + return { data }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.svelte new file mode 100644 index 000000000000..f7f814d31b4d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.svelte @@ -0,0 +1,8 @@ + + +
+

Server Load Fetch

+

{JSON.stringify(data, null, 2)}

+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+page.svelte new file mode 100644 index 000000000000..3d682e7e3462 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Server Route error

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+page.ts new file mode 100644 index 000000000000..298240827714 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+page.ts @@ -0,0 +1,7 @@ +export const load = async ({ fetch }) => { + const res = await fetch('/server-route-error'); + const data = await res.json(); + return { + msg: data, + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+server.ts new file mode 100644 index 000000000000..f1a4b94b7706 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+server.ts @@ -0,0 +1,6 @@ +export const GET = async () => { + throw new Error('Server Route Error'); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-error/+page.svelte new file mode 100644 index 000000000000..dc2d311a0ece --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-error/+page.svelte @@ -0,0 +1,17 @@ + + +

Universal load error

+ +

+ To trigger from client: Load on another route, then navigate to this route. +

+ +

+ To trigger from server: Load on this route +

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-error/+page.ts new file mode 100644 index 000000000000..3d72bf4a890f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-error/+page.ts @@ -0,0 +1,8 @@ +import { browser } from '$app/environment'; + +export const load = async () => { + throw new Error(`Universal Load Error (${browser ? 'browser' : 'server'})`); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-fetch/+page.svelte new file mode 100644 index 000000000000..563c51e8c850 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-fetch/+page.svelte @@ -0,0 +1,14 @@ + + +

Fetching in universal load

+ +

Here's a list of a few users:

+ +
    + {#each data.users as user} +
  • {user}
  • + {/each} +
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-fetch/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-fetch/+page.ts new file mode 100644 index 000000000000..63c1ee68e1cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/universal-load-fetch/+page.ts @@ -0,0 +1,5 @@ +export const load = async ({ fetch }) => { + const usersRes = await fetch('/api/users'); + const data = await usersRes.json(); + return { users: data.users }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.server.ts new file mode 100644 index 000000000000..a34c5450f682 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.server.ts @@ -0,0 +1,5 @@ +export const load = async () => { + return { + msg: 'Hi everyone!', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.svelte new file mode 100644 index 000000000000..aa804a4518fa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.svelte @@ -0,0 +1,10 @@ + +

+ All Users: +

+ +

+ message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/[id]/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/[id]/+page.server.ts new file mode 100644 index 000000000000..9388f3927018 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/[id]/+page.server.ts @@ -0,0 +1,5 @@ +export const load = async ({ params }) => { + return { + msg: `This is a special message for user ${params.id}`, + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/[id]/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/[id]/+page.svelte new file mode 100644 index 000000000000..d348a8c57dad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/[id]/+page.svelte @@ -0,0 +1,14 @@ + + +

Route with dynamic params

+ +

+ User id: {$page.params.id} +

+ +

+ Secret message for user: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/start-event-proxy.mjs new file mode 100644 index 000000000000..eea0add65374 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'sveltekit-2-kit-tracing', +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/static/favicon.png b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH { + test('captures error thrown on click', async ({ page }) => { + await waitForInitialPageload(page, { route: '/client-error' }); + + const errorEventPromise = waitForError('sveltekit-2-kit-tracing', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Click Error'; + }); + + await page.getByText('Throw error').click(); + + await expect(errorEventPromise).resolves.toBeDefined(); + + const errorEvent = await errorEventPromise; + + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: expect.stringContaining('HTMLButtonElement'), + lineno: 1, + in_app: true, + }), + ); + + expect(errorEvent.transaction).toEqual('/client-error'); + }); + + test('captures universal load error', async ({ page }) => { + await waitForInitialPageload(page); + await page.reload(); + + const errorEventPromise = waitForError('sveltekit-2-kit-tracing', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (browser)'; + }); + + // navigating triggers the error on the client + await page.getByText('Universal Load error').click(); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + const lastFrame = errorEventFrames?.[errorEventFrames?.length - 1]; + expect(lastFrame).toEqual( + expect.objectContaining({ + lineno: 1, + in_app: true, + }), + ); + + expect(errorEvent.transaction).toEqual('/universal-load-error'); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts new file mode 100644 index 000000000000..2d799b527198 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts @@ -0,0 +1,91 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', () => { + test('captures universal load error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit-2-kit-tracing', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (server)'; + }); + + await page.goto('/universal-load-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: 'load', + in_app: true, + }), + ); + + expect(errorEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + // SvelteKit's node adapter defaults to https in the protocol even if served on http + url: 'http://localhost:3030/universal-load-error', + }); + }); + + test('captures server load error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit-2-kit-tracing', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Server Load Error'; + }); + + await page.goto('/server-load-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: 'load', + in_app: true, + }), + ); + + expect(errorEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/server-load-error', + }); + }); + + test('captures server route (GET) error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit-2-kit-tracing', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Server Route Error'; + }); + + await page.goto('/server-route-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + filename: expect.stringMatching(/app:\/\/\/chunks\/_server.ts-.+.js/), + function: 'GET', + in_app: true, + }), + ); + + expect(errorEvent.transaction).toEqual('GET /server-route-error'); + + expect(errorEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/server-route-error', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/test.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/test.json new file mode 100644 index 000000000000..d676d9de3189 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/test.json @@ -0,0 +1,49 @@ +[ + { + "data": { "sentry.op": "function.sveltekit.handle", "sentry.origin": "auto.function.sveltekit.handle" }, + "description": "sveltekit.handle.sequenced.sentryRequestHandler", + "op": "function.sveltekit.handle", + "origin": "auto.function.sveltekit.handle", + "parent_span_id": "db7a47050d2108af", + "span_id": "470560c2ddb69363", + "start_timestamp": 1756220521.186, + "status": "ok", + "timestamp": 1756220521.1872811, + "trace_id": "f46904c28f8d1f0a8fc085e876af06ed" + }, + { + "data": { + "http.response.body.size": "67", + "http.response.status_code": 200, + "http.route": "/form-action", + "sentry.op": "function.sveltekit.resolve", + "sentry.origin": "auto.http.sveltekit" + }, + "description": "sveltekit.resolve", + "op": "function.sveltekit.resolve", + "origin": "auto.http.sveltekit", + "parent_span_id": "470560c2ddb69363", + "span_id": "b39cdd0c654300fc", + "start_timestamp": 1756220521.186, + "status": "ok", + "timestamp": 1756220521.1871505, + "trace_id": "f46904c28f8d1f0a8fc085e876af06ed" + }, + { + "data": { + "http.route": "/form-action", + "sentry.op": "function.sveltekit.form_action", + "sentry.origin": "auto.function.sveltekit.action", + "sveltekit.form_action.name": "default" + }, + "description": "sveltekit.form_action", + "op": "function.sveltekit.form_action", + "origin": "auto.function.sveltekit.action", + "parent_span_id": "b39cdd0c654300fc", + "span_id": "9dccc58b455187cb", + "start_timestamp": 1756220521.186, + "status": "ok", + "timestamp": 1756220521.1866386, + "trace_id": "f46904c28f8d1f0a8fc085e876af06ed" + } +] diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.client.test.ts new file mode 100644 index 000000000000..e9f4a9c1425f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.client.test.ts @@ -0,0 +1,115 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForInitialPageload } from './utils'; + +test.describe('client-specific performance events', () => { + test('multiple navigations have distinct traces', async ({ page }) => { + const navigationTxn1EventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === '/nav1' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + const navigationTxn2EventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === '/' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + const navigationTxn3EventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === '/nav2' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + await waitForInitialPageload(page); + + await page.getByText('Nav 1').click(); + const navigationTxn1Event = await navigationTxn1EventPromise; + + await page.goBack(); + const navigationTxn2Event = await navigationTxn2EventPromise; + + await page.getByText('Nav 2').click(); + const navigationTxn3Event = await navigationTxn3EventPromise; + + expect(navigationTxn1Event).toMatchObject({ + transaction: '/nav1', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }); + + expect(navigationTxn2Event).toMatchObject({ + transaction: '/', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }); + + expect(navigationTxn3Event).toMatchObject({ + transaction: '/nav2', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + }, + }); + + // traces should NOT be connected + expect(navigationTxn1Event.contexts?.trace?.trace_id).not.toBe(navigationTxn2Event.contexts?.trace?.trace_id); + expect(navigationTxn2Event.contexts?.trace?.trace_id).not.toBe(navigationTxn3Event.contexts?.trace?.trace_id); + expect(navigationTxn1Event.contexts?.trace?.trace_id).not.toBe(navigationTxn3Event.contexts?.trace?.trace_id); + }); + + test('records manually added component tracking spans', async ({ page }) => { + const componentTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === '/components'; + }); + + await waitForInitialPageload(page); + + await page.getByText('Component Tracking').click(); + + const componentTxnEvent = await componentTxnEventPromise; + + expect(componentTxnEvent.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' }, + description: '', + op: 'ui.svelte.init', + origin: 'auto.ui.svelte', + }), + expect.objectContaining({ + data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' }, + description: '', + op: 'ui.svelte.init', + origin: 'auto.ui.svelte', + }), + expect.objectContaining({ + data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' }, + description: '', + op: 'ui.svelte.init', + origin: 'auto.ui.svelte', + }), + expect.objectContaining({ + data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' }, + description: '', + op: 'ui.svelte.init', + origin: 'auto.ui.svelte', + }), + ]), + ); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts new file mode 100644 index 000000000000..c6de70d0e6a1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts @@ -0,0 +1,190 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/sveltekit'; + +test('server pageload request span has nested request span for sub request', async ({ page }) => { + const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === 'GET /server-load-fetch'; + }); + + await page.goto('/server-load-fetch'); + + const serverTxnEvent = await serverTxnEventPromise; + const spans = serverTxnEvent.spans; + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /server-load-fetch', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + 'http.method': 'GET', + 'http.route': '/server-load-fetch', + 'sveltekit.tracing.original_name': 'sveltekit.handle.root', + }, + }, + }, + }); + + expect(spans).toHaveLength(6); + + expect(spans).toEqual( + expect.arrayContaining([ + // initial resolve span: + expect.objectContaining({ + data: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.sveltekit.resolve', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + 'http.route': '/server-load-fetch', + }), + op: 'function.sveltekit.resolve', + description: 'sveltekit.resolve', + origin: 'auto.http.sveltekit', + status: 'ok', + }), + + // sequenced handler span: + expect.objectContaining({ + data: expect.objectContaining({ + 'sentry.origin': 'auto.function.sveltekit.handle', + 'sentry.op': 'function.sveltekit.handle', + }), + description: 'sveltekit.handle.sequenced.sentryRequestHandler', + op: 'function.sveltekit.handle', + origin: 'auto.function.sveltekit.handle', + status: 'ok', + }), + + // load span where the server load function initiates the sub request: + expect.objectContaining({ + data: expect.objectContaining({ + 'http.route': '/server-load-fetch', + 'sentry.op': 'function.sveltekit.load', + 'sentry.origin': 'auto.function.sveltekit.load', + 'sveltekit.load.environment': 'server', + 'sveltekit.load.node_id': 'src/routes/server-load-fetch/+page.server.ts', + 'sveltekit.load.node_type': '+page.server', + }), + description: 'sveltekit.load', + op: 'function.sveltekit.load', + origin: 'auto.function.sveltekit.load', + status: 'ok', + }), + + // sub request http.server span: + expect.objectContaining({ + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.route': '/api/users', + 'http.url': 'http://localhost:3030/api/users', + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.sveltekit', + 'sentry.source': 'route', + 'sveltekit.is_data_request': false, + 'sveltekit.is_sub_request': true, + 'sveltekit.tracing.original_name': 'sveltekit.handle.root', + url: 'http://localhost:3030/api/users', + }), + description: 'GET /api/users', + op: 'http.server', + origin: 'auto.http.sveltekit', + status: 'ok', + }), + + // sub requestsequenced handler span: + expect.objectContaining({ + data: expect.objectContaining({ + 'sentry.origin': 'auto.function.sveltekit.handle', + 'sentry.op': 'function.sveltekit.handle', + }), + description: 'sveltekit.handle.sequenced.sentryRequestHandler', + op: 'function.sveltekit.handle', + origin: 'auto.function.sveltekit.handle', + status: 'ok', + }), + + // sub request resolve span: + expect.objectContaining({ + data: expect.objectContaining({ + 'http.route': '/api/users', + 'sentry.op': 'function.sveltekit.resolve', + 'sentry.origin': 'auto.http.sveltekit', + }), + description: 'sveltekit.resolve', + op: 'function.sveltekit.resolve', + origin: 'auto.http.sveltekit', + status: 'ok', + }), + ]), + ); + + expect(serverTxnEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + accept: expect.any(String), + 'user-agent': expect.any(String), + }), + method: 'GET', + url: 'http://localhost:3030/server-load-fetch', + }); +}); + +test('server trace includes form action span', async ({ page }) => { + const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === 'POST /form-action'; + }); + + await page.goto('/form-action'); + + await page.locator('#inputName').fill('H4cktor'); + await page.locator('#buttonSubmit').click(); + + const serverTxnEvent = await serverTxnEventPromise; + + expect(serverTxnEvent).toMatchObject({ + transaction: 'POST /form-action', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + expect(serverTxnEvent.spans).toHaveLength(3); + + expect(serverTxnEvent.spans).toEqual( + expect.arrayContaining([ + // sequenced handler span + expect.objectContaining({ + description: 'sveltekit.handle.sequenced.sentryRequestHandler', + op: 'function.sveltekit.handle', + origin: 'auto.function.sveltekit.handle', + }), + + // resolve span + expect.objectContaining({ + description: 'sveltekit.resolve', + op: 'function.sveltekit.resolve', + origin: 'auto.http.sveltekit', + }), + + // form action span + expect.objectContaining({ + description: 'sveltekit.form_action', + op: 'function.sveltekit.form_action', + origin: 'auto.function.sveltekit.action', + data: expect.objectContaining({ + 'sveltekit.form_action.name': 'default', + }), + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts new file mode 100644 index 000000000000..8f9f1b34bf47 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts @@ -0,0 +1,424 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { waitForInitialPageload } from './utils'; + +test('capture a distributed pageload trace', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === '/users/[id]'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === 'GET /users/[id]'; + }); + + const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([ + page.goto('/users/123xyz'), + clientTxnEventPromise, + serverTxnEventPromise, + expect(page.getByText('User id: 123xyz')).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/users/[id]', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /users/[id]', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + expect(clientTxnEvent.spans?.length).toBeGreaterThan(5); + + const serverKitResolveSpan = serverTxnEvent.spans?.find(s => s.description === 'sveltekit.resolve'); + expect(serverKitResolveSpan).toMatchObject({ + description: 'sveltekit.resolve', + op: 'function.sveltekit.resolve', + origin: 'auto.http.sveltekit', + status: 'ok', + }); + + // connected trace + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + + // Sveltekit resolve span is the parent span of the client span + expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverKitResolveSpan?.span_id); +}); + +test('capture a distributed navigation trace', async ({ page }) => { + const clientNavigationTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === '/users' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === 'GET /users'; + }); + + await waitForInitialPageload(page); + + // navigation to page + const clickPromise = page.getByText('Route with Server Load').click(); + + const [clientTxnEvent, serverTxnEvent, _1, _2] = await Promise.all([ + clientNavigationTxnEventPromise, + serverTxnEventPromise, + clickPromise, + expect(page.getByText('Hi everyone')).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/users', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /users', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + // trace is connected + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); +}); + +test('record client-side universal load fetch span and trace', async ({ page }) => { + await waitForInitialPageload(page); + + const clientNavigationTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === '/universal-load-fetch' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + // this transaction should be created because of the fetch call + // it should also be part of the trace + const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === 'GET /api/users'; + }); + + // navigation to page + const clickPromise = page.getByText('Route with fetch in universal load').click(); + + const [clientTxnEvent, serverTxnEvent, _1, _2] = await Promise.all([ + clientNavigationTxnEventPromise, + serverTxnEventPromise, + clickPromise, + expect(page.getByText('alice')).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/universal-load-fetch', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /api/users', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + // trace is connected + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + + const clientFetchSpan = clientTxnEvent.spans?.find(s => s.op === 'http.client'); + + expect(clientFetchSpan).toMatchObject({ + description: expect.stringMatching(/^GET.*\/api\/users/), + op: 'http.client', + origin: 'auto.http.browser', + data: { + url: expect.stringContaining('/api/users'), + type: 'fetch', + 'http.method': 'GET', + 'http.response.status_code': 200, + 'network.protocol.version': '1.1', + 'network.protocol.name': 'http', + 'http.request.redirect_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + }, + }); +}); + +test('captures a navigation transaction directly after pageload', async ({ page }) => { + const clientPageloadTxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.contexts?.trace?.op === 'pageload'; + }); + + const clientNavigationTxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.contexts?.trace?.op === 'navigation'; + }); + + await waitForInitialPageload(page, { route: '/' }); + + const navigationClickPromise = page.locator('#routeWithParamsLink').click(); + + const [pageloadTxnEvent, navigationTxnEvent, _] = await Promise.all([ + clientPageloadTxnPromise, + clientNavigationTxnPromise, + navigationClickPromise, + ]); + + expect(pageloadTxnEvent).toMatchObject({ + transaction: '/', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.sveltekit', + }, + }, + }); + + expect(navigationTxnEvent).toMatchObject({ + transaction: '/users/[id]', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + data: { + 'sentry.sveltekit.navigation.from': '/', + 'sentry.sveltekit.navigation.to': '/users/[id]', + 'sentry.sveltekit.navigation.type': 'link', + }, + }, + }, + }); + + const routingSpans = navigationTxnEvent.spans?.filter(s => s.op === 'ui.sveltekit.routing'); + expect(routingSpans).toHaveLength(1); + + const routingSpan = routingSpans && routingSpans[0]; + expect(routingSpan).toMatchObject({ + op: 'ui.sveltekit.routing', + description: 'SvelteKit Route Change', + data: { + 'sentry.op': 'ui.sveltekit.routing', + 'sentry.origin': 'auto.ui.sveltekit', + 'sentry.sveltekit.navigation.from': '/', + 'sentry.sveltekit.navigation.to': '/users/[id]', + 'sentry.sveltekit.navigation.type': 'link', + }, + }); +}); + +test('captures one navigation transaction per redirect', async ({ page }) => { + const clientNavigationRedirect1TxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/redirect1'; + }); + + const clientNavigationRedirect2TxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/redirect2'; + }); + + const clientNavigationRedirect3TxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/users/[id]'; + }); + + await waitForInitialPageload(page, { route: '/' }); + + const navigationClickPromise = page.locator('#redirectLink').click(); + + const [redirect1TxnEvent, redirect2TxnEvent, redirect3TxnEvent, _] = await Promise.all([ + clientNavigationRedirect1TxnPromise, + clientNavigationRedirect2TxnPromise, + clientNavigationRedirect3TxnPromise, + navigationClickPromise, + ]); + + expect(redirect1TxnEvent).toMatchObject({ + transaction: '/redirect1', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + data: { + 'sentry.origin': 'auto.navigation.sveltekit', + 'sentry.op': 'navigation', + 'sentry.source': 'route', + 'sentry.sveltekit.navigation.type': 'link', + 'sentry.sveltekit.navigation.from': '/', + 'sentry.sveltekit.navigation.to': '/redirect1', + 'sentry.sample_rate': 1, + }, + }, + }, + }); + + const redirect1Spans = redirect1TxnEvent.spans?.filter(s => s.op === 'ui.sveltekit.routing'); + expect(redirect1Spans).toHaveLength(1); + + const redirect1Span = redirect1Spans && redirect1Spans[0]; + expect(redirect1Span).toMatchObject({ + op: 'ui.sveltekit.routing', + description: 'SvelteKit Route Change', + data: { + 'sentry.op': 'ui.sveltekit.routing', + 'sentry.origin': 'auto.ui.sveltekit', + 'sentry.sveltekit.navigation.from': '/', + 'sentry.sveltekit.navigation.to': '/redirect1', + 'sentry.sveltekit.navigation.type': 'link', + }, + }); + + expect(redirect2TxnEvent).toMatchObject({ + transaction: '/redirect2', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + data: { + 'sentry.origin': 'auto.navigation.sveltekit', + 'sentry.op': 'navigation', + 'sentry.source': 'route', + 'sentry.sveltekit.navigation.type': 'goto', + 'sentry.sveltekit.navigation.from': '/', + 'sentry.sveltekit.navigation.to': '/redirect2', + 'sentry.sample_rate': 1, + }, + }, + }, + }); + + const redirect2Spans = redirect2TxnEvent.spans?.filter(s => s.op === 'ui.sveltekit.routing'); + expect(redirect2Spans).toHaveLength(1); + + const redirect2Span = redirect2Spans && redirect2Spans[0]; + expect(redirect2Span).toMatchObject({ + op: 'ui.sveltekit.routing', + description: 'SvelteKit Route Change', + data: { + 'sentry.op': 'ui.sveltekit.routing', + 'sentry.origin': 'auto.ui.sveltekit', + 'sentry.sveltekit.navigation.from': '/', + 'sentry.sveltekit.navigation.to': '/redirect2', + 'sentry.sveltekit.navigation.type': 'goto', + }, + }); + + expect(redirect3TxnEvent).toMatchObject({ + transaction: '/users/[id]', + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + data: { + 'sentry.origin': 'auto.navigation.sveltekit', + 'sentry.op': 'navigation', + 'sentry.source': 'route', + 'sentry.sveltekit.navigation.type': 'goto', + 'sentry.sveltekit.navigation.from': '/', + 'sentry.sveltekit.navigation.to': '/users/[id]', + 'sentry.sample_rate': 1, + }, + }, + }, + }); + + const redirect3Spans = redirect3TxnEvent.spans?.filter(s => s.op === 'ui.sveltekit.routing'); + expect(redirect3Spans).toHaveLength(1); + + const redirect3Span = redirect3Spans && redirect3Spans[0]; + expect(redirect3Span).toMatchObject({ + op: 'ui.sveltekit.routing', + description: 'SvelteKit Route Change', + data: { + 'sentry.op': 'ui.sveltekit.routing', + 'sentry.origin': 'auto.ui.sveltekit', + 'sentry.sveltekit.navigation.from': '/', + 'sentry.sveltekit.navigation.to': '/users/[id]', + 'sentry.sveltekit.navigation.type': 'goto', + }, + }); +}); + +test('captures remote function call spans', async ({ page }) => { + const clientTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === '/remote-functions'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === 'GET /remote-functions'; + }); + + const remoteFunctionTracePromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + return txnEvent?.transaction === 'sveltekit.handle.root'; + }); + + await waitForInitialPageload(page, { route: '/remote-functions' }); + + const [clientTxnEvent, serverTxnEvent, remoteFunctionEvent] = await Promise.all([ + clientTxnEventPromise, + serverTxnEventPromise, + remoteFunctionTracePromise, + ]); + + const clientRemoteCallSpans = clientTxnEvent.spans?.filter(s => s.op === 'http.client'); + + // only the `getLikes()` function is called from the client + // `getPosts()` seems to have been only called on the server + expect(clientRemoteCallSpans).toHaveLength(1); + + const clientRemoteCallSpan = clientRemoteCallSpans && clientRemoteCallSpans[0]; + expect(clientRemoteCallSpan).toMatchObject({ + op: 'http.client', + description: expect.stringMatching(/^GET \/_app\/remote\/.+\/getLikes$/), + origin: 'auto.http.browser', + }); + + console.log(JSON.stringify(remoteFunctionEvent, null, 2)); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/utils.ts new file mode 100644 index 000000000000..a628f558a4bf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/utils.ts @@ -0,0 +1,49 @@ +import { Page } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +/** + * Helper function that waits for the initial pageload to complete. + * + * This function + * - loads the given route ("/" by default) + * - waits for SvelteKit's hydration + * - waits for the pageload transaction to be sent (doesn't assert on it though) + * + * Useful for tests that test outcomes of _navigations_ after an initial pageload. + * Waiting on the pageload transaction excludes edge cases where navigations occur + * so quickly that the pageload idle transaction is still active. This might lead + * to cases where the routing span would be attached to the pageload transaction + * and hence eliminates a lot of flakiness. + * + */ +export async function waitForInitialPageload( + page: Page, + opts?: { route?: string; parameterizedRoute?: string; debug?: boolean }, +) { + const route = opts?.route ?? '/'; + const txnName = opts?.parameterizedRoute ?? route; + const debug = opts?.debug ?? false; + + const clientPageloadTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { + debug && + console.log({ + txn: txnEvent?.transaction, + op: txnEvent.contexts?.trace?.op, + trace: txnEvent.contexts?.trace?.trace_id, + span: txnEvent.contexts?.trace?.span_id, + parent: txnEvent.contexts?.trace?.parent_span_id, + }); + + return txnEvent?.transaction === txnName && txnEvent.contexts?.trace?.op === 'pageload'; + }); + + await Promise.all([ + page.goto(route), + // the test app adds the "hydrated" class to the body when hydrating + page.waitForSelector('body.hydrated'), + // also waiting for the initial pageload txn so that later navigations don't interfere + clientPageloadTxnEventPromise, + ]); + + debug && console.log('hydrated'); +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tsconfig.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tsconfig.json new file mode 100644 index 000000000000..ba6aa4e6610a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + }, + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/vite.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/vite.config.ts new file mode 100644 index 000000000000..1a410bee7e11 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/vite.config.ts @@ -0,0 +1,12 @@ +import { sentrySvelteKit } from '@sentry/sveltekit'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sentrySvelteKit({ + autoUploadSourceMaps: false, + }), + sveltekit(), + ], +}); From b70134ba75bb169a5b90d6d48d0507d61065e7bc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 26 Aug 2025 18:28:56 +0200 Subject: [PATCH 10/16] fix span name, origin inconsistencies --- .../sveltekit/src/server-common/handle.ts | 6 ++--- .../src/server-common/processKitSpans.ts | 10 ++++---- .../server-common/processKitSpans.test.ts | 25 ++++++++++++++++--- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 61f68dcb7258..f94e12540857 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -177,13 +177,13 @@ async function instrumentHandle( const routeName = kitRootSpanAttributes['http.route']; if (routeName && typeof routeName === 'string') { - updateSpanName(kitRootSpan, routeName); + updateSpanName(kitRootSpan, `${event.request.method ?? 'GET'} ${routeName}`); } kitRootSpan.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltejs.kit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeName ? 'route' : 'url', 'sveltekit.tracing.original_name': originalName, }); } diff --git a/packages/sveltekit/src/server-common/processKitSpans.ts b/packages/sveltekit/src/server-common/processKitSpans.ts index 078817e5ce27..a08510eb65dd 100644 --- a/packages/sveltekit/src/server-common/processKitSpans.ts +++ b/packages/sveltekit/src/server-common/processKitSpans.ts @@ -29,9 +29,12 @@ export function _enhanceKitSpan(span: SpanJSON): void { const spanName = span.description; + const previousOp = span.op || span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]; + const previousOrigin = span.origin || span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]; + switch (spanName) { case 'sveltekit.resolve': - op = 'http.sveltekit.resolve'; + op = 'function.sveltekit.resolve'; origin = 'auto.http.sveltekit'; break; case 'sveltekit.load': @@ -59,15 +62,12 @@ export function _enhanceKitSpan(span: SpanJSON): void { } } - 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) { + if ((!previousOrigin || previousOrigin === 'manual') && origin) { span.origin = origin; span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin; } diff --git a/packages/sveltekit/test/server-common/processKitSpans.test.ts b/packages/sveltekit/test/server-common/processKitSpans.test.ts index 3bcb2be0a5ea..d001775e99cf 100644 --- a/packages/sveltekit/test/server-common/processKitSpans.test.ts +++ b/packages/sveltekit/test/server-common/processKitSpans.test.ts @@ -31,15 +31,15 @@ describe('svelteKitSpansIntegration', () => { svelteKitSpansIntegration().preprocessEvent?.(event, {}, {}); expect(event.spans).toHaveLength(1); - expect(event.spans?.[0]?.op).toBe('http.sveltekit.resolve'); + expect(event.spans?.[0]?.op).toBe('function.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_OP]).toBe('function.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.resolve', 'function.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'], @@ -140,5 +140,24 @@ describe('svelteKitSpansIntegration', () => { expect(span.origin).toBe('auto.custom.origin'); expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); }); + + it('overwrites previously set "manual" origins on sveltekit spans', () => { + // for example, if users manually set this (for whatever reason) + const span = { + description: 'sveltekit.resolve', + origin: 'manual', + 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.http.sveltekit'); + expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op'); + }); }); }); From 927cbf6684159827f9d604c57eaece6b4f0b4351 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 26 Aug 2025 18:37:42 +0200 Subject: [PATCH 11/16] lint and remove remote function test --- .../tests/tracing.test.ts | 37 ------------------- .../server-common/processKitSpans.test.ts | 2 +- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts index 8f9f1b34bf47..004182b32fb3 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts @@ -385,40 +385,3 @@ test('captures one navigation transaction per redirect', async ({ page }) => { }, }); }); - -test('captures remote function call spans', async ({ page }) => { - const clientTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { - return txnEvent?.transaction === '/remote-functions'; - }); - - const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { - return txnEvent?.transaction === 'GET /remote-functions'; - }); - - const remoteFunctionTracePromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => { - return txnEvent?.transaction === 'sveltekit.handle.root'; - }); - - await waitForInitialPageload(page, { route: '/remote-functions' }); - - const [clientTxnEvent, serverTxnEvent, remoteFunctionEvent] = await Promise.all([ - clientTxnEventPromise, - serverTxnEventPromise, - remoteFunctionTracePromise, - ]); - - const clientRemoteCallSpans = clientTxnEvent.spans?.filter(s => s.op === 'http.client'); - - // only the `getLikes()` function is called from the client - // `getPosts()` seems to have been only called on the server - expect(clientRemoteCallSpans).toHaveLength(1); - - const clientRemoteCallSpan = clientRemoteCallSpans && clientRemoteCallSpans[0]; - expect(clientRemoteCallSpan).toMatchObject({ - op: 'http.client', - description: expect.stringMatching(/^GET \/_app\/remote\/.+\/getLikes$/), - origin: 'auto.http.browser', - }); - - console.log(JSON.stringify(remoteFunctionEvent, null, 2)); -}); diff --git a/packages/sveltekit/test/server-common/processKitSpans.test.ts b/packages/sveltekit/test/server-common/processKitSpans.test.ts index d001775e99cf..1f0b92c377fb 100644 --- a/packages/sveltekit/test/server-common/processKitSpans.test.ts +++ b/packages/sveltekit/test/server-common/processKitSpans.test.ts @@ -1,4 +1,4 @@ -import type { EventType, SpanJSON, TransactionEvent } from '@sentry/core'; +import type { 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'; From eebd5cca68220c12f548b1d2a5f7d2fa6199a67f Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 27 Aug 2025 11:12:53 +0200 Subject: [PATCH 12/16] remove tracing config runtime check, cleanup, move some files --- .../tests/errors.server.test.ts | 2 +- .../sveltekit-2-kit-tracing/tests/test.json | 49 ------------------- .../sveltekit/src/server-common/handle.ts | 2 +- .../rewriteFramesIntegration.ts | 4 +- .../svelteKitSpans.ts} | 0 packages/sveltekit/src/server-common/utils.ts | 14 ------ packages/sveltekit/src/server/index.ts | 2 +- .../sveltekit/src/server/integrations/http.ts | 39 +++++++++++++++ packages/sveltekit/src/server/sdk.ts | 36 +++++--------- .../sveltekit/src/vite/injectGlobalValues.ts | 18 ++----- .../sveltekit/src/vite/sentryVitePlugins.ts | 7 +-- packages/sveltekit/src/vite/svelteConfig.ts | 23 ++------- packages/sveltekit/src/vite/types.ts | 13 ----- packages/sveltekit/src/worker/cloudflare.ts | 4 +- .../rewriteFramesIntegration.test.ts | 4 +- .../svelteKitSpans.test.ts} | 2 +- .../test/server-common/utils.test.ts | 36 +------------- .../test/server/integrations/http.test.ts | 27 ++++++++++ .../test/vite/injectGlobalValues.test.ts | 18 ------- 19 files changed, 101 insertions(+), 199 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/test.json rename packages/sveltekit/src/server-common/{ => integrations}/rewriteFramesIntegration.ts (95%) rename packages/sveltekit/src/server-common/{processKitSpans.ts => integrations/svelteKitSpans.ts} (100%) create mode 100644 packages/sveltekit/src/server/integrations/http.ts rename packages/sveltekit/test/server-common/{ => integrations}/rewriteFramesIntegration.test.ts (93%) rename packages/sveltekit/test/server-common/{processKitSpans.test.ts => integrations/svelteKitSpans.test.ts} (99%) create mode 100644 packages/sveltekit/test/server/integrations/http.test.ts diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts index 2d799b527198..e60531b43d6e 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts @@ -71,7 +71,7 @@ test.describe('server-side errors', () => { expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( expect.objectContaining({ - filename: expect.stringMatching(/app:\/\/\/chunks\/_server.ts-.+.js/), + filename: expect.stringMatching(/app:\/\/\/_server.ts-.+.js/), function: 'GET', in_app: true, }), diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/test.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/test.json deleted file mode 100644 index d676d9de3189..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/test.json +++ /dev/null @@ -1,49 +0,0 @@ -[ - { - "data": { "sentry.op": "function.sveltekit.handle", "sentry.origin": "auto.function.sveltekit.handle" }, - "description": "sveltekit.handle.sequenced.sentryRequestHandler", - "op": "function.sveltekit.handle", - "origin": "auto.function.sveltekit.handle", - "parent_span_id": "db7a47050d2108af", - "span_id": "470560c2ddb69363", - "start_timestamp": 1756220521.186, - "status": "ok", - "timestamp": 1756220521.1872811, - "trace_id": "f46904c28f8d1f0a8fc085e876af06ed" - }, - { - "data": { - "http.response.body.size": "67", - "http.response.status_code": 200, - "http.route": "/form-action", - "sentry.op": "function.sveltekit.resolve", - "sentry.origin": "auto.http.sveltekit" - }, - "description": "sveltekit.resolve", - "op": "function.sveltekit.resolve", - "origin": "auto.http.sveltekit", - "parent_span_id": "470560c2ddb69363", - "span_id": "b39cdd0c654300fc", - "start_timestamp": 1756220521.186, - "status": "ok", - "timestamp": 1756220521.1871505, - "trace_id": "f46904c28f8d1f0a8fc085e876af06ed" - }, - { - "data": { - "http.route": "/form-action", - "sentry.op": "function.sveltekit.form_action", - "sentry.origin": "auto.function.sveltekit.action", - "sveltekit.form_action.name": "default" - }, - "description": "sveltekit.form_action", - "op": "function.sveltekit.form_action", - "origin": "auto.function.sveltekit.action", - "parent_span_id": "b39cdd0c654300fc", - "span_id": "9dccc58b455187cb", - "start_timestamp": 1756220521.186, - "status": "ok", - "timestamp": 1756220521.1866386, - "trace_id": "f46904c28f8d1f0a8fc085e876af06ed" - } -] diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index f94e12540857..6cf518868577 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -170,7 +170,7 @@ async function instrumentHandle( // 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. + // Other spans are enhanced in the `processKitSpans` integration. const spanJson = spanToJSON(kitRootSpan); const kitRootSpanAttributes = spanJson.data; const originalName = spanJson.description; diff --git a/packages/sveltekit/src/server-common/rewriteFramesIntegration.ts b/packages/sveltekit/src/server-common/integrations/rewriteFramesIntegration.ts similarity index 95% rename from packages/sveltekit/src/server-common/rewriteFramesIntegration.ts rename to packages/sveltekit/src/server-common/integrations/rewriteFramesIntegration.ts index 412dd6f98815..9f6f0add6944 100644 --- a/packages/sveltekit/src/server-common/rewriteFramesIntegration.ts +++ b/packages/sveltekit/src/server-common/integrations/rewriteFramesIntegration.ts @@ -7,8 +7,8 @@ import { join, rewriteFramesIntegration as originalRewriteFramesIntegration, } from '@sentry/core'; -import { WRAPPED_MODULE_SUFFIX } from '../common/utils'; -import type { GlobalWithSentryValues } from '../vite/injectGlobalValues'; +import { WRAPPED_MODULE_SUFFIX } from '../../common/utils'; +import type { GlobalWithSentryValues } from '../../vite/injectGlobalValues'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; interface RewriteFramesOptions { diff --git a/packages/sveltekit/src/server-common/processKitSpans.ts b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts similarity index 100% rename from packages/sveltekit/src/server-common/processKitSpans.ts rename to packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts diff --git a/packages/sveltekit/src/server-common/utils.ts b/packages/sveltekit/src/server-common/utils.ts index 821ce0a66eaf..432e56e373c6 100644 --- a/packages/sveltekit/src/server-common/utils.ts +++ b/packages/sveltekit/src/server-common/utils.ts @@ -53,17 +53,3 @@ 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/index.ts b/packages/sveltekit/src/server/index.ts index 56400dcc5423..d287331df14d 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -53,7 +53,6 @@ export { getTraceMetaTags, graphqlIntegration, hapiIntegration, - httpIntegration, // eslint-disable-next-line deprecation/deprecation inboundFiltersIntegration, eventFiltersIntegration, @@ -141,6 +140,7 @@ export { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../server-common/l export { sentryHandle } from '../server-common/handle'; export { initCloudflareSentryHandle } from './handle'; export { wrapServerRouteWithSentry } from '../server-common/serverRoute'; +export { httpIntegration } from './integrations/http'; /** * Tracks the Svelte component's initialization and mounting operation as well as diff --git a/packages/sveltekit/src/server/integrations/http.ts b/packages/sveltekit/src/server/integrations/http.ts new file mode 100644 index 000000000000..4d6844017d1d --- /dev/null +++ b/packages/sveltekit/src/server/integrations/http.ts @@ -0,0 +1,39 @@ +import type { IntegrationFn } from '@sentry/core'; +import { httpIntegration as originalHttpIntegration } from '@sentry/node'; + +type HttpOptions = Parameters[0]; + +/** + * The http integration instruments Node's internal http and https modules. + * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. + * + * For SvelteKit, does not create spans for incoming requests but instead we use SvelteKit's own spans. + * If you need to create incoming spans, set the `disableIncomingRequestSpans` option to `false`. + * (You likely don't need this!) + * + */ +export const httpIntegration = ((options: HttpOptions = {}) => { + /* + * This is a slightly modified version of the original httpIntegration: We avoid creating + * incoming request spans because: + * + * - If Kit-tracing is available and enabled, we take the `sveltekit.handle.root` span + * as the root span and make it the `http.server` span. This gives us a single root + * span across all deployment plaftorms (while httpIntegration doesn't apply on e.g. + * AWS Lambda or edge) + * - If Kit-tracing is N/A or disabled and users follow the current/old docs, httpIntegration + * does nothing anyway, so this isn't a concern. + * - Which leaves the undocumented case that users --import an instrument.mjs file + * in which they initialize the SDK. IMHO it's fine to ignore this for now since it was + * well ... undocumented. Given in the future there won't be be an easy way for us + * to detect where the SDK is initialized, we should simply redirect users to use + * instrumentation.server.ts instead. If users want to, they can simply import and + * register `httpIntegration` and explicitly enable incoming request spans. + */ + + return originalHttpIntegration({ + // We disable incoming request spans here, because otherwise we'd end up with duplicate spans. + disableIncomingRequestSpans: true, + ...options, + }); +}) satisfies IntegrationFn; diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 6e7dfa478024..37c4715c9683 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -1,35 +1,21 @@ -import { applySdkMetadata } from '@sentry/core'; +import { applySdkMetadata, hasSpansEnabled } from '@sentry/core'; import type { NodeClient, NodeOptions } 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'; +import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node'; +import { rewriteFramesIntegration } from '../server-common/integrations/rewriteFramesIntegration'; +import { svelteKitSpansIntegration } from '../server-common/integrations/svelteKitSpans'; +import { httpIntegration } from './integrations/http'; /** * 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 - // In old (hooks.server.ts) based SDK setups, adding the default version of the integration does nothing - // for incoming requests. We still add it in default config for the undocumented case that anyone is - // using the SDK by `--import`ing the SDK setup directly (e.g. with adapter-node). - defaultIntegrations.push(httpIntegration({ disableIncomingRequestSpans: true })); - if (config.tracing) { - // If `tracing` is enabled, we need to instrument spans for the server - defaultIntegrations.push(svelteKitSpansIntegration()); - } - } + const defaultIntegrations = [ + ...getDefaultNodeIntegrations(options).filter(integration => integration.name !== 'Http'), + rewriteFramesIntegration(), + httpIntegration(), + svelteKitSpansIntegration(), + ]; const opts = { defaultIntegrations, diff --git a/packages/sveltekit/src/vite/injectGlobalValues.ts b/packages/sveltekit/src/vite/injectGlobalValues.ts index f7fcc516f3de..20f446b6b46f 100644 --- a/packages/sveltekit/src/vite/injectGlobalValues.ts +++ b/packages/sveltekit/src/vite/injectGlobalValues.ts @@ -1,18 +1,11 @@ import { type InternalGlobal, escapeStringForRegex } from '@sentry/core'; import MagicString from 'magic-string'; import type { Plugin } from 'vite'; -import { - type BackwardsForwardsCompatibleSvelteConfig, - type SvelteKitTracingConfig, - getAdapterOutputDir, - getHooksFileName, - getTracingConfig, -} from './svelteConfig'; +import { type BackwardsForwardsCompatibleSvelteConfig, getAdapterOutputDir, getHooksFileName } from './svelteConfig'; import type { SentrySvelteKitPluginOptions } from './types'; export type GlobalSentryValues = { __sentry_sveltekit_output_dir?: string; - __sentry_sveltekit_tracing_config?: SvelteKitTracingConfig; }; /** @@ -54,7 +47,6 @@ export async function makeGlobalValuesInjectionPlugin( const globalSentryValues: GlobalSentryValues = { __sentry_sveltekit_output_dir: adapterOutputDir, - __sentry_sveltekit_tracing_config: getTracingConfig(svelteConfig), }; if (debug) { @@ -62,6 +54,9 @@ export async function makeGlobalValuesInjectionPlugin( console.log('[Sentry SvelteKit] Global values:', globalSentryValues); } + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway + const hooksFileRegexp = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`); + return { name: 'sentry-sveltekit-global-values-injection-plugin', resolveId: (id, _importer, _ref) => { @@ -85,10 +80,7 @@ export async function makeGlobalValuesInjectionPlugin( }, transform: async (code, id) => { - const isServerEntryFile = - /instrumentation\.server\./.test(id) || - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway - new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id); + const isServerEntryFile = /instrumentation\.server\./.test(id) || hooksFileRegexp.test(id); if (isServerEntryFile) { if (debug) { diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts index 628c2ae524e5..a0fe38c778de 100644 --- a/packages/sveltekit/src/vite/sentryVitePlugins.ts +++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts @@ -10,7 +10,6 @@ import type { CustomSentryVitePluginOptions, SentrySvelteKitPluginOptions } from const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = { autoUploadSourceMaps: true, autoInstrument: true, - injectGlobalValues: true, debug: false, }; @@ -33,6 +32,8 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} const sentryPlugins: Plugin[] = []; if (mergedOptions.autoInstrument) { + // TODO: Once tracing is promoted stable, we need to adjust this check! + // We probably need to make a instrumentation.server.ts|js file lookup instead. const kitTracingEnabled = !!svelteConfig.kit?.experimental?.tracing?.server; const pluginOptions: AutoInstrumentSelection = { @@ -45,7 +46,7 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} makeAutoInstrumentationPlugin({ ...pluginOptions, debug: options.debug || false, - // if kit-internal tracing is enabled, we only want to instrument client-side code. + // if kit-internal tracing is enabled, we only want to wrap and instrument client-side code. onlyInstrumentClient: kitTracingEnabled, }), ); @@ -53,7 +54,7 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} const sentryVitePluginsOptions = generateVitePluginOptions(mergedOptions); - if (mergedOptions.injectGlobalValues) { + if (mergedOptions.autoUploadSourceMaps) { sentryPlugins.push(await makeGlobalValuesInjectionPlugin(svelteConfig, mergedOptions)); } diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts index 6ccaab87e6de..f1e908af9c93 100644 --- a/packages/sveltekit/src/vite/svelteConfig.ts +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -8,14 +8,15 @@ export type SvelteKitTracingConfig = { tracing?: { server: boolean; }; + // TODO: Once instrumentation is promoted stable, this will be removed! instrumentation?: { server: boolean; }; }; /** - * Experimental tracing and instrumentation config is available - * @since 2.31.0 + * Experimental tracing and instrumentation config is available @since 2.31.0 + * // TODO: Once instrumentation and tracing is promoted stable, adjust this type!s */ type BackwardsForwardsCompatibleKitConfig = Config['kit'] & { experimental?: SvelteKitTracingConfig }; @@ -129,21 +130,3 @@ 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, - }, - }; -} diff --git a/packages/sveltekit/src/vite/types.ts b/packages/sveltekit/src/vite/types.ts index e7b4fb5273a3..4267ce378bb1 100644 --- a/packages/sveltekit/src/vite/types.ts +++ b/packages/sveltekit/src/vite/types.ts @@ -224,17 +224,4 @@ export type SentrySvelteKitPluginOptions = { * Options related to source maps upload to Sentry */ sourceMapsUploadOptions?: SourceMapsUploadOptions; - - /** - * Controls whether application config values should be injected into the server's global object at runtime. - * - * The Sentry SDK needs some information about your application config at runtime, like for example - * the used build output directory or your Sveltekit Tracing config. - * - * Important: You most likely don't want to disable this! Only disable injection if you're suspecting any - * kinds of build problems to be related to Sentry. - * - * @default true - */ - injectGlobalValues?: boolean; }; diff --git a/packages/sveltekit/src/worker/cloudflare.ts b/packages/sveltekit/src/worker/cloudflare.ts index e27a5aca96ef..612b174f6c69 100644 --- a/packages/sveltekit/src/worker/cloudflare.ts +++ b/packages/sveltekit/src/worker/cloudflare.ts @@ -6,8 +6,8 @@ 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'; +import { rewriteFramesIntegration } from '../server-common/integrations/rewriteFramesIntegration'; +import { svelteKitSpansIntegration } from '../server-common/integrations/svelteKitSpans'; /** * Initializes Sentry SvelteKit Cloudflare SDK diff --git a/packages/sveltekit/test/server-common/rewriteFramesIntegration.test.ts b/packages/sveltekit/test/server-common/integrations/rewriteFramesIntegration.test.ts similarity index 93% rename from packages/sveltekit/test/server-common/rewriteFramesIntegration.test.ts rename to packages/sveltekit/test/server-common/integrations/rewriteFramesIntegration.test.ts index 20f9c52a8e27..836152a81eb0 100644 --- a/packages/sveltekit/test/server-common/rewriteFramesIntegration.test.ts +++ b/packages/sveltekit/test/server-common/integrations/rewriteFramesIntegration.test.ts @@ -2,8 +2,8 @@ import { rewriteFramesIntegration } from '@sentry/browser'; import type { Event, StackFrame } from '@sentry/core'; import { basename } from '@sentry/core'; import { describe, expect, it } from 'vitest'; -import { rewriteFramesIteratee } from '../../src/server-common/rewriteFramesIntegration'; -import type { GlobalWithSentryValues } from '../../src/vite/injectGlobalValues'; +import { rewriteFramesIteratee } from '../../../src/server-common/integrations/rewriteFramesIntegration'; +import type { GlobalWithSentryValues } from '../../../src/vite/injectGlobalValues'; describe('rewriteFramesIteratee', () => { it('removes the module property from the frame', () => { diff --git a/packages/sveltekit/test/server-common/processKitSpans.test.ts b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts similarity index 99% rename from packages/sveltekit/test/server-common/processKitSpans.test.ts rename to packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts index 1f0b92c377fb..bb9863623638 100644 --- a/packages/sveltekit/test/server-common/processKitSpans.test.ts +++ b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts @@ -1,7 +1,7 @@ import type { 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'; +import { _enhanceKitSpan, svelteKitSpansIntegration } from '../../../src/server-common/integrations/svelteKitSpans'; describe('svelteKitSpansIntegration', () => { it('has a name and a preprocessEventHook', () => { diff --git a/packages/sveltekit/test/server-common/utils.test.ts b/packages/sveltekit/test/server-common/utils.test.ts index bb002da927ad..df0858ed4654 100644 --- a/packages/sveltekit/test/server-common/utils.test.ts +++ b/packages/sveltekit/test/server-common/utils.test.ts @@ -1,5 +1,5 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { getKitTracingConfig, getTracePropagationData } from '../../src/server-common/utils'; +import { describe, expect, it } from 'vitest'; +import { getTracePropagationData } from '../../src/server-common/utils'; const MOCK_REQUEST_EVENT: any = { request: { @@ -48,35 +48,3 @@ describe('getTracePropagationData', () => { expect(baggage).toBeUndefined(); }); }); - -describe('getKitTracingConfig', () => { - beforeEach(() => { - delete globalThis.__sentry_sveltekit_tracing_config; - }); - - it('returns the tracing config from the global object', () => { - globalThis.__sentry_sveltekit_tracing_config = { - instrumentation: { - server: true, - }, - tracing: { - server: false, - }, - }; - - const config = getKitTracingConfig(); - - expect(config).toEqual({ - instrumentation: true, - tracing: false, - }); - }); - - it('returns instrumentation and tracing being disabled by default', () => { - const config = getKitTracingConfig(); - expect(config).toEqual({ - instrumentation: false, - tracing: false, - }); - }); -}); diff --git a/packages/sveltekit/test/server/integrations/http.test.ts b/packages/sveltekit/test/server/integrations/http.test.ts new file mode 100644 index 000000000000..08a6fffcd06f --- /dev/null +++ b/packages/sveltekit/test/server/integrations/http.test.ts @@ -0,0 +1,27 @@ +import * as SentryNode from '@sentry/node'; +import { describe, expect, it, vi } from 'vitest'; +import { httpIntegration as svelteKitHttpIntegration } from '../../../src/server/integrations/http'; + +describe('httpIntegration', () => { + it('calls the original httpIntegration with incoming request span recording disabled', () => { + const sentryNodeHttpIntegration = vi.spyOn(SentryNode, 'httpIntegration'); + svelteKitHttpIntegration({ breadcrumbs: false }); + + expect(sentryNodeHttpIntegration).toHaveBeenCalledTimes(1); + expect(sentryNodeHttpIntegration).toHaveBeenCalledWith({ + breadcrumbs: false, // leaves other options untouched + disableIncomingRequestSpans: true, + }); + }); + + it('allows users to override incoming request span recording', () => { + const sentryNodeHttpIntegration = vi.spyOn(SentryNode, 'httpIntegration'); + svelteKitHttpIntegration({ breadcrumbs: false, disableIncomingRequestSpans: false }); + + expect(sentryNodeHttpIntegration).toHaveBeenCalledTimes(1); + expect(sentryNodeHttpIntegration).toHaveBeenCalledWith({ + breadcrumbs: false, + disableIncomingRequestSpans: false, + }); + }); +}); diff --git a/packages/sveltekit/test/vite/injectGlobalValues.test.ts b/packages/sveltekit/test/vite/injectGlobalValues.test.ts index 01075fb33ade..50f41c84880f 100644 --- a/packages/sveltekit/test/vite/injectGlobalValues.test.ts +++ b/packages/sveltekit/test/vite/injectGlobalValues.test.ts @@ -5,19 +5,10 @@ describe('getGlobalValueInjectionCode', () => { it('returns code that injects values into the global object', () => { const injectionCode = getGlobalValueInjectionCode({ __sentry_sveltekit_output_dir: '.svelte-kit/output', - __sentry_sveltekit_tracing_config: { - tracing: { - server: true, - }, - instrumentation: { - server: true, - }, - }, }); expect(injectionCode).toMatchInlineSnapshot(` "globalThis["__sentry_sveltekit_output_dir"] = ".svelte-kit/output"; - globalThis["__sentry_sveltekit_tracing_config"] = {"tracing":{"server":true},"instrumentation":{"server":true}}; " `); @@ -25,17 +16,8 @@ describe('getGlobalValueInjectionCode', () => { // The return value of eval here is the value of the last expression in the code eval(injectionCode); expect(globalThis.__sentry_sveltekit_output_dir).toEqual('.svelte-kit/output'); - expect(globalThis.__sentry_sveltekit_tracing_config).toEqual({ - tracing: { - server: true, - }, - instrumentation: { - server: true, - }, - }); delete globalThis.__sentry_sveltekit_output_dir; - delete globalThis.__sentry_sveltekit_tracing_config; }); it('returns empty string if no values are passed', () => { From 05091f55d26c18f0b6c669df5e078b4763e9e835 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 27 Aug 2025 11:14:32 +0200 Subject: [PATCH 13/16] lint --- packages/sveltekit/src/server-common/utils.ts | 3 +-- packages/sveltekit/src/server/sdk.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/sveltekit/src/server-common/utils.ts b/packages/sveltekit/src/server-common/utils.ts index 432e56e373c6..b861bf758697 100644 --- a/packages/sveltekit/src/server-common/utils.ts +++ b/packages/sveltekit/src/server-common/utils.ts @@ -1,7 +1,6 @@ -import { captureException, GLOBAL_OBJ, objectify } from '@sentry/core'; +import { captureException, 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 diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 37c4715c9683..fb7a5dbbb471 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -1,4 +1,4 @@ -import { applySdkMetadata, hasSpansEnabled } from '@sentry/core'; +import { applySdkMetadata } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node'; import { rewriteFramesIntegration } from '../server-common/integrations/rewriteFramesIntegration'; From cd9c441c33a3498b62151edffe7a5611a13bc526 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 27 Aug 2025 11:24:12 +0200 Subject: [PATCH 14/16] fix failing unit test --- packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts index 020206b4790b..fa8df96f03a6 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -70,7 +70,7 @@ describe('sentrySvelteKit()', () => { it("doesn't return the sentry source maps plugins if autoUploadSourcemaps is `false`", async () => { const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: false }); - expect(plugins).toHaveLength(2); // auto instrument + global values injection + expect(plugins).toHaveLength(1); // auto instrument }); it("doesn't return the sentry source maps plugins if `NODE_ENV` is development", async () => { From d265348e666ec834b4ec63859a588bd42930dbf5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 27 Aug 2025 15:55:49 +0200 Subject: [PATCH 15/16] improve tracing check, optimize svelteKitSpansIntegration --- .../sveltekit/src/server-common/handle.ts | 2 +- .../integrations/svelteKitSpans.ts | 3 +- .../integrations/svelteKitSpans.test.ts | 44 ++++++++++++++++++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 6cf518868577..a013a7ea65b8 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -162,7 +162,7 @@ async function instrumentHandle( }), }); - const kitRootSpan = event.tracing?.root; + const kitRootSpan = event.tracing?.enabled ? event.tracing?.root : undefined; if (sentrySpan) { setHttpStatus(sentrySpan, res.status); diff --git a/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts index a08510eb65dd..eee2a7a2625e 100644 --- a/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts +++ b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts @@ -12,7 +12,8 @@ export function svelteKitSpansIntegration(): Integration { // Using preprocessEvent to ensure the processing happens before user-configured // event processors are executed preprocessEvent(event) { - if (event.type === 'transaction') { + // only iterate over the spans if the root span was emitted by SvelteKit + if (event.type === 'transaction' && event.contexts?.trace?.data?.['sveltekit.tracing.original_name']) { event.spans?.forEach(_enhanceKitSpan); } }, diff --git a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts index bb9863623638..bfa415786664 100644 --- a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts +++ b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts @@ -11,9 +11,18 @@ describe('svelteKitSpansIntegration', () => { expect(typeof integration.preprocessEvent).toBe('function'); }); - it('enhances spans from SvelteKit', () => { + it('enhances spans from SvelteKit, if root span was emitted by SvelteKit', () => { const event: TransactionEvent = { type: 'transaction', + contexts: { + trace: { + span_id: '123', + trace_id: 'abc', + data: { + 'sveltekit.tracing.original_name': 'sveltekit.handle.root', + }, + }, + }, spans: [ { description: 'sveltekit.resolve', @@ -37,6 +46,39 @@ describe('svelteKitSpansIntegration', () => { expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); + it('does not enhance spans from SvelteKit, if root span was not emitted by SvelteKit', () => { + const event: TransactionEvent = { + type: 'transaction', + contexts: { + trace: { + span_id: '123', + trace_id: 'abc', + data: {}, + }, + }, + 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).toBeUndefined(); + expect(event.spans?.[0]?.origin).toBeUndefined(); + expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); + expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); + }); + describe('_enhanceKitSpan', () => { it.each([ ['sveltekit.resolve', 'function.sveltekit.resolve', 'auto.http.sveltekit'], From 87c4109346105cb85aac35bee5200d8284f169ed Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 27 Aug 2025 18:59:49 +0200 Subject: [PATCH 16/16] review suggestions, comments, revert kitSpansIntegrations optimization, change root span enhancement time --- .../sveltekit/src/server-common/handle.ts | 21 ++++++----- .../integrations/svelteKitSpans.ts | 7 +++- .../sveltekit/src/vite/sentryVitePlugins.ts | 7 +++- .../integrations/svelteKitSpans.test.ts | 37 +------------------ 4 files changed, 24 insertions(+), 48 deletions(-) diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index a013a7ea65b8..3f5797efd211 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -155,18 +155,9 @@ 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, - }), - }); - const kitRootSpan = event.tracing?.enabled ? event.tracing?.root : undefined; - if (sentrySpan) { - setHttpStatus(sentrySpan, res.status); - } else if (kitRootSpan) { + 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.) @@ -188,6 +179,16 @@ async function instrumentHandle( }); } + const res = await resolve(event, { + transformPageChunk: addSentryCodeToPage({ + injectFetchProxyScript: options.injectFetchProxyScript ?? true, + }), + }); + + if (sentrySpan) { + setHttpStatus(sentrySpan, res.status); + } + return res; }; diff --git a/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts index eee2a7a2625e..5ab24a731279 100644 --- a/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts +++ b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts @@ -8,12 +8,15 @@ import { type SpanJSON, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ */ export function svelteKitSpansIntegration(): Integration { return { - name: 'SvelteKitSpansEnhancment', + name: 'SvelteKitSpansEnhancement', // Using preprocessEvent to ensure the processing happens before user-configured // event processors are executed preprocessEvent(event) { // only iterate over the spans if the root span was emitted by SvelteKit - if (event.type === 'transaction' && event.contexts?.trace?.data?.['sveltekit.tracing.original_name']) { + // TODO: Right now, we can't optimize this to only check traces with a kit-emitted root span + // this is because in Cloudflare, the kit-emitted root span is missing but our cloudflare + // SDK emits the http.server span. + if (event.type === 'transaction') { event.spans?.forEach(_enhanceKitSpan); } }, diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts index a0fe38c778de..61b388a94cf2 100644 --- a/packages/sveltekit/src/vite/sentryVitePlugins.ts +++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts @@ -33,7 +33,6 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} if (mergedOptions.autoInstrument) { // TODO: Once tracing is promoted stable, we need to adjust this check! - // We probably need to make a instrumentation.server.ts|js file lookup instead. const kitTracingEnabled = !!svelteConfig.kit?.experimental?.tracing?.server; const pluginOptions: AutoInstrumentSelection = { @@ -55,6 +54,12 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {} const sentryVitePluginsOptions = generateVitePluginOptions(mergedOptions); if (mergedOptions.autoUploadSourceMaps) { + // When source maps are enabled, we need to inject the output directory to get a correct + // stack trace, by using this SDK's `rewriteFrames` integration. + // This integration picks up the value. + // TODO: I don't think this is technically correct. Either we always or never inject the output directory. + // Stack traces shouldn't be different, depending on source maps config. With debugIds, we might not even + // need to rewrite frames anymore. sentryPlugins.push(await makeGlobalValuesInjectionPlugin(svelteConfig, mergedOptions)); } diff --git a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts index bfa415786664..0d95cb3d6fb6 100644 --- a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts +++ b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts @@ -7,11 +7,11 @@ describe('svelteKitSpansIntegration', () => { it('has a name and a preprocessEventHook', () => { const integration = svelteKitSpansIntegration(); - expect(integration.name).toBe('SvelteKitSpansEnhancment'); + expect(integration.name).toBe('SvelteKitSpansEnhancement'); expect(typeof integration.preprocessEvent).toBe('function'); }); - it('enhances spans from SvelteKit, if root span was emitted by SvelteKit', () => { + it('enhances spans from SvelteKit', () => { const event: TransactionEvent = { type: 'transaction', contexts: { @@ -46,39 +46,6 @@ describe('svelteKitSpansIntegration', () => { expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit'); }); - it('does not enhance spans from SvelteKit, if root span was not emitted by SvelteKit', () => { - const event: TransactionEvent = { - type: 'transaction', - contexts: { - trace: { - span_id: '123', - trace_id: 'abc', - data: {}, - }, - }, - 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).toBeUndefined(); - expect(event.spans?.[0]?.origin).toBeUndefined(); - expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined(); - expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined(); - }); - describe('_enhanceKitSpan', () => { it.each([ ['sveltekit.resolve', 'function.sveltekit.resolve', 'auto.http.sveltekit'],