diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts
index a17208711eff..8c3ac217716f 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts
+++ b/dev-packages/e2e-tests/test-applications/vue-3/src/router/index.ts
@@ -17,6 +17,10 @@ const router = createRouter({
path: '/users/:id',
component: () => import('../views/UserIdView.vue'),
},
+ {
+ path: '/users-error/:id',
+ component: () => import('../views/UserIdErrorView.vue'),
+ },
],
});
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdErrorView.vue b/dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdErrorView.vue
new file mode 100644
index 000000000000..dab7ea873f37
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/vue-3/src/views/UserIdErrorView.vue
@@ -0,0 +1,10 @@
+
+
+
+ (Error) User ID: {{ $route.params.id }}
+
+
diff --git a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts
index b9933299c8c0..f829339bfb33 100644
--- a/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts
+++ b/dev-packages/e2e-tests/test-applications/vue-3/tests/errors.test.ts
@@ -25,5 +25,34 @@ test('sends an error', async ({ page }) => {
},
],
},
+ transaction: '/',
+ });
+});
+
+test('sends an error with a parameterized transaction name', async ({ page }) => {
+ const errorPromise = waitForError('vue-3', async errorEvent => {
+ return !errorEvent.type;
+ });
+
+ await page.goto(`/users-error/456`);
+
+ await page.locator('#userErrorBtn').click();
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'This is a Vue test error',
+ mechanism: {
+ type: 'generic',
+ handled: false,
+ },
+ },
+ ],
+ },
+ transaction: '/users-error/:id',
});
});
diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts
index 8edb6eefd640..016dac913986 100644
--- a/packages/vue/src/router.ts
+++ b/packages/vue/src/router.ts
@@ -3,6 +3,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
getActiveSpan,
+ getCurrentScope,
getRootSpan,
spanToJSON,
} from '@sentry/core';
@@ -77,22 +78,24 @@ export function instrumentVueRouter(
}
// Determine a name for the routing transaction and where that name came from
- let transactionName: string = to.path;
+ let spanName: string = to.path;
let transactionSource: TransactionSource = 'url';
if (to.name && options.routeLabel !== 'path') {
- transactionName = to.name.toString();
+ spanName = to.name.toString();
transactionSource = 'custom';
} else if (to.matched[0] && to.matched[0].path) {
- transactionName = to.matched[0].path;
+ spanName = to.matched[0].path;
transactionSource = 'route';
}
+ getCurrentScope().setTransactionName(spanName);
+
if (options.instrumentPageLoad && isPageLoadNavigation) {
const activeRootSpan = getActiveRootSpan();
if (activeRootSpan) {
const existingAttributes = spanToJSON(activeRootSpan).data || {};
if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') {
- activeRootSpan.updateName(transactionName);
+ activeRootSpan.updateName(spanName);
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource);
}
// Set router attributes on the existing pageload transaction
@@ -108,7 +111,7 @@ export function instrumentVueRouter(
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource;
attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.navigation.vue';
startNavigationSpanFn({
- name: transactionName,
+ name: spanName,
op: 'navigation',
attributes,
});
diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts
index 1a27e84961b1..8ff42d49e2b9 100644
--- a/packages/vue/test/router.test.ts
+++ b/packages/vue/test/router.test.ts
@@ -276,6 +276,35 @@ describe('instrumentVueRouter()', () => {
expect(mockRootSpan.name).toEqual('customTxnName');
});
+ it("updates the scope's `transactionName` when a route is resolved", () => {
+ const mockStartSpan = jest.fn().mockImplementation(_ => {
+ return {};
+ });
+
+ const scopeSetTransactionNameSpy = jest.fn();
+
+ // @ts-expect-error - only creating a partial scope but that's fine
+ jest.spyOn(SentryCore, 'getCurrentScope').mockImplementation(() => ({
+ setTransactionName: scopeSetTransactionNameSpy,
+ }));
+
+ instrumentVueRouter(
+ mockVueRouter,
+ { routeLabel: 'name', instrumentPageLoad: true, instrumentNavigation: true },
+ mockStartSpan,
+ );
+
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
+
+ const from = testRoutes['initialPageloadRoute'];
+ const to = testRoutes['normalRoute1'];
+
+ beforeEachCallback(to, from, mockNext);
+
+ expect(scopeSetTransactionNameSpy).toHaveBeenCalledTimes(1);
+ expect(scopeSetTransactionNameSpy).toHaveBeenCalledWith('/books/:bookId/chapter/:chapterId');
+ });
+
test.each([
[false, 0],
[true, 1],