Skip to content

Commit 1faa587

Browse files
committed
feat(vue): Implement vue browserTracingIntegration()
This replaces the `vueRouterInstrumentation` and allows to deprecate browser tracing in the vue package.
1 parent 3b2b18c commit 1faa587

File tree

4 files changed

+180
-69
lines changed

4 files changed

+180
-69
lines changed

packages/vue/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ const app = createApp({
2828
Sentry.init({
2929
app,
3030
dsn: '__PUBLIC_DSN__',
31+
integrations: [
32+
// Or omit `router` if you're not using vue-router
33+
Sentry.browserTracingIntegration({ router }),
34+
],
3135
});
3236
```
3337

@@ -42,12 +46,16 @@ import * as Sentry from '@sentry/vue'
4246
Sentry.init({
4347
Vue: Vue,
4448
dsn: '__PUBLIC_DSN__',
45-
})
49+
integrations: [
50+
// Or omit `router` if you're not using vue-router
51+
Sentry.browserTracingIntegration({ router }),
52+
],
53+
});
4654

4755
new Vue({
4856
el: '#app',
4957
router,
5058
components: { App },
5159
template: '<App/>'
52-
})
60+
});
5361
```
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {
2+
browserTracingIntegration as originalBrowserTracingIntegration,
3+
startBrowserTracingNavigationSpan,
4+
} from '@sentry/browser';
5+
import type { Integration, StartSpanOptions } from '@sentry/types';
6+
import { instrumentVueRouter } from './router';
7+
8+
// The following type is an intersection of the Route type from VueRouter v2, v3, and v4.
9+
// This is not great, but kinda necessary to make it work with all versions at the same time.
10+
export type Route = {
11+
/** Unparameterized URL */
12+
path: string;
13+
/**
14+
* Query params (keys map to null when there is no value associated, e.g. "?foo" and to an array when there are
15+
* multiple query params that have the same key, e.g. "?foo&foo=bar")
16+
*/
17+
query: Record<string, string | null | (string | null)[]>;
18+
/** Route name (VueRouter provides a way to give routes individual names) */
19+
name?: string | symbol | null | undefined;
20+
/** Evaluated parameters */
21+
params: Record<string, string | string[]>;
22+
/** All the matched route objects as defined in VueRouter constructor */
23+
matched: { path: string }[];
24+
};
25+
26+
interface VueRouter {
27+
onError: (fn: (err: Error) => void) => void;
28+
beforeEach: (fn: (to: Route, from: Route, next?: () => void) => void) => void;
29+
}
30+
31+
type VueBrowserTracingIntegrationOptions = Parameters<typeof originalBrowserTracingIntegration>[0] & {
32+
/**
33+
* If a router is specified, navigation spans will be created based on the router.
34+
*/
35+
router?: VueRouter;
36+
37+
/**
38+
* What to use for route labels.
39+
* By default, we use route.name (if set) and else the path.
40+
*
41+
* Default: 'name'
42+
*/
43+
routeLabel?: 'name' | 'path';
44+
};
45+
46+
/**
47+
* A custom BrowserTracing integration for Vue.
48+
*/
49+
export function browserTracingIntegration(options: VueBrowserTracingIntegrationOptions = {}): Integration {
50+
// If router is not passed, we just use the normal implementation
51+
if (!options.router) {
52+
return originalBrowserTracingIntegration(options);
53+
}
54+
55+
const integration = originalBrowserTracingIntegration({
56+
...options,
57+
instrumentNavigation: false,
58+
});
59+
60+
const { router, instrumentNavigation = true, instrumentPageLoad = true, routeLabel = 'name' } = options;
61+
62+
return {
63+
...integration,
64+
afterAllSetup(client) {
65+
integration.afterAllSetup(client);
66+
67+
const startNavigationSpan = (options: StartSpanOptions): void => {
68+
startBrowserTracingNavigationSpan(client, options);
69+
};
70+
71+
instrumentVueRouter(router, { routeLabel, instrumentNavigation, instrumentPageLoad }, startNavigationSpan);
72+
},
73+
};
74+
}

packages/vue/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export * from '@sentry/browser';
22

33
export { init } from './sdk';
4+
// eslint-disable-next-line deprecation/deprecation
45
export { vueRouterInstrumentation } from './router';
6+
export { browserTracingIntegration } from './browserTracingIntegration';
57
export { attachErrorHandler } from './errorhandler';
68
export { createTracingMixins } from './tracing';
79
export {

packages/vue/src/router.ts

Lines changed: 94 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { WINDOW, captureException } from '@sentry/browser';
2-
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core';
3-
import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types';
2+
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core';
3+
import type { SpanAttributes, Transaction, TransactionContext, TransactionSource } from '@sentry/types';
44

55
import { getActiveTransaction } from './tracing';
66

@@ -50,6 +50,8 @@ interface VueRouter {
5050
* * `routeLabel`: Set this to `route` to opt-out of using `route.name` for transaction names.
5151
*
5252
* @param router The Vue Router instance that is used
53+
*
54+
* @deprecated Use `browserTracingIntegration()` from `@sentry/vue` instead - this includes the vue router instrumentation.
5355
*/
5456
export function vueRouterInstrumentation(
5557
router: VueRouter,
@@ -60,10 +62,6 @@ export function vueRouterInstrumentation(
6062
startTransactionOnPageLoad: boolean = true,
6163
startTransactionOnLocationChange: boolean = true,
6264
) => {
63-
const tags = {
64-
'routing.instrumentation': 'vue-router',
65-
};
66-
6765
// We have to start the pageload transaction as early as possible (before the router's `beforeEach` hook
6866
// is called) to not miss child spans of the pageload.
6967
// We check that window & window.location exists in order to not run this code in SSR environments.
@@ -72,76 +70,105 @@ export function vueRouterInstrumentation(
7270
name: WINDOW.location.pathname,
7371
op: 'pageload',
7472
origin: 'auto.pageload.vue',
75-
tags,
76-
data: {
73+
attributes: {
74+
'routing.instrumentation': 'vue-router',
7775
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
7876
},
7977
});
8078
}
8179

82-
router.onError(error => captureException(error, { mechanism: { handled: false } }));
83-
84-
router.beforeEach((to, from, next) => {
85-
// According to docs we could use `from === VueRouter.START_LOCATION` but I couldnt get it working for Vue 2
86-
// https://router.vuejs.org/api/#router-start-location
87-
// https://next.router.vuejs.org/api/#start-location
88-
89-
// from.name:
90-
// - Vue 2: null
91-
// - Vue 3: undefined
92-
// hence only '==' instead of '===', because `undefined == null` evaluates to `true`
93-
const isPageLoadNavigation = from.name == null && from.matched.length === 0;
94-
95-
const data: Record<string, unknown> = {
96-
params: to.params,
97-
query: to.query,
98-
};
99-
100-
// Determine a name for the routing transaction and where that name came from
101-
let transactionName: string = to.path;
102-
let transactionSource: TransactionSource = 'url';
103-
if (to.name && options.routeLabel !== 'path') {
104-
transactionName = to.name.toString();
105-
transactionSource = 'custom';
106-
} else if (to.matched[0] && to.matched[0].path) {
107-
transactionName = to.matched[0].path;
108-
transactionSource = 'route';
80+
instrumentVueRouter(
81+
router,
82+
{
83+
routeLabel: options.routeLabel || 'name',
84+
instrumentNavigation: startTransactionOnLocationChange,
85+
instrumentPageLoad: startTransactionOnPageLoad,
86+
},
87+
startTransaction,
88+
);
89+
};
90+
}
91+
92+
/**
93+
* Instrument the Vue router to create navigation spans.
94+
*/
95+
export function instrumentVueRouter(
96+
router: VueRouter,
97+
options: {
98+
routeLabel: 'name' | 'path';
99+
instrumentPageLoad: boolean;
100+
instrumentNavigation: boolean;
101+
},
102+
startNavigationSpanFn: (context: TransactionContext) => void,
103+
): void {
104+
router.onError(error => captureException(error, { mechanism: { handled: false } }));
105+
106+
router.beforeEach((to, from, next) => {
107+
// According to docs we could use `from === VueRouter.START_LOCATION` but I couldnt get it working for Vue 2
108+
// https://router.vuejs.org/api/#router-start-location
109+
// https://next.router.vuejs.org/api/#start-location
110+
111+
// from.name:
112+
// - Vue 2: null
113+
// - Vue 3: undefined
114+
// hence only '==' instead of '===', because `undefined == null` evaluates to `true`
115+
const isPageLoadNavigation = from.name == null && from.matched.length === 0;
116+
117+
const attributes: SpanAttributes = {
118+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue',
119+
'routing.instrumentation': 'vue-router',
120+
};
121+
122+
for (const key of Object.keys(to.params)) {
123+
attributes[`params.${key}`] = to.params[key];
124+
}
125+
for (const key of Object.keys(to.query)) {
126+
const value = to.query[key];
127+
if (value) {
128+
attributes[`query.${key}`] = value;
109129
}
130+
}
110131

111-
if (startTransactionOnPageLoad && isPageLoadNavigation) {
112-
// eslint-disable-next-line deprecation/deprecation
113-
const pageloadTransaction = getActiveTransaction();
114-
if (pageloadTransaction) {
115-
const attributes = spanToJSON(pageloadTransaction).data || {};
116-
if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') {
117-
pageloadTransaction.updateName(transactionName);
118-
pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource);
119-
}
120-
// TODO: We need to flatten these to make them attributes
121-
// eslint-disable-next-line deprecation/deprecation
122-
pageloadTransaction.setData('params', data.params);
123-
// eslint-disable-next-line deprecation/deprecation
124-
pageloadTransaction.setData('query', data.query);
132+
// Determine a name for the routing transaction and where that name came from
133+
let transactionName: string = to.path;
134+
let transactionSource: TransactionSource = 'url';
135+
if (to.name && options.routeLabel !== 'path') {
136+
transactionName = to.name.toString();
137+
transactionSource = 'custom';
138+
} else if (to.matched[0] && to.matched[0].path) {
139+
transactionName = to.matched[0].path;
140+
transactionSource = 'route';
141+
}
142+
143+
if (options.instrumentPageLoad && isPageLoadNavigation) {
144+
// eslint-disable-next-line deprecation/deprecation
145+
const pageloadTransaction = getActiveTransaction();
146+
if (pageloadTransaction) {
147+
const existingAttributes = spanToJSON(pageloadTransaction).data || {};
148+
if (existingAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') {
149+
pageloadTransaction.updateName(transactionName);
150+
pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource);
125151
}
152+
// Set router attributes on the existing pageload transaction
153+
// This will the origin, and add params & query attributes
154+
pageloadTransaction.setAttributes(attributes);
126155
}
156+
}
127157

128-
if (startTransactionOnLocationChange && !isPageLoadNavigation) {
129-
data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource;
130-
startTransaction({
131-
name: transactionName,
132-
op: 'navigation',
133-
origin: 'auto.navigation.vue',
134-
tags,
135-
data,
136-
});
137-
}
158+
if (options.instrumentNavigation && !isPageLoadNavigation) {
159+
attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource;
160+
startNavigationSpanFn({
161+
name: transactionName,
162+
op: 'navigation',
163+
attributes,
164+
});
165+
}
138166

139-
// Vue Router 4 no longer exposes the `next` function, so we need to
140-
// check if it's available before calling it.
141-
// `next` needs to be called in Vue Router 3 so that the hook is resolved.
142-
if (next) {
143-
next();
144-
}
145-
});
146-
};
167+
// Vue Router 4 no longer exposes the `next` function, so we need to
168+
// check if it's available before calling it.
169+
// `next` needs to be called in Vue Router 3 so that the hook is resolved.
170+
if (next) {
171+
next();
172+
}
173+
});
147174
}

0 commit comments

Comments
 (0)