diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.client.test.ts index c31e51bf9e99..a2131287ec2e 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/tests/performance.client.test.ts @@ -109,30 +109,6 @@ test.describe('client-specific performance events', () => { op: 'ui.svelte.init', origin: 'auto.ui.svelte', }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), - expect.objectContaining({ - data: { 'sentry.op': 'ui.svelte.update', 'sentry.origin': 'auto.ui.svelte' }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - }), ]), ); }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/+page.svelte index eff3fa3f2e8d..3c1052bbbe9c 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/+page.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/+page.svelte @@ -5,7 +5,7 @@ import Component2 from "./Component2.svelte"; import Component3 from "./Component3.svelte"; - Sentry.trackComponent({componentName: 'components/+page'}) + Sentry.trackComponent({componentName: 'components/+page', trackUpdates: true})

Demonstrating Component Tracking

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component1.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component1.svelte index a675711e4b68..dfcf01de0b07 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component1.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component1.svelte @@ -2,7 +2,7 @@ import Component2 from "./Component2.svelte"; import {trackComponent} from '@sentry/sveltekit'; - trackComponent({componentName: 'Component1'}); + trackComponent({componentName: 'Component1', trackUpdates: true});

Howdy, I'm component 1

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component2.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component2.svelte index 2b2f38308077..1b3ad103b3b7 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component2.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component2.svelte @@ -2,7 +2,7 @@ import Component3 from "./Component3.svelte"; import {trackComponent} from '@sentry/sveltekit'; - trackComponent({componentName: 'Component2'}); + trackComponent({componentName: 'Component2', trackUpdates: true});

Howdy, I'm component 2

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component3.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component3.svelte index 9b4e028f78e7..9b813ff2c744 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component3.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/components/Component3.svelte @@ -1,6 +1,6 @@

Howdy, I'm component 3

diff --git a/packages/svelte/src/config.ts b/packages/svelte/src/config.ts index b4a0ae7d4f35..4bd11e0f659e 100644 --- a/packages/svelte/src/config.ts +++ b/packages/svelte/src/config.ts @@ -3,7 +3,7 @@ import type { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; import { componentTrackingPreprocessor, defaultComponentTrackingOptions } from './preprocessors'; import type { SentryPreprocessorGroup, SentrySvelteConfigOptions, SvelteConfig } from './types'; -const DEFAULT_SENTRY_OPTIONS: SentrySvelteConfigOptions = { +const defaultSentryOptions: SentrySvelteConfigOptions = { componentTracking: defaultComponentTrackingOptions, }; @@ -20,32 +20,25 @@ export function withSentryConfig( sentryOptions?: SentrySvelteConfigOptions, ): SvelteConfig { const mergedOptions = { - ...DEFAULT_SENTRY_OPTIONS, + ...defaultSentryOptions, ...sentryOptions, + componentTracking: { + ...defaultSentryOptions.componentTracking, + ...sentryOptions?.componentTracking, + }, }; const originalPreprocessors = getOriginalPreprocessorArray(originalConfig); - // Map is insertion-order-preserving. It's important to add preprocessors - // to this map in the right order we want to see them being executed. - // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map - const sentryPreprocessors = new Map(); - - const shouldTrackComponents = mergedOptions.componentTracking?.trackComponents; - if (shouldTrackComponents) { - const firstPassPreproc: SentryPreprocessorGroup = componentTrackingPreprocessor(mergedOptions.componentTracking); - sentryPreprocessors.set(firstPassPreproc.sentryId || '', firstPassPreproc); + // Bail if users already added the preprocessor + if (originalPreprocessors.find((p: PreprocessorGroup) => !!(p as SentryPreprocessorGroup).sentryId)) { + return originalConfig; } - // We prioritize user-added preprocessors, so we don't insert sentry processors if they - // have already been added by users. - originalPreprocessors.forEach((p: SentryPreprocessorGroup) => { - if (p.sentryId) { - sentryPreprocessors.delete(p.sentryId); - } - }); - - const mergedPreprocessors = [...sentryPreprocessors.values(), ...originalPreprocessors]; + const mergedPreprocessors = [...originalPreprocessors]; + if (mergedOptions.componentTracking.trackComponents) { + mergedPreprocessors.unshift(componentTrackingPreprocessor(mergedOptions.componentTracking)); + } return { ...originalConfig, diff --git a/packages/svelte/src/constants.ts b/packages/svelte/src/constants.ts deleted file mode 100644 index cb8255040c03..000000000000 --- a/packages/svelte/src/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const UI_SVELTE_INIT = 'ui.svelte.init'; - -export const UI_SVELTE_UPDATE = 'ui.svelte.update'; - -export const DEFAULT_COMPONENT_NAME = 'Svelte Component'; diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index b50be258bc58..05f33fe1cfdf 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -2,8 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/browser'; import type { Span } from '@sentry/core'; import { afterUpdate, beforeUpdate, onMount } from 'svelte'; -import { startInactiveSpan } from '@sentry/core'; -import { DEFAULT_COMPONENT_NAME, UI_SVELTE_INIT, UI_SVELTE_UPDATE } from './constants'; +import { logger, startInactiveSpan } from '@sentry/core'; import type { TrackComponentOptions } from './types'; const defaultTrackComponentOptions: { @@ -12,7 +11,7 @@ const defaultTrackComponentOptions: { componentName?: string; } = { trackInit: true, - trackUpdates: true, + trackUpdates: false, }; /** @@ -29,21 +28,27 @@ export function trackComponent(options?: TrackComponentOptions): void { const customComponentName = mergedOptions.componentName; - const componentName = `<${customComponentName || DEFAULT_COMPONENT_NAME}>`; + const componentName = `<${customComponentName || 'Svelte Component'}>`; if (mergedOptions.trackInit) { recordInitSpan(componentName); } if (mergedOptions.trackUpdates) { - recordUpdateSpans(componentName); + try { + recordUpdateSpans(componentName); + } catch { + logger.warn( + "Cannot track component updates. This is likely because you're using Svelte 5 in Runes mode. Set `trackUpdates: false` in `withSentryConfig` or `trackComponent` to disable this warning.", + ); + } } } function recordInitSpan(componentName: string): void { const initSpan = startInactiveSpan({ onlyIfParent: true, - op: UI_SVELTE_INIT, + op: 'ui.svelte.init', name: componentName, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte' }, }); @@ -58,7 +63,7 @@ function recordUpdateSpans(componentName: string): void { beforeUpdate(() => { updateSpan = startInactiveSpan({ onlyIfParent: true, - op: UI_SVELTE_UPDATE, + op: 'ui.svelte.update', name: componentName, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.svelte' }, }); diff --git a/packages/svelte/src/preprocessors.ts b/packages/svelte/src/preprocessors.ts index c966c6e00eef..67936be39858 100644 --- a/packages/svelte/src/preprocessors.ts +++ b/packages/svelte/src/preprocessors.ts @@ -6,7 +6,7 @@ import type { ComponentTrackingInitOptions, SentryPreprocessorGroup, TrackCompon export const defaultComponentTrackingOptions: Required = { trackComponents: true, trackInit: true, - trackUpdates: true, + trackUpdates: false, }; export const FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID = 'FIRST_PASS_COMPONENT_TRACKING_PREPROCESSOR'; diff --git a/packages/svelte/src/types.ts b/packages/svelte/src/types.ts index 8079019d8568..ff79920ab9a4 100644 --- a/packages/svelte/src/types.ts +++ b/packages/svelte/src/types.ts @@ -29,7 +29,7 @@ export type SpanOptions = { * onMount lifecycle hook. This span tells how long it takes a component * to be created and inserted into the DOM. * - * Defaults to true if component tracking is enabled + * @default `true` if component tracking is enabled */ trackInit?: boolean; @@ -37,7 +37,10 @@ export type SpanOptions = { * If true, a span is recorded between a component's beforeUpdate and afterUpdate * lifecycle hooks. * - * Defaults to true if component tracking is enabled + * Caution: Component updates can only be tracked in Svelte versions prior to version 5 + * or in Svelte 5 in legacy mode (i.e. without Runes). + * + * @default `false` if component tracking is enabled */ trackUpdates?: boolean; }; diff --git a/packages/svelte/test/config.test.ts b/packages/svelte/test/config.test.ts index a8c84297082a..21f51dc66518 100644 --- a/packages/svelte/test/config.test.ts +++ b/packages/svelte/test/config.test.ts @@ -60,7 +60,7 @@ describe('withSentryConfig', () => { const wrappedConfig = withSentryConfig(originalConfig); - expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: [sentryPreproc] }); + expect(wrappedConfig).toEqual({ ...originalConfig }); }); it('handles multiple wraps correctly by only adding our preprocessors once', () => { diff --git a/packages/svelte/test/performance.test.ts b/packages/svelte/test/performance.test.ts index fdd2ed089a87..64e38599cdda 100644 --- a/packages/svelte/test/performance.test.ts +++ b/packages/svelte/test/performance.test.ts @@ -9,7 +9,6 @@ import { getClient, getCurrentScope, getIsolationScope, init, startSpan } from ' import type { TransactionEvent } from '@sentry/core'; -// @ts-expect-error svelte import import DummyComponent from './components/Dummy.svelte'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -37,7 +36,7 @@ describe('Sentry.trackComponent()', () => { }); }); - it('creates init and update spans on component initialization', async () => { + it('creates init spans on component initialization by default', async () => { startSpan({ name: 'outer' }, span => { expect(span).toBeDefined(); render(DummyComponent, { props: { options: {} } }); @@ -47,7 +46,7 @@ describe('Sentry.trackComponent()', () => { expect(transactions).toHaveLength(1); const transaction = transactions[0]!; - expect(transaction.spans).toHaveLength(2); + expect(transaction.spans).toHaveLength(1); const rootSpanId = transaction.contexts?.trace?.span_id; expect(rootSpanId).toBeDefined(); @@ -68,29 +67,14 @@ describe('Sentry.trackComponent()', () => { timestamp: expect.any(Number), trace_id: expect.stringMatching(/[a-f0-9]{32}/), }); - - expect(transaction.spans![1]).toEqual({ - data: { - 'sentry.op': 'ui.svelte.update', - 'sentry.origin': 'auto.ui.svelte', - }, - description: '', - op: 'ui.svelte.update', - origin: 'auto.ui.svelte', - parent_span_id: rootSpanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }); }); - it('creates an update span, when the component is updated', async () => { + it('creates an update span, if `trackUpdates` is `true`', async () => { startSpan({ name: 'outer' }, async span => { expect(span).toBeDefined(); // first we create the component - const { component } = render(DummyComponent, { props: { options: {} } }); + const { component } = render(DummyComponent, { props: { options: { trackUpdates: true } } }); // then trigger an update // (just changing the trackUpdates prop so that we trigger an update. # @@ -175,7 +159,7 @@ describe('Sentry.trackComponent()', () => { startSpan({ name: 'outer' }, span => { expect(span).toBeDefined(); - render(DummyComponent, { props: { options: { trackInit: false } } }); + render(DummyComponent, { props: { options: { trackInit: false, trackUpdates: true } } }); }); await getClient()?.flush(); @@ -206,7 +190,13 @@ describe('Sentry.trackComponent()', () => { expect(span).toBeDefined(); render(DummyComponent, { - props: { options: { componentName: 'CustomComponentName' } }, + props: { + options: { + componentName: 'CustomComponentName', + // enabling updates to check for both span names in one test + trackUpdates: true, + }, + }, }); }); @@ -220,7 +210,7 @@ describe('Sentry.trackComponent()', () => { expect(transaction.spans![1]?.description).toEqual(''); }); - it("doesn't do anything, if there's no ongoing transaction", async () => { + it("doesn't do anything, if there's no ongoing parent span", async () => { render(DummyComponent, { props: { options: { componentName: 'CustomComponentName' } }, }); @@ -230,11 +220,11 @@ describe('Sentry.trackComponent()', () => { expect(transactions).toHaveLength(0); }); - it("doesn't record update spans, if there's no ongoing root span at that time", async () => { + it("doesn't record update spans, if there's no ongoing parent span at that time", async () => { const component = startSpan({ name: 'outer' }, span => { expect(span).toBeDefined(); - const { component } = render(DummyComponent, { props: { options: {} } }); + const { component } = render(DummyComponent, { props: { options: { trackUpdates: true } } }); return component; }); diff --git a/packages/svelte/test/preprocessors.test.ts b/packages/svelte/test/preprocessors.test.ts index f816c67e706c..b4d607e35a40 100644 --- a/packages/svelte/test/preprocessors.test.ts +++ b/packages/svelte/test/preprocessors.test.ts @@ -24,7 +24,7 @@ function expectComponentCodeToBeModified( preprocessedComponents.forEach(cmp => { const expectedFunctionCallOptions = { trackInit: options?.trackInit ?? true, - trackUpdates: options?.trackUpdates ?? true, + trackUpdates: options?.trackUpdates ?? false, componentName: cmp.name, }; const expectedFunctionCall = `trackComponent(${JSON.stringify(expectedFunctionCallOptions)});\n`; @@ -115,7 +115,7 @@ describe('componentTrackingPreprocessor', () => { expect(cmp2?.newCode).toEqual(cmp2?.originalCode); - expectComponentCodeToBeModified([cmp1!, cmp3!], { trackInit: true, trackUpdates: true }); + expectComponentCodeToBeModified([cmp1!, cmp3!], { trackInit: true, trackUpdates: false }); }); it('doesnt inject the function call to the same component more than once', () => { @@ -149,7 +149,7 @@ describe('componentTrackingPreprocessor', () => { return { ...cmp, newCode: res.code, map: res.map }; }); - expectComponentCodeToBeModified([cmp11!, cmp2!], { trackInit: true, trackUpdates: true }); + expectComponentCodeToBeModified([cmp11!, cmp2!], { trackInit: true }); expect(cmp12!.newCode).toEqual(cmp12!.originalCode); }); @@ -228,7 +228,7 @@ describe('componentTrackingPreprocessor', () => { expect(processedCode.code).toEqual( '\n' + "

I'm just a plain component

\n" + '', @@ -248,7 +248,7 @@ describe('componentTrackingPreprocessor', () => { expect(processedCode.code).toEqual( '\n" + "

I'm a component with a script

\n" + '', @@ -267,7 +267,7 @@ describe('componentTrackingPreprocessor', () => { expect(processedCode.code).toEqual( '", ); });