diff --git a/.eslintrc.js b/.eslintrc.js index 973a346..a0e61f2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,12 @@ module.exports = { root: true, extends: [ 'codex/ts' ], + ignorePatterns: [ + 'dist/', + 'node_modules/', + 'package.json', + 'tsconfig.json', + ], env: { browser: true, }, diff --git a/package.json b/package.json index fa8d40e..5140407 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@hawk.so/javascript", - "version": "3.1.0", + "type": "commonjs", + "version": "3.2.0", "description": "JavaScript errors tracking for Hawk.so", "files": [ "dist" diff --git a/src/catcher.ts b/src/catcher.ts index fb2207c..5a77243 100644 --- a/src/catcher.ts +++ b/src/catcher.ts @@ -2,7 +2,7 @@ import Socket from './modules/socket'; import Sanitizer from './modules/sanitizer'; import log from './utils/log'; import StackParser from './modules/stackParser'; -import type { CatcherMessage, HawkInitialSettings } from '@/types'; +import type { CatcherMessage, HawkInitialSettings } from './types'; import { VueIntegration } from './integrations/vue'; import { id } from './utils/id'; import type { @@ -34,6 +34,11 @@ export default class Catcher { */ public readonly version: string = VERSION; + /** + * Vue.js integration instance + */ + public vue: VueIntegration | null = null; + /** * Catcher Type */ @@ -202,7 +207,7 @@ export default class Catcher { */ public connectVue(vue): void { // eslint-disable-next-line no-new - new VueIntegration(vue, (error: Error, addons: VueIntegrationAddons) => { + this.vue = new VueIntegration(vue, (error: Error, addons: VueIntegrationAddons) => { void this.formatAndSend(error, { vue: addons, }); diff --git a/src/integrations/vue.ts b/src/integrations/vue.ts index 5b91aec..391070e 100644 --- a/src/integrations/vue.ts +++ b/src/integrations/vue.ts @@ -49,6 +49,24 @@ export class VueIntegration { } } + /** + * Extract additional useful information from the Vue app + * + * Can be used outside of this class, for example, by Nuxt integration + * + * @param vm - component instance + * @param info - a Vue-specific error info, e.g. which lifecycle hook the error was found in. + */ + public spoilAddons(vm: { [key: string]: unknown }, info: string): VueIntegrationAddons { + const isVue3 = vm.$ !== undefined; + + if (isVue3) { + return this.spoilAddonsFromVue3(vm, info); + } else { + return this.spoilAddonsFromVue2(vm, info); + } + } + /** * Setups event handlers for Vue.js instance */ @@ -76,13 +94,13 @@ export class VueIntegration { } /** - * Extract additional useful information from the Vue app + * Extract additional useful information from the Vue 2 app * - * @param vm - vue VM - * @param info - a Vue-specific error info, e.g. which lifecycle hook the error was found in. + * @param vm - component instance + * @param info - which lifecycle hook the error was found in. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - private spoilAddons(vm: { [key: string]: any }, info: string): VueIntegrationAddons { + private spoilAddonsFromVue2(vm: { [key: string]: any }, info: string): VueIntegrationAddons { const addons: VueIntegrationAddons = { lifecycle: info, component: null, @@ -130,6 +148,89 @@ export class VueIntegration { return addons; } + /** + * Extract additional useful information from the Vue 3 app + * + * @param vm - component instance + * @param info - which lifecycle hook the error was found in. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private spoilAddonsFromVue3(vm: { [key: string]: any }, info: string): VueIntegrationAddons { + const addons: VueIntegrationAddons = { + lifecycle: this.getRuntimeErrorSourceByCode(info), + component: null, + }; + + /** + * Extract the component name + */ + if (vm.$options !== undefined) { + addons['component'] = `<${vm.$options.__name || vm.$options.name || vm.$options._componentTag || 'Anonymous'}>`; + } + + /** + * Fill props + */ + if (Object.keys(vm.$props).length) { + addons['props'] = vm.$props; + } + + return addons; + } + + /** + * In production, the error code is a link with reference to doc. + * This method returns the error message by the code extracted from the link + * + * @param code - Error source info (3rd argument of the vue:error hook) + * https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured + */ + private getRuntimeErrorSourceByCode(code: string): string { + if (!code.includes('https://vuejs.org/error-reference/#runtime-')) { + return code; + } + + const codeParts = code.split('https://vuejs.org/error-reference/#runtime-'); + const errorCode = codeParts[codeParts.length - 1]; + + const errorCodeMap = new Map([ + ['0', 'setup function'], + ['1', 'render function'], + ['2', 'watcher getter'], + ['3', 'watcher callback'], + ['4', 'watcher cleanup function'], + ['5', 'native event handler'], + ['6', 'component event handler'], + ['7', 'vnode hook'], + ['8', 'directive hook'], + ['9', 'transition hook'], + ['10', 'app errorHandler'], + ['11', 'app warnHandler'], + ['12', 'ref function'], + ['13', 'async component loader'], + ['14', 'scheduler flush'], + ['15', 'component update'], + ['16', 'app unmount cleanup function'], + ['sp', 'serverPrefetch hook'], + ['bc', 'beforeCreate hook'], + ['c', 'created hook'], + ['bm', 'beforeMount hook'], + ['m', 'mounted hook'], + ['bu', 'beforeUpdate hook'], + ['u', 'updated'], + ['bum', 'beforeUnmount hook'], + ['um', 'unmounted hook'], + ['a', 'activated hook'], + ['da', 'deactivated hook'], + ['ec', 'errorCaptured hook'], + ['rtc', 'renderTracked hook'], + ['rtg', 'renderTriggered hook'], + ]); + + return errorCodeMap.get(errorCode) || code; + } + + /** * Write error to the console * @@ -138,13 +239,15 @@ export class VueIntegration { * @param component - where error was occurred */ private printError(err: Error, info: string, component: string | null): void { + const source = this.getRuntimeErrorSourceByCode(info); + if (component === null) { - console.error(`${info}`, err); + console.error(`${source}`, err); return; } - console.error(`${component} @ ${info}`, err); + console.error(`${component} @ ${source}`, err); } } diff --git a/tsconfig.json b/tsconfig.json index b90c2fc..9624c27 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,12 +7,14 @@ "declaration": true, "outDir": "dist", "rootDir": "src", - "moduleResolution": "Bundler", + "module": "NodeNext", + "moduleResolution": "nodenext", "lib": ["dom", "es2017", "es2018"], "baseUrl": ".", "paths": { "@/types": ["src/types"] - } + }, + "allowSyntheticDefaultImports": true }, "include": [ "src/**/*",