Skip to content

Commit 14ccb55

Browse files
authored
feat(vue): Rework tracing and add support for Vue 3 (#3804)
* feat(vue): Rework tracing and add support for Vue 3
1 parent 41cf211 commit 14ccb55

File tree

13 files changed

+440
-4824
lines changed

13 files changed

+440
-4824
lines changed

packages/vue/.eslintrc.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,10 @@ module.exports = {
1717
project: './tsconfig.json',
1818
},
1919
},
20-
{
21-
files: ['test/**'],
22-
rules: {
23-
'@typescript-eslint/no-explicit-any': 'off',
24-
},
25-
},
2620
],
2721
rules: {
2822
'react/prop-types': 'off',
2923
'@typescript-eslint/no-unsafe-member-access': 'off',
24+
'@typescript-eslint/no-explicit-any': 'off',
3025
},
3126
};

packages/vue/package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
"tslib": "^1.9.3"
2525
},
2626
"peerDependencies": {
27-
"vue": "2.x",
28-
"vue-router": "3.x"
27+
"vue": "2.x || 3.x",
28+
"vue-router": "3.x || 4.x"
2929
},
3030
"devDependencies": {
3131
"@sentry-internal/eslint-config-sdk": "6.9.0",
@@ -40,9 +40,7 @@
4040
"rollup-plugin-node-resolve": "^4.2.3",
4141
"rollup-plugin-terser": "^4.0.4",
4242
"rollup-plugin-typescript2": "^0.21.0",
43-
"typescript": "3.7.5",
44-
"vue": "^2.6",
45-
"vue-router": "^3.0.1"
43+
"typescript": "3.7.5"
4644
},
4745
"scripts": {
4846
"build": "run-p build:es5 build:esm build:bundle",

packages/vue/src/components.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { ViewModel } from './types';
2+
3+
// Vendored directly from https://github.com/vuejs/vue/blob/master/src/core/util/debug.js with types only changes.
4+
const classifyRE = /(?:^|[-_])(\w)/g;
5+
const classify = (str: string): string => str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '');
6+
7+
const ROOT_COMPONENT_NAME = '<Root>';
8+
const ANONYMOUS_COMPONENT_NAME = '<Anonymous>';
9+
10+
const repeat = (str: string, n: number): string => {
11+
let res = '';
12+
while (n) {
13+
if (n % 2 === 1) {
14+
res += str;
15+
}
16+
if (n > 1) {
17+
str += str; // eslint-disable-line no-param-reassign
18+
}
19+
n >>= 1; // eslint-disable-line no-bitwise, no-param-reassign
20+
}
21+
return res;
22+
};
23+
24+
export const formatComponentName = (vm?: ViewModel, includeFile?: boolean): string => {
25+
if (!vm) {
26+
return ANONYMOUS_COMPONENT_NAME;
27+
}
28+
29+
if (vm.$root === vm) {
30+
return ROOT_COMPONENT_NAME;
31+
}
32+
33+
const options = vm.$options;
34+
35+
let name = options.name || options._componentTag;
36+
const file = options.__file;
37+
if (!name && file) {
38+
const match = file.match(/([^/\\]+)\.vue$/);
39+
if (match) {
40+
name = match[1];
41+
}
42+
}
43+
44+
return (
45+
(name ? `<${classify(name)}>` : ANONYMOUS_COMPONENT_NAME) + (file && includeFile !== false ? ` at ${file}` : ``)
46+
);
47+
};
48+
49+
export const generateComponentTrace = (vm?: ViewModel): string => {
50+
if (vm?._isVue && vm?.$parent) {
51+
const tree = [];
52+
let currentRecursiveSequence = 0;
53+
while (vm) {
54+
if (tree.length > 0) {
55+
const last = tree[tree.length - 1] as any;
56+
if (last.constructor === vm.constructor) {
57+
currentRecursiveSequence += 1;
58+
vm = vm.$parent; // eslint-disable-line no-param-reassign
59+
continue;
60+
} else if (currentRecursiveSequence > 0) {
61+
tree[tree.length - 1] = [last, currentRecursiveSequence];
62+
currentRecursiveSequence = 0;
63+
}
64+
}
65+
tree.push(vm);
66+
vm = vm.$parent; // eslint-disable-line no-param-reassign
67+
}
68+
69+
const formattedTree = tree
70+
.map(
71+
(vm, i) =>
72+
`${(i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) +
73+
(Array.isArray(vm)
74+
? `${formatComponentName(vm[0])}... (${vm[1]} recursive calls)`
75+
: formatComponentName(vm))}`,
76+
)
77+
.join('\n');
78+
79+
return `\n\nfound in\n\n${formattedTree}`;
80+
}
81+
82+
return `\n\n(found in ${formatComponentName(vm)})`;
83+
};

packages/vue/src/errorhandler.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { getCurrentHub } from '@sentry/browser';
2+
3+
import { formatComponentName, generateComponentTrace } from './components';
4+
import { Options, ViewModel, Vue } from './types';
5+
6+
export const attachErrorHandler = (app: Vue, options: Options): void => {
7+
const { errorHandler, warnHandler, silent } = app.config;
8+
9+
app.config.errorHandler = (error: Error, vm: ViewModel, lifecycleHook: string): void => {
10+
const componentName = formatComponentName(vm, false);
11+
const trace = vm ? generateComponentTrace(vm) : '';
12+
const metadata: Record<string, unknown> = {
13+
componentName,
14+
lifecycleHook,
15+
trace,
16+
};
17+
18+
if (options.attachProps) {
19+
// Vue2 - $options.propsData
20+
// Vue3 - $props
21+
metadata.propsData = vm.$options.propsData || vm.$props;
22+
}
23+
24+
// Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time.
25+
setTimeout(() => {
26+
getCurrentHub().withScope(scope => {
27+
scope.setContext('vue', metadata);
28+
getCurrentHub().captureException(error);
29+
});
30+
});
31+
32+
if (typeof errorHandler === 'function') {
33+
errorHandler.call(app, error, vm, lifecycleHook);
34+
}
35+
36+
if (options.logErrors) {
37+
const hasConsole = typeof console !== 'undefined';
38+
const message = `Error in ${lifecycleHook}: "${error && error.toString()}"`;
39+
40+
if (warnHandler) {
41+
warnHandler.call(null, message, vm, trace);
42+
} else if (hasConsole && !silent) {
43+
// eslint-disable-next-line no-console
44+
console.error(`[Vue warn]: ${message}${trace}`);
45+
}
46+
}
47+
};
48+
};

packages/vue/src/index.bundle.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ import { Integrations as BrowserIntegrations } from '@sentry/browser';
5252
import { getGlobalObject } from '@sentry/utils';
5353

5454
export { init } from './sdk';
55+
export { vueRouterInstrumentation } from './router';
56+
export { attachErrorHandler } from './errorhandler';
57+
export { createTracingMixins } from './tracing';
5558

5659
let windowIntegrations = {};
5760

packages/vue/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
export * from '@sentry/browser';
22

33
export { init } from './sdk';
4-
export { vueRouterInstrumentation } from './vuerouter';
4+
export { vueRouterInstrumentation } from './router';
5+
export { attachErrorHandler } from './errorhandler';
6+
export { createTracingMixins } from './tracing';

packages/vue/src/vuerouter.ts renamed to packages/vue/src/router.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import { captureException } from '@sentry/browser';
22
import { Transaction, TransactionContext } from '@sentry/types';
3-
import VueRouter from 'vue-router';
4-
5-
export type Action = 'PUSH' | 'REPLACE' | 'POP';
63

74
export type VueRouterInstrumentation = <T extends Transaction>(
85
startTransaction: (context: TransactionContext) => T | undefined,
96
startTransactionOnPageLoad?: boolean,
107
startTransactionOnLocationChange?: boolean,
118
) => void;
129

13-
let firstLoad = true;
10+
// This is not great, but kinda necessary to make it work with VueRouter@3 and VueRouter@4 at the same time.
11+
type Route = {
12+
params: any;
13+
query: any;
14+
name: any;
15+
path: any;
16+
matched: any[];
17+
};
18+
interface VueRouter {
19+
onError: (fn: (err: Error) => void) => void;
20+
beforeEach: (fn: (to: Route, from: Route, next: () => void) => void) => void;
21+
}
1422

1523
/**
1624
* Creates routing instrumentation for Vue Router v2
@@ -25,17 +33,24 @@ export function vueRouterInstrumentation(router: VueRouter): VueRouterInstrument
2533
) => {
2634
router.onError(error => captureException(error));
2735

28-
const tags = {
29-
'routing.instrumentation': 'vue-router',
30-
};
36+
router.beforeEach((to, from, next) => {
37+
// According to docs we could use `from === VueRouter.START_LOCATION` but I couldnt get it working for Vue 2
38+
// https://router.vuejs.org/api/#router-start-location
39+
// https://next.router.vuejs.org/api/#start-location
3140

32-
router.beforeEach((to, _from, next) => {
41+
// Vue2 - null
42+
// Vue3 - undefined
43+
const isPageLoadNavigation = from.name == null && from.matched.length === 0;
44+
45+
const tags = {
46+
'routing.instrumentation': 'vue-router',
47+
};
3348
const data = {
3449
params: to.params,
3550
query: to.query,
3651
};
3752

38-
if (startTransactionOnPageLoad && firstLoad) {
53+
if (startTransactionOnPageLoad && isPageLoadNavigation) {
3954
startTransaction({
4055
name: to.name || to.path,
4156
op: 'pageload',
@@ -44,7 +59,7 @@ export function vueRouterInstrumentation(router: VueRouter): VueRouterInstrument
4459
});
4560
}
4661

47-
if (startTransactionOnLocationChange && !firstLoad) {
62+
if (startTransactionOnLocationChange && !isPageLoadNavigation) {
4863
startTransaction({
4964
name: to.name || to.matched[0].path || to.path,
5065
op: 'navigation',
@@ -53,7 +68,6 @@ export function vueRouterInstrumentation(router: VueRouter): VueRouterInstrument
5368
});
5469
}
5570

56-
firstLoad = false;
5771
next();
5872
});
5973
};

0 commit comments

Comments
 (0)