Skip to content

Commit cc59c02

Browse files
committed
fix(vue): Make pageload span handling more reliable
We used to rely on a somewhat complex heuristic to determine if a router change is a pageload or not. This somehow did not work anymore here: #16783 in nuxt-3-min. Likely some vue router difference... However, I think this can be simplified anyhow, by just checking if we have an active pageload span. That seems to work reliably enough.
1 parent 7f3f6f7 commit cc59c02

File tree

2 files changed

+32
-46
lines changed

2 files changed

+32
-46
lines changed

dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.client.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForTransaction } from '@sentry-internal/test-utils';
3-
import type { Span } from '@sentry/nuxt';
43

54
test('sends a pageload root span with a parameterized URL', async ({ page }) => {
65
const transactionPromise = waitForTransaction('nuxt-3-min', async transactionEvent => {
7-
return transactionEvent.transaction === '/test-param/:param()';
6+
return (
7+
transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/test-param/:param()'
8+
);
89
});
910

1011
await page.goto(`/test-param/1234`);
@@ -39,7 +40,7 @@ test('sends component tracking spans when `trackComponents` is enabled', async (
3940
await page.goto(`/client-error`);
4041

4142
const rootSpan = await transactionPromise;
42-
const errorButtonSpan = rootSpan.spans.find((span: Span) => span.description === 'Vue <ErrorButton>');
43+
const errorButtonSpan = rootSpan.spans.find(span => span.description === 'Vue <ErrorButton>');
4344

4445
const expected = {
4546
data: { 'sentry.origin': 'auto.ui.vue', 'sentry.op': 'ui.vue.mount' },

packages/vue/src/router.ts

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -50,32 +50,15 @@ export function instrumentVueRouter(
5050
},
5151
startNavigationSpanFn: (context: StartSpanOptions) => void,
5252
): void {
53-
let isFirstPageLoad = true;
53+
let hasHandledFirstPageLoad = false;
5454

5555
router.onError(error => captureException(error, { mechanism: { handled: false } }));
5656

57-
router.beforeEach((to, from, next) => {
58-
// According to docs we could use `from === VueRouter.START_LOCATION` but I couldn't get it working for Vue 2
59-
// https://router.vuejs.org/api/#router-start-location
60-
// https://next.router.vuejs.org/api/#start-location
61-
// Additionally, Nuxt does not provide the possibility to check for `from.matched.length === 0` (this is never 0).
62-
// Therefore, a flag was added to track the page-load: isFirstPageLoad
63-
64-
// from.name:
65-
// - Vue 2: null
66-
// - Vue 3: undefined
67-
// - Nuxt: undefined
68-
// hence only '==' instead of '===', because `undefined == null` evaluates to `true`
69-
const isPageLoadNavigation =
70-
(from.name == null && from.matched.length === 0) || (from.name === undefined && isFirstPageLoad);
71-
72-
if (isFirstPageLoad) {
73-
isFirstPageLoad = false;
74-
}
57+
router.beforeEach((to, _from, next) => {
58+
// We avoid trying to re-fetch the page load span when we know we already handled it the first time
59+
const activePageLoadSpan = !hasHandledFirstPageLoad ? getActivePageLoadSpan() : undefined;
7560

76-
const attributes: SpanAttributes = {
77-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue',
78-
};
61+
const attributes: SpanAttributes = {};
7962

8063
for (const key of Object.keys(to.params)) {
8164
attributes[`params.${key}`] = to.params[key];
@@ -102,30 +85,33 @@ export function instrumentVueRouter(
10285

10386
getCurrentScope().setTransactionName(spanName);
10487

105-
if (options.instrumentPageLoad && isPageLoadNavigation) {
106-
const activeRootSpan = getActiveRootSpan();
107-
if (activeRootSpan) {
108-
const existingAttributes = spanToJSON(activeRootSpan).data;
109-
if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') {
110-
activeRootSpan.updateName(spanName);
111-
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource);
112-
}
113-
// Set router attributes on the existing pageload transaction
114-
// This will override the origin, and add params & query attributes
115-
activeRootSpan.setAttributes({
116-
...attributes,
117-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue',
118-
});
88+
// Update the existing page load span with parametrized route information
89+
if (options.instrumentPageLoad && activePageLoadSpan) {
90+
const existingAttributes = spanToJSON(activePageLoadSpan).data;
91+
if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') {
92+
activePageLoadSpan.updateName(spanName);
93+
activePageLoadSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource);
11994
}
95+
96+
// Set router attributes on the existing pageload transaction
97+
// This will override the origin, and add params & query attributes
98+
activePageLoadSpan.setAttributes({
99+
...attributes,
100+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue',
101+
});
102+
103+
hasHandledFirstPageLoad = true;
120104
}
121105

122-
if (options.instrumentNavigation && !isPageLoadNavigation) {
123-
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource;
124-
attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.navigation.vue';
106+
if (options.instrumentNavigation && !activePageLoadSpan) {
125107
startNavigationSpanFn({
126108
name: spanName,
127109
op: 'navigation',
128-
attributes,
110+
attributes: {
111+
...attributes,
112+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue',
113+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: transactionSource,
114+
},
129115
});
130116
}
131117

@@ -138,7 +124,7 @@ export function instrumentVueRouter(
138124
});
139125
}
140126

141-
function getActiveRootSpan(): Span | undefined {
127+
function getActivePageLoadSpan(): Span | undefined {
142128
const span = getActiveSpan();
143129
const rootSpan = span && getRootSpan(span);
144130

@@ -148,6 +134,5 @@ function getActiveRootSpan(): Span | undefined {
148134

149135
const op = spanToJSON(rootSpan).op;
150136

151-
// Only use this root span if it is a pageload or navigation span
152-
return op === 'navigation' || op === 'pageload' ? rootSpan : undefined;
137+
return op === 'pageload' ? rootSpan : undefined;
153138
}

0 commit comments

Comments
 (0)