隨着業務的快速發展,咱們對生產環境下的問題感知能力愈來愈關注。做爲距離用戶最近的一層,前端的表現是否可靠、穩定、好用,很大程度上決定着用戶對整個產品的體驗和感覺。所以,對於前端的監控不容忽視。css
搭建一套前端監控平臺須要考慮的方面不少,好比數據採集、埋點模式、數據處理和分析、報警以及監控平臺在具體業務中的應用等等。在這全部環節中,準確、完整、全面的數據採集是一切的前提,也爲後續的用戶精細化運營提供基礎。html
前端技術的突飛猛進給數據採集也帶來了變化和挑戰,傳統的手工打點模式已經不能知足需求。如何在新的技術背景下讓前端數據採集工做更加完善、高效,是本文討論的重點。前端
在採集數據以前,首先要考慮採集什麼樣的數據。咱們重點關注兩類數據,一類是與用戶體驗相關的,如首屏時間、文件加載時間、頁面性能等;另外是幫助咱們及時感知產品上線後是否出現異常的,好比資源錯誤、API 響應時間等。具體來講,咱們對前端的數據採集具體主要分爲:vue
路由切換 (href、hashchange、pushState)react
JsErrorwebpack
性能 (performance)ios
資源錯誤web
APIajax
日誌上報vue-router
Vue、React、Angular 等前端技術的快速發展使單頁面應用盛行。咱們都知道,傳統的頁面應用是用一些超連接來實現頁面切換和跳轉的,而單頁面應用是使用各自的路由系統來管理前端的每個頁面切換,例如 vue-router、react-router 等,跳轉時僅刷新局部資源 ,js、css 等公共資源只須要加載一次,這就使傳統網頁進入離開的方式只有第一次打開能被記錄。單頁應用後續全部路由切換的方式有兩種,一種是 Hash,一種是 HTML5 推出的 History API。
1. href
href 爲頁面初始化的第一次進入,這裏只須要單純上報「進入頁面」事件便可。
2. hashchange
Hash 路由一個明顯的標誌是帶有「 # 」。Hash 的優點是兼容性更好,但問題在於 URL 中一直存在「 # 」並不美觀。咱們主要經過監聽 URL 中的 hashchange 來捕獲具體的 hash 值進行檢測。
window.addEventListener('hashchange', function() { // 上報【進入頁面】事件 }, true)
須要注意的是,在新版 vue-router 中若是瀏覽器支持 history,即便 mode 選擇 hash 也會優先選擇 history 模式,雖然表現形式暫時仍是 # 號,但其實是模擬的,因此千萬不要認爲本身在 mode 選擇了hash 就必定會是 hash。
3. History API
History 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法進行路由切換,是目前主流的無刷新切換路由方式。與 hashchange 只能改變 # 後面的代碼片斷相比,History API (pushState、replaceState) 給了前端徹底的自由。
PopState 是瀏覽器返回事件的回調,可是更新路由的 pushState、replaceState 並無回調事件,所以,還須要分別在 history.pushState() 和 history.replaceState() 方法裏處理 URL 的變化。在這裏,咱們運用到了一種相似 Java 的 AOP 編程思想,對 pushState 和 replaceState 進行改造。
AOP (Aspect-oriented programming)即面向切面編程,提倡針對同一類問題進行統一處理。AOP 的核心思想是讓某個模塊可以重用,它採用橫向抽取機制,將功能代碼從業務邏輯代碼中分離出來,擴展功能而不修改源代碼,相比封裝來講隔離得更加完全。
下面介紹咱們的具體改造方式:
// 第一階段:咱們對原生方法進行包裝,調用前執行 dispatchEvent 了一個一樣的事件 function aop (type) { var source = window.history[type]; return function () { var event = new Event(type); event.arguments = arguments; window.dispatchEvent(event); var rewrite = source.apply(this, arguments); return rewrite; }; } // 第二階段:將 pushState 和 replaceState 進行基於 AOP 思想的代碼注入 window.history.pushState = aop('pushState'); window.history.replaceState = aop('replaceState'); // 更改路由,不會留下歷史記錄 // 第三階段:捕獲pushState 和 replaceState window.addEventListener('pushState', function() { // 上報【進入頁面】事件 }, true) window.addEventListener('replaceState', function() { // 上報【進入頁面】事件 }, true)
window.history.pushState 實際調用關係如圖:
至此,咱們對 pushState、replaceState 改造完畢,實現了有效地捕獲路由切換。能夠看到,咱們在不侵入業務代碼的狀況下,對 window.history.pushState 進行了擴展,在調用的同時會主動 dispatchEvent 一個 pushState。
但在這裏咱們也能看到一個弊端,就是若是 AOP 代理函數發生 JS 錯誤,將會阻斷後續的調用關係,使實際的 window.history.pushState 沒法被調用。因此在使用此方式的時候,要對 AOP 代理函數的內容作好完善的 try catch,來防止業務上出現異常。
_*_Tips:想自動捕獲頁面停留時間只須要在下一個進入頁面事件觸發時,經過上一個頁面的打點時間和當前時間作差值便可,這時候能夠上報一個【離開頁面】事件。
前端項目中,因爲 JavaScript 自己是一個弱類型語言,加上瀏覽器環境的複雜性、網絡問題等,很容易發生錯誤。所以作好網頁錯誤監控,不斷優化代碼,提升代碼健壯性是一項很重要的工做。
JsError 的捕獲能夠幫助咱們分析和監控線上問題,它與咱們在 Chrome 瀏覽器的調試工具 Console 中看到的內容一致。
1. window.onerror
咱們使用 window.onerror 捕獲通常狀況下 JS 錯誤的異常信息。捕獲 JS 錯誤的方式有兩種,window.onerror 和 window.addEventListener(‘error’)。通常狀況下,捕獲 JS 異常不推薦使用 addEventListener(‘error’),主要是由於它沒有堆棧信息,並且還須要對捕獲到的信息作區分,由於它會將全部異常信息捕獲到,包括資源加載錯誤等。
window.onerror = function (msg, url, lineno, colno, stack) { // 上報 【js錯誤】事件 }
2. Uncaught (in promise)
當 Promise 內發生 JS 錯誤或者 reject 信息未被業務處理的狀況時,會拋出一個 unhandledrejection,而且這個錯誤不會被 window.onerror 以及 window.addEventListener('error') 捕獲,這裏須要用專門的 window.addEventListener('unhandledrejection') 進行捕獲處理:
window.addEventListener('unhandledrejection', function (e) { var reg_url = /\(([^)]*)\)/; var fileMsg = e.reason.stack.split('\n')[1].match(reg_url)[1]; var fileArr = fileMsg.split(':'); var lineno = fileArr[fileArr.length - 2]; var colno = fileArr[fileArr.length - 1]; var url = fileMsg.slice(0, -lno.length - cno.length - 2);}, true); var msg = e.reason.message; // 上報 【js錯誤】事件 }
咱們注意到 unhandledrejection 由於繼承自 PromiseRejectionEvent,PromiseRejectionEvent 又繼承自 Event,因此 msg、url、lineno、colno、stack 以字符串形式放到了 e.reason.stack 中,咱們須要解析出來上述參數來和 onerror 參數對齊,爲後續監控平臺的指標統一化打下基礎。
3. 常見問題
若是出現捕獲的 msg 所有爲 "Script error." ,問題在於你的 JS 地址和當前網頁不在同一個域下。由於咱們要常常在線上的版本作靜態資源 CDN 化,會致使常訪問的頁面跟腳本文件來自不一樣的域名。這時若是沒有進行額外的配置,瀏覽器出於安全方面的設計就容易出現 "Script error."。咱們能夠利用目前流行的 Webpack 打包工具來處理此類問題。
// webpack config 配置 // 處理 html 注入 js 添加跨域標識 plugins: [ new HtmlWebpackPlugin({ filename: 'html/index.html', template: HTML_PATH, attributes: { crossorigin: 'anonymous' } }), new HtmlWebpackPluginCrossorigin({ inject: true }) ] // 處理按需加載的 js 添加跨域標識 output: { crossOriginLoading: true }
大部分場景下,生產環境中的代碼都是通過壓縮合並的,這使得咱們捕獲到的錯誤很難映射到具體的源碼,爲咱們解決問題帶來很大困擾,這裏簡要提出 2 個解決方案的思路。
生產環境咱們須要添加 sourceMap 配置,這會致使安全隱患,由於這樣外網就能夠經過 sourceMap 進行源碼映射。爲了下降風險,咱們能夠經過以下方式:
將 sourceMap 生成的 .map 文件設置公司內網訪問,下降源碼安全風險
在發佈代碼到 CDN 的時候,將 .map 文件存儲到公司內網下
這時咱們已經擁有了 .map 文件,後續要作的就是經過捕獲到的 lineno、colno、url 調用 mozilla/source-map 庫進行源碼映射,便可拿到真實的源碼錯誤信息。
性能指標的獲取相對比較簡單,在 onload 以後讀取 window.performance 便可,裏面包含了性能、內存等信息。這部份內容在不少現有的文章中都有介紹,因篇幅所限不在本文作過多展開,以後在相關主題文章中咱們會有相關探討,感興趣的朋友能夠添加「馬蜂窩技術」公衆號持續關注。
首先咱們要明確下資源錯誤捕獲的使用場景,更多的是感知 DNS 劫持 及 CDN 節點異常等,具體方式以下:
window.addEventListener('error', function (e) { var target = e.target || e.srcElement; if (target instanceof HTMLScriptElement) { // 上報 【資源錯誤】事件 } }, true)
這裏只作基本演示,實際環境中咱們會關心更多的 Element 錯誤,如 css、img、woff 等,你們能夠根據不一樣的場景自行添加。
_*資源錯誤的使用場景更多依賴其餘幾個維度,如:_地域、運營商等,後續的篇幅中咱們會具體講解。
市面上主流的框架(如 Axios、jQuery.ajax 等)中,基本上全部的 API 請求都是基於xmlHttpRequest 或者 fetch,因此捕獲全局接口錯誤的方式就是封裝 xmlHttpRequest 或者 fetch。這裏,咱們的 SDK 仍然使用到上文說起的 AOP 思想,對 API 進行攔截。
1. XmlHttpRequest
var xhr = window.XMLHttpRequest; var _open = xhr.prototype.open; var _send = xhr.prototype.send; var attr = {}; var openReplacement = function (method, url) { // 能夠存儲method、url、時間打點等信息 attr.duration = new Date().getTime(); _open.apply(this, arguments); } var sendReplacement = function () { methods.addEvent(this, 'readystatechange', function (attr) { // 能夠存儲response的status、計算客戶端實際響應時間 attr.status = this.status; attr.duration = new Date().getTime() - attr.duration; // 上報【API】事件 }.bind(this, , JSON.parse(JSON.stringify(attr)))); _send.apply(this, arguments); } xmlhttp.prototype.open = openReplacement; xmlhttp.prototype.send = sendReplacement;
2. Fetch
須要注意的是,API 攔截必定要對 SDK 本身上報的 API 設置好忽略,不然將會致使循環上報問題。
var _fetch = window.fetch; window.fetch = function () { var attr = { method: arguments[1].method, url: arguments[0], duration: new Date().getTime() }; return _fetch.apply(this, arguments).then(res => { attr.status = res.status; attr.duration = new Date().getTime() - attr.duration; // 上報【API】事件 return res; }); }
爲了監控前端應用是否正常運行,一般會在前端收集錯誤與性能等數據,最終將這些數據上報到服務端。由於日誌上報並非應用的主要功能邏輯,優先級比較低,因此咱們在確保日誌數據上報更高效的同時,還應該考慮如何儘量地減小與其餘關鍵操做的資源爭搶。
1. sendBeacon
navigator.sendBeacon() 方法主要用於知足統計和診斷代碼的須要。這些代碼一般會在卸載文檔以前,嘗試經過 HTTP 將少許數據異步傳輸到 Web 服務器。它解決了日誌上報在 unload 時成功率很低的問題。咱們在埋點時有不少對離開頁面時上報的需求,由於 SendBeacon 是異步的,不會影響當前頁到下一個頁面的跳轉速度,能夠更可靠地保障事件上報成功率,而且不影響路由切換。
window.navigator.sendBeacon('上報事件的api', '數據參數')
2. img.src
當瀏覽器不支持 navigator.sendBeacon 時,咱們能夠採用模擬圖片加載的方式發送日誌上報事件,且不會存在跨域問題。
var img = new Image(); img.src = API + '?' + '數據參數'
3. 關於 XmlHttpRequest
這裏不推薦用 XmlHttpRequest。XHR 雖然支持異步請求,直接發送數據到後端,可是會受到跨域和同源的限制。而經過日誌上報 API 跟業務是不在一個域下的,若是採用這種模式須要設置 Access-Control-Allow-Origin:* 跨域,很是不方便,而且在 unload 狀況下上報發生的丟包率最高。
總結來看,日誌上報推薦採用 sendBeacon -> img.src。在不影響用戶路由切換和阻塞用戶的狀況下丟包率能夠控制在 10%-30%,具體要看用戶羣體對應的環境。
高效的前端數據採集對於搭建前端監控平臺來講很是關鍵。本文咱們分享了馬蜂窩在保證數據採集及時、準確、全面等方面的一些思路和實踐。須要提示你們注意的是,文中涉及到的演示只作了核心代碼的關鍵描述,不具有生產使用,咱們在實際使用中須要作好兼容及容錯。
本文也將做爲馬蜂窩前端監控平臺系列文章的開篇,從此還將陸續推出埋點模式、數據處理和分析、報警以及監控平臺在具體業務中的應用等內容,歡迎你們持續關注。
本文做者:王崢,馬蜂窩大數據平臺前端技術專家。
(馬蜂窩技術原創內容,轉載務必註明出處保存文末二維碼圖片,謝謝配合。)
關注馬蜂窩技術,找到更多你想要的內容