To create a plug-in type front-end monitoring system that can be plugged at will

I. Data collection

1. Abnormal data

1.1 Static Resource Exception

Using the window. The addEventListener (‘ error ‘, cb)

Since this method catches a lot of errors, we need to filter out static resource file loading errors. Only JS, CSS, and IMG are monitored

// Catch static resource loading failure js CSS img
window.addEventListener('error'.e= > {
    const target = e.targetl
    if(! target)return
    const typeName = e.target.localName;
    let sourceUrl = "";
    if (typeName === "link") {
        sourceUrl = e.target.href;
    } else if (typeName === "script" || typeName === "img") {
        sourceUrl = e.target.src;
    }

    if (sourceUrl) {
        lazyReportCache({
            url: sourceUrl,
            type: 'error'.subType: 'resource'.startTime: e.timeStamp,
            html: target.outerHTML,
            resourceType: target.tagName,
            paths: e.path.map(item= > item.tagName).filter(Boolean),
            pageURL: getPageURL(),
        })
    }
}, true)

Copy the code

1.2 js error

Use window. onError to get the row, column number, and error stack when the error occurred

The production environment needs to upload the map file generated after packaging, and use source-map to obtain the number of error lines and source files before compression based on the compressed code file and row number

// parseErrorMsg.js
const fs = require('fs');
const path = require('path');
const sourceMap = require('source-map');

export default async function parseErrorMsg(error) {
  const mapObj = JSON.parse(getMapFileContent(error.url))
  const consumer = await new sourceMap.SourceMapConsumer(mapObj)
  // Remove the./ from webpack://source-map-demo/./ SRC /index.js
  const sources = mapObj.sources.map(item= > format(item))
  // According to the compressed error information, get the number of error lines and source files before compression
  const originalInfo = consumer.originalPositionFor({ line: error.line, column: error.column })
  // sourcesContent contains the uncompressed source code of each file
  const originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
  return {
    file: originalInfo.source,
    content: originalFileContent,
    line: originalInfo.line,
    column: originalInfo.column,
    msg: error.msg,
    error: error.error
  }
}

function format(item) {
  return item.replace(/(\.\/)*/g.' ')}function getMapFileContent(url) {
  return fs.readFileSync(path.resolve(__dirname, `./dist/${url.split('/').pop()}.map`), 'utf-8')}Copy the code

1.3 Custom Exceptions

This is printed through console.error, which we consider to be a custom error

Use window.console.error to report custom exceptions

1.4 Abnormal Interface

  1. An exception is reported when the status code is abnormal
  2. Override the onloadEnd method to report an exception if the code value in its response object is not ‘000000’
  3. Rewrite the onError method, when the network is interrupted, the onLoad (end) event cannot be triggered, the onError event will be triggered, and an exception will be reported

1.5 Listening for unhandled Promise errors

When a Promise is rejected and there is no Reject handler, the unhandledrejection event is emitted

Using the window. The addEventListener (‘ unhandledrejection, cb)

2. Performance data

2.1 FP/FCP/LCP/CLS

The Chrome team came up with a number of metrics to measure web page performance:

  • First-paint (FP), the time from the start of page loading until the first pixel is drawn on the screen
  • FCP(first-Contentful-paint), the time from the time the page loads until any part of the page content completes rendering on the screen
  • LCP(largest- Contentful-paint), the time from the start of page loading until the largest text block or image element is rendered on the screen
  • CLS(layout-Shift), the cumulative score for all unexpected layout offsets that occur from the time the page is loaded and its life cycle state becomes hidden

The first three of these performance metrics can be obtained directly from the PerformanceObserver, a performance monitoring object used to monitor performance measurement events. CLS requires some calculation.

Before we look at the calculation, let’s look at the concept of a session window: one or more layout offsets have a time interval of less than 1 second between them, and the time interval between the first and last layout offsets is capped at 5 seconds. Layout offsets exceeding 5 seconds will be divided into a new session window.

After a large scale analysis, the Chrome Speed Metrics team used the cumulative maximum offset in all session Windows to reflect the worst-case page layout (CLS).

As shown below: Session window 2 has only one tiny layout offset, so session window 2 is ignored and CLS only calculates the sum of layout offsets in session window 1.

2.2 DOMContentLoaded event and onLoad event

  • DOMContentLoaded: The HTML document is loaded and parsed. DOMContentLoaded is triggered when the browser parses the document without a script; When there is a script in the document, the script blocks parsing of the document, and the script can’t be executed until the CSS in front of the script has loaded. But in any case, DOMContentLoaded doesn’t need to wait for images and other resources to parse.
  • Onload: this function is triggered only after all other resources on the page, such as images, videos, and audio, are loaded.

Why do we emphasize CSS in the head and JS in the tail?

First, the order of file placement determines the priority of the download. To avoid rearranging or redrawing the page due to style changes, the browser will block the rendering of the content until all the CSS has been loaded and parsed, during which time the “white screen” will appear.

Modern browsers optimize the user experience by not building a layout rendering tree until all HTML documents have been parsed, which means that browsers can render incomplete DOM trees and CSSOM and reduce white screen time as quickly as possible.

If we put JS in the header, JS will block parsing of the DOM, causing FP(First Paint) to be delayed, so we put JS in the tail to reduce the time for FP, but not the time for DOMContentLoaded to trigger.

2.3 Resource loading Time and cache matching

Through PerformanceObserver collection, when the browser does not support PerformanceObserver, can also through the performance. The getEntriesByType (entryType) to down-cycled, including:

  • Navigation Timing collects performance metrics for HTML documents
  • Resource Timing collects performance metrics for the resources that a document depends on, such as CSS, JS, images, etc

The following resource types are not counted:

  • Beacon: Used to report data without statistics
  • Xmlhttprequest: Separate statistics

We can obtain the following information about the resource object:

Use performance.now() to accurately calculate program execution time:

  • Performance.now (), unlike date.now (), returns the time in microseconds (millionths of a second), which is more precise. And unlike date.now (), which is affected by system program execution blocking, performing.now () ‘s time increases at a constant rate and is not affected by system time (which can be adjusted manually or by software).
  • Date. Now () output is UNIX time, namely from 1970, and the performance, now () output is relative to the performance. The timing. The navigationStart initialization (page).
  • The difference used with date.now () is not absolutely accurate because the calculation time is limited by the system (which may block). However, using the difference of performance.now() does not affect our calculation of the exact execution time of the program.

Determine whether the resource hits the cache: There is a transferSize field in these resource objects, which indicates the size of the fetched resource, including the size of the response header field and the response data. If this value is 0, it is read directly from cache (mandatory cache). If this value is not 0, but the encodedBodySize field is 0, it is going through the negotiated cache (encodedBodySize indicates the size of the body of the request response data). If the preceding conditions are not met, the cache is not matched.

2.4 Interface Request Time and Failure of interface invocation

Overwrite the send and open methods on the XMLHttpRequest prototype chain

import { originalOpen, originalSend, originalProto } from '.. /utils/xhr'
import { lazyReportCache } from '.. /utils/report'

function overwriteOpenAndSend() {
    originalProto.open = function newOpen(. args) {
        this.url = args[1]
        this.method = args[0]
        originalOpen.apply(this, args)
    }

    originalProto.send = function newSend(. args) {
        this.startTime = Date.now()

        const onLoadend = () = > {
            this.endTime = Date.now()
            this.duration = this.endTime - this.startTime

            const { duration, startTime, endTime, url, method } = this
            const { readyState, status, statusText, response, responseUrl, responseText } = this
            console.log(this)
            const reportData = {
                status,
                duration,
                startTime,
                endTime,
                url,
                method: (method || 'GET').toUpperCase(),
                success: status >= 200 && status < 300.subType: 'xhr'.type: 'performance',
            }

            lazyReportCache(reportData)

            this.removeEventListener('loadend', onLoadend, true)}this.addEventListener('loadend', onLoadend, true)
        originalSend.apply(this, args)
    }
}

export default function xhr() {
    overwriteOpenAndSend()
}
Copy the code

2. Data reporting

1. Reporting method

Use sendBeacon and XMLHttpRequest combined

Why sendBeacon?

Statistical and diagnostic code typically initiates a synchronous XMLHttpRequest in the UNLOAD or beforeUnload (EN-US) event handler to send data. Synchronous XMLHttpRequest forces the user agent to delay unloading the document and makes the next navigation appear later. The next page can do nothing about this poor load performance. The navigator.sendBeacon() method can be used to asynchronously transfer a small amount of data to the Web server over HTTP without delaying the unloading of the page or affecting the loading performance of the next navigation. This solves all the problems of submitting analysis data: the data is reliable, the transfer is asynchronous, and the next page load is not affected.

2. Timing of reporting

  1. Cache first reported data, the cache to a certain number after using requestIdleCallback/setTimeout delay report.
  2. Before leaving the current page, refresh or close the report (onBeforeUnload)/report when the page is not visible (onVisibilitychange, judgment document. VisibilityState/document. Hidden state)