本系列文章旨在讲解如何从零开始搭建前端监控系统。css
项目已经开源html
项目地址:前端
您的支持是咱们不断前进的动力。node
喜欢请start!!!git
喜欢请start!!!github
喜欢请start!!!web
本文是该系列第一篇,web探针sdk的设计与开发,重点讲解sdk包含的功能与实现。ajax
window.onerror = function (msg, url, row, col, error) { console.log({ msg, url, row, col, error }) return true; };
注意:json
因此咱们通常不用window.onerror,而采用window.addEventListener('error',callback)api
window.addEventListener('error', (msg, url, row, col, error) => { console.log( msg, url, row, col, error ); return true; }, true);
tips: 如何区分是捕获的异常仍是资源错误,能够经过 instanceof
区分,捕获的异常instanceof是 ErrorEvent
, 而资源错误instanceof是 Event
能够参考以下代码
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') }
promise异常没法用onerror或 try-catch捕获。能够监听unhandledrejection
事件
window.addEventListener("unhandledrejection", function(e){ e.preventDefault() console.log(e.reason); return true; });
iframe异常抛出的异常是Script error.
,咱们通常直接忽略,不进行上报
经过window.performance
咱们能够获取到如下各个阶段的耗时,从而计算出关键性能指标。
tips: 经过window.navigator.connection.bandwidth
咱们能够预估带宽
这里的用户行为暂时只click
事件和console
window.addEventListener('click', handleClick, true); // handleClick事件定义 export 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), } } // 空信息不上报 if (!behavior.data.message) return let commonMsg = getCommonMsg() let msg: behaviorMsg = { ...commonMsg, ...{ t: 'behavior', behavior, } } report(msg) } }
最终上报数据格式以下
{ "type": "ui.click", "data": { "message": "div#mescroll.mescroll.mescroll-bar > div.index__search-content___1Q2eh" } }
要监听console,咱们就得重写window.console方法
// 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) } } }
目前不少监控都不支持单页面,要实现支持单页面咱们必须知道单页面跳转原理。目前通常有hash和history两种方式
hash比较简单,监听hashchange
就能够
on('hashchange', handleHashchange)
history依赖 HTML5 History API 和服务器配置。主要依赖history.pushState和history.replaceState
下面咱们想浏览器执行这两个方法的时候,派发同一个事件historystatechanged
出来,那就须要重写着两个方法
/** * hack pushstate replaceState * 派送historystatechange historystatechange事件 * @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(); // 调用pushState或replaceState时hack Onpopstate 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(/^\/?(.*)/, "$1"), v = h[1] && h[1].replace(/^\/?(.*)/, "$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)) }
而后只须要监听historystatechanged
就能够了
on('historystatechanged', handleHistorystatechange)
tips: 这里用到了window.CustomEvent
这个api
资源是指网页外部资源,如图片、js、css等
原理就是经过performance.getEntriesByType("resource")
获取页面加载的资源
export function handleResource() { var performance = window.performance if (!performance || "object" != typeof performance || "function" != typeof performance.getEntriesByType) return null; let commonMsg = getCommonMsg() let msg: 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) } }) // 过滤忽略的url o = o.filter(item => { var include = getConfig('ignore').ignoreApis.findIndex(ignoreApi => item.name.indexOf(ignoreApi) > -1) return include > -1 ? false : true }) msg.res = JSON.stringify(o) report(msg) }
这里会经过改写ajax或fetch来实现自动上报接口调用成功失败的信息,固然若是不是经过这两种方式发起网络请求的,也额外支持__bb.api()
手动上报
// 若是返回过长,会被截断,最长1000个字符 function 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 } } }
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(0,1000) || '', begin) } else { handleApi(page, !1, time, status, res.substr(0,1000) || '', begin) } }) return e }) } } }
支持sum avg api msg等多种手动上报方式