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 packagedsourcemapOf the filewebpack plugin
  • How to combine error logs on the server sidesourcemapLocation 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) =>{})

  • usetry... catchUnable 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

  • useaddEventListener('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
    • writeerrorUpload 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
    • writeerrorUpload 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
    • writeerrorUpload 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

Eggjs is logged as an error

Method:

  • You can usefsWrite 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.jsIn the file
config.customLogger = {
    frontendLogger: {
      file: path.join(appInfo.root, 'logs/frontend.log')}}Copy the code
  • inapp/controller/monitor.jsFile 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.logThe 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, builderror
// 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 downeslint, 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
  • deletedistIn the directorysourcemapMapping 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:

  • versionSource Map version, currently 3
  • soruces Converted file name
  • namesAll variable and attribute names before conversion
  • mappingsA string that records location information
  • fileConverted file name
  • sourcesContentSource content list (optional, in the same order as source file list)
  • sourceRootSource 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

  • writewebpack 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:

  • readsourceMapfile
  • Will read thesourceMapThe 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 downcsrf
// /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 installationerror-stack-parser:
yarn add error-stack-parser
Copy the code

Write test cases:

  • parsingerror.stackinformation
// /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/uploadWithin the.mapCopy 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 serversourceMapFile 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