|
6 | 6 | URL_PATH_PARAMETER_KEY_BASE, |
7 | 7 | URL_TEMPLATE, |
8 | 8 | } 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'; |
10 | 10 | import { |
| 11 | + createUrlRouteProvider, |
11 | 12 | getActiveSpan, |
12 | 13 | getClient, |
13 | 14 | getCurrentScope, |
@@ -45,6 +46,45 @@ interface VueRouter { |
45 | 46 | // Vue Router 3 exposes a `mode` property ('hash' | 'history' | 'abstract'). |
46 | 47 | // Vue Router 4+ replaced it with `options.history`. Used for version detection. |
47 | 48 | mode?: string; |
| 49 | + // Vue Router 3 resolves to `{ route }`, Vue Router 4+ returns the route itself. Optional because |
| 50 | + // this interface is hand-rolled across Vue Router 2, 3 and 4+ rather than taken from the library. |
| 51 | + resolve?: (to: string) => Route | { route: Route }; |
| 52 | +} |
| 53 | + |
| 54 | +/** |
| 55 | + * Builds a route provider backed by the Vue router's own matcher. |
| 56 | + * |
| 57 | + * Labels routes the same way the navigation instrumentation does, so a route resolved here can't |
| 58 | + * disagree with the one on the pageload or navigation span. |
| 59 | + */ |
| 60 | +export function createVueRouteProvider(router: VueRouter, routeLabel: 'name' | 'path'): RouteProvider { |
| 61 | + return createUrlRouteProvider(url => { |
| 62 | + const resolved = router.resolve?.(`${url.pathname}${url.search}${url.hash}`); |
| 63 | + if (!resolved) { |
| 64 | + return undefined; |
| 65 | + } |
| 66 | + |
| 67 | + const route = 'matched' in resolved ? resolved : resolved.route; |
| 68 | + |
| 69 | + return getRouteLabel(route, routeLabel)?.name; |
| 70 | + }); |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * The label for a matched route and where it came from, or `undefined` when nothing matched and only |
| 75 | + * the raw path is left. |
| 76 | + */ |
| 77 | +function getRouteLabel( |
| 78 | + route: Route, |
| 79 | + routeLabel: 'name' | 'path', |
| 80 | +): { name: string; source: TransactionSource } | undefined { |
| 81 | + if (route.name && routeLabel !== 'path') { |
| 82 | + return { name: route.name.toString(), source: 'custom' }; |
| 83 | + } |
| 84 | + |
| 85 | + const matchedPath = route.matched[route.matched.length - 1]?.path; |
| 86 | + |
| 87 | + return matchedPath ? { name: matchedPath, source: 'route' } : undefined; |
48 | 88 | } |
49 | 89 |
|
50 | 90 | /** |
@@ -94,17 +134,9 @@ export function instrumentVueRouter( |
94 | 134 | } |
95 | 135 |
|
96 | 136 | // 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 | | - } |
| 137 | + const routeLabel = getRouteLabel(to, options.routeLabel); |
| 138 | + const spanName = routeLabel?.name ?? to.path; |
| 139 | + const transactionSource: TransactionSource = routeLabel?.source ?? 'url'; |
108 | 140 |
|
109 | 141 | if (transactionSource === 'route') { |
110 | 142 | attributes[URL_TEMPLATE] = spanName; |
|
0 commit comments