FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(vue): Register a route provider backed by the Vue router matcher · getsentry/sentry-javascript@3232b67 · GitHub

Commit 3232b67

Browse files
committed
feat(vue): Register a route provider backed by the Vue router matcher
Labels routes through the same helper the navigation instrumentation uses, so a route resolved by the provider can't disagree with the one on the span. Handles both resolve shapes: Vue Router 3 returns `{ route }`, Vue Router 4+ returns the route itself.
1 parent af59909 commit 3232b67

3 files changed

Lines changed: 128 additions & 13 deletions

File tree

‎packages/vue/src/browserTracingIntegration.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
startBrowserTracingNavigationSpan,
44
} from '@sentry/browser';
55
import type { Integration, StartSpanOptions } from '@sentry/core';
6-
import { instrumentVueRouter } from './router';
6+
import { setRouteProvider } from '@sentry/core';
7+
import { createVueRouteProvider, instrumentVueRouter } from './router';
78

89
// The following type is an intersection of the Route type from VueRouter v2, v3, and v4.
910
// This is not great, but kinda necessary to make it work with all versions at the same time.
@@ -61,6 +62,13 @@ export function browserTracingIntegration(options: VueBrowserTracingIntegrationO
6162

6263
return {
6364
...integration,
65+
setup(client) {
66+
// Registered before `afterAllSetup` so the provider is in place by the time the pageload span
67+
// is named, rather than only once the router reports its first navigation.
68+
setRouteProvider(createVueRouteProvider(router, routeLabel), client);
69+
70+
integration.setup?.(client);
71+
},
6472
afterAllSetup(client) {
6573
integration.afterAllSetup(client);
6674

‎packages/vue/src/router.ts‎

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
URL_PATH_PARAMETER_KEY_BASE,
77
URL_TEMPLATE,
88
} from '@sentry/conventions/attributes';
9-
import type { Span, SpanAttributes, StartSpanOptions, TransactionSource } from '@sentry/core';
9+
import type { RouteProvider, Span, SpanAttributes, StartSpanOptions, TransactionSource } from '@sentry/core';
1010
import {
11+
createUrlRouteProvider,
1112
getActiveSpan,
1213
getClient,
1314
getCurrentScope,
@@ -45,6 +46,44 @@ interface VueRouter {
4546
// Vue Router 3 exposes a `mode` property ('hash' | 'history' | 'abstract').
4647
// Vue Router 4+ replaced it with `options.history`. Used for version detection.
4748
mode?: string;
49+
// Vue Router 3 resolves to `{ route }`, Vue Router 4+ returns the route itself.
50+
resolve?: (to: string) => Route | { route: Route };
51+
}
52+
53+
/**
54+
* Builds a route provider backed by the Vue router's own matcher.
55+
*
56+
* Labels routes the same way the navigation instrumentation does, so a route resolved here can't
57+
* disagree with the one on the pageload or navigation span.
58+
*/
59+
export function createVueRouteProvider(router: VueRouter, routeLabel: 'name' | 'path'): RouteProvider {
60+
return createUrlRouteProvider(url => {
61+
const resolved = router.resolve?.(`${url.pathname}${url.search}${url.hash}`);
62+
if (!resolved) {
63+
return undefined;
64+
}
65+
66+
const route = 'matched' in resolved ? resolved : resolved.route;
67+
68+
return getRouteLabel(route, routeLabel)?.name;
69+
});
70+
}
71+
72+
/**
73+
* The label for a matched route and where it came from, or `undefined` when nothing matched and only
74+
* the raw path is left.
75+
*/
76+
function getRouteLabel(
77+
route: Route,
78+
routeLabel: 'name' | 'path',
79+
): { name: string; source: TransactionSource } | undefined {
80+
if (route.name && routeLabel !== 'path') {
81+
return { name: route.name.toString(), source: 'custom' };
82+
}
83+
84+
const matchedPath = route.matched[route.matched.length - 1]?.path;
85+
86+
return matchedPath ? { name: matchedPath, source: 'route' } : undefined;
4887
}
4988

5089
/**
@@ -94,17 +133,9 @@ export function instrumentVueRouter(
94133
}
95134

96135
// Determine a name for the routing transaction and where that name came from
97-
let spanName: string = to.path;
98-
let transactionSource: TransactionSource = 'url';
99-
if (to.name && options.routeLabel !== 'path') {
100-
spanName = to.name.toString();
101-
transactionSource = 'custom';
102-
} else if (to.matched.length > 0) {
103-
const lastIndex = to.matched.length - 1;
104-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
105-
spanName = to.matched[lastIndex]!.path;
106-
transactionSource = 'route';
107-
}
136+
const routeLabel = getRouteLabel(to, options.routeLabel);
137+
const spanName = routeLabel?.name ?? to.path;
138+
const transactionSource: TransactionSource = routeLabel?.source ?? 'url';
108139

109140
if (transactionSource === 'route') {
110141
attributes[URL_TEMPLATE] = spanName;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { GLOBAL_OBJ } from '@sentry/core';
2+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3+
import type { Route } from '../src/router';
4+
import { createVueRouteProvider } from '../src/router';
5+
6+
function makeRoute(overrides: Partial<Route> = {}): Route {
7+
return { path: '/users/42', query: {}, params: {}, matched: [{ path: '/users/:id' }], ...overrides };
8+
}
9+
10+
/** Vue Router 4+ returns the route itself. */
11+
function makeV4Router(route: Route | undefined) {
12+
return { onError: () => {}, beforeEach: () => {}, resolve: () => route as Route };
13+
}
14+
15+
/** Vue Router 3 wraps the route in `{ route }` and exposes `mode`. */
16+
function makeV3Router(route: Route) {
17+
return { onError: () => {}, beforeEach: () => {}, mode: 'history', resolve: () => ({ route }) };
18+
}
19+
20+
describe('createVueRouteProvider', () => {
21+
beforeEach(() => {
22+
(GLOBAL_OBJ as { document?: unknown }).document = { location: { href: 'https://example.com/users/42' } };
23+
});
24+
25+
afterEach(() => {
26+
delete (GLOBAL_OBJ as { document?: unknown }).document;
27+
});
28+
29+
it('resolves the matched route path for Vue Router 4+', () => {
30+
const provider = createVueRouteProvider(makeV4Router(makeRoute()), 'path');
31+
32+
expect(provider.resolveRoute(new URL('https://example.com/users/42'))).toBe('/users/:id');
33+
});
34+
35+
it('unwraps the `{ route }` shape Vue Router 3 resolves to', () => {
36+
const provider = createVueRouteProvider(makeV3Router(makeRoute()), 'path');
37+
38+
expect(provider.resolveRoute(new URL('https://example.com/users/42'))).toBe('/users/:id');
39+
});
40+
41+
it('prefers the route name when labelling by name', () => {
42+
const provider = createVueRouteProvider(makeV4Router(makeRoute({ name: 'UserProfile' })), 'name');
43+
44+
expect(provider.resolveRoute(new URL('https://example.com/users/42'))).toBe('UserProfile');
45+
});
46+
47+
it('uses the matched path when labelling by path even if the route is named', () => {
48+
const provider = createVueRouteProvider(makeV4Router(makeRoute({ name: 'UserProfile' })), 'path');
49+
50+
expect(provider.resolveRoute(new URL('https://example.com/users/42'))).toBe('/users/:id');
51+
});
52+
53+
it('returns undefined when nothing matched, rather than the raw path', () => {
54+
const provider = createVueRouteProvider(makeV4Router(makeRoute({ matched: [] })), 'path');
55+
56+
expect(provider.resolveRoute(new URL('https://example.com/nope'))).toBeUndefined();
57+
});
58+
59+
it('returns undefined when the router cannot resolve', () => {
60+
const provider = createVueRouteProvider(makeV4Router(undefined), 'path');
61+
62+
expect(provider.resolveRoute(new URL('https://example.com/nope'))).toBeUndefined();
63+
});
64+
65+
it('returns undefined for a router without `resolve`, as in Vue Router 2', () => {
66+
const provider = createVueRouteProvider({ onError: () => {}, beforeEach: () => {} }, 'path');
67+
68+
expect(provider.resolveRoute(new URL('https://example.com/users/42'))).toBeUndefined();
69+
});
70+
71+
it('resolves the current route from the document location', () => {
72+
const provider = createVueRouteProvider(makeV4Router(makeRoute()), 'path');
73+
74+
expect(provider.getCurrentRoute()).toBe('/users/:id');
75+
});
76+
});

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL