This document describes how to obtain detailed error information when errors or exceptions occur after Vue3 project deployment and automatically notify relevant developers to handle them. Project Address, let’s get started!

Pain points

Usually when we are developing locally, Vue3 will give us a stack error message in the browser if an error or exception occurs

You can also see which component and which file the error occurred in which line

Clicking on the file opens the file in Console Sources and locates the error location

This greatly facilitates the process of finding the cause of the error, but when our code is deployed to the server, it is usually not done for project optimizationsourcemapFile packaging, at this point, error messages usually look like this

Is that a blank look? Clicking on the corresponding file is also the packaged and compressed code, so there is no way to see the error message and corresponding to the specific file of the source code and the specific error location. Not only that, when you receive business feedback, but you open the browser and do not encounter problems, do you feel at a loss to start? The next step is to collect error information when the online environment reports an error and notify the developers in a timely manner.

Exceptions are typically caught in a browser using the following methods

addEventListener('error', callback)||window.onerror= callback
Copy the code

I won’t repeat it here.

Vue3 provides us with the API errorHandler for exception catching.

Note: my background service is created with node.js eggJS framework, you can use any of their familiar backend language and framework, the logic is very simple.

Main function points:

1. Generate the SourcemAP file and upload it to the background

Here I use Vite to create Vue3 project, because Vue3 uses rollup as the module packer, so I need to write a rollup plug-in, which is used to read all sourcemap uploaded to the background after the package is finished. And remove the sourcemap file from the packaged input directory to reduce resource requests for the online environment

Introduced and used in vite. Config. js

import { loadEnv } from "vite"; Import vue from '@vitejs/plugin-vue' upload sourcemap rollup plugin import uploadSourceMap from "./src/plugins/rollup-plugin-upload-sourcemap"; Development production export default ({mode}) => {const env = loadEnv(mode, process.cwd()); Return {server: {open: true, port: 3000, host: "0.0.0.0", proxy: {// local test exception monitoring "/mointor": {target: 'http://127.0.0.1:7001', changeOrigin: true,,}},}, plugins: [vue(), // use upload sourcemap rollup plugin uploadSourceMap({ Env.vite_base_api, // Handle target folder interface address handleTargetFolderUrl: '${env.vite_monitor_upload_api}/emptyFolder', // Sourcemap uploadUrl: '${env.vite_monitor_upload_API}/uploadSourceMap',}),], build: {sourcemap: true,}}Copy the code

rollup-plugin-upload-sourcemap.js

import glob from "glob"; import path from "path"; import fs from "fs"; import http from "http"; Export default function uploadSourceMap({handleTargetFolderUrl, // upload sourcemap uploadUrl}) {return {name: "Upload-sourcemap ", // Hook closeBundle() {console.log('closeBundle'); Let env = "uat"; if (baseUrl === "production_base_api") { env = "prod"; Function upload(url, file, env) {return new Promise((resolve) => {const req = http.request(' ${url}? name=${path.basename(file)}&&env=${env}`, { method: "POST", headers: { "Content-Type": "application/octet-stream", Connection: "keep-alive", "Transfer-Encoding": "chunked", }, } ); Fs.createreadstream (file).on("data", chunk => {req.write(chunk); }) .on("end", () => { req.end(); resolve("end"); }); }); Function handleTargetFolder() {http.get(' ${handleTargetFolderUrl}? env=${env}`, () => { console.log("handleTargetFolderUrl success"); }) .on("error", (e) => { console.log(`handle folder error: ${e.message}`); }); } handleTargetFolder(); Async function uploadDel() {const list = glob.sync(path.join("./dist", "./**/*.{js.map,}")); for (const filename of list) { await upload(uploadUrl, filename, env); await fs.unlinkSync(filename); } } uploadDel(); }}; }Copy the code

2. The background service receives and saves the uploaded Sourcemap file

Async uploadSourceMap() {const {CTX} = this; const stream = ctx.req, filename = ctx.query.name, env = ctx.query.env; // Const dir = path.join(this.config.baseDir, 'upload/${env}'); // Const target = path.join(dir, filename); Const writeStream = fs.createWritestream (target); stream.pipe(writeStream); }Copy the code

3. Use errorHandler to catch exceptions and send error information to the background

Introduced and used in main.js

import handleError from "./utils/monitor"; Const app = createApp(app) // Interface address for sending error messages handleError(app, import.meta.env.VITE_MONITOR_REPORT_API);Copy the code

mointor.js

import axios from "axios"; / / browser information function getBrowserInfo () {const agent. = the navigator userAgent. ToLowerCase (); const regIE = /msie [\d.]+; /gi; const regIE11 = /rv:[\d.]+/gi; const regFireFox = /firefox/[\d.]+/gi; const regQQ = /qqbrowser/[\d.]+/gi; const regEdg = /edg/[\d.]+/gi; const regSafari = /safari/[\d.]+/gi; const regChrome = /chrome/[\d.]+/gi; If (regie.test (agent)) {return agent. Match (regIE)[0]; } // IE11 if (regIE11.test(agent)) { return "IE11"; } // firefox if (regFireFox.test(agent)) { return agent.match(regFireFox)[0]; } // QQ if (regQQ.test(agent)) { return agent.match(regQQ)[0]; } // Edg if (regEdg.test(agent)) { return agent.match(regEdg)[0]; } // Chrome if (regChrome.test(agent)) { return agent.match(regChrome)[0]; } // Safari if (regSafari.test(agent)) { return agent.match(regSafari)[0]; Export default function handleError(Vue,baseUrl) {if (! baseUrl) { console.log("baseUrl", baseUrl); return; } vue.config. errorHandler = (err, vm) => {let environment = "test environment "; If (import.meta.env.vite_base_api === "production_base_API ") {environment = "production "; }/ / Axios ({method: "post", url: '${baseUrl}/reportError', data: {environment, location: Window.location. href, message: err. Message, stack: err. Stack, // Browser message browserInfo: GetBrowserInfo (), // the following information can be maintained in vuex store // userId userId:"001", // userName userName:" zhang SAN ", RouterHistory :[{fullPath:"/login", name:" login", query:{}, params:{},},{fullPath:"/home", name:" home", Query :{}, params:{},}], // Click clickHistory:[{pageX:50, pageY:50, nodeName:"div", className:"test", id:"test", InnerText :" Test button "}],},}); }; }Copy the code

4. The background receives an error message, parses the error message with the Sourcemap file, and notifies the developer of the error message

Errorasync reportError() {const {CTX} = this; const { environment, location, message, stack, browserInfo, userId, userName, routerHistory, clickHistory } = ctx.request.body; let env = ''; If (environment === 'env ') {env = 'uat'; } else if (environment === 'env ') {env = 'prod'; } const sourceMapDir = path.join(this.config.baseDir, 'upload/${env}'); Const stackParser = new stackapparser (sourceMapDir); let routerHistoryStr = '<h3>router history</h3>', clickHistoryStr = '<h3>click history</h3>'; ForEach (item => {routerHistoryStr += `<p>name:${item.name} | fullPath:${item.fullPath}</p>`; routerHistoryStr += `<p>params:${JSON.stringify(item.params)} | query:${JSON.stringify(item.query)}</p><p>--------------------</p>`; }); Clickhistory.length &&clickHistory.foreach (item => {clickHistoryStr += `<p>pageX:${item.pageX} | pageY:${item.pageY}</p>`; clickHistoryStr += `<p>nodeName:${item.nodeName} | className:${item.className} | id:${item.id}</p>`; clickHistoryStr += `<p>innerText:${item.innerText}</p><p>--------------------</p>`; }); / / by giving sourcemap file, cooperate with the error information, parse error message const errInfo = await stackParser. ParseStackTrack (stack, message); // Get the current time const now = new Date(); const time = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()} ${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; Const mailMsg = '<h3>message:${message}</h3> <h3>location:${location}</h3> <p>source:${errinfo. source}</p> <p>line::${errInfo.lineNumber}</p> <p>column:${errInfo.columnNumber}</p> <p>fileName:${errInfo.fileName}</p> <p>functionName:${errInfo.functionName}</p> <p>time::${time}</p> <p>browserInfo::${browserInfo}</p> <p>userId::${userId}</p> <p>userName::${userName}</p> ${routerHistoryStr} ${clickHistoryStr} `; // sendMail(' outbox address ', 'outbox authorization code ',' inbox address ', subject environment, body mailMsg); SendMail (' outbox address ', 'outbox authorization code ',' inbox address ', environment, mailMsg); ctx.body = { header: { code: 0, message: 'OK', } }; ctx.status = 200; }Copy the code

sendMail

'use strict'; const nodemailer = require('nodemailer'); Function sendMail(from, fromPass, receivers, subject, msg) { const smtpTransport = nodemailer.createTransport({ host: 'smtp.qq.email', service: 'qq', secureConnection: true, // use SSL secure: true, port: 465, auth: { user: from, pass: fromPass, }, }); Smtptransport. sendMail({from, // Receivers, multiple mailboxes are separated by a comma between them. To: Receivers, // Mail subject, // Mail body HTML: msg, }, err => { if (err) { console.log('send mail error: ', err); }}); } module.exports = sendMail;Copy the code

End

Vue3 project from zero to one to create and build a background management system based on Vite+Vue3+Vue Router+Vuex+TS+Element3+ AXIos +Jest+Cypress, interested in the point of attention!

If you have any questions or suggestions about this article, please leave a comment!