By reading this article, you will learn:
- Common front-end error types and ways to catch them
- How can front-end errors be reported to the server
- How does the server receive errors and logs from the front-end
- How to write one that can be uploaded when a project is packaged
sourcemap
Of the filewebpack plugin
- How to combine error logs on the server side
sourcemap
Location of the file restore error code - Simple Jest unit test writing
The text start
Using a try… catch
const func = () = > {
console.log('fun start')
err
console.log('fun end')}try {
func()
} catch (err) {
console.log('err', err)
}
Copy the code
Preview code: trycatch catches an exception
- Disadvantages: Unable to catch asynchronous errors
const func = () = > {
console.log('fun start')
err
console.log('fun end')}try {
setTimeout(() = > {
func()
})
} catch (err) {
console.log('err', err)
}
Copy the code
The sample
So how do you catch asynchronous errors?
Window. onerror Catches asynchronous errors
const func = () = > {
console.log('fun start')
err
console.log('fun end')}setTimeout(() = > {
func()
})
window.onerror = (. args) = > {
console.log('args:', args)
}
Copy the code
The sample
Here we can start by using window.onError to catch our asynchronous error.
But does it catch all types of errors?
For example: resource loading address error?
<img src="//xxsdfsdx.jpg" alt="">
window.onerror = (. args) = > {
console.log('args:', args)
}
Copy the code
Example: Resource loading error
At this point, we see that the resource address error is not printed, so how do we catch this type of error?
window.addEventListener(‘error)
How to catch resource address errors?
<img src="/xxx.png" />
window.addEventListener('error'.(event) = > {
console.log('event err:', event)
}, true) // The third argument is true, select the capture mode to listen
Copy the code
Example: resource load error capture
How do promises get caught?
Window.addeventlistener (‘ unHandledrejection ‘, (err) =>{})
- use
try... catch
Unable to capture
const asyncFunc = () = > {
return new Promise((res) = > {
err
})
}
try {
asyncFunc()
} catch(e) {
console.log('err:', e)
}
Copy the code
Example: try-catch fails to catch a promise
- use
addEventListener('unhandledrejection')
const asyncFunc = () = > {
return new Promise((res) = > {
err
})
}
asyncFunc()
window.addEventListener('unhandledrejection'.(event) = > {
console.log('event err:', event)
})
Copy the code
- Promise to capture
Question: Can you use one capture method to catch all errors?
const asyncFunc = () = > {
return new Promise((res) = > {
err
})
}
asyncFunc()
// Actively throws a captured promise-type error
window.addEventListener('unhandledrejection'.(event) = > {
throw event.reason
})
window.addEventListener('error'.(err) = > {
console.log('err:', err)
}, true)
Copy the code
The sample
summary
Exception types | Synchronized methods | Asynchronous methods | Resource to load | Promise | async / await |
---|---|---|---|---|---|
try/catch | y | y | |||
onerror | y | y | |||
addEventListener(‘error’) | y | y | y | ||
addEventListener(‘unhandledrejection’) | y | y |
Exception Reporting Server
The exception reporting server has two main methods: one is to dynamically create img tags, the other is to directly use Ajax to send requests to report. The first is the focus here
Dynamically create an IMG tag
- Error monitoring and reporting code
// Error reported
function uploadError({lineno, colno, error: { stack }, message, filename }) {
console.log('uploadError---', event)
// Sort out the error messages we want
const errorInfo = {
lineno,
colno,
stack,
message,
filename
}
// Use Base64 encoding to serialize error messages to avoid errors caused by special characters
const str = window.btoa(JSON.stringify(errorInfo))
// Create an image and use the image to send a get request to the backend server that collected the error,
// Upload information: wrong resource, wrong time
new Image().src = `http://localhost:7001/monitor/error? info=${str}`
}
window.addEventListener('unhandledrejection'.(event) = > {
// Actively throw again
throw event.reason
})
window.addEventListener('error'.(err) = > {
console.log('error:', err)
// Error reported
uploadError(err)
})
Copy the code
-
Backend collection error
-
To build the EggJS project, please refer to the official website of egg.js
npm i egg-init -g egg-init backend --type=simple cd backend npm i npm run dev Copy the code
- write
error
Upload interface – Add route
// /app/router.js module.exports = app= > { const { router, controller } = app; router.get('/', controller.home.index); router.get('/monitor/error', controller.monitor.index) }; Copy the code
- write
error
Upload interface – Write interface, used hereBuffer-Nodejs
// app/controller/monitor.js 'use strict'; const Controller = require('egg').Controller; class MonitorController extends Controller { async index() { const { ctx } = this; const { info } = ctx.query // Buffer accepts a base64 encoded data const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8')) console.log('error-info', json) ctx.body = 'hi, json'; }}module.exports = MonitorController; Copy the code
- write
error
Upload interface – tests
const info = window.btoa(JSON.stringify({test: 'err'})) // "eyJ0ZXN0IjoiZXJyIn0=" // Rest-client test interface test GET http://localhost:7001/monitor/error? info=eyJ0ZXN0IjoiZXJyIn0= Error-info {test: 'err'} Copy the code
- write
Eggjs is logged as an error
Method:
- You can use
fs
Write to a file for recording - You can also use a mature logging library like Log4j
Of course, eggJS supports custom logging, so we can use this functionality to customize a front-end error log.
- in
/config/config.default.js
In the file
config.customLogger = {
frontendLogger: {
file: path.join(appInfo.root, 'logs/frontend.log')}}Copy the code
- in
app/controller/monitor.js
File for log collection
async index() {
const { ctx } = this;
const { info } = ctx.query
// Buffer accepts a base64 encoded data
const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
console.log('error-info', json)
// Write to the log
this.ctx.getLogger('frontendLogger').error(json)
ctx.body = 'hi, json';
}
Copy the code
- test
/ / rest - client test
GET http://localhost:7001/monitor/error? info=eyJ0ZXN0IjoiZXJyIn0=
Copy the code
- Result: View
/logs/frontend.log
The specific log information is contained in the file
2021-04-03 11:58:48,543 ERROR 2180 [-/127.0.0.1/-/4ms GET /monitor/ ERROR? Info =eyJ0ZXN0IjoiZXJyIn0=] {test: 'err'}Copy the code
How to collect exceptions in Vue project
Vue3. X’s official website
- Initialize the VUE project
npm i @vue/cli -g
vue create vue-app
cd vue-app
yarn install
yarn serve
Copy the code
- Write code, build
error
// src/components/HelloWorld.vue
/ /... Omit some code
export default {
name: 'HelloWorld'.props: {
msg: String
},
mounted() {
// methods did not define method ABC, error
abc()
}
}
Copy the code
- Shut down
eslint
, reduce the impact, get the front-end service running, new/editvue.config.js
// /vue.config.js
module.exports = {
// close eslint setting
devServer: {
overlay: {
warning: true.errors: true}},lintOnSave: false
}
Copy the code
- Capture the error
// src/main.js
// Use this method uniformly in vue to catch errors
Vue.config.errorHandler = (err, vm, info) = > {
console.log('errHandler:', err)
uploadError(err)
}
function uploadError({ message, stack }) {
console.log('uploadError---')
// Sort out the error messages we want
const errorInfo = {
stack,
message,
}
// Use Base64 encoding to serialize error messages to avoid errors caused by special characters
const str = window.btoa(JSON.stringify(errorInfo))
// Create an image and use the image to send a get request to the backend server that collected the error,
// Upload information: wrong resource, wrong time
new Image().src = `http://localhost:7001/monitor/error? info=${str}`
}
new Vue({
render: h= > h(App)
}).$mounted('#app')
Copy the code
- Package the VUE project and run tests to see if errors are caught
yarn build
cd dist
hs
Copy the code
- delete
dist
In the directorysourcemap
Mapping file, at this time error location code is not the source code, but after the compression of the code, not beautiful
Because there are two main types of packaged code JS files
app.xxx.js
app.xxx.js.map
Copy the code
We can look at the content structure of the.map file:
{
"version": 3."sources": [
"webpack:///webpack/bootstrap"."webpack:///./src/App.vue"."webpack:///./src/components/HelloWorld.vue"."webpack:///./src/components/HelloWorld.vue? 354f"."webpack:///./src/App.vue? eabf"."webpack:///./src/main.js"."webpack:///./src/assets/logo.png"."webpack:///./src/App.vue? 7d22"]."names": [
"webpackJsonpCallback"."data"./ /...]."mappings": "aACE,SAASA,EAAqBC..."."file": "js/app.9a4488cf.js"."sourcesContent": [" \t// install a JSONP callback..."]."sourceRoot": ""
}
Copy the code
It mainly contains these things:
version
Source Map version, currently 3soruces
Converted file namenames
All variable and attribute names before conversionmappings
A string that records location informationfile
Converted file namesourcesContent
Source content list (optional, in the same order as source file list)sourceRoot
Source file root directory (optional)
About Source map, you can refer to these two articles source-map- Ruan Yifeng and Source map principle and source code exploration – Jooger article – Zhihu
Later, we will parse from app.xxx.js.map and restore the error code
Sourcemap upload plugin
Write a UploadSourceMapWebpackPlugin plug-in, every time used for packaging code automatically uploaded to the server specified directory
- write
webpack plugin
// frontend/plugin/uploadSourceMapWebpackPlugin.js
class UploadSourceMapWebpackPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
console.log('UploadSourceMapWebpackPlugin apply')}}module.exports = UploadSourceMapWebpackPlugin
Copy the code
- Configure the plug-in
// /vue.config.js
// refer:https://cli.vuejs.org/zh/config/#configurewebpack
const UploadSourceMapWebpackPlugin = require('./plugin/uploadSourceMapWebpackPlugin')
module.exports = {
configureWebpack: {
plugins: [new UploadSourceMapWebpackPlugin({
uploadUrl: 'http://localhost:7001/monitor/sourcemap'}})],// close eslint setting
devServer: {
overlay: {
warning: true.errors: true}},lintOnSave: false
}
Copy the code
- Packaging test
yarn build
At this point, we can see the log on the command line
Building forproduction... UploadSourceMapWebpackPlugin applyCopy the code
Next, complete UploadSourceMapWebpackPlugin plugin function in detail
const path = require('path')
const glob = require('glob')
const fs = require('fs')
const http = require('http')
class UploadSourceMapWebpackPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
console.log('UploadSourceMapWebpackPlugin apply')
// Definitions are executed after packaging
compiler.hooks.done.tap('UploadSourceMapWebpackPlugin'.async status => {
// Read the sourceMap file
const list = glob.sync(path.join(status.compilation.outputOptions.path, `./**/*.{js.map,}`))
console.log('list', list)
// list [
// '/mnt/d/Desktop/err-catch-demo/vue-app/dist/js/app.d15f69c0.js.map',
// '/mnt/d/Desktop/err-catch-demo/vue-app/dist/js/chunk-vendors.f3b66fea.js.map'
// ]
for (let filename of list) {
await this.upload(this.options.uploadUrl, filename)
}
})
}
upload(url, file) {
return new Promise(resolve= > {
console.log('upload Map: ', file)
const req = http.request(`${url}? name=${path.basename(file)}`, {
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()
})
})
}
}
module.exports = UploadSourceMapWebpackPlugin
Copy the code
Function:
For each build done:
- read
sourceMap
file - Will read the
sourceMap
The file is uploaded to the specified server
The Eggjs server sourceMap upload interface
- The back-end route is added
'use strict';
// /app/router.js
/ * * *@param {Egg.Application} app - egg application
*/
module.exports = app= > {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/monitor/error', controller.monitor.index)
+ router.post('/monitor/sourcemap', controller.monitor.upload)
};
Copy the code
- New interface to write file information
'use strict';
/app/controller/monitor.js
const Controller = require('egg').Controller;
const path = require('path')
const fs = require('fs')
class MonitorController extends Controller {
// ...
async upload() {
const { ctx } = this
// Get a stream
const stream = ctx.req
const filename = ctx.query.name
const dir = path.join(this.config.baseDir, 'upload')
// Check whether the upload exists
if(! fs.existsSync(dir)) { fs.mkdirSync(dir) }const target = path.join(dir, filename)
// Create a write stream to write information
console.log('writeFile====', target);
const writeStream = fs.createWriteStream(target)
stream.pipe(writeStream)
}
}
module.exports = MonitorController;
Copy the code
- Shut down
csrf
// /config/config.default.js
config.security = {
// There may be SCRF risk
csrf: {
enable: false}}Copy the code
- test
yarn build
# egg-server log info
writeFile==== D:\Desktop\err-catch-demo\backend\upload\app.d15f69c0.js.map
writeFile==== D:\Desktop\err-catch-demo\backend\upload\chunk-vendors.f3b66fea.js.map
Copy the code
Stack parsing function
- The installation
error-stack-parser
:
yarn add error-stack-parser
Copy the code
Write test cases:
- parsing
error.stack
information
// /app/utils/stackparser.js
'use strict';
const ErrorStackParser = require('error-stack-parser');
const { SourceMapConsumer } = require('source-map');
const path = require('path');
const fs = require('fs');
module.exports = class StackParser {
constructor(sourceMapDir) {
this.sourceMapDir = sourceMapDir;
this.consumers = {};
}
parseStackTrack(stack, message) {
const error = new Error(message);
error.stack = stack;
const stackFrame = ErrorStackParser.parse(error);
return stackFrame;
}
async getOriginalErrorStack(stackFrame) {
const origin = [];
for (const v of stackFrame) {
origin.push(await this.getOriginPosition(v));
}
return origin;
}
// Read an error message from the sourceMap file
async getOriginPosition(stackFrame) {
let { columnNumber, lineNumber, fileName } = stackFrame;
fileName = path.basename(fileName);
// Determine whether consumers exist
let consumer = this.consumers[fileName];
if(! consumer) {/ / read sourceMap
const sourceMapPath = path.resolve(this.sourceMapDir, fileName + '.map');
// Check whether the file exists
if(! fs.existsSync(sourceMapPath)) {// If not, return the source file
return stackFrame;
}
const content = fs.readFileSync(sourceMapPath, 'utf-8');
consumer = await new SourceMapConsumer(content, null);
this.consumers[fileName] = consumer;
}
const parseData = consumer.originalPositionFor({ line: lineNumber, columnNumber });
returnparseData; }};Copy the code
- Test preparation: will first
/upload
Within the.map
Copy files toapp/utils/__test__
In the directory - Test cases:
// How do I manually restore error information from sourcemap? https://www.zhihu.com/question/285449738
// /app/utils/stackparser.spec.js
'use strict';
const StackParser = require('.. /stackparser');
// const { resolve } = require('path');
// const { hasUncaughtExceptionCaptureCallback } = require('process');
const error = {
stack: 'ReferenceError: abc is not defined\n' +
' at Proxy.mounted (http://127.0.0.1:8080/js/app.c82461cf.js:1:606)\n' +
'the at (http://127.0.0.1:8080/js/chunk-vendors.b64c81c0.js:1:8614) I \ n' +
'the at c (http://127.0.0.1:8080/js/chunk-vendors.b64c81c0.js:1:8697) \ n' +
'at Array. E. __weh. E. __weh (http://127.0.0.1:8080/js/chunk-vendors.b64c81c0.js:1:15852) \ n ' +
'the at (http://127.0.0.1:8080/js/chunk-vendors.b64c81c0.js:1:10078) I \ n' +
'(http://127.0.0.1:8080/js/chunk-vendors.b64c81c0.js:1:31862) at Q \ n' +
'at the mount (http://127.0.0.1:8080/js/chunk-vendors.b64c81c0.js:1:22532) \ n' +
'at the Object. The e.m mount (http://127.0.0.1:8080/js/chunk-vendors.b64c81c0.js:1:50901) \ n' +
'at the Object. 8287 (http://127.0.0.1:8080/js/app.c82461cf.js:1:1066) \ n' +
'(http://127.0.0.1:8080/js/app.c82461cf.js:1:1178) at o'.message: 'abc is not defined'.filename: 'http://127.0.0.1:8080/js/app.c82461cf.js:1:606'}; it('test==========>'.async() = > {const stackParser = new StackParser(__dirname);
// console.log('path', path.basename(__dirname));
// console.log('Stack:', error.stack);
const stackFrame = stackParser.parseStackTrack(error.stack, error);
stackFrame.map(v= > {
// console.log('stackFrame: ', v);
return v;
});
const originStack = await stackParser.getOriginalErrorStack(stackFrame);
console.log('originStack=======>0', originStack[0]);
// assertion, you need to manually modify the following assertion information, and only test the 0th example
// eslint-disable-next-line no-undef
expect(originStack[0]).toMatchObject({
line: 15.column: 8.name: 'abc'.source: 'webpack://front/src/components/HelloWorld.vue'}); });Copy the code
Here, we can see that we need to restore the file path of sourceMap and the number of lines of the error code from the compressed error message:
{
line: 15.column: 8.name: 'abc'.source: 'webpack://front/src/components/HelloWorld.vue',}Copy the code
- test
cd backend/app/utils
npx jest stackparser --watch
Copy the code
Show the test case pass, test, and we’re done:
- Common front-end faults are reported to the server
- Through the server
sourceMap
File for error scenario restoration: error code file and line number
At this point, we can pinpoint the error code.
The above ~ ~ ~
- Code: the error – catch – demo
- Original address: blog/jsmond2016
References:
- The source – the map – nguyen
- Source Map principle and Source code exploration – Jooger article – Zhihu
- How do I manually restore error details using Sourcemap?
- webpack plugin
- source-map/github
- Summary of front-end error monitoring and reporting methods
- How to monitor front-end exceptions?
- Front-end Anomaly Monitoring
- Jest official website documentation