Skip to content

Commit 6b656b4

Browse files
authored
fix(vue): Ensure root component render span always ends (#16488)
This PR fixes a bug discovered in #16486 (comment) where the root component span would not end correctly if `trackComponents` was `false`. Also added a comment to explain the purpose of the root component `ui.vue.render` span. We might want to look into renaming or removing this span in the future but for now, let's fix the behaviour.
1 parent 6fc18fe commit 6b656b4

File tree

2 files changed

+33
-24
lines changed

2 files changed

+33
-24
lines changed

packages/vue/src/tracing.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ const HOOKS: { [key in Operation]: Hook[] } = {
3131
update: ['beforeUpdate', 'updated'],
3232
};
3333

34-
/** Finish top-level component span and activity with a debounce configured using `timeout` option */
35-
function finishRootComponentSpan(vm: VueSentry, timestamp: number, timeout: number): void {
34+
/** End the top-level component span and activity with a debounce configured using `timeout` option */
35+
function maybeEndRootComponentSpan(vm: VueSentry, timestamp: number, timeout: number): void {
3636
if (vm.$_sentryRootComponentSpanTimer) {
3737
clearTimeout(vm.$_sentryRootComponentSpanTimer);
3838
}
@@ -66,6 +66,8 @@ export const createTracingMixins = (options: Partial<TracingOptions> = {}): Mixi
6666

6767
const mixins: Mixins = {};
6868

69+
const rootComponentSpanFinalTimeout = options.timeout || 2000;
70+
6971
for (const operation of hooks) {
7072
// Retrieve corresponding hooks from Vue lifecycle.
7173
// eg. mount => ['beforeMount', 'mounted']
@@ -91,6 +93,9 @@ export const createTracingMixins = (options: Partial<TracingOptions> = {}): Mixi
9193
},
9294
onlyIfParent: true,
9395
});
96+
97+
// call debounced end function once directly, just in case no child components call it
98+
maybeEndRootComponentSpan(this, timestampInSeconds(), rootComponentSpanFinalTimeout);
9499
}
95100

96101
// 2. Component tracking filter
@@ -102,7 +107,10 @@ export const createTracingMixins = (options: Partial<TracingOptions> = {}): Mixi
102107
? findTrackComponent(options.trackComponents, componentName)
103108
: options.trackComponents);
104109

110+
// We always want to track root component
105111
if (!shouldTrack) {
112+
// even if we don't track `this` component, we still want to end the root span eventually
113+
maybeEndRootComponentSpan(this, timestampInSeconds(), rootComponentSpanFinalTimeout);
106114
return;
107115
}
108116

@@ -117,7 +125,7 @@ export const createTracingMixins = (options: Partial<TracingOptions> = {}): Mixi
117125
if (activeSpan) {
118126
// Cancel any existing span for this operation (safety measure)
119127
// We're actually not sure if it will ever be the case that cleanup hooks were not called.
120-
// However, we had users report that spans didn't get finished, so we finished the span before
128+
// However, we had users report that spans didn't end, so we end the span before
121129
// starting a new one, just to be sure.
122130
const oldSpan = this.$_sentryComponentSpans[operation];
123131
if (oldSpan) {
@@ -142,8 +150,8 @@ export const createTracingMixins = (options: Partial<TracingOptions> = {}): Mixi
142150
if (!span) return; // Skip if no span was created in the "before" hook
143151
span.end();
144152

145-
// For any "after" hook, also schedule the root component span to finish
146-
finishRootComponentSpan(this, timestampInSeconds(), options.timeout || 2000);
153+
// For any "after" hook, also schedule the root component span to end
154+
maybeEndRootComponentSpan(this, timestampInSeconds(), rootComponentSpanFinalTimeout);
147155
}
148156
};
149157
}

packages/vue/test/tracing/tracingMixin.test.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ vi.mock('../../src/vendor/components', () => {
2727
};
2828
});
2929

30-
const mockSpanFactory = (): { name?: string; op?: string; end: Mock; startChild: Mock } => ({
30+
const mockSpanFactory = (): { name?: string; op?: string; end: Mock } => ({
3131
name: undefined,
3232
op: undefined,
3333
end: vi.fn(),
34-
startChild: vi.fn(),
3534
});
3635

3736
vi.useFakeTimers();
@@ -127,23 +126,25 @@ describe('Vue Tracing Mixins', () => {
127126
);
128127
});
129128

130-
it('should finish root component span on timer after component spans end', () => {
131-
// todo/fixme: This root component span is only finished if trackComponents is true --> it should probably be always finished
132-
const mixins = createTracingMixins({ trackComponents: true, timeout: 1000 });
133-
const rootMockSpan = mockSpanFactory();
134-
mockRootInstance.$_sentryRootComponentSpan = rootMockSpan;
135-
136-
// Create and finish a component span
137-
mixins.beforeMount.call(mockVueInstance);
138-
mixins.mounted.call(mockVueInstance);
139-
140-
// Root component span should not end immediately
141-
expect(rootMockSpan.end).not.toHaveBeenCalled();
142-
143-
// After timeout, root component span should end
144-
vi.advanceTimersByTime(1001);
145-
expect(rootMockSpan.end).toHaveBeenCalled();
146-
});
129+
it.each([true, false])(
130+
'should finish root component span on timer after component spans end, if trackComponents is %s',
131+
trackComponents => {
132+
const mixins = createTracingMixins({ trackComponents, timeout: 1000 });
133+
const rootMockSpan = mockSpanFactory();
134+
mockRootInstance.$_sentryRootComponentSpan = rootMockSpan;
135+
136+
// Create and finish a component span
137+
mixins.beforeMount.call(mockVueInstance);
138+
mixins.mounted.call(mockVueInstance);
139+
140+
// Root component span should not end immediately
141+
expect(rootMockSpan.end).not.toHaveBeenCalled();
142+
143+
// After timeout, root component span should end
144+
vi.advanceTimersByTime(1001);
145+
expect(rootMockSpan.end).toHaveBeenCalled();
146+
},
147+
);
147148
});
148149

149150
describe('Component Span Lifecycle', () => {

0 commit comments

Comments
 (0)