Skip to content

Commit a9ef859

Browse files
authored
feat(vue): Update scope's transactionName when resolving a route (#11423)
1 parent 532ce3f commit a9ef859

File tree

5 files changed

+80
-5
lines changed

5 files changed

+80
-5
lines changed

dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ const router = createRouter({
1717
path: '/users/:id',
1818
component: () => import('../views/UserIdView.vue'),
1919
},
20+
{
21+
path: '/users-error/:id',
22+
component: () => import('../views/UserIdErrorView.vue'),
23+
},
2024
],
2125
});
2226

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script setup lang="ts">
2+
function throwError() {
3+
throw new Error('This is a Vue test error');
4+
}
5+
</script>
6+
7+
<template>
8+
<h1>(Error) User ID: {{ $route.params.id }}</h1>
9+
<button id="userErrorBtn" @click="throwError">Throw Error</button>
10+
</template>

dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,34 @@ test('sends an error', async ({ page }) => {
2525
},
2626
],
2727
},
28+
transaction: '/',
29+
});
30+
});
31+
32+
test('sends an error with a parameterized transaction name', async ({ page }) => {
33+
const errorPromise = waitForError('vue-3', async errorEvent => {
34+
return !errorEvent.type;
35+
});
36+
37+
await page.goto(`/users-error/456`);
38+
39+
await page.locator('#userErrorBtn').click();
40+
41+
const error = await errorPromise;
42+
43+
expect(error).toMatchObject({
44+
exception: {
45+
values: [
46+
{
47+
type: 'Error',
48+
value: 'This is a Vue test error',
49+
mechanism: {
50+
type: 'generic',
51+
handled: false,
52+
},
53+
},
54+
],
55+
},
56+
transaction: '/users-error/:id',
2857
});
2958
});

packages/vue/src/router.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
44
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
55
getActiveSpan,
6+
getCurrentScope,
67
getRootSpan,
78
spanToJSON,
89
} from '@sentry/core';
@@ -77,22 +78,24 @@ export function instrumentVueRouter(
7778
}
7879

7980
// Determine a name for the routing transaction and where that name came from
80-
let transactionName: string = to.path;
81+
let spanName: string = to.path;
8182
let transactionSource: TransactionSource = 'url';
8283
if (to.name && options.routeLabel !== 'path') {
83-
transactionName = to.name.toString();
84+
spanName = to.name.toString();
8485
transactionSource = 'custom';
8586
} else if (to.matched[0] && to.matched[0].path) {
86-
transactionName = to.matched[0].path;
87+
spanName = to.matched[0].path;
8788
transactionSource = 'route';
8889
}
8990

91+
getCurrentScope().setTransactionName(spanName);
92+
9093
if (options.instrumentPageLoad && isPageLoadNavigation) {
9194
const activeRootSpan = getActiveRootSpan();
9295
if (activeRootSpan) {
9396
const existingAttributes = spanToJSON(activeRootSpan).data || {};
9497
if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') {
95-
activeRootSpan.updateName(transactionName);
98+
activeRootSpan.updateName(spanName);
9699
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource);
97100
}
98101
// Set router attributes on the existing pageload transaction
@@ -108,7 +111,7 @@ export function instrumentVueRouter(
108111
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource;
109112
attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.navigation.vue';
110113
startNavigationSpanFn({
111-
name: transactionName,
114+
name: spanName,
112115
op: 'navigation',
113116
attributes,
114117
});

packages/vue/test/router.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,35 @@ describe('instrumentVueRouter()', () => {
276276
expect(mockRootSpan.name).toEqual('customTxnName');
277277
});
278278

279+
it("updates the scope's `transactionName` when a route is resolved", () => {
280+
const mockStartSpan = jest.fn().mockImplementation(_ => {
281+
return {};
282+
});
283+
284+
const scopeSetTransactionNameSpy = jest.fn();
285+
286+
// @ts-expect-error - only creating a partial scope but that's fine
287+
jest.spyOn(SentryCore, 'getCurrentScope').mockImplementation(() => ({
288+
setTransactionName: scopeSetTransactionNameSpy,
289+
}));
290+
291+
instrumentVueRouter(
292+
mockVueRouter,
293+
{ routeLabel: 'name', instrumentPageLoad: true, instrumentNavigation: true },
294+
mockStartSpan,
295+
);
296+
297+
const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
298+
299+
const from = testRoutes['initialPageloadRoute'];
300+
const to = testRoutes['normalRoute1'];
301+
302+
beforeEachCallback(to, from, mockNext);
303+
304+
expect(scopeSetTransactionNameSpy).toHaveBeenCalledTimes(1);
305+
expect(scopeSetTransactionNameSpy).toHaveBeenCalledWith('/books/:bookId/chapter/:chapterId');
306+
});
307+
279308
test.each([
280309
[false, 0],
281310
[true, 1],

0 commit comments

Comments
 (0)