How to realize front-end monitoring

Front-end monitoring project, our primary purpose is to detect errors in the page and report them. Generally, we can think of error sources mainly include JavaScript errors, static resource errors, Promise exceptions, Ajax request exceptions, as well as exceptions of Vue, React and other frameworks.

JavaScript abnormal

Static resource exceptions and JavaScript exceptions both throw errors, so we can catch them via addEventListener(‘error’) or window.onerror. However, the addEventListener does not get the detailed stack information returned by JavaScript exceptions, so we can simply catch them with window.onError. However, static resource exception Error events do not bubble up to the window, so we can only use addEventListener to block static resource errors in the capture phase rather than the bubble phase.

This makes it clear that static resource exceptions are handled by addEventListener(Handler, true) and JavaScript errors are handled by window.onerror. JavaScript exceptions are caught by both, so we also need to strip this type out during the addEventListener capture phase to make it through the bubble phase.

Reserving only resource loading errors can be done as follows:

window.addEventListener('error'.(event) = >{
    let target = event.target
    var isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
    if(isElementTarget){
        ...
    }
Copy the code

Ajax abnormal

We all know that Ajax is implemented by constructing an instance of XMLHttpRequest. A typical POST request looks like this

   let xhr = new XMLHttpRequest();
   xhr.open("POST", url, true); xhr.setRequestHeader(..) ; xhr.send(JSON.stringify(defaults.data));// Send takes the request body;
   xhr.onreadystatechange = function () {
      if (xmlhttp.readyState==4 && xmlhttp.status==200)
         success(JSON.parse(xhr.responseText))
   }
Copy the code

You can see that a full XHR request calls the.open(),.send() methods on the prototype, so we just need to rewrite the function to add a listener and execute the old method at the end

let origin = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
         this.addEventListener('error', _handleEvent);
         this.addEventListener('load', _handleEvent);
         this.addEventListener('abort', _handleEvent);
         return origin.apply(this.arguments)};Copy the code

Hash to heavy

Imagine you have a piece of code in your project,

var obj = {}
while(obj.num++){
    ...
}
Copy the code

Then the monitoring system will continuously report exactly the same information, so hash removal is necessary. We add hash and amount fields inside the error object, then simply call the MD5 library, enter the error description + the error URL and print a short summary hash. If there is an object in the current error queue with the hash value, add one to its amount, and otherwise queue the object.

Performance testing

The Performance Timing API is used for Performance detection. Performance is an object of window.

var performance = {  
    timing: {
        navigationStart: 1441112691935.// The time when the callback of the load event completes
        loadEventEnd: 1441112693215}};Copy the code

Performance. Timing Has a large number of load times inside the object, and subtracting the load times of some content can be obtained

Traffic statistics

Visitors generally have PV, UV, IP three kinds, we mainly focus on the first two kinds — PV means Page Visitor, Page click once count a visit; UV stands for Unique Visitor, where the same client counts only one visit in a day. As you can see, PV is relatively simple, while UV requires browser identification — we use localStorage to generate a random string mark_uv to identify the user when the user first enters the page, and a mark_time to record its generation time. If date.now () is found to be the day after mark_time in one of the fires, we generate a brand new mark_UV again. PV and UV statistics are thus completed.

Class structure design

Identify current needs:

  • Page error existsajaxError, jsError, promiseError, resourceError, vueErrorAnd they have common data and methods, so they are designed as classes
  • Error message queue, error data reporting is the above page error shared data area and method, page error and a large number of similar fields belong to their own, so design a base classBaseAnd base classstaticMethod to implement
  • There is no complex relationship between performance and device listening

Design classes:

To sum up, an entry class is required to receive the configuration and construct the above three, so the Monitor class is designed to uniformly register the timing of monitoring errors and triggering reporting, as shown in the UML diagram below

class Monitor {
    constructor(options) {
                // If the configuration exists, register to listen for such errors
    		options.ajaxError && new AjaxError(options).registerError();
		options.jsError && newjsError(options).registerError(); .// Data is reported when the page is closed
		window.addEventListener("beforeunload".() = > {
			newPerfomance(options).record(); }); }}export default Monitor;
Copy the code

Packaging and publishing

Clear function calls and split files

First of all, the point of webpack is to make it easier for script tags to be imported directly into index.min.js. For front-end projects that support ES6 or AMD specifications, we can use import, require directly. So:

  • If THE ES6, AMD specification is introduced, no additional processing is required
  • If script tags are introduced, additional interfaces need to be exposed to Global, so the Webpack entry file needs to be split

To achieve the latter, we create a separate entry.js file to expose the external interface. This does not affect the former global variable and creates an entry point for webpack packaging:

// entry.js
import Monitor from './index'
window.Monitor = Monitor;
Copy the code

Webpack packaging

To generate the index.min.js file, we first add a quick call script for package.json

  "scripts": {
    "pack": "npx webpack --config ./monitor/webpack.config.js"
  },
Copy the code

Then write webpack.config.js

const path = require("path");
module.exports = {
	// Compress the script in build mode
	mode: "production".entry: "./monitor/entry.js".output: {
		// filename: specifies the name of the packed JS file
		filename: "index.min.js".//path: specifies where to put the packed files
		path: path.resolve(__dirname, ""),},...Copy the code

Finally, execute the NPM run pack script to get the packaged single JS file, which can be easily referenced through the CDN:

<script src="< https://cdn.vansin.top/jssdk-0.1.2.min.js >"></script>
Copy the code

NPM release

Once packaged, we can publish to NPM,

  1. Let’s start with the root directorypackage.json.
{
  "name": "byte-monitor-jssdk"./ / name
  "version": "0.1.0 from"."private": false."description": "Monitor errors in front-end"./ / description
  "main": "./monitor/dist/index.js"."readmeFilename": "./monitor/README.md"."dependencies": {
    "js-md5": "^ 0.7.3"},...Copy the code

The main field is important — it’s used to reference the NPM package to find the entry file from node_modules, so just specify the entry index.js in the directory

  1. NPM packages have the concept of versioning, butindex.min.jsThere is no version record information of its own, and because NPM mirroring can have delays, packages that are released will not be downloaded immediately, so it is important to identify issues and differences between versions – we resolve this by carrying the version number in the file name when packaging, for exampleindex.min.jsName toJSSDK - 0.1.2. Min. Js
const {version} = require('.. /package.json')
module.exports = {
        ...
	output: {
		filename: `jssdk-${version}.min.js`,},Copy the code
  1. create.npmignoreFiles, configuration and Github.gitignoreSimilarly, the specified file is ignored when publishing
  2. writeREADME.mdAnd then after logging innpm publishCompletes the

More can be done