本系列文章旨在講解如何從零開始搭建前端監控系統。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等多種手動上報方式