別再讓你的web頁面在用戶瀏覽器端裸奔
作者: 阿里巴巴國際UED團隊/戊子
頁面在用戶那裡運行,如果10%的用戶頁面出現問題而自己本地沒有辦法重現?
如何先一步了解到前端出現的問題,而不是等用戶反饋?
能不能像查看服務端日誌一樣來定位前端頁面運行的問題?
前端在業務複雜度越來越高的情況下,本地即使做了充分的測試,依照caniuse做了很多兼容,依然無法讓人放心頁面能否正常運行或者運行得怎麼樣。
當一個前端頁面發布出去了之後,頁面所運行的設備、瀏覽器、網路環境、用戶操作習慣等等因素都可能是造成頁面不正常的原因。
所以對前端頁面需要做一定的監控,而最可行的前端監控方式就是將頁面的日誌選擇上報到監控日誌伺服器中。
前端日誌上報可以很簡單
對業務邏輯的執行收集了日誌數據之後可以參數的形式構造一個url,再通過一個Image請求發送到到伺服器就完成了日誌的上報。
(newImage).src=`/r.png?page=${location.href}¶m1=${param1}...`;
這樣一行代碼就搞定了日誌的上報,然鵝,在生產環境中,日誌上報所延伸的問題要複雜很多。
日誌上報帶來的問題
日誌上報最終是為了服務業務,監控業務的運行狀態,一般而言前端運行的場景中開發者最期望監控的不外乎頁面&API請求是否正常響應和頁面js邏輯是否正常執行。
為了覆蓋這兩個監控目標,需要通過很多類型的日誌來覆蓋,還有一些特殊場景下,開發者還希望能與具體業務靈活結合,實現自定義上報。所以常見的日誌類型如下
– 頁面&API請求是否正常響應
– API調用日誌 – API調用成功與否及其耗時
– 頁面性能日誌 – 頁面連接耗時、首次渲染時間、資源載入耗時等
– 訪問統計日誌 – PV/UV,短時間內斷崖式的量變化很容易反應問題
– 頁面js邏輯是否正常執行
– 頁面穩定性日誌 – 頁面載入和頁面交互產生的js error信息
– 業務相關日誌
– 自定義上報 – 某些業務邏輯的結果、速度、統計值等自定義內容
隨著前端業務的壯大,日誌監控上報的量會快速增加,監控的邏輯也會越來越複雜,而在生產環境中,前端監控的最基本原則是日誌獲取和上報本身不能拋出異常或者影響頁面性能。
這麼多的日誌類型代表了日誌獲取的邏輯複雜,同時各種各樣的瀏覽器和環境會讓這個問題變得更棘手,例如想用console.warn列印異常信息,但是可能會出現warn函數調用報錯;例如捕捉到了error但是error.message全是Script error.…
瀏覽器的兼容性,前端業務邏輯依賴,日誌上報方式,日誌上報效率,用戶操作習慣,網路環境等因素都可能讓日誌上報產生問題甚至影響業務。這些因素會給日誌上報帶來可靠和性能兩方面的問題
日誌上報的可靠性問題
瀏覽器兼容性
在不同端和瀏覽器中,因為兼容性的不同,日誌獲取邏輯的和上報方法需要兼容多種方式來進行,例如fetch方法方法是否可用,頁面性能(performance)計算是否可以使用NT2標準,這些問題可能會帶來上報邏輯本身報錯污染業務日誌統計;
上報可靠性
日誌採集sdk可能因為網路原因無法載入,所以安全的方式是sdk注入的位置合理的靠後,那麼頁面打開到sdk初始化這段時間就會產生漏報;
後端為了業務分離,通常會獨立設定一個日誌採集伺服器,這種情況下日誌上報可能會遇到跨域問題;
用戶的頻繁操作和關閉頁面會可能造成很多已經收集的數據漏報。
日誌上報的性能問題
在一個複雜站點中,這些日誌數據可能會非常多,上報可能會因為瀏覽器並發數量的限制阻塞業務的網路請求,或者影響頁面性能。
更優雅的上報姿勢
姿勢一 隔離業務
資源隔離
為了避免影響業務,那麼理所當然,為了不佔用業務計算資源,日誌上報需要單獨設定後端服務。
同時也不能使用與業務相同的域名,這跟頁面盡量使用CDN引入資源的原理相似,瀏覽器會對同一個域名有一定的並發數限制。
而頁面性能、資源載入、初始化API、PV/UV、初始化js邏輯錯誤等日誌都是頁面初始化的時候觸發上報,這種短時間大量的上報可能會造成網路請求延時。例如chrome對同一個域名的最大並發連接數為6個,如果日誌同時上報了6次以上,就會對同域名的業務造成影響;更壞的情況如頁面有一些錯誤、網路連接質量質量不高會讓日誌上報阻礙頁面渲染。
因此日誌上報可以像使用CDN服務一樣,使用單獨域名和日誌處理服務。
既然使用了不同的域名,那麼跨域問題隨之而來,這需要前後端共同支持。伺服器需要允許外部訪問Access-Control-Allow-Origin:*;前端在進行日誌上報的時候要添加避免跨域標識,如fetch方式:
varurl="https://arms-retcode.aliyuncs.com/r.png";
fetch(
`${url}?t=perf&page=qar.alibaba-inc.com&load=1168`,
{mode:"no-cors"}
)
不同域名一個性能缺點是增加首次DNS解析時間,不過可以通過在頁面添加DNS預解析來避免。
異常隔離
在資源隔離的基礎上,日誌上報的異常處理也需要隔離,日誌本身拋出的異常絕對不能和業務異常混在一起上報。
進行充分測試的前提下,最簡單粗暴的方式是在整個監控sdk外面添加try...catch...,好處是永遠不會出現sdk本身錯誤上報,不過同時也讓開發者失去了發現sdk問題的途徑。所以兩者兼得的方式是必要的。
這裡提供一個關鍵模塊埋點的方法,它對整個前端監控sdk多個關鍵點上埋點並且收集的結果中只標記是否成功。話不多說,直接上示例代碼:
// 全局標記匯總,初始化為36個點位均為1的數組
varN=36;
varsdkStat=Array.from({length:N},()=>1);
/** 日誌上報功能模塊
* 對應模塊報錯設置對應點位為0,多個點位為0可以幫助找到錯誤發生鏈路
*/
try{/* sdk module 0*/}catch(){sdkStat[]=;}
/* other modules */
try{/* sdk module 35*/}catch(){sdkStat[35]=;}
// 日誌上報發送模塊
varstatStr=parseInt(sdkStat.join(""),2).toString(36);
(newImage).src=`/r.png?¶m1=${param1}&sdkStat=${statStr}...`;
姿勢二 壓縮請求響應報文
壓縮之前重新審視一下(new Image).src的日誌發送方式:
HTTP Request: 前端日誌數據以多組key=value的字元串形式接在一個Image資源請求的url後面,前端發送Image請求。
HTTP Responce: 伺服器返迴響應結果或者空圖片。
日誌數據直接放到url中的好處是網路傳輸效率高。然而url長度是有限制的,例如IE瀏覽器是2083個字元,同時伺服器也會對url長度進行限制。
類似如下的js error信息就沒有辦法完整上報,
$isnotdefined@https://www.example.com.cn/catalog/?spm=a2o4k.customer.0.0.37c1379dmQwdrW&q=pediasure&searchclickposition=hint:3:231
...
Tg@https://www.googletagmanager.com/gtm.js?id=GTM-KTVS7D9&l=shadowDatalayerKi7l:64:32
...
不僅僅是js error的錯誤棧深還因為urlencode對特殊字元和漢字的轉碼,這兩個因素會使url長度輕鬆突破限制。
另外業務邏輯實際上不關注而且也應關注日誌上報的響應結果,所以這個請求的結果應該儘可能省去。
針對報文壓縮有以下方式:
HTTP/2頭部壓縮
http請求中,每次請求都會傳輸一系列的請求頭來描述請求的資源及其特性,然而實際上每次請求都有很多相同的值,如Host:,user-agent:,Accept等。這些數據能夠佔用到300-800byte的傳輸量,如果攜帶大的cookie,請求頭甚至可以佔用1kb的空間,而實際真正需要上報的日誌數據僅僅只有10~50byte的大小。在HTTP 1.x中,每次日誌上報請求頭都攜帶了大量的重複數據導致性能浪費。
HTTP/2頭部壓縮採用Huffman Code壓縮請求頭,並用動態表更新每次請求不同的數據來把每次請求的頭部壓縮到很小。
HTTP/1.1效果
HTTP/2.0效果
頭部壓縮後每條日誌請求的size都大大減小,響應的速度也有提升。
壓縮日誌的長度
最需要壓縮即js error的錯誤棧,錯誤棧當中佔位最多是錯誤定位的文件地址,而很多錯誤棧有很多相同的文件,壓縮空間就來源於stack中js文件的url重複。
一個典型的jserror stack經常會出現這種形式如下:
obj0.fn0 at(http://loooooooooonnnnnnnnnnng/loooooong/long.js 123:1)
obj1.fn1 at(http://loooooooooonnnnnnnnnnng/loooooong/long.js 234:1)
obj2.fn2 at(http://loooooooooonnnnnnnnnnng/laaaaaang/lang.js 345:1)
...
可考慮把文件url抽取出來單獨作為一個字典,那麼上報內容可縮減為
files={"f1":"http://loooooooooonnnnnnnnnnng/loooooong/long.js","f2":"..."}
obj0.fn0 at(f1123:1)
obj1.fn1 at(f1234:1)
obj2.fn2 at(f2345:1)
...
即可大大縮減日誌長度。
省去響應體
日誌上報本身只關注日誌有沒有上報,而對上報請求的返回內容並不關注,甚至完全可以不需要返回內容。所以使用HTTP HEAD的方式上報,並且返回的響應體為空,避免響應體傳輸資源損耗。
這時候只需要設置一個nginx伺服器來記錄日誌內容並返回200狀態碼即可。
fetch(`${url}?t=perf&page=lazada-home&load=1168`,
{mode:"no-cors",method:"HEAD"}
)
姿勢三 合併上報
既然一個頁面上報的次數那麼多,一個更容易想到的idea應該是把日誌合併上報來減小請求數量。
HTTP/2多路復用
用戶瀏覽器和日誌伺服器之間產生多次HTTP請求,而在HTTP/1.1 Keep-Alive下,日誌上報會以串列的方式傳輸,會讓後面的日誌上報延時。通過HTTP/2的多路復用來合併上報,節省網路連接的開銷。
HTTP POST合併
POST請求因為request body可以有更大施展空間,在HTTP POST中只要一次包含多條日誌的內容,那麼相對於一條日誌一次HTTP HEAD請求的方式會更加經濟。
在HTTP POST的基礎上,可以順便解決用戶關掉或者切換頁面造成的漏報問題。
以前常見的解決方式是監聽頁面的unload或者beforeunload事件,並以通過同步的XMLHttpRequest請求或者構造一個特定src的標籤來延遲上報。
window.addEventListener("unload",uploadLog,false);
functionuploadLog(){
varxhr=newXMLHttpRequest();
xhr.open("POST","/r.png",false);// false表示同步
xhr.send(logData);
}
這種上報的缺點是會給下一個頁面的性能造成影響。更優雅的方式是使用navigator.sendBeacon(),它能夠非同步地發送日誌數據。
window.addEventListener("unload",uploadLog,false);
functionuploadLog(){
navigator.sendBeacon("/r.png",logData);
}
合併前
合併後(navigator.sendBeacon)
理想情況下,合併n個日誌上報耗費的總時間能達到原來的1/n
小結
前端業務場景和瀏覽器的兼容性千差萬別,所以日誌上報要兼容多種方式;頁面生命周期、業務邏輯影響了日誌是否可獲取和是否漏報,所以對應的日誌類型和上報時機要嚴格把握;前端業務邏輯快速迭代且場景多樣,所以日誌上報要做到與業務解耦合同時可以自定義上報…
這些大大小小的坑促使我們把前端日誌監控沉澱為一個獨立且系統性的工程來做,在打磨這個工程的過程中我們同樣還在探索是否有更加高效、穩定的日誌上報方式。
Frontender -
持續關注互聯網、web前端開發、IT編程資料分享。
其它功能正在完善,不定期更新....
點贊和分享是對我們最大的支持


TAG:郭旗美工 |