diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts index f4a01d4285c5..13064ce04080 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/main.ts @@ -19,6 +19,7 @@ Sentry.init({ }), ], tunnel: `http://localhost:3031/`, // proxy server + trackComponents: ['ComponentMainView', ''], }); app.use(router); diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts index aac6fb815f43..3a05e4f1055a 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts @@ -30,6 +30,10 @@ const router = createRouter({ }, ], }, + { + path: '/components', + component: () => import('../views/ComponentMainView.vue'), + }, ], }); diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentMainView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentMainView.vue new file mode 100644 index 000000000000..69f60c769f54 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentMainView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentOneView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentOneView.vue new file mode 100644 index 000000000000..02c5d133ffb3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentOneView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentTwoView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentTwoView.vue new file mode 100644 index 000000000000..f5a1c3c5f46d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/ComponentTwoView.vue @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts index d9a594b5abe7..03c2cdbb01e5 100644 --- a/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/performance.test.ts @@ -122,3 +122,86 @@ test('sends a pageload transaction with a route name as transaction name if avai }, }); }); + +test('sends a lifecycle span for each tracked components', async ({ page }) => { + const transactionPromise = waitForTransaction('vue-3', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/components`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + spans: expect.arrayContaining([ + // enabled by default + expect.objectContaining({ + data: { + 'sentry.op': 'ui.vue.render', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Application Render', + op: 'ui.vue.render', + origin: 'auto.ui.vue', + }), + // enabled by default + expect.objectContaining({ + data: { + 'sentry.op': 'ui.vue.mount', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Vue ', + op: 'ui.vue.mount', + origin: 'auto.ui.vue', + }), + + // without `<>` + expect.objectContaining({ + data: { + 'sentry.op': 'ui.vue.mount', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Vue ', + op: 'ui.vue.mount', + origin: 'auto.ui.vue', + }), + + // with `<>` + expect.objectContaining({ + data: { + 'sentry.op': 'ui.vue.mount', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Vue ', + op: 'ui.vue.mount', + origin: 'auto.ui.vue', + }), + + // not tracked + expect.not.objectContaining({ + data: { + 'sentry.op': 'ui.vue.mount', + 'sentry.origin': 'auto.ui.vue', + }, + description: 'Vue ', + op: 'ui.vue.mount', + origin: 'auto.ui.vue', + }), + ]), + transaction: '/components', + transaction_info: { + source: 'route', + }, + }); +}); diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 70f662559adf..dba67aee4f5f 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -46,6 +46,19 @@ function finishRootSpan(vm: VueSentry, timestamp: number, timeout: number): void }, timeout); } +/** Find if the current component exists in the provided `TracingOptions.trackComponents` array option. */ +export function findTrackComponent(trackComponents: string[], formattedName: string): boolean { + function extractComponentName(name: string): string { + return name.replace(/^<([^\s]*)>(?: at [^\s]*)?$/, '$1'); + } + + const isMatched = trackComponents.some(compo => { + return extractComponentName(formattedName) === extractComponentName(compo); + }); + + return isMatched; +} + export const createTracingMixins = (options: TracingOptions): Mixins => { const hooks = (options.hooks || []) .concat(DEFAULT_HOOKS) @@ -84,8 +97,9 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { // Skip components that we don't want to track to minimize the noise and give a more granular control to the user const name = formatComponentName(this, false); + const shouldTrack = Array.isArray(options.trackComponents) - ? options.trackComponents.indexOf(name) > -1 + ? findTrackComponent(options.trackComponents, name) : options.trackComponents; // We always want to track root component @@ -109,7 +123,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { } this.$_sentrySpans[operation] = startInactiveSpan({ - name: `Vue <${name}>`, + name: `Vue ${name}`, op: `${VUE_OP}.${operation}`, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', diff --git a/packages/vue/test/tracing/trackComponents.test.ts b/packages/vue/test/tracing/trackComponents.test.ts new file mode 100644 index 000000000000..9115522a3033 --- /dev/null +++ b/packages/vue/test/tracing/trackComponents.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { findTrackComponent } from '../../src/tracing'; + +describe('findTrackComponent', () => { + describe('when user-defined array contains ``', () => { + it('returns true if a match is found', () => { + // arrange + const trackComponents = ['', '']; + const formattedComponentName = ''; + + // act + const shouldTrack = findTrackComponent(trackComponents, formattedComponentName); + + // assert + expect(shouldTrack).toBe(true); + }); + }); + describe('when user-defined array contains `Component` without the `<>`', () => { + it('returns true if a match is found', () => { + // arrange + const trackComponents = ['ABC', 'XYZ']; + const formattedComponentName = ''; + + // act + const shouldTrack = findTrackComponent(trackComponents, formattedComponentName); + + // assert + expect(shouldTrack).toBe(true); + }); + }); + describe('when the vue file name is include in the formatted component name', () => { + it('returns true if a match is found', () => { + // arrange + const trackComponents = ['ABC', 'XYZ']; + const formattedComponentName = ' at XYZ.vue'; + + // act + const shouldTrack = findTrackComponent(trackComponents, formattedComponentName); + + // assert + expect(shouldTrack).toBe(true); + }); + }); +});