29
头图

Hello everyone, my name is Yang Chenggong.

In the first two articles, we introduced why the front-end should have a monitoring system and the overall steps to build front-end monitoring. You must have understood the Why and What of front-end monitoring. Next we address the question of how to implement it.

If you don't understand front-end monitoring, it is recommended to read the first two articles:

In this article, we introduce how the front-end collects data, starting with the collection of abnormal data.

What is abnormal data?

Exception data refers to the execution exception or loading exception triggered by the front-end in the process of operating the page. At this time, the browser will throw an error message.

For example, if your front-end code uses an undeclared variable, the console will print a red error to tell you the reason for the error. Or there is an error in the interface request, and the abnormal situation can also be found in the network panel, whether it is the abnormality of the request sending or the abnormality of the interface response.

In our actual development scenario, the exceptions captured by the front-end are mainly divided into two categories, 接口异常 and 前端异常 . Let's take a look at how to capture these two types of exceptions.

Interface exception

The interface exception must be triggered when the request is made. At present, most of the front-end requests are initiated with axios , so just get the exceptions that may occur in axios.

If you use Promise notation, use .catch to capture:

 axios
  .post('/test')
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    // err 就是捕获到的错误对象
    handleError(err);
  });

If you write async/await, use try..catch.. to capture:

 async () => {
  try {
    let res = await axios.post('/test');
    console.log(res);
  } catch (err) {
    // err 就是捕获到的错误对象
    handleError(err);
  }
};

When an exception is caught, it will be handed over to the handleError function for processing. This function will process the received exception and call 上报接口 to transmit the exception data to the server to complete the collection.

The exception capture we wrote above is logically no problem. In practice, you will find the first hurdle: With so many pages, does each request have to include a layer of catch?

Yes, if we are developing a new project, it is understandable to stipulate that each request should include a layer of catch at the beginning, but if we are accessing front-end monitoring in an existing project that is not small, this time Catching on every page or every request is obviously not practical.

Therefore, in order to minimize the access cost and reduce the intrusiveness, we use the second solution: catching exceptions in the axios interceptor .

Front-end projects, in order to uniformly process requests, such as 401 jumps or global error prompts, will write an axios instance globally, add an interceptor to this instance, and then directly import this instance into other pages for use, such as:

 // 全局请求:src/request/axios.js

const instance = axios.create({
  baseURL: 'https://api.test.com'
  timeout: 15000,
  headers: {
    'Content-Type': 'application/json',
  },
})

export default instance

Then make a request on a specific page like this:

 // a 页面:src/page/a.jsx
import http from '@/src/request/axios.js';

async () => {
  let res = await http.post('/test');
  console.log(res);
};

In this case, we find that the request of each page will go to the global axios instance, so we only need to catch the exception at the location of the global request, and we don't need to capture it on each page, so the access cost will be greatly reduced.

According to this plan, we will implement it in this file src/request/axios.js .

Catch exception in interceptor

First we add a response interceptor for axios:

 // 响应拦截器
instance.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (error) => {
    // 发生异常会走到这里
    if (error.response) {
      let response = error.response;
      if (response.status >= 400) {
        handleError(response);
      }
    } else {
      handleError(null);
    }
    return Promise.reject(error);
  },
);

The second parameter of the response interceptor is the function to execute when an error occurs, and the parameter is the exception. We first need to determine whether there is error.response . If it exists, it means that the interface is responding, that is, the interface is connected, but an error is returned; if it does not exist, it means that the interface is not connected, the request keeps hanging, and most of the interfaces are crashed.

If there is a response, first obtain the status code, and judge when the exception needs to be collected according to the status code. The above judgment method is simple and rude. As long as the status code is greater than 400, it is regarded as an exception, the response data is obtained, and the reporting logic is executed.

If there is no response, it can be regarded as an interface timeout exception, and you can pass a null when calling the exception handling function.

Front-end exception

Above we introduced how to capture interface exceptions in axios interceptors, and in this part we will introduce how to capture front-end exceptions.

The most common way to catch exceptions in front-end code is to use try..catch.. Any synchronized code block can be placed in the try block, and the catch will be executed whenever an exception occurs:

 try {
  // 任意同步代码
} catch (err) {
  console.log(err);
}

The above says "arbitrary synchronization code" instead of "arbitrary code", mainly because the ordinary Promise writing method try..catch.. cannot be captured, and can only be captured with .catch() , such as:

 try {
  Promise.reject(new Error('出错了')).catch((err) => console.log('1:', err));
} catch (err) {
  console.log('2:', err);
}

Throw this code into the browser, and the print result is:

 1: Error: 出错了

Obviously just .catch caught the exception. However, like the logic of the above interface exception, there is no problem in handling the current page exception in this way, but from the perspective of the entire application, this way of capturing exceptions is highly intrusive and the access cost is high, so our idea is still to capture globally.

It is also relatively simple to capture js exceptions globally, just use window.addEventLinstener('error') :

 // js 错误捕获
window.addEventListener('error', (error) => {
  // error 就是js的异常
});

Why not use window.onerror?

Many friends here have questions, why not use window.onerror global monitoring? What is the difference between window.addEventLinstener('error') and window.onerror ?

First of all, the functions of these two functions are basically the same, and both can catch js exceptions globally. But there is a class of exceptions called 资源加载异常 , which are exceptions caused by non-existent pictures, js, css and other static resources referenced in the code, such as:

 const loadCss = ()=> {
  let link = document.createElement('link')
  link.type = 'text/css'
  link.rel = 'stylesheet'
  link.href = 'https://baidu.com/15.css'
  document.getElementsByTagName('head')[10].append(link)
}
render() {
  return <div>
    <img src='./bbb.png'/>
    <button onClick={loadCss}>加载样式<button/>
  </div>
}

The baidu.com/15.css and bbb.png in the above code do not exist, and JS execution here will definitely report a resource not found error. But by default, the global listener functions on the above two window objects cannot listen for such exceptions.

Because the exception of resource loading will only be triggered on the current element, the exception will not bubble to the window, so the exception on the monitor window cannot be caught. then what should we do?

If you are familiar with DOM events, you will understand that since the bubbling phase cannot be monitored, then the capture phase must be able to listen.

The method is to specify the third parameter for the window.addEventListene function, which is very simple: true , indicating that the monitoring function will be executed in the capture phase, so that the resource loading exception can be monitored.

 // 捕获阶段全局监听
window.addEventListene(
  'error',
  (error) => {
    if (error.target != window) {
      console.log(error.target.tagName, error.target.src);
    }
    handleError(error);
  },
  true,
);

The above method can easily monitor the abnormal image loading, which is why it is more recommended window.addEventListene . But remember, the third parameter is set to true , listening to event capture, you can globally capture JS exceptions and resource loading exceptions.

Note that window.addEventListene also cannot catch Promise exceptions. Whether it is Promise.then() written or async/await written, it cannot be caught when an exception occurs.

Therefore, we also need to listen globally to a unhandledrejection function to catch unhandled Promise exceptions.

 // promise 错误捕获
window.addEventListener('unhandledrejection', (error) => {
  // 打印异常原因
  console.log(error.reason);
  handleError(error);
  // 阻止控制台打印
  error.preventDefault();
});

unhandledrejection The event will be triggered when a Promise exception occurs and catch is not specified, which is equivalent to a global Promise exception solution. This function will catch Promise exceptions that occur unexpectedly at runtime, which is very useful for us to troubleshoot.

By default, when a Promise throws an exception and is not caught, it will print the exception to the console. If we want to prevent abnormal printing, we can use the above error.preventDefault() method.

exception handler

Earlier we called an exception handling function handleError when an exception was caught. All exceptions and reporting logic are handled in this function. Next, we implement this function.

 const handleError = (error: any, type: 1 | 2) {
  if(type == 1) {
    // 处理接口异常
  }
  if(type == 2) {
    // 处理前端异常
  }
}

In order to distinguish the exception type, the function adds a second parameter type to indicate whether the current exception belongs to the front end or the interface. Used in different scenarios as follows:

  • Handling front-end exceptions: handleError(error, 1)
  • Handling interface exceptions: handleError(error, 2)

Handling interface exceptions

To handle interface exceptions, we need to parse the obtained error parameter, and then get the required data. The data fields generally required for interface exceptions are as follows:

  • code : http status code
  • url : interface request address
  • method : interface request method
  • params : interface request parameters
  • error : Interface error message

These fields can be obtained in the error parameter, as follows:

 const handleError = (error: any, type: 1 | 2) {
  if(type == 1) {
    // 此时的 error 响应,它的 config 字段中包含请求信息
    let { url, method, params, data } = error.config
    let err_data = {
       url, method,
       params: { query: params, body: data },
       error: error.data?.message || JSON.stringify(error.data),
    })
  }
}

params in the config object represents the query parameter of the GET request, data represents the body parameter of the POST request, so when I process the parameters, I combine these two parameters into one, use An attribute params to represent.

 params: { query: params, body: data }

There is also a error attribute that indicates error information, and this method of obtaining should be obtained according to the return format of your interface. To avoid getting super long error messages that may be returned by the interface, most of them are not processed by the interface, which may lead to the failure of writing data. It must be specified in advance with the background.

Handling front-end exceptions

Most of the front-end exceptions are js exceptions. The exceptions correspond to the Error object of js. Before processing, let's look at the types of Errors:

  • ReferenceError : reference error
  • RangeError : out of valid range
  • TypeError : type error
  • URIError : URI parsing error

The reference objects of these types of exceptions are all Error , so they can be obtained as follows:

 const handleError = (error: any, type: 1 | 2) {
  if(type == 2) {
    let err_data = null
    // 监测 error 是否是标准类型
    if(error instanceof Error) {
      let { name, message } = error
      err_data = {
        type: name,
        error: message
      }
    } else {
      err_data = {
        type: 'other',
        error: JSON.strigify(error)
      }
    }
  }
}

In the above judgment, first judge whether the abnormality is an instance of Error . In fact, most of the code exceptions are standard JS Errors, but let's judge here. If so, get the exception type and exception information directly. If not, set the exception type to other .

Let's just write an exception code and take a look at the captured results:

 function test() {
  console.aaa('ccc');
}
test();

Then the exception caught is this:

 const handleError = (error: any) => {
  if (error instanceof Error) {
    let { name, message } = error;
    console.log(name, message);
    // 打印结果:TypeError console.aaa is not a function
  }
};

Get environmental data

Obtaining environmental data means that, whether it is an interface exception or a front-end exception, in addition to the data of the exception itself, we also need some other information to help us locate the error faster and more accurately.

We call this type of data "environmental data", which is the environment in which the exception was triggered. For example, who triggered the error in which page and where, with these, we can immediately find the source of the error, and then solve the error according to the exception information.

Environmental data includes at least the following:

  • app : the name/id of the app
  • env : Application environment, generally development, testing, production
  • version : the version number of the application
  • user_id : The user ID that triggered the exception
  • user_name : The username that triggered the exception
  • page_route : abnormal page routing
  • page_title : Abnormal page name

app and version are application configurations, which can determine which version of which application the exception occurs in. I suggest to directly obtain the package.json name and version attributes under --- 36843ad1d02f2a5f7d43608c3dff0d46--- and the version number can be changed in time when the application is upgraded.

The rest of the fields need to be obtained according to the configuration of the framework. Below I will introduce how to obtain them in Vue and React respectively.

in Vue

Obtaining user information in Vue is generally obtained directly from Vuex. If your user information is not stored in Vuex, the same is true for obtaining it from localStorage.

If you are in Vuex, you can do it like this:

 import store from '@/store'; // vuex 导出目录
let user_info = store.state;
let user_id = user_info.id;
let user_name = user_info.name;

User information exists in state management, and page routing information is generally defined in vue-router . The routing address of the front end can be obtained directly from vue-router, and the page name can be configured in meta , such as:

 {
  path: '/test',
  name: 'test',
  meta: {
    title: '测试页面'
  },
  component: () => import('@/views/test/Index.vue')
},

After this configuration, getting the current page route and page name is simple:

 window.vm = new Vue({...})

let route = vm.$route
let page_route = route.path
let page_title = route.meta.title

The last step is to get the current environment. The current environment is represented by an environment variable VUE_APP_ENV with three values:

  • dev : Development environment
  • test : Test environment
  • pro : Production environment

Then create three environment files in the root directory and write environment variables:

  • .env.development : VUE_APP_ENV=dev
  • .env.staging : VUE_APP_ENV=test
  • .env.production : VUE_APP_ENV=pro

Now when you get the env environment, you can get it like this:

 {
  env: process.env.VUE_APP_ENV;
}

The last step, when performing packaging, pass in a pattern to match the corresponding environment file:

 # 测试环境打包
$ num run build --mode staging
# 生产环境打包
$ num run build --mode production

After obtaining the environmental data and adding the abnormal data, we are ready to report the data.

in React

Like Vue, user information can be taken directly from state management. Because there is no shortcut to globally get the current tour in React, I will also put the page information in the state management. The state management I use is Mobx, and the way to get it is as follows:

 import { TestStore } from '@/stores'; // mobx 导出目录
let { user_info, cur_path, cur_page_title } = TestStore;
// 用户信息:user_info
// 页面信息:cur_path,cur_page_title

In this case, you need to update the routing information in mobx every time you switch pages. How to do it?

In fact, you can listen in the root routing page (usually the home page) useEffect :

 import { useLocation } from 'react-router';
import { observer, useLocalObservable } from 'mobx-react';
import { TestStore } from '@/stores';

export default observer(() => {
  const { pathname, search } = useLocation();
  const test_inst = useLocalObservable(() => TestStore);
  useEffect(() => {
    test_inst.setCurPath(pathname, search);
  }, [pathname]);
});

After obtaining user information and page information, the next step is the current environment. Like Vue, specify the mode through --mode and load the corresponding environment variables, but the setting method is slightly different. Most React projects are probably created with create-react-app , we use this as an example to show how to modify it.

First, open the scripts/start.js file, which is the file that is executed when npm run start is executed. We add the code to the 6th line at the beginning:

 process.env.REACT_APP_ENV = 'dev';

That's right, the environment variable we specified is REACT_APP_ENV , because only the environment variables starting with REACT_ can be read.

Then modify the scripts/build.js line 48 of the file, after the modification is as follows:

 if (argv.length >= 2 && argv[0] == '--mode') {
  switch (argv[1]) {
    case 'staging':
      process.env.REACT_APP_ENV = 'test';
      break;
    case 'production':
      process.env.REACT_APP_ENV = 'pro';
      break;
    default:
  }
}

At this time, you can obtain the env environment in this way:

 {
  env: process.env.REACT_APP_ENV;
}

Summarize

After the previous series of operations, we have obtained the abnormal data in a comprehensive manner, as well as the environmental data when an abnormality occurs. The next step is to call the reporting interface and transfer the data to the background for storage. It will be very convenient for us to find and track in the future.

If you also need front-end monitoring, you might as well spend half an hour collecting abnormal data according to the method described in the article. I believe it will be very helpful to you.

The article's first public account programmer was successful . This official account is only original, focusing on the sharing of front-end engineering and architecture, follow me to see more hard-core knowledge. I also have a front-end engineering and architecture group. If you are interested, you can add me to the group on WeChat .


杨成功
3.9k 声望12k 粉丝

分享小厂可落地的前端工程与架构