about

  • Wechat official account: Love-Fed
  • My blog: Loeb’s blog
  • Zhihu column: Front hula hoop

preface

Hello, everyone, it’s time to meet you again. This time, I will share with you some key points of the anomaly capture and reporting mechanism that need to be understood in the monitoring of front-end anomalies, including reference codes and processes of actual combat nature.

In the first place, why do we do exception catching and reporting?

So-called imperfection, a through a large amount of test and alignment of the project in some cases very hidden bugs will still exist, the complexity and unpredictability of the problem only through perfect monitoring mechanism can effectively reduce the losses, so for face to face with the front end of the user, exception handling and reporting is essential.

Although there are some very perfect front-end monitoring systems on the market at present, such as Sentry, BugSNag, etc., only by knowing yourself and knowing your opponent can you overcome the danger of a hundred battles. Only by understanding the principle and logic can you use it with ease.

Exception catching method

1. try catch

In general, to determine if there is an exception in a piece of code, we would write:

try {
    var a = 1;
    var b = a + c;
} catch (e) {
    // capture processing
    console.log(e); // ReferenceError: c is not defined
}
Copy the code

Try catch is a good way to catch exceptions and handle them accordingly, so that the page does not hang. However, it has some disadvantages, such as wrapping the code that catches the exception, which leads to a bloated page and is not suitable for the entire project.

2. window.onerror

Window. onerror provides global listening for exceptions in contrast to try catch:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {
    console.log('errorMessage: ' + errorMessage); // Exception information
    console.log('scriptURI: ' + scriptURI); // Abnormal file path
    console.log('lineNo: ' + lineNo); // Exception line number
    console.log('columnNo: ' + columnNo); // Exception column number
    console.log('error: ' + error); // Abnormal stack information
};

console.log(a);
Copy the code

As shown in figure:

Window. onerror provides us with the error information and the error line number, which can be accurately located, which seems to be exactly what we want, but then comes the pit filling process.

Exception catch problem

1. Script error.

It makes sense to try to catch exceptions on local pages, such as:

<! -- http://localhost:3031/ -->
<script>
window.onerror = function() {
    console.log(arguments);
};
</script>
<script src="http://cdn.xxx.com/index.js"></script>
Copy the code

Here we put the static resource on the foreign domain for optimized loading, but catch the exception message is:

<script src="http://cdn.xxx.com/index.js" crossorigin="anonymous"></script>
Copy the code

General CDN sites will set access-Control-allow-Origin to *, meaning that all domains are accessible.

2. sourceMap

You may have to compress the code for distribution after cross-domain or co-domain scripting, and the compressed code will not be able to find the original error location. Here we use Webpack to pack the code into bundle.js:

// webpack.config.js
var path = require('path');

/ / webpack 4.4.1
module.exports = {
    mode: 'development'.entry: './client/index.js'.output: {
        filename: 'bundle.js'.path: path.resolve(__dirname, 'client')}}Copy the code

The final script file introduced in our page looks like this:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1.exports: {}}... ;Copy the code

So the exception message we see looks like this:

So how to solve it? If you are smart, you may have guessed that source-map is enabled. Yes, we can use WebPack to generate a map file corresponding to the script to trace.

module.exports = {
    ...
    devtool: '#source-map'. }Copy the code

A comment at the end of a packed and compressed file reads:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1.exports: {}}... ;//# sourceMappingURL=bundle.js.map
Copy the code

The map file corresponding to this file is bundle.js.map. Here is the contents of a source-map file, which is a JSON object:

version: 3.// The Source map version
sources: ["webpack:///webpack/bootstrap". ] .// File before conversion
names: ["installedModules"."__webpack_require__". ] .// All variable and attribute names before conversion
mappings: "aACA,IAAAA,KAGA,SAAAC...".// A string to record location information
file: "bundle.js".// The converted file name
sourcesContent: ["// The module cache var installedModules = {}; ..."]./ / the source code
sourceRoot: "" // The directory where the file was stored before the conversion
Copy the code

If you want to learn more about sourceMap, you can go to JavaScript Source Map

So, now that we have the map file of the corresponding script, how can we parse and get the exception information of the file before compression? I’ll talk about this next when exceptions are reported.

3. MVVM framework

More and more projects are using front-end frameworks, and if you try to catch exceptions with window.onError in the MVVM framework as usual, you will probably get nothing, or not at all, because your exception information will be caught by the framework’s own exception mechanism. For example, in Vue 2.x we would catch global exceptions like this:

Vue.config.errorHandler = function (err, vm, info) {
	let { 
	    message, // Exception information
	    name, // Exception name
	    script,  // Exception script URL
	    line,  // Exception line number
	    column,  // Exception column number
	    stack  // Abnormal stack information
	} = err;
	
	// VM is the Vue instance that throws an exception
	// info for Vue specific error information, such as the lifecycle hook where the error occurred
}
Copy the code

At present, script, line and column are printed as undefined, but these information can be found in the stack, which can be obtained through regular matching and reported.

React 16.x has introduced Error Boundary:

class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }

    componentDidCatch(error, info) {
        this.setState({ hasError: true });
        
        // Reports the exception information to the server
        logErrorToMyService(error, info); 
    }

    render() {
        if (this.state.hasError) {
            return 'Wrong';
        }
    
        return this.props.children; }}Copy the code

We can then use this component like this:

<ErrorBoundary>
    <MyWidget />
</ErrorBoundary>
Copy the code

For details, see Error Handling in React 16

Exception reporting

The above introduced the front-end exception capture related knowledge points, so now that we successfully captured the exception, so how to report?

In the case that the script code is not compressed, you can directly capture and upload the corresponding exception information. The following describes common methods for handling compressed file reporting.

1. Submit an exception

When an exception is caught, we can pass the exception information to the interface, using window.onerror as an example:

window.onerror = function(errorMessage, scriptURI, lineNo, columnNo, error) {

    // Build the error object
    var errorObj = {
    	errorMessage: errorMessage || null.scriptURI: scriptURI || null.lineNo: lineNo || null.columnNo: columnNo || null.stack: error && error.stack ? error.stack : null
    };

    if (XMLHttpRequest) {
    	var xhr = new XMLHttpRequest();
    
    	xhr.open('post'.'/middleware/errorMsg'.true); // Report to the node middle layer for processing
    	xhr.setRequestHeader('Content-Type'.'application/json'); // Set the request header
    	xhr.send(JSON.stringify(errorObj)); // Send parameters}}Copy the code

2. SourceMap parsing

In fact, source-map format file is a data type, since it is a data type, there must be a way to parse it. Currently, there are corresponding toolkits for parsing it in the market, and a plug-in called ‘source-Map’ is popular in the browser environment or Node environment.

Through the require plug-in, the map file can be parsed by the front-end browser, but because the front-end parsing speed is slow, so we do not recommend here, we will use the server parsing. If your application has a Node middle tier, you can submit the exception information to the middle tier, and then parse the map file to pass the data to the backend server. The middle tier code looks like this:

const express = require('express');
const fs = require('fs');
const router = express.Router();
const fetch = require('node-fetch');
const sourceMap = require('source-map');
const path = require('path');
const resolve = file= > path.resolve(__dirname, file);

// Define the POST interface
router.post('/errorMsg/'.function(req, res) {
    let error = req.body; // Get the error object from the front end
    let url = error.scriptURI; // Zip file path

    if (url) {
        let fileUrl = url.slice(url.indexOf('client/')) + '.map'; // Map file path

        / / parsing sourceMap
        let smc = new sourceMap.SourceMapConsumer(fs.readFileSync(resolve('.. / ' + fileUrl), 'utf8')); // Return a Promise object
        
        smc.then(function(result) {
        
            // Parse the original error data
            let ret = result.originalPositionFor({
                line: error.lineNo, // The compressed line number
                column: error.columnNo // Compressed column number
            });
            
            let url = ' '; // Report the address
        
            // Report the exception to the background
            fetch(url, {
                method: 'POST'.headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    errorMessage: error.errorMessage, // An error message was reported
                    source: ret.source, // Error file path
                    line: ret.line, // Error file line number
                    column: ret.column, // Error file column number
                    stack: error.stack // Error stack
                })
            }).then(function(response) {
                return response.json();
            }).then(function(json) { res.json(json); }); }}}));module.exports = router;
Copy the code

We get the map file address from the front end by using the exception file path, and then pass the compressed line and column number to the promise object returned by sourceMap for parsing. We get the original line and column number and file address by using the originalPositionFor method. Finally, the required exception information is transferred to the background storage through Ajax to complete the exception reporting. Here you can see that the console prints the parsed error location and file:

Attached: the source – the map API

3. Pay attention to the point

Above is exception handling and reporting the main facts and processes, and some need to pay attention to the places, such as your application traffic is very big, so a small anomaly may hang your server to get, so report, such as information filtering and sampling can be performed to set up a control switch, the server can also be for similar exception filters, Do not store multiple times in a period. In addition, it is important to note that window.onerror does not catch the promise exception error message.

The final general flow chart is as follows:

conclusion

Front-end anomaly capture and reporting is the premise of front-end anomaly monitoring. Only by understanding and analyzing anomaly data can we realize a perfect error response and processing mechanism and finally achieve data visualization. This article detailed example code address: github.com/luozhihao/e…