-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathrouter.ts
153 lines (135 loc) · 5.25 KB
/
router.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import { captureException } from '@sentry/browser';
import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
getActiveSpan,
getCurrentScope,
getRootSpan,
spanToJSON,
} from '@sentry/core';
import type { Span, SpanAttributes, StartSpanOptions, TransactionSource } from '@sentry/core';
// The following type is an intersection of the Route type from VueRouter v2, v3, and v4.
// This is not great, but kinda necessary to make it work with all versions at the same time.
export type Route = {
/** Unparameterized URL */
path: 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")
*/
query: Record<string, string | null | (string | null)[]>;
/** Route name (VueRouter provides a way to give routes individual names) */
name?: string | symbol | null | undefined;
/** Evaluated parameters */
params: Record<string, string | string[]>;
/** All the matched route objects as defined in VueRouter constructor */
matched: { path: string }[];
};
interface VueRouter {
onError: (fn: (err: Error) => void) => void;
beforeEach: (fn: (to: Route, from: Route, next?: () => void) => void) => void;
}
/**
* Instrument the Vue router to create navigation spans.
*/
export function instrumentVueRouter(
router: VueRouter,
options: {
/**
* What to use for route labels.
* By default, we use route.name (if set) and else the path.
*
* Default: 'name'
*/
routeLabel: 'name' | 'path';
instrumentPageLoad: boolean;
instrumentNavigation: boolean;
},
startNavigationSpanFn: (context: StartSpanOptions) => void,
): void {
let isFirstPageLoad = true;
router.onError(error => captureException(error, { mechanism: { handled: false } }));
router.beforeEach((to, from, next) => {
// According to docs we could use `from === VueRouter.START_LOCATION` but I couldn't get it working for Vue 2
// https://router.vuejs.org/api/#router-start-location
// https://next.router.vuejs.org/api/#start-location
// Additionally, Nuxt does not provide the possibility to check for `from.matched.length === 0` (this is never 0).
// Therefore, a flag was added to track the page-load: isFirstPageLoad
// from.name:
// - Vue 2: null
// - Vue 3: undefined
// - Nuxt: undefined
// hence only '==' instead of '===', because `undefined == null` evaluates to `true`
const isPageLoadNavigation =
(from.name == null && from.matched.length === 0) || (from.name === undefined && isFirstPageLoad);
if (isFirstPageLoad) {
isFirstPageLoad = false;
}
const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue',
};
for (const key of Object.keys(to.params)) {
attributes[`params.${key}`] = to.params[key];
}
for (const key of Object.keys(to.query)) {
const value = to.query[key];
if (value) {
attributes[`query.${key}`] = value;
}
}
// Determine a name for the routing transaction and where that name came from
let spanName: string = to.path;
let transactionSource: TransactionSource = 'url';
if (to.name && options.routeLabel !== 'path') {
spanName = to.name.toString();
transactionSource = 'custom';
} else if (to.matched.length > 0) {
const lastIndex = to.matched.length - 1;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
spanName = to.matched[lastIndex]!.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(spanName);
activeRootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource);
}
// Set router attributes on the existing pageload transaction
// This will override the origin, and add params & query attributes
activeRootSpan.setAttributes({
...attributes,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.vue',
});
}
}
if (options.instrumentNavigation && !isPageLoadNavigation) {
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource;
attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = 'auto.navigation.vue';
startNavigationSpanFn({
name: spanName,
op: 'navigation',
attributes,
});
}
// Vue Router 4 no longer exposes the `next` function, so we need to
// check if it's available before calling it.
// `next` needs to be called in Vue Router 3 so that the hook is resolved.
if (next) {
next();
}
});
}
function getActiveRootSpan(): Span | undefined {
const span = getActiveSpan();
const rootSpan = span && getRootSpan(span);
if (!rootSpan) {
return undefined;
}
const op = spanToJSON(rootSpan).op;
// Only use this root span if it is a pageload or navigation span
return op === 'navigation' || op === 'pageload' ? rootSpan : undefined;
}