Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ test('sends a pageload root span with a parameterized URL', async ({ page }) =>
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'params.param': '1234',
'url.template': '/test-param/:param()',
'url.path': '/test-param/1234',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ test('sends a pageload root span with a parameterized URL', async ({ page }) =>
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'params.param': '1234',
'url.template': '/test-param/:param()',
'url.path': '/test-param/1234',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ test('sends a pageload root span with a parameterized URL', async ({ page }) =>
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'params.param': '1234',
'url.template': '/test-param/:param()',
'url.path': '/test-param/1234',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ test('sends a pageload root span with a parameterized URL', async ({ page }) =>
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'params.param': '1234',
'url.template': '/test-param/:param()',
'url.path': '/test-param/1234',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ test('sends a pageload root span with a parameterized URL', async ({ page }) =>
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'params.param': '1234',
'url.template': '/test-param/:param()',
'url.path': '/test-param/1234',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand All @@ -31,6 +34,41 @@ test('sends a pageload root span with a parameterized URL', async ({ page }) =>
});
});

test('sends a navigation root span with a parameterized URL', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-4', async transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/test-param/:param()'
);
});

await page.goto(`/`);
await page.getByText('Fetch Param').click();

const rootSpan = await transactionPromise;

expect(rootSpan).toMatchObject({
contexts: {
trace: {
data: {
'sentry.source': 'route',
'sentry.origin': 'auto.navigation.vue',
'sentry.op': 'navigation',
'params.param': '1234',
'url.template': '/test-param/:param()',
'url.path': '/test-param/1234',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/),
},
op: 'navigation',
origin: 'auto.navigation.vue',
},
},
transaction: '/test-param/:param()',
transaction_info: {
source: 'route',
},
});
});

test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-4', async transactionEvent => {
return transactionEvent.transaction === '/client-error';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ test('sends a pageload root span with a parameterized URL', async ({ page }) =>
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'params.param': '1234',
'url.template': '/test-param/:param()',
'url.path': '/test-param/1234',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand All @@ -31,6 +34,41 @@ test('sends a pageload root span with a parameterized URL', async ({ page }) =>
});
});

test('sends a navigation root span with a parameterized URL', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-5', async transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/test-param/:param()'
);
});

await page.goto(`/`);
await page.getByText('Fetch Param').click();

const rootSpan = await transactionPromise;

expect(rootSpan).toMatchObject({
contexts: {
trace: {
data: {
'sentry.source': 'route',
'sentry.origin': 'auto.navigation.vue',
'sentry.op': 'navigation',
'params.param': '1234',
'url.template': '/test-param/:param()',
'url.path': '/test-param/1234',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-param\/1234$/),
},
op: 'navigation',
origin: 'auto.navigation.vue',
},
},
transaction: '/test-param/:param()',
transaction_info: {
source: 'route',
},
});
});

test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => {
const transactionPromise = waitForTransaction('nuxt-5', async transactionEvent => {
return transactionEvent.transaction === '/client-error';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) =
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'params.id': '456',
'url.template': '/users/:id',
'url.path': '/users/456',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/456$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand Down Expand Up @@ -54,6 +57,9 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
'sentry.origin': 'auto.navigation.vue',
'sentry.op': 'navigation',
'params.id': '123',
'url.template': '/users/:id',
'url.path': '/users/123',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/users\/123$/),
},
op: 'navigation',
origin: 'auto.navigation.vue',
Expand Down Expand Up @@ -83,6 +89,9 @@ test('sends a pageload transaction with a nested route URL', async ({ page }) =>
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'params.id': '123',
'url.template': '/categories/:id',
'url.path': '/categories/123',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/categories\/123$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand Down Expand Up @@ -111,6 +120,8 @@ test('sends a pageload transaction with a route name as transaction name if avai
'sentry.source': 'custom',
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'url.path': '/about',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/about$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand Down Expand Up @@ -139,6 +150,9 @@ test('sends a lifecycle span for each tracked components', async ({ page }) => {
'sentry.source': 'route',
'sentry.origin': 'auto.pageload.vue',
'sentry.op': 'pageload',
'url.template': '/components',
'url.path': '/components',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/components$/),
},
op: 'pageload',
origin: 'auto.pageload.vue',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ test('sends pageload transaction with web vitals measurements', async ({ page })
trace: {
op: 'pageload',
origin: 'auto.pageload.vue.tanstack_router',
data: {
'sentry.origin': 'auto.pageload.vue.tanstack_router',
'sentry.source': 'route',
'sentry.op': 'pageload',
'url.template': '/',
'url.path': '/',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/),
},
},
},
transaction: '/',
Expand Down Expand Up @@ -59,6 +67,9 @@ test('sends a pageload transaction with a parameterized URL', async ({ page }) =
'sentry.origin': 'auto.pageload.vue.tanstack_router',
'sentry.op': 'pageload',
'url.path.parameter.postId': '456',
'url.template': '/posts/$postId',
'url.path': '/posts/456',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/456$/),
},
op: 'pageload',
origin: 'auto.pageload.vue.tanstack_router',
Expand Down Expand Up @@ -101,6 +112,9 @@ test('sends a navigation transaction with a parameterized URL', async ({ page })
'sentry.origin': 'auto.navigation.vue.tanstack_router',
'sentry.op': 'navigation',
'url.path.parameter.postId': '2',
'url.template': '/posts/$postId',
'url.path': '/posts/2',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/),
},
op: 'navigation',
origin: 'auto.navigation.vue.tanstack_router',
Expand Down Expand Up @@ -132,6 +146,9 @@ test('sends a pageload transaction named after the resolved route when a redirec
'sentry.origin': 'auto.pageload.vue.tanstack_router',
'sentry.op': 'pageload',
'url.path.parameter.postId': '1',
'url.template': '/posts/$postId',
'url.path': '/posts/1',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/),
},
op: 'pageload',
origin: 'auto.pageload.vue.tanstack_router',
Expand Down Expand Up @@ -172,6 +189,9 @@ test('sends a navigation transaction when a redirect is thrown in beforeLoad', a
'sentry.origin': 'auto.navigation.vue.tanstack_router',
'sentry.op': 'navigation',
'url.path.parameter.postId': '1',
'url.template': '/posts/$postId',
'url.path': '/posts/1',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/1$/),
},
op: 'navigation',
origin: 'auto.navigation.vue.tanstack_router',
Expand Down Expand Up @@ -217,6 +237,9 @@ test('sends a navigation transaction for a normal navigation that happens after
'sentry.origin': 'auto.navigation.vue.tanstack_router',
'sentry.op': 'navigation',
'url.path.parameter.postId': '2',
'url.template': '/posts/$postId',
'url.path': '/posts/2',
'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/posts\/2$/),
},
op: 'navigation',
origin: 'auto.navigation.vue.tanstack_router',
Expand Down
3 changes: 2 additions & 1 deletion packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
},
"dependencies": {
"@sentry/browser": "10.64.0",
"@sentry/core": "10.64.0"
"@sentry/core": "10.64.0",
"@sentry/conventions": "^0.15.1"
},
"peerDependencies": {
"@tanstack/vue-router": "^1.64.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/vue/src/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export function browserTracingIntegration(options: VueBrowserTracingIntegrationO
afterAllSetup(client) {
integration.afterAllSetup(client);

const startNavigationSpan = (options: StartSpanOptions): void => {
startBrowserTracingNavigationSpan(client, options);
const startNavigationSpan = (options: StartSpanOptions, destinationUrl: string): void => {
startBrowserTracingNavigationSpan(client, options, { url: destinationUrl });
};

instrumentVueRouter(router, { routeLabel, instrumentNavigation, instrumentPageLoad }, startNavigationSpan);
Expand Down
34 changes: 22 additions & 12 deletions packages/vue/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { captureException } from '@sentry/browser';
import { captureException, getAbsoluteUrl } from '@sentry/browser';
import { PARAMS_KEY, URL_PATH_PARAMETER_KEY, URL_TEMPLATE } from '@sentry/conventions/attributes';
import type { Span, SpanAttributes, StartSpanOptions, TransactionSource } from '@sentry/core';
import {
getActiveSpan,
Expand All @@ -14,6 +15,8 @@ import {
export type Route = {
/** Unparameterized URL */
path: string;
/** Resolved URL including query and hash */
fullPath?: string;
/**
* Query params (keys map to null when there is no value associated, e.g. "?foo" and to an array when there are
* multiple query params that have the same key, e.g. "?foo&foo=bar")
Expand Down Expand Up @@ -51,7 +54,7 @@ export function instrumentVueRouter(
instrumentPageLoad: boolean;
instrumentNavigation: boolean;
},
startNavigationSpanFn: (context: StartSpanOptions) => void,
startNavigationSpanFn: (context: StartSpanOptions, destinationUrl: string) => void,
): void {
let hasHandledFirstPageLoad = false;

Expand All @@ -71,8 +74,8 @@ export function instrumentVueRouter(
const attributes: SpanAttributes = {};

for (const key of Object.keys(to.params)) {
attributes[`url.path.parameter.${key}`] = to.params[key];
attributes[`params.${key}`] = to.params[key]; // params.[key] is an alias
attributes[URL_PATH_PARAMETER_KEY.replace('<key>', key)] = to.params[key];
attributes[PARAMS_KEY.replace('<key>', key)] = to.params[key]; // params.[key] is an alias
}
for (const key of Object.keys(to.query)) {
const value = to.query[key];
Expand All @@ -94,6 +97,10 @@ export function instrumentVueRouter(
transactionSource = 'route';
}

if (transactionSource === 'route') {
attributes[URL_TEMPLATE] = spanName;
}

getCurrentScope().setTransactionName(spanName);

// Update the existing page load span with parametrized route information
Comment thread
sentry[bot] marked this conversation as resolved.
Expand All @@ -115,15 +122,18 @@ export function instrumentVueRouter(
}

if (options.instrumentNavigation && !activePageLoadSpan) {
startNavigationSpanFn({
name: spanName,
Comment thread
cursor[bot] marked this conversation as resolved.
op: 'navigation',
attributes: {
...attributes,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: transactionSource,
startNavigationSpanFn(
{
name: spanName,
op: 'navigation',
attributes: {
...attributes,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: transactionSource,
},
},
});
getAbsoluteUrl(to.fullPath ?? to.path),
Comment thread
cursor[bot] marked this conversation as resolved.
);
}

// Vue Router 3 requires `next()` to be called to resolve the navigation guard.
Expand Down
Loading
Loading