Skip to content

ref(vue): Clarify Vue tracing #16487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 42 additions & 36 deletions packages/vue/src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ type Mixins = Parameters<Vue['mixin']>[0];

interface VueSentry extends ViewModel {
readonly $root: VueSentry;
$_sentrySpans?: {
$_sentryComponentSpans?: {
[key: string]: Span | undefined;
};
$_sentryRootSpan?: Span;
$_sentryRootSpanTimer?: ReturnType<typeof setTimeout>;
$_sentryRootComponentSpan?: Span;
$_sentryRootComponentSpanTimer?: ReturnType<typeof setTimeout>;
}

// Mappings from operation to corresponding lifecycle hook.
Expand All @@ -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);
}
Expand Down Expand Up @@ -77,11 +77,12 @@ export const createTracingMixins = (options: Partial<TracingOptions> = {}): 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`,
Expand All @@ -92,35 +93,39 @@ export const createTracingMixins = (options: Partial<TracingOptions> = {}): 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',
Expand All @@ -131,13 +136,14 @@ export const createTracingMixins = (options: Partial<TracingOptions> = {}): 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);
}
};
}
Expand Down
16 changes: 8 additions & 8 deletions packages/vue/test/tracing/tracingMixin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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', () => {
Expand All @@ -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);
Expand Down
Loading