Skip to content

Commit

Permalink
feat(vue): Rework tracing and add support for Vue 3 (#3804)
Browse files Browse the repository at this point in the history
* feat(vue): Rework tracing and add support for Vue 3
  • Loading branch information
kamilogorek authored Jul 16, 2021
1 parent 41cf211 commit 14ccb55
Show file tree
Hide file tree
Showing 13 changed files with 440 additions and 4,824 deletions.
7 changes: 1 addition & 6 deletions packages/vue/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,10 @@ module.exports = {
project: './tsconfig.json',
},
},
{
files: ['test/**'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
},
],
rules: {
'react/prop-types': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
8 changes: 3 additions & 5 deletions packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"tslib": "^1.9.3"
},
"peerDependencies": {
"vue": "2.x",
"vue-router": "3.x"
"vue": "2.x || 3.x",
"vue-router": "3.x || 4.x"
},
"devDependencies": {
"@sentry-internal/eslint-config-sdk": "6.9.0",
Expand All @@ -40,9 +40,7 @@
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-terser": "^4.0.4",
"rollup-plugin-typescript2": "^0.21.0",
"typescript": "3.7.5",
"vue": "^2.6",
"vue-router": "^3.0.1"
"typescript": "3.7.5"
},
"scripts": {
"build": "run-p build:es5 build:esm build:bundle",
Expand Down
83 changes: 83 additions & 0 deletions packages/vue/src/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ViewModel } from './types';

// Vendored directly from https://github.com/vuejs/vue/blob/master/src/core/util/debug.js with types only changes.
const classifyRE = /(?:^|[-_])(\w)/g;
const classify = (str: string): string => str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '');

const ROOT_COMPONENT_NAME = '<Root>';
const ANONYMOUS_COMPONENT_NAME = '<Anonymous>';

const repeat = (str: string, n: number): string => {
let res = '';
while (n) {
if (n % 2 === 1) {
res += str;
}
if (n > 1) {
str += str; // eslint-disable-line no-param-reassign
}
n >>= 1; // eslint-disable-line no-bitwise, no-param-reassign
}
return res;
};

export const formatComponentName = (vm?: ViewModel, includeFile?: boolean): string => {
if (!vm) {
return ANONYMOUS_COMPONENT_NAME;
}

if (vm.$root === vm) {
return ROOT_COMPONENT_NAME;
}

const options = vm.$options;

let name = options.name || options._componentTag;
const file = options.__file;
if (!name && file) {
const match = file.match(/([^/\\]+)\.vue$/);
if (match) {
name = match[1];
}
}

return (
(name ? `<${classify(name)}>` : ANONYMOUS_COMPONENT_NAME) + (file && includeFile !== false ? ` at ${file}` : ``)
);
};

export const generateComponentTrace = (vm?: ViewModel): string => {
if (vm?._isVue && vm?.$parent) {
const tree = [];
let currentRecursiveSequence = 0;
while (vm) {
if (tree.length > 0) {
const last = tree[tree.length - 1] as any;
if (last.constructor === vm.constructor) {
currentRecursiveSequence += 1;
vm = vm.$parent; // eslint-disable-line no-param-reassign
continue;
} else if (currentRecursiveSequence > 0) {
tree[tree.length - 1] = [last, currentRecursiveSequence];
currentRecursiveSequence = 0;
}
}
tree.push(vm);
vm = vm.$parent; // eslint-disable-line no-param-reassign
}

const formattedTree = tree
.map(
(vm, i) =>
`${(i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) +
(Array.isArray(vm)
? `${formatComponentName(vm[0])}... (${vm[1]} recursive calls)`
: formatComponentName(vm))}`,
)
.join('\n');

return `\n\nfound in\n\n${formattedTree}`;
}

return `\n\n(found in ${formatComponentName(vm)})`;
};
48 changes: 48 additions & 0 deletions packages/vue/src/errorhandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { getCurrentHub } from '@sentry/browser';

import { formatComponentName, generateComponentTrace } from './components';
import { Options, ViewModel, Vue } from './types';

export const attachErrorHandler = (app: Vue, options: Options): void => {
const { errorHandler, warnHandler, silent } = app.config;

app.config.errorHandler = (error: Error, vm: ViewModel, lifecycleHook: string): void => {
const componentName = formatComponentName(vm, false);
const trace = vm ? generateComponentTrace(vm) : '';
const metadata: Record<string, unknown> = {
componentName,
lifecycleHook,
trace,
};

if (options.attachProps) {
// Vue2 - $options.propsData
// Vue3 - $props
metadata.propsData = vm.$options.propsData || vm.$props;
}

// Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time.
setTimeout(() => {
getCurrentHub().withScope(scope => {
scope.setContext('vue', metadata);
getCurrentHub().captureException(error);
});
});

if (typeof errorHandler === 'function') {
errorHandler.call(app, error, vm, lifecycleHook);
}

if (options.logErrors) {
const hasConsole = typeof console !== 'undefined';
const message = `Error in ${lifecycleHook}: "${error && error.toString()}"`;

if (warnHandler) {
warnHandler.call(null, message, vm, trace);
} else if (hasConsole && !silent) {
// eslint-disable-next-line no-console
console.error(`[Vue warn]: ${message}${trace}`);
}
}
};
};
3 changes: 3 additions & 0 deletions packages/vue/src/index.bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ import { Integrations as BrowserIntegrations } from '@sentry/browser';
import { getGlobalObject } from '@sentry/utils';

export { init } from './sdk';
export { vueRouterInstrumentation } from './router';
export { attachErrorHandler } from './errorhandler';
export { createTracingMixins } from './tracing';

let windowIntegrations = {};

Expand Down
4 changes: 3 additions & 1 deletion packages/vue/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from '@sentry/browser';

export { init } from './sdk';
export { vueRouterInstrumentation } from './vuerouter';
export { vueRouterInstrumentation } from './router';
export { attachErrorHandler } from './errorhandler';
export { createTracingMixins } from './tracing';
36 changes: 25 additions & 11 deletions packages/vue/src/vuerouter.ts → packages/vue/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { captureException } from '@sentry/browser';
import { Transaction, TransactionContext } from '@sentry/types';
import VueRouter from 'vue-router';

export type Action = 'PUSH' | 'REPLACE' | 'POP';

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

let firstLoad = true;
// This is not great, but kinda necessary to make it work with VueRouter@3 and VueRouter@4 at the same time.
type Route = {
params: any;
query: any;
name: any;
path: any;
matched: any[];
};
interface VueRouter {
onError: (fn: (err: Error) => void) => void;
beforeEach: (fn: (to: Route, from: Route, next: () => void) => void) => void;
}

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

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

router.beforeEach((to, _from, next) => {
// Vue2 - null
// Vue3 - undefined
const isPageLoadNavigation = from.name == null && from.matched.length === 0;

const tags = {
'routing.instrumentation': 'vue-router',
};
const data = {
params: to.params,
query: to.query,
};

if (startTransactionOnPageLoad && firstLoad) {
if (startTransactionOnPageLoad && isPageLoadNavigation) {
startTransaction({
name: to.name || to.path,
op: 'pageload',
Expand All @@ -44,7 +59,7 @@ export function vueRouterInstrumentation(router: VueRouter): VueRouterInstrument
});
}

if (startTransactionOnLocationChange && !firstLoad) {
if (startTransactionOnLocationChange && !isPageLoadNavigation) {
startTransaction({
name: to.name || to.matched[0].path || to.path,
op: 'navigation',
Expand All @@ -53,7 +68,6 @@ export function vueRouterInstrumentation(router: VueRouter): VueRouterInstrument
});
}

firstLoad = false;
next();
});
};
Expand Down
Loading

0 comments on commit 14ccb55

Please sign in to comment.