“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.onerror
AOP’s ability to catch exceptions, whether asynchronous or non-asynchronous,onerror
Can catch runtime errors.window.onerror
Page resource loading errors cannot be caught, but resource loading errors can bewindow.addEventListener
Capture in the capture phase. Due to theaddEventListener
Js errors can also be caught, so you need to filter to avoid repeatedly firing event hookswindow.onerror
Unable to catch unhandled exceptions in the Promise task, passunhandledrejection
Can 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