diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index e4e2fdb70f4e..5aadfdd876be 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -12,11 +12,11 @@ type Mixins = Parameters[0]; interface VueSentry extends ViewModel { readonly $root: VueSentry; - $_sentrySpans?: { + $_sentryComponentSpans?: { [key: string]: Span | undefined; }; - $_sentryRootSpan?: Span; - $_sentryRootSpanTimer?: ReturnType; + $_sentryRootComponentSpan?: Span; + $_sentryRootComponentSpanTimer?: ReturnType; } // Mappings from operation to corresponding lifecycle hook. @@ -31,16 +31,16 @@ const HOOKS: { [key in Operation]: Hook[] } = { update: ['beforeUpdate', 'updated'], }; -/** Finish top-level span and activity with a debounce configured using `timeout` option */ -function finishRootSpan(vm: VueSentry, timestamp: number, timeout: number): void { - if (vm.$_sentryRootSpanTimer) { - clearTimeout(vm.$_sentryRootSpanTimer); +/** Finish top-level component span and activity with a debounce configured using `timeout` option */ +function finishRootComponentSpan(vm: VueSentry, timestamp: number, timeout: number): void { + if (vm.$_sentryRootComponentSpanTimer) { + clearTimeout(vm.$_sentryRootComponentSpanTimer); } - vm.$_sentryRootSpanTimer = setTimeout(() => { - if (vm.$root?.$_sentryRootSpan) { - vm.$root.$_sentryRootSpan.end(timestamp); - vm.$root.$_sentryRootSpan = undefined; + vm.$_sentryRootComponentSpanTimer = setTimeout(() => { + if (vm.$root?.$_sentryRootComponentSpan) { + vm.$root.$_sentryRootComponentSpan.end(timestamp); + vm.$root.$_sentryRootComponentSpan = undefined; } }, timeout); } @@ -77,11 +77,12 @@ export const createTracingMixins = (options: Partial = {}): Mixi for (const internalHook of internalHooks) { mixins[internalHook] = function (this: VueSentry) { - const isRoot = this.$root === this; + const isRootComponent = this.$root === this; - if (isRoot) { - this.$_sentryRootSpan = - this.$_sentryRootSpan || + // 1. Root Component span creation + if (isRootComponent) { + this.$_sentryRootComponentSpan = + this.$_sentryRootComponentSpan || startInactiveSpan({ name: 'Application Render', op: `${VUE_OP}.render`, @@ -92,35 +93,39 @@ export const createTracingMixins = (options: Partial = {}): Mixi }); } - // 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); + // 2. Component tracking filter + const componentName = formatComponentName(this, false); - const shouldTrack = Array.isArray(options.trackComponents) - ? findTrackComponent(options.trackComponents, name) - : options.trackComponents; + const shouldTrack = + isRootComponent || // We always want to track the root component + (Array.isArray(options.trackComponents) + ? findTrackComponent(options.trackComponents, componentName) + : options.trackComponents); - // We always want to track root component - if (!isRoot && !shouldTrack) { + if (!shouldTrack) { return; } - this.$_sentrySpans = this.$_sentrySpans || {}; + this.$_sentryComponentSpans = this.$_sentryComponentSpans || {}; - // Start a new span if current hook is a 'before' hook. - // Otherwise, retrieve the current span and finish it. - if (internalHook == internalHooks[0]) { - const activeSpan = this.$root?.$_sentryRootSpan || getActiveSpan(); + // 3. Span lifecycle management based on the hook type + const isBeforeHook = internalHook === internalHooks[0]; + const activeSpan = this.$root?.$_sentryRootComponentSpan || getActiveSpan(); + + if (isBeforeHook) { + // Starting a new span in the "before" hook if (activeSpan) { - // Cancel old span for this hook operation in case it didn't get cleaned up. We're not actually sure if it - // will ever be the case that cleanup hooks re not called, but we had users report that spans didn't get - // finished so we finish the span before starting a new one, just to be sure. - const oldSpan = this.$_sentrySpans[operation]; + // Cancel any existing span for this operation (safety measure) + // We're actually not sure if it will ever be the case that cleanup hooks were not called. + // However, we had users report that spans didn't get finished, so we finished the span before + // starting a new one, just to be sure. + const oldSpan = this.$_sentryComponentSpans[operation]; if (oldSpan) { oldSpan.end(); } - this.$_sentrySpans[operation] = startInactiveSpan({ - name: `Vue ${name}`, + this.$_sentryComponentSpans[operation] = startInactiveSpan({ + name: `Vue ${componentName}`, op: `${VUE_OP}.${operation}`, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.vue', @@ -131,13 +136,14 @@ export const createTracingMixins = (options: Partial = {}): Mixi } } else { // The span should already be added via the first handler call (in the 'before' hook) - const span = this.$_sentrySpans[operation]; + const span = this.$_sentryComponentSpans[operation]; // The before hook did not start the tracking span, so the span was not added. // This is probably because it happened before there is an active transaction - if (!span) return; + if (!span) return; // Skip if no span was created in the "before" hook span.end(); - finishRootSpan(this, timestampInSeconds(), options.timeout || 2000); + // For any "after" hook, also schedule the root component span to finish + finishRootComponentSpan(this, timestampInSeconds(), options.timeout || 2000); } }; } diff --git a/packages/vue/test/tracing/tracingMixin.test.ts b/packages/vue/test/tracing/tracingMixin.test.ts index b9a92a4f7395..d67690271ed2 100644 --- a/packages/vue/test/tracing/tracingMixin.test.ts +++ b/packages/vue/test/tracing/tracingMixin.test.ts @@ -46,14 +46,14 @@ describe('Vue Tracing Mixins', () => { mockRootInstance = { $root: null, componentName: 'RootComponent', - $_sentrySpans: {}, + $_sentryComponentSpans: {}, }; mockRootInstance.$root = mockRootInstance; // Self-reference for root mockVueInstance = { $root: mockRootInstance, componentName: 'TestComponent', - $_sentrySpans: {}, + $_sentryComponentSpans: {}, }; (getActiveSpan as any).mockReturnValue({ id: 'parent-span' }); @@ -131,7 +131,7 @@ describe('Vue Tracing Mixins', () => { // todo/fixme: This root component span is only finished if trackComponents is true --> it should probably be always finished const mixins = createTracingMixins({ trackComponents: true, timeout: 1000 }); const rootMockSpan = mockSpanFactory(); - mockRootInstance.$_sentryRootSpan = rootMockSpan; + mockRootInstance.$_sentryRootComponentSpan = rootMockSpan; // Create and finish a component span mixins.beforeMount.call(mockVueInstance); @@ -160,10 +160,10 @@ describe('Vue Tracing Mixins', () => { op: 'ui.vue.mount', }), ); - expect(mockVueInstance.$_sentrySpans.mount).toBeDefined(); + expect(mockVueInstance.$_sentryComponentSpans.mount).toBeDefined(); // 2. Get the span for verification - const componentSpan = mockVueInstance.$_sentrySpans.mount; + const componentSpan = mockVueInstance.$_sentryComponentSpans.mount; // 3. End span in "after" hook mixins.mounted.call(mockVueInstance); @@ -175,14 +175,14 @@ describe('Vue Tracing Mixins', () => { // Create an existing span first const oldSpan = mockSpanFactory(); - mockVueInstance.$_sentrySpans.mount = oldSpan; + mockVueInstance.$_sentryComponentSpans.mount = oldSpan; // Create a new span for the same operation mixins.beforeMount.call(mockVueInstance); // Verify old span was ended and new span was created expect(oldSpan.end).toHaveBeenCalled(); - expect(mockVueInstance.$_sentrySpans.mount).not.toBe(oldSpan); + expect(mockVueInstance.$_sentryComponentSpans.mount).not.toBe(oldSpan); }); it('should gracefully handle when "after" hook is called without "before" hook', () => { @@ -197,7 +197,7 @@ describe('Vue Tracing Mixins', () => { // Remove active spans (getActiveSpan as any).mockReturnValue(null); - mockRootInstance.$_sentryRootSpan = null; + mockRootInstance.$_sentryRootComponentSpan = null; // Try to create a span mixins.beforeMount.call(mockVueInstance);