Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vue): Rework tracing and add support for Vue 3 #3804

Merged
merged 9 commits into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 => {
kamilogorek marked this conversation as resolved.
Show resolved Hide resolved
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))}`,
kamilogorek marked this conversation as resolved.
Show resolved Hide resolved
)
.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