The testing resources of the company I am working for are relatively limited (the testing sisters are all hot property). For several projects launched last year, it is impossible to say that there are no customer complaints, but I have been invited to talk with them a lot. Therefore, during the year-end summary of the project team, we discussed the necessity of online error log collection. When I went to work after the New Year, I had to go back because I was the only one in the front end of the project team. I had to continue to finish some previous projects. Until recently, I began to study the error log of the front end.
First of all, before you start, check out previous experiences online to see if you can reduce your workload. We realized that there were already a lot of perfect solutions on the web, like FunDebug and Sentry, but because we needed to integrate with the existing logging plugins on the back end, we had to do it ourselves.
Common methods for collecting error information
try… catch
Collect errors, the first thing that comes to mind is this statement.
try {
//....
} catch(err) {
//err indicates a captured error
}
Copy the code
But we can’t pass it through all the code, it’s too inefficient.
window.onerror
This method is used to catch errors when they occur globally.
window.onerror = function() {
// Get an error message
let errInfo = format(arguments);
};
Copy the code
Unfortunately, errors cannot be caught with this method in a VUE project.
errorHandler
Window.onerror cannot catch errors because in vUE these exceptions are caught in advance by the Vue itself. ErrorHandler is a hook provided by vue to catch errors. It can be configured globally through vue. config.
Vue.config.errorHandler = function (err, vm, info) {
// error information captured by err
// 'vm' has an error vue instance
// 'info' is Vue specific error information, such as the lifecycle hook where the error occurred
}
Copy the code
Writing a plug-in
Lead to
Vue specifies that we need to expose an Install method when we write the plug-in, which takes two parameters, the first being a Vue constructor and the second an optional configuration item. Official website address.
Synchronous exception capture
First of all, we need to consider whether we need to configure items. First of all, the error logs we collected will be sent to the backend for library entry, so the interface address must be configured in detail.
import axios from 'axios'
import qs from 'qs'
let errorLog = {}
errorLog.install = function (Vue, options) {
function pushErrorLog({ errInfo, vueErrInfo }) {
// Check whether the background interface address is passed in the configuration item, otherwise the plugin will be put into an infinite loop
if (options && options.interfaceUrl) {
let obj = {
errInfo,
vueErrInfo
}
// Invoke the background interface to push logsaxios.post(options.interfaceUrl, qs.stringify({ ... obj })).then(res= >{})}}function errorHandler(err, vm, info) {
Vue.nextTick(() = > {
pushErrorLog({
errInfo: err.stack.toString(),
vueErrInfo,
})
})
}
Vue.config.errorHandler = errorHandler
}
export default errorLog
Copy the code
In this way, we completed a basic version of exception catching, but errorHandler can only catch synchronous errors, so we can’t catch errors in asynchronous requests that interact with the background in our project.
Asynchronous exception catching
I had no idea what to do here for a long time. At first, I thought to do it in axios public wrapper interceptor, but this can only collect the information specified by the back end when the error request occurs, which is not very meaningful. The back end error log can already catch the exception here. In this case, I need to catch exceptions that may be caused by the interface returning data after the request is successful.
After searching for a long time on the Internet, I found an idea to intercept promise, and rewrite promise.prototype. then into try… Catch Catches exceptions.
// Determine whether the current environment supports Promise
if (Promise && Promise.prototype.then) {
let promiseThen = Promise.prototype.then
Promise.prototype.then = function (resolve, reject) {
return promiseThen.call(this, coverPromiseFunction(resolve), coverPromiseFunction(reject))
}
}
// Add exception capture
let coverPromiseFunction = (fn) = > {
// If fn is a function, run it directly in a try-catch, otherwise wrap the class's methods. Fn returns null in a Promise
if (typeoffn ! = ='function') {
return null
}
return function () {
try {
return fn.apply(this.arguments)}catch (error) {
pushErrorLog({
errStr: error.stack.toString(),
})
throw (error)
}
}
}
Copy the code
At this point, we’ve pretty much written down the error messages that occur in the code, and it’s time to move on to other error messages.
Record client information when exceptions occur
Compatibility issues are the enemy of the front-end for life
Although the small outsourcing company I work for doesn’t have much of a requirement for this area, it usually recommends Chrome for projects like PC. However, if something goes wrong with the phone, it’s hard to ask the customer to change the phone, so keep a record of the client information when the error occurs.
Mobile terminal information collection
Mobile terminal information collection is implemented based on mobile-detect.js. Official website address.
const MobileDetect = require('mobile-detect')
export const getClientDetail = () = > {
let device_type = navigator.userAgent;
let md = new MobileDetect(device_type); // Initialize mobile-detect using the userAgent information
let os = md.os(); // Get the mobile operating system
let model = "";
if (os == "iOS") { // Ios processing
os = md.os() + md.version("iPhone");
model = md.mobile();
} else if (os == "AndroidOS") { //Android system processing
os = md.os() + md.version("Android");
let sss = device_type.split(";");
let i = sss.contains("Build/");
if (i > -1) {
model = sss[i].substring(0, sss[i].indexOf("Build/")); }}return {
clientOS: os,
clientModel: model,
clientType: md.mobile() === null ? 'pc' : 'mobile'}}Array.prototype.contains = function (needle) {
for (let i in this) {
console.log(this[i])
if (typeof this[i] === 'string' && this[i].indexOf(needle) > 0) {
returni; }}return -1;
}
Copy the code
Here, we collected clientOS, clientModel and clientType information as operating system, mobile phone model and clientType respectively.
Collect information on the PC
At first, I wanted to collect what browser the user was using, but after checking online, I found that the common judgment method was to judge by the navigator. UserAgent attribute, but the browser update was too fast, and the character intercession was not very accurate. I didn’t think of any good method at the moment, and I hope you can give me some advice if you have a good solution.
Perfect the plug-in
Since the development is online log collection, we added an execution environment for the plug-in, so there is no need to push logs to the back end for local development. I also added configuration items enabled by the local environment for local testing, so I improved the code and attached the complete code.
import { getClientDetail } from './util'
import axios from 'axios'
import qs from 'qs'
// Get the environment for the error log
let needErrorLog = ['production'] //'development'
// Get the client environment
const clientInfo = getClientDetail()
let errorLog = {}
errorLog.install = function (Vue, options) {
// store.commit('addBaseUrl', options.interfaceUrl)
function checkNeed() {
if(options && options.env) {
if(typeof options.env === 'string') {
needErrorLog.push(options.env)
}
if(Array.isArray(options.env)) {
needErrorLog = [...needErrorLog, ...options.env]
}
}
const env = process.env.NODE_ENV
return needErrorLog.includes(env)
}
function pushErrorLog({ errInfo, vueErrInfo }) {
// Invoke the background interface to push logs
if (options && options.interfaceUrl) {
letobj = { ... clientInfo,errInfo: errStr,
vueErrInfo: info, } axios.post(options.interfaceUrl, qs.stringify({ ... obj })).then(res= > {
// console.log(res)}}})function errorHandler(err, vm, info) {
Vue.nextTick(() = > {
pushErrorLog({
errInfo: err.stack.toString(),
vueErrInfo,
})
})
}
// Determine whether the current environment supports Promise
if (Promise && Promise.prototype.then) {
let promiseThen = Promise.prototype.then
Promise.prototype.then = function (resolve, reject) {
return promiseThen.call(this, coverPromiseFunction(resolve), coverPromiseFunction(reject))
}
}
// Add exception capture
let coverPromiseFunction = (fn) = > {
// If fn is a function, run it directly in a try-catch, otherwise wrap the class's methods. Fn returns null in a Promise
if (typeoffn ! = ='function') {
return null
}
return function () {
try {
return fn.apply(this.arguments)}catch (error) {
pushErrorLog({
errStr: error.stack.toString(),
})
throw (error)
}
}
}
Vue.config.errorHandler = errorHandler
}
export default errorLog
Copy the code
Then use it in your project
import errorLog from 'lyf-vue-error-log'
Vue.use(errorLog, { interfaceUrl: 'Interface address' })
Copy the code
Release the plugin
Once the plug-in is developed, publishing is simple.
Create an NPM account
First go to NPM official website to create an account for publishing.
Release preparation
First, NPM recommends that we distribute packages that include the following files:
- Package. json (basic package information, where the main property is defined as our entry file)
- Readme.md (documentation)
- Index.js (entry file)
Attached is my current file directory
├ ─ ─ the SRC │ ├ ─ ─ lib | | └ ─ ─ util. Js utility methods │ └ ─ ─ index. The js entry documents ├ ─ ─ the README. Md ├ ─ ─ package. The jsonCopy the code
release
NPM login // publish NPM publishCopy the code
Execute these two commands to publish the package, and then log in to NPM to view the package you published.
Then, if we need to update our package, we just need to change the version in package.json and do NPM publish.
Rule: for “version”:” X.Y.Z “1. Bug fix, minor change, add 2. New features have been added, but backward compatibility is still possible, adding 3. There are big changes, backward compatibility is not possible, adding X
Finally, attach the address of the plug-in I developed this time
Processing error data
The local test, which had been used locally in the plugin’s use, looked fine, but was eventually released to the online test, so I picked up a local Tomat, threw the packaged file on it to simulate the online release, and sure enough, I ran into problems again.
We can see that we get the error stack information is packaged error message, do not know the specific location.
Through uniApp development can also find the corresponding file through its spliced JS file name, but the normal VUE project can not understand, so such a record error can not give us to locate the specific code location, we have to continue to optimize.
The general solution on the Internet is to generate the corresponding. Map file when packaging, connect the file name with the corresponding error reporting JS, and then parse through the source Map parsing method to get the desired error reporting information.
Here I directly borrowed sourceMap on NPM for parsing, directly into the code.
const fs = require('fs'); // Read the file
const sourceMap = require('source-map'); // Parse source-map contents
/ * * *@description: Parse error stack information *@param {String} info
* @author: longyunfei
*/
async function analyzingErrInfo(info) {
let infoList = []; // Error message list
let lineList = []; / / an error
let columnList = []; / / an error
let sourceList = []; // List of packed map files
let geJsMap = / (? <=js\/).*? (? =) /; // Split the error stack information into multiple messages in a JS file
info.split('at').forEach(item= > {
if (item.match(geJsMap)) {
infoList.push(item.match(geJsMap)[0])
// The number of rows and columns must be separated according to the error message format
lineList.push(item.split(':') [3])
columnList.push(item.split(':') [4].split(') ') [0])}})let pormiseList = getSourceMapList(infoList)
Promise.all(pormiseList).then(res= > {
res.forEach((item, index) = > {
let ret = item.originalPositionFor({
line: parseInt(lineList[index]), // The compressed line number
column: parseInt(columnList[index])// Compressed column number
})
sourceList[index] = ret
})
console.log(sourceList)
})
}
/ * * *@description: converts the error js file name to the map file name and returns the parsed SMC object return *@param {Array} list
* @return {Array}
* @author: longyunfei
*/
function getSourceMapList(list) {
let result = []
for (let i in list) {
// Get the.map file corresponding to the js file
let fileUrl = `${list[i]}.map`;
// Resolve the SMC object of the map file by sourceMap
let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(`./js/${fileUrl}`.'utf8')); // Returns a Promise object
result.push(smc)
}
return result
}
// Get an error message
let str = "TypeError: Cannot set property 'name' of undefined at o.clickBtn (http://localhost:8080/js/chunk-3308011b.3c4f470b.js:1:680067) at nt (http://localhost:8080/js/chunk-vendors.6b75dfdf.js:14:11644) at HTMLLIElement.n (http://localhost:8080/js/chunk-vendors.6b75dfdf.js:14:13456) at HTMLLIElement.Zr.a._wrapper (http://localhost:8080/js/chunk-vendors.6b75dfdf.js:14:51712)"
analyzingErrInfo(str)
Copy the code
Here we get the error message we want
conclusion
When I was in the meeting before, I was worried that I could not handle a small thing, but after the overall development of a plug-in, I found that it was not as difficult as I thought. The whole process down also learned a lot, like vUE plug-in development, NPM package release and so on…
The plugin has not been officially put into production, there may be a lot of problems, but gradually improved, here is just to record the development process.