preface
The purpose of this series of articles is to show you how to build a front-end monitoring system from scratch.
The project is open source
Project Address:
- Github.com/bombayjs/bo… (web sdk)
- Github.com/bombayjs/bo… (Server side, used to provide API)
- Github.com/bombayjs/bo… (Background management system, visual data, etc.)
Your support is what keeps us moving forward.
Like please start!!!!!!
Like please start!!!!!!
Like please start!!!!!!
This article is the first part of the series, the design and development of Web Probe SDK, focusing on the functions and implementation of THE SDK.
function
- Report the pv uv
- Capture the error
- Reporting Performance performance
- Report user tracks
- Single page support
- hack ajax fetch
- Report the loaded resources
- hack console
- hack onpopstate
- Expose the global variable __bb
- Bury the sum AVg MSG API
Catch exceptions
Window. onError exception handling
window.onerror = function (msg, url, row, col, error) {
console.log({
msg, url, row, col, error
})
return true;
};
Copy the code
Note:
-
The window. Onerror function returns true only when the exception is not raised
-
Window. onError cannot catch resource exception errors because network request exceptions do not event bubble
So instead of window.onError, we use window.addeventListener (‘error’,callback).
window.addEventListener('error', (msg, url, row, col, error) => {
console.log(
msg, url, row, col, error
);
return true;
}, true);
Copy the code
How do you distinguish between a caught exception and a resource error by instanceof? A caught exception instanceof is an ErrorEvent and a resource error instanceof is an Event
You can refer to the following code
export function handleErr(error): void {
switch (error.type) {
case 'error':
error instanceof ErrorEvent ? reportCaughtError(error) : reportResourceError(error)
break;
case 'unhandledrejection':
reportPromiseError(error)
break;
// case 'httpError':
// reportHttpError(error)
// break;
}
setGlobalHealth('error')}Copy the code
Abnormal promise
Promise exceptions cannot be caught with onError or try-catch. You can listen for unhandledrejection events
window.addEventListener("unhandledrejection".function(e){
e.preventDefault()
console.log(e.reason);
return true;
});
Copy the code
The iframe abnormal
If an iframe exception is a Script error, we ignore it and do not report it
Script error. Solution: www.alibabacloud.com/help/zh/doc…
Page performance
Window.performance allows us to calculate key performance indicators (KPIs) by obtaining the time spent in each of the following phases.
Tips: through the window. The navigator. Connection. The bandwidth we can estimate the bandwidth
User behavior
For the time being, the user behavior here is just the click event and console
Listen for click events
window.addEventListener('click', handleClick, true); // handleClick event definitionexport function handleClick(event) {
var target;
try {
target = event.target
} catch (u) {
target = "<unknown>"
}
if (0 !== target.length) {
var behavior: clickBehavior = {
type: 'ui.click',
data: {
message: function (e) {
if(! e || 1 ! == e.nodeType)return "";
for (var t = e || null, n = [], r = 0, a = 0,i = ">".length, o = ""; t && r++ < 5 &&! ("html" === (o = normalTarget(t)) || r > 1 && a + n.length * i + o.length >= 80);)
n.push(o), a += o.length, t = t.parentNode;
return n.reverse().join(">")}(target),}} // Empty information is not reportedif(! behavior.data.message)return
let commonMsg = getCommonMsg()
letmsg: behaviorMsg = { ... commonMsg, ... { t:'behavior',
behavior,
}
}
report(msg)
}
}
Copy the code
The final data format is as follows
{
"type": "ui.click"."data": {
"message": "div#mescroll.mescroll.mescroll-bar > div.index__search-content___1Q2eh"}}Copy the code
Rewrite the console
To listen on console, we have to override the window.console method
// hack console // config.behavior. console"debug"."info"."warn"."log"."error"]
export function hackConsole() {
if (window && window.console) {
for (var e = Config.behavior.console, n = 0; e.length; n++) {
var r = e[n];
var action = window.console[r]
if(! window.console[r])return;
(function (r, action) {
window.console[r] = function() {
var i = Array.prototype.slice.apply(arguments)
var s: consoleBehavior = {
type: "console",
data: {
level: r,
message: JSON.stringify(i),
}
};
handleBehavior(s)
action && action.apply(null, i)
}
})(r, action)
}
}
}
Copy the code
Single page support
At present, many monitors do not support a single page, to support a single page we must know the single page jump principle. Currently, there are two methods: hash and History
hash
Hash is easy, just listen for hashchange
on('hashchange', handleHashchange)
Copy the code
history
History relies on the HTML5 History API and server configuration. Rely mainly on history.pushState and history.replacestate
Next, we want the browser to send the same event HistoryStatechanged when executing both methods, which requires overwriting both methods
/** * hack pushState replaceState * send historyStatechange event * @export
* @param {('pushState' | 'replaceState')} e
*/
export function hackState(e: 'pushState' | 'replaceState') {
var t = history[e]
"function" == typeof t && (history[e] = function(n, i, s) { ! window['__bb_onpopstate_'] && hackOnpopstate(); Var c = 1 === arguments.length? [arguments[0]] : Array.apply(null, arguments), u = location.href, f = t.apply(history, c);
if(! s ||"string"! = typeof s)return f;
if (s === u) return f;
try {
var l = u.split("#"),
h = s.split("#"), p = parseUrl(l[0]), d = parseUrl(h[0]), g = l[1] && l[1].replace(/^\/? (. *) /,"The $1"), v = h[1] && h[1].replace(/^\/? (. *) /,"The $1"); p ! == d ? dispatchCustomEvent("historystatechanged", d) : g ! == v && dispatchCustomEvent("historystatechanged", v)
} catch (m) {
warn("[retcode] error in " + e + ":" + m)
}
return f
}, history[e].toString = fnToString(e))
}
Copy the code
Then just listen on HistoryStatechanged
on('historystatechanged', handleHistorystatechange)
Copy the code
Tips: Window.CustomEvent API is used here
Report the resource
Resources refer to the external resources of web pages, such as pictures, JS, CSS and so on
The principle is through the performance. GetEntriesByType (” resource “) obtain the resources page is loaded
export function handleResource() {
var performance = window.performance
if(! performance ||"object"! = typeof performance ||"function"! = typeof performance.getEntriesByType)return null;
let commonMsg = getCommonMsg()
letmsg: ResourceMsg = { ... commonMsg, ... { dom: 0, load: 0, t:'res',
res: ' ',
}
}
var i = performance.timing || {},
o = performance.getEntriesByType("resource") | | [];if ("function" == typeof window.PerformanceNavigationTiming) {
var s = performance.getEntriesByType("navigation") [0]; s && (i = s) } each({ dom: [10, 8], load: [14, 1] },function (e, t) {
var r = i[TIMING_KEYS[e[1]]],
o = i[TIMING_KEYS[e[0]]];
if(r > 0 && o > 0) { var s = Math.round(o - r); S >= 0 && s < 36e5 && (MSG [t] = s)}}) o = o.filter(item => {var include = getConfig(item => {var include = getConfig(item => {var include = getConfig(item => {var include = getConfig(item)}}))'ignore').ignoreApis.findIndex(ignoreApi => item.name.indexOf(ignoreApi) > -1)
return include > -1 ? false : true
})
msg.res = JSON.stringify(o)
report(msg)
}
Copy the code
Listening API
In this case, ajax or FETCH will be rewritten to automatically report the success or failure of the interface call. Of course, __bb.api() will also be supported for manual reporting if the network request is not made by either of these methods
Rewrite the ajax
// If the return is too long, it will be truncated to a maximum of 1000 charactersfunction hackAjax() {
if ("function" == typeof window.XMLHttpRequest) {
var begin = 0,
url =' ',
page = ' '
;
var __oXMLHttpRequest_ = window.XMLHttpRequest
window['__oXMLHttpRequest_'] = __oXMLHttpRequest_
window.XMLHttpRequest = function(t) {
var xhr = new __oXMLHttpRequest_(t)
if(! xhr.addEventListener)return xhr
var open = xhr.open,
send = xhr.send
xhr.open = function(method: string, url? : string) { var a = 1 === arguments.length ? [arguments[0]] : Array.apply(null, arguments); url = url page = parseUrl(url) open.apply(xhr,a) } xhr.send =function() {
begin = Date.now()
var a = 1 === arguments.length ? [arguments[0]] : Array.apply(null, arguments);
send.apply(xhr,a)
}
xhr.onreadystatechange = function() {
if (page && 4=== xhr.readyState) {
var time = Date.now() - begin
if (xhr.status >= 200 && xhr.status <= 299) {
var status = xhr.status || 200
if ("function" == typeof xhr.getResponseHeader) {
var r = xhr.getResponseHeader("Content-Type");
if(r && ! /(text)|(json)/.test(r))return} handleApi(page, ! 0, time, status, xhr.responseText.substr(0,Config.maxLength) ||' ', begin)
} else{ handleApi(page, ! 1, time, status ||'FAILED', xhr.responseText.substr(0,Config.maxLength) || ' ', begin)
}
}
}
return xhr
}
}
}
Copy the code
Rewrite the fetch
function hackFetch() {if ("function" == typeof window.fetch) {
var __oFetch_ = window.fetch
window['__oFetch_'] = __oFetch_
window.fetch = function(t, o) {
var a = 1 === arguments.length ? [arguments[0]] : Array.apply(null, arguments);
var begin = Date.now(),
url = (t && "string"! = typeof t ? t.url : t) ||"",
page = parseUrl((url as string));
if(! page)return __oFetch_.apply(window, a)
return __oFetch_.apply(window, a).then(function (e) {
var response = e.clone(),
headers = response.headers;
if (headers && 'function' === typeof headers.get) {
var ct = headers.get('content-type')
if(ct && ! /(text)|(json)/.test(ct))return e
}
var time = Date.now() - begin;
response.text().then(function(res) {
if(response.ok) { handleApi(page, ! 0, time, status, res. Substr (0100) | |' ', begin)
} else{ handleApi(page, ! 1, time, status, res. Substr (0100) | |' ', begin)
}
})
return e
})
}
}
}
Copy the code
Manual buried point
Supports multiple manual reporting methods such as SUM avG API MSG
More resources
Github.com/abc-club/fr…