“This article has participated in the call for good writing activities, click to view: the back end, the big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!”

Front-end error monitoring and performance data often have a very important impact on the stability of the business. Even if we are very careful in the development stage, it is inevitable that there will be an exception after the online, and we often realize the exception after the online. The performance data of the page is related to the user experience, so this part of the data is also within the scope of our collection.

Now the third party complete solution has Sentry abroad, fundebug, FrontJS in China, they provide front-end access SDK and data services, and then have a certain amount of free, beyond the need to use the paid solution. Front-end SDK users monitor client exceptions and performance. Back-end service users can create applications. Each application is assigned an APPKEY, and the SDK automatically reports the application.

This paper does not consider the data service, only analyzes the front-end monitoring, and describes how the Web monitors and collects these data, and makes a set of front-end monitoring SDK through TS integration of these functions.

Since we need to collect data, we need to clarify what data may be needed. At present, there are some data as follows:

  • Page error data
  • Page resource loading status
  • Page performance data
  • The interface data
  • Mobile phone and browser data
  • Page access data
  • User behavior data
  • .

Here’s how to get the data:

Page error data

  • window.onerrorAOP’s ability to catch exceptions, whether asynchronous or non-asynchronous,onerrorCan catch runtime errors.
  • window.onerrorPage resource loading errors cannot be caught, but resource loading errors can bewindow.addEventListenerCapture in the capture phase. Due to theaddEventListenerJs errors can also be caught, so you need to filter to avoid repeatedly firing event hooks
  • window.onerrorUnable to catch unhandled exceptions in the Promise task, passunhandledrejectionCan capture

Page resource loading is abnormal

window.addEventListener(
  "error".function (event) {
    const target: any = event.target || event.srcElement;
    const isElementTarget =
      target instanceof HTMLScriptElement ||
      target instanceof HTMLLinkElement ||
      target instanceof HTMLImageElement;
    if(! isElementTarget)return false;

    consturl = target.src || target.href; onResourceError? .call(this, url);
  },
  true
);
Copy the code

Page logic and uncaught promise exceptions

 const oldOnError = window.onerror;
 const oldUnHandleRejection = window.onunhandledrejection;

 window.onerror = function (. args) {
   if(oldOnError) { oldOnError(... args); }const[msg, url, line, column, error] = args; onError? .call(this, {
     msg,
     url,
     line,
     column,
     error
   });
 };

 window.onunhandledrejection = function (e: PromiseRejectionEvent) {
   if (oldUnHandleRejection) {
     oldUnHandleRejection.call(window, e);
   }

   onUnHandleRejection && onUnHandleRejection(e);
 };
Copy the code

ErrorHandler = function(err, vm, info) {}; errorHandler = function(err, vm, info) {}; Do exception catching so that you can get more context information.

React 16 provides a built-in function componentDidCatch to retrieve React errors

componentDidCatch(error, info) {
    console.log(error, info);
}
Copy the code

Page performance data

Typically we focus on the following performance metrics:

  • White screen time: the time from the time when the browser enters the address and press Enter to the time when the page starts to contain content.
  • First screen time: the time from the time when the browser enters the address and press Enter to the time when the first screen content is rendered.
  • User operable time node: DomReady trigger node, click the event response;
  • Total download time: window.onload trigger node.

Bad time

White screen time node refers to the time node calculated from the moment when the user enters the website (enter URL, refresh, jump, etc.) until the page has content displayed. This process includes DNS query, establishing TCP connection, sending the first HTTP request (TLS validation time is involved if HTTPS is used), returning THE HTML document, and the HTML document head is parsed.

The first screen time

First screen time is complicated because it involves many elements, such as images, and asynchronous rendering. Observing the loading view reveals that the main factors affecting the loading of the picture on the first screen. The first screen rendering completion time can be obtained by counting the loading time of images in the first screen.

  • If a page has an IFrame, you need to determine the load time
  • GIF images may trigger load events repeatedly on Internet Explorer
  • In the case of asynchronous rendering, the first screen should be calculated after the asynchronous fetch data is inserted
  • CSS important background images can be counted by JS image URL request (browser will not reload)
  • If there is no picture, JS execution time will be counted as the first screen, that is, the time when the text appears

User operable time

DomReady is the time when DOM parsing is complete, since events are usually bound at this point

To obtain Performance data on the Web, you only need to use the Performance interface of the browser

Collect page performance data

The Performance interface retrieves Performance related information from the current page and is part of the High Resolution Time API. It also integrates the Performance Timeline API, Navigation Timing API, User Timing API, and Resource Timing API.

It can be seen from the figure that many indicators appear in pairs. Here we can directly calculate the difference value to calculate the time consumption of key nodes in the corresponding page loading process. Here we introduce several commonly used ones, such as:

const timingInfo = window.performance.timing;

// DNS resolution, DNS query time
timingInfo.domainLookupEnd - timingInfo.domainLookupStart;

// The TCP connection takes time
timingInfo.connectEnd - timingInfo.connectStart;

// Getting the first byte takes time, also called TTFB
timingInfo.responseStart - timingInfo.navigationStart;

// *: domReady time (corresponding to DomContentLoad event)
timingInfo.domContentLoadedEventStart - timingInfo.navigationStart;

// DOM resource download
timingInfo.responseEnd - timingInfo.responseStart;

// Preparing a new page takes time
timingInfo.fetchStart - timingInfo.navigationStart;

// The redirection takes time
timingInfo.redirectEnd - timingInfo.redirectStart;

/ / Appcache time-consuming
timingInfo.domainLookupStart - timingInfo.fetchStart;

// Document time before unload
timingInfo.unloadEventEnd - timingInfo.unloadEventStart;

// request Request time
timingInfo.responseEnd - timingInfo.requestStart;

// The request completes until DOM is loaded
timingInfo.domInteractive - timingInfo.responseEnd;

// Dom tree interpretation takes time
timingInfo.domComplete - timingInfo.domInteractive;

// * : Total time from start to load
timingInfo.loadEventEnd - timingInfo.navigationStart;

// *: blank screen time
timingInfo.responseStart - timingInfo.fetchStart;

// *: first screen time
timingInfo.domComplete - timingInfo.fetchStart;
Copy the code

The interface data

Interface data mainly includes interface time and interface request exceptions. Time consumption can be counted during the interception of XmlHttpRequest and FETCH requests. Exceptions can be determined by the readyState and status attributes of XHR.

Intercept XmlHttpRequest: modify the XmlHttpRequest prototype, when sending a request to open event listeners, inject the SDK hooks XmlHttpRequest. ReadyState five ready state:

  • 0: The request is not initialized (open() has not been called).
  • 1: The request has been established, but has not yet been sent (send() has not been called).
  • 2: The request has been sent and is being processed (you can usually now get the content header from the response).
  • 3: The request is being processed. Usually some data is already available in the response, but the server has not finished generating the response.
  • 4: The response is complete. You are ready to get and use the server’s response.
XMLHttpRequest.prototype.open = function (method: string, url: string) {
  / /... omit
  return open.call(this, method, url, true);
};
XMLHttpRequest.prototype.send = function (. rest:any[]) {
  / /... omit
  const body = rest[0];

  this.addEventListener("readystatechange".function () {
    if (this.readyState === 4) {
      if (this.status >= 200 && this.status < 300) {
        / /... omit
      } else {
        / /... omit}}});return send.call(this, body);
};
Copy the code

Fetch interception: Object.defineProperty

Object.defineProperty(window."fetch", {
  configurable: true.enumerable: true.get() {
    return (url: string, options: any = {}) = > {
      return originFetch(url, options)
        .then((res) = > {
	        // ...})}; }});Copy the code

Mobile phone and browser data

Parsing was performed through navigatorAPI capture, and third-party package mobile-detect was used to help us get parsing

Page access data

The URL, page title, and user ID are added to the global data. The SDK can automatically assign a random user label to the web session to identify a single user

User behavior data

Mainly contains the user click page elements, console information, user mouse movement track.

  • The user clicks on the element: Window event agent
  • Console information: Rewrite console
  • User mouse movement trajectory: rrWeb third-party library

The following is a unified monitoring SDK design for these data

The SDK development

To better decouple modules, I decided to use an event-based subscription approach. The ENTIRE SDK is divided into several core modules. Since ts is developed and the code remains well named and semantically, there are only comments in key areas.

  • Class: WebMonitor: core monitoring class
  • Class: AjaxInterceptor: Intercepts Ajax requests
  • Class: ErrorObserver: monitors global errors
  • Class: FetchInterceptor: Intercepts the fetch request
  • Class: Reporter
  • Class: Performance: Monitors Performance data
  • Class: RrwebObserver: Access rrWeb to obtain user behavior trajectories
  • Class: SpaHandler: Handles SPA applications
  • Util: DeviceUtil: auxiliary function for obtaining device information
  • Event: indicates the event center

Events provided by the SDK

External exposure events, starting with _ are internal events

export enum TrackerEvents {
  // External exposure events
  performanceInfoReady = "performanceInfoReady".// The page performance data has been obtained
  reqStart = "reqStart".// Interface request started
  reqEnd = "reqEnd".// The interface request completed
  reqError = "reqError".// Request error
  jsError = "jsError".// The page logic is abnormal
  vuejsError = "vuejsError".// VUE error monitoring event
  unHandleRejection = "unHandleRejection".// The PROMISE exception is not handled
  resourceError = "resourceError".// Resource loading error
  batchErrors = "batchErrors".// The user merges the reported events to save the number of requests
  mouseTrack = "mouseTrack".// User mouse behavior tracking
}
Copy the code

use

import { WebMonitor } from "femonitor-web";
const monitor = Monitor.init();
/* Listen single event */
monitor.on([event], (emitData) => {});
/* Or Listen all event */
monitor.on("event", (eventName, emitData) => {})
Copy the code

Core Module analysis

WebMonitor, errorObserver, ajaxInterceptor, fetchInterceptor, and Performance

WebMonitor

Integrates with other classes of the framework, deepmerge incoming and default configurations, and initialize according to the configuration

this.initOptions(options);

this.getDeviceInfo();
this.getNetworkType();
this.getUserAgent();

this.initGlobalData(); // set some globalData to be carried in globalData for all events
this.initInstances();
this.initEventListeners();
Copy the code

API

Support chain operation

  • On: Listening event
  • Off: The event is removed
  • UseVueErrorListener: Use Vue error monitoring to get more detailed component data
  • ChangeOptions: Modifies the configuration
  • ConfigData: Sets global data

errorObserver

Listening to the window. The onerror and window. Onunhandledrejection and to err. Message parsing, obtain want to emit error data.

window.onerror = function (. args) {
  // Call the original method
  if(oldOnError) { oldOnError(... args); }const [msg, url, line, column, error] = args;

  const stackTrace = error ? ErrorStackParser.parse(error) : [];
  const msgText = typeof msg === "string" ? msg : msg.type;
  const errorObj: IError = {};

  myEmitter.customEmit(TrackerEvents.jsError, errorObj);
};

window.onunhandledrejection = function (error: PromiseRejectionEvent) {
  if (oldUnHandleRejection) {
    oldUnHandleRejection.call(window, error);
  }

  const errorObj: IUnHandleRejectionError = {};
  myEmitter.customEmit(TrackerEvents.unHandleRejection, errorObj);
};

window.addEventListener(
  "error".function (event) {
    const target: any = event.target || event.srcElement;
    const isElementTarget =
      target instanceof HTMLScriptElement ||
      target instanceof HTMLLinkElement ||
      target instanceof HTMLImageElement;
    if(! isElementTarget)return false;

    const url = target.src || target.href;

    const errorObj: BaseError = {};
    myEmitter.customEmit(TrackerEvents.resourceError, errorObj);
  },
  true
);

Copy the code

ajaxInterceptor

Intercepts ajax requests and fires custom events. Overwrite the open and send methods of XMLHttpRequest

XMLHttpRequest.prototype.open = function (method: string, url: string) {
  const reqStartRes: IAjaxReqStartRes = {
  };

  myEmitter.customEmit(TrackerEvents.reqStart, reqStartRes);
  return open.call(this, method, url, true);
};

XMLHttpRequest.prototype.send = function (. rest:any[]) {
  const body = rest[0];
  const requestData: string = body;
  const startTime = Date.now();

  this.addEventListener("readystatechange".function () {
    if (this.readyState === 4) {
      if (this.status >= 200 && this.status < 300) {
        const reqEndRes: IReqEndRes = {};

        myEmitter.customEmit(TrackerEvents.reqEnd, reqEndRes);
      } else {
        constreqErrorObj: IHttpReqErrorRes = {}; myEmitter.customEmit(TrackerEvents.reqError, reqErrorObj); }}});return send.call(this, body);
};
Copy the code

fetchInterceptor

The FETCH is intercepted and a custom event is triggered.

Object.defineProperty(window."fetch", {
  configurable: true.enumerable: true.get() {
    return (url: string, options: any = {}) = > {
      const reqStartRes: IFetchReqStartRes = {};
      myEmitter.customEmit(TrackerEvents.reqStart, reqStartRes);

      return originFetch(url, options)
        .then((res) = > {
          const status = res.status;
          const reqEndRes: IReqEndRes = {};

          const reqErrorRes: IHttpReqErrorRes = {};

          if (status >= 200 && status < 300) {
            myEmitter.customEmit(TrackerEvents.reqEnd, reqEndRes);
          } else {
            if (this._url !== self._options.reportUrl) {
              myEmitter.customEmit(TrackerEvents.reqError, reqErrorRes);
            }
          }

          return Promise.resolve(res);
        })
        .catch((e: Error) = > {
          constreqErrorRes: IHttpReqErrorRes = {}; myEmitter.customEmit(TrackerEvents.reqError, reqErrorRes); }); }; }});Copy the code

performance

Get page Performance by Performance and emit events when the Performance data is complete

const {
  domainLookupEnd,
  domainLookupStart,
  connectEnd,
  connectStart,
  responseEnd,
  requestStart,
  domComplete,
  domInteractive,
  domContentLoadedEventEnd,
  loadEventEnd,
  navigationStart,
  responseStart,
  fetchStart
} = this.timingInfo;

const dnsLkTime = domainLookupEnd - domainLookupStart;
const tcpConTime = connectEnd - connectStart;
const reqTime = responseEnd - requestStart;
const domParseTime = domComplete - domInteractive;
const domReadyTime = domContentLoadedEventEnd - fetchStart;
const loadTime = loadEventEnd - navigationStart;
const fpTime = responseStart - fetchStart;
const fcpTime = domComplete - fetchStart;

const performanceInfo: IPerformanceInfo<number> = {
  dnsLkTime,
  tcpConTime,
  reqTime,
  domParseTime,
  domReadyTime,
  loadTime,
  fpTime,
  fcpTime
};

myEmitter.emit(TrackerEvents.performanceInfoReady, performanceInfo);
Copy the code

See Github repository address below for full SDK implementation, welcome star, fork, issue.

Web Front-end monitoring SDK: github.com/alex1504/fe…

If this article has been helpful to you, please like it, bookmark it and share it with us in the comments section below. Your support keeps me going.

PS: If you are interested in the next chapter on error and performance monitoring of applets, you can click here directly