前言
一般來說,產品做出的原型多多少少會帶有“個人”傾向,UI設計的交互也會人所不同,而當公司生存下來了后,數據沉淀達到一定量了后,這種迭代就決不能拍腦袋了,因為人是具有偏見的,如果帶有“偏見”的產品上線后,其反響是不能預估的,我們不能將公司的生存放在“可能”這種說法上。
小步快跑,通過迭代來優化產品,但如果每個迭代都顛覆了之前的設計,那就是原地踏步,每一次迭代都要知道這個迭代哪里出了問題,然后再針對問題做優化,而不是頻繁的改版,持續優化,這個就必須建立在比較良好的數據監控與數據分析上,人有偏見但是數據不會,。
所以大公司的核心產品,每一個決策,每一個迭代都需要分析各種數據,建立完善的AB Testing與小流量機制,待收到了充分的信息證明這次迭代是有效的后再做真正的全量更新。
數據中往往會有我們需要的答案,比如前段時間,我們發現我們的訂單轉化率比較低,那么我們盯着轉換率本身是沒有意義的,我們可以考慮影響幾個數據的其他指標:
① 頁面PV,一般來說增大PV能有效增加轉化率
② 按鈕點擊的前提,比如需要登錄后才能下單,和匿名下單的轉化率對比
③ 優惠券使用情況(據說,中國沒有5元買不到的用戶)
④ ......
我們不同的渠道,很有可能產生這不同的場景,不同的場景下獲得的數據,便能知道哪種是我們真實需要的,如此一來研發才能真正幫助公司做出正確的判斷,為后續迭代提供參考。
系列文章:
【數據可視化之數據定義】如何設計一個前端監控系統 描述如何獲取各種指標數據,如何歸類,首篇博客補足
【數據可視化之持久化】如何設計一個前端監控系統 描述如何做存儲(涉及大數據部分由其他同事整理)
【數據可視化之圖表呈現(dashboard)】如何設計一個前端監控系統 描述如何將數據變為有效的展示
代碼地址:https://github.com/yexiaochai/medlog
如果文中有誤的地方請您指出。
統計數據
統計屬於海量數據的范疇,產品分析做的越細,所產生的數據量越大,比如我要做一個用戶點擊熱點的話,就需要收集用戶所有的點擊數據,這個可能是pv的數十倍;另一方面,海量統計應該是脫離業務本身的,用戶可定制化打點需求,以滿足不同業務的變化。
了解了基本概念,我們便可以確定我們到底需要什么數據,這個拍腦袋想不出來,就可以先進行基礎窮舉:
① pv&uv
② 頁面點擊(pv&uv)
③ 頁面來源(web處理這個有些困難),定義頁面從哪里來,在海量數據的情況下也可以不記錄
④ 頁面停留時間(web不一定准確)
⑤ 前端錯誤日志(這個比較龐大,后面詳述)
⑥ 首屏載入速度
⑦ 用戶環境收集(一般來說這個是附帶的)
⑧ 跨域資源監測(監測所有非白名單腳本,發現腳本注入行為,附件特性)
而因為現在一套H5代碼會用於不同的平台入口,所以這些數據又會額外具有“渠道信息”的標志。
再我們有了以上數據的情況下,我們能很輕易的得出某個渠道的轉化率:
因為不同渠道表現也許會有所不同,有可能微信渠道的入口在首頁,他的轉化率計算一般會經過這么一個過程:
首頁pv -> 列表頁pv -> 訂單填寫頁pv -> 下單按鈕點擊pv -> server最終成單數
而搜索引擎入口,可能直接就到了訂單填寫頁,所以轉化率計算公式又變成了:
訂單填寫頁pv -> 下單按鈕點擊pv -> server最終成單數
這里結合首屏載入速度與頁面停留時間,輔以用戶點擊軌跡,就能從某些程度上,追蹤分析一個用戶的行為了。
曾今有一次我們發現我們訂單轉化率下降了50%,於是老板讓我們馬上給出原因,我們當時懷疑過BUG,懷疑過運營商接口有問題,但是我們所有的推論都沒有很好的佐證,於是各種查詢數據庫,最后與整個打點的pv數據,我們便得出了一個結論:
因為,多數用戶的優惠券過期了,所以轉化率急劇下降!!!
為了證明這個猜想,我們由將某一個渠道的優惠券加上,第二天轉化率就回歸了,我們這里能判斷出轉化率下降的原因和我們平時完善的打點是息息相關的。
錯誤日志
另一方面,當代碼迭代到一定量的時候,code review也就只能解決很小一部分問題了,前端預警和前端錯誤日志產生的蛛絲馬跡才會將一些隱藏的很深的BUG揪出來,所有的這一切都需要從數據采集開始。
我原來也遇到一個BUG,潛伏期很長,而且只有在特定的場景才會觸發,這種BUG一般來說測試是無力的,當時我發現2個版本的日志有些奇怪的錯誤,再一步步抽絲剝繭,終於定位到了那個錯誤,當我們代碼量大了后,合理的錯誤埋點+前端監控才能讓系統變得更健康。
這里引用一張錯誤監控圖(http://rapheal.sinaapp.com/2014/11/06/javascript-error-monitor/):


這里將上一周的錯誤數與本周做對比,如果沒有大的迭代,稍微有異常就會產生報警,一般來說用戶才是最好的測試,上線后沒有報警就沒有BUG。
PS:原來我們每次大版本發布,60%的幾率要回滾......
錯誤捕捉
前端錯誤捕捉,一般使用onerror,這個偶爾會被try cache影響:
1 window.onerror = function (msg, url, line, col, error) { 2 //...... 3 }
當時生產上的錯誤日志因為是壓縮過的,真實抓到的錯誤信息十分難看:

錯誤信息全部是第一行,報錯的地方也是做過混淆的,如果不是頁面划分的過開,這個錯誤會讓人一頭霧水,要想深入了解錯誤信息,這里便可以了解下source map了
sourcemap
簡單來說,sourcemap是一個信息文件,里面存儲着位置信息,也就是說,在js代碼壓縮混淆合並后的每個代碼位置,對應的源碼行列都是有標志的,有了這個source map,我們就能直接將源碼對應的錯誤上報回去,大大降低我們的錯誤定位成本。
這里不同的業務使用的不同的構建工具,這里以grunt為例,grunt打包一般來說是使用的require,這里需要為其配置加入一段代碼即可:
1 "generateSourceMaps": true, 2 "preserveLicenseComments": false, 3 "optimize": "uglify2",

上面那個有一些問題,他將我的關鍵字過濾了,最后采用的這個:

然后就會生成你要的sourcemap了

可以看到壓縮文件中,包含了map引用:

於是我們線上代碼就會變成這個樣子:

這個時候,我們故意寫個錯誤的話,這里查看報錯:

雖然看到的是源碼,但是上報的數據似乎沒有什么意義,這個時候可以借助一些第三方工具對日志做二次解析:
Sentry(GitHub - getsentry/sentry: Sentry is cross-platform crash reporting built with love)
並且,顯然我們並不希望我們的源代碼被人看到,所以我們將sourcemap文件存到線下,在線下將日志反應為我們看得懂的源碼,這里簡單看看這個文件定義:
1 - version:Source map的版本,目前為3。 2 - file:轉換后的文件名。 3 - sourceRoot:轉換前的文件所在的目錄。如果與轉換前的文件在同一目錄,該項為空。 4 - sources:轉換前的文件。該項是一個數組,表示可能存在多個文件合並。 5 - names:轉換前的所有變量名和屬性名。 6 - mappings:記錄位置信息的字符串。
線下翻譯
sourcemap的機制什么的,就不是我關注的重點,想了解的可以看阮老師的博客,我現在的需求是:
獲取了列號和行,如何可以在線下映射成我們要的真實行號
比如我們拿到了上述的行號與列號為{1,13310},那么我們這里真實映射的是,合並文件中的某一行:

要完成這一切,我們需要一套“錯誤還原”的后台系統,這個直接坐到js監控其中一環就好,比如我們可以簡單這樣做:

這個被一國外網站實現了(一般來說要錢的......),所以是可以實現的,我們便去追尋他的實現即可。
后續在github找了一個庫,完成了類似的功能,這里使用nodejs:
1 var mapData = require('./index.json'); 2 // console.log(sourceMap); 3 var sourceMap = require('source-map'); 4 var consumer = new sourceMap.SourceMapConsumer(mapData); 5 var numInfo = consumer.originalPositionFor({ line: 1, column: 13330 }) 6 console.log(numInfo)
輸出==>
1 { source: 'pages/index/index.js', 2 line: 182, 3 column: 0, 4 name: 'layoutHtml' }
於是,我們已經找到了自己要的東西了。最初,在快速調研的時候,我們不要知道https://github.com/mozilla/source-map是干什么的,但是如果我們決定使用的話,就需要去仔細研究一番了。
總而言之,線上錯誤日志搜集的行號信息,在線下平台便能很好的翻譯了,這里方案有了,我接下來馬上想法落地,落地情況在存儲篇反饋
錯誤日志這里,因為比較重要,也與普通的打點不一樣,占的篇幅有點長,我們這里先繼續往下說,等日志簡單落地后再詳述。
采集系統
本來,我們數據采集可以使用百度或者友盟,但是總有那么一些東西得不到滿足,而且也沒有數據對外輸出的API,而公司如果穩步上升的話,做這塊是遲早的事情,所以宜早不宜遲吧,而我這里主要還是先關注的移動體系,所以不太會關注兼容性,這個可以少考慮一些東西,真的遇到一些情況如跨域什么的,我們后面再說吧。
關於存儲一塊有很多需要考慮,比如如何計算首屏加載時間,webapp和傳統網易的異同,hybrid的差異,uv的計算方法等都需要考慮,但是我們今天變只將采集代碼實現即可,剩下的下篇再處理。
簡單來講,日志采集,其實就是一個get請求,你就算想用ajax發出去也是沒有問題的,為了加入額外信息可能我們會做一個收口:
1 ajax(url, { 2 s: '' 3 b: '' 4 c: '' 5 });
但是這個不是主流的做法,一般來說,我們打點信息使用的圖片的方式發出,而因為重復的請求會被瀏覽器忽略,我們甚至會加入uniqueId做標志:
1 var log = function () { 2 var img = new Image(); 3 img.src = 'http://domain.com/bi/event?'+ uniqueId; 4 };
基本的采集實現就這么簡單,但是后續逐步完善的功能,會增加復雜度,於是我建立了一個git倉庫存儲代碼,后續大數據一塊的代碼也將放到這里:
https://github.com/yexiaochai/medlog
閉門造車的意義不大,翻看前輩的一些采集代碼比如alog,會發現他打點的一塊是這樣做的:
1 /** 2 * 上報數據 3 * 4 * @param {string} url 目標鏈接 5 * @param {Object} data 上報數據 6 */ 7 function report(url, data) { 8 if (!url || !data) { 9 return; 10 } 11 // @see http://jsperf.com/new-image-vs-createelement-img 12 var image = doc.createElement('img'); 13 var items = []; 14 for (var key in data) { 15 if (data[key]) { 16 items.push(key + '=' + encodeURIComponent(data[key])); 17 } 18 } 19 var name = 'img_' + (+new Date()); 20 entry[name] = image; 21 image.onload = image.onerror = function () { 22 entry[name] = 23 image = 24 image.onload = 25 image.onerror = null; 26 delete entry[name]; 27 }; 28 image.src = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&'); 29 }
其中有一塊差異是綁定了onload等事件,應該是想釋放資源吧?
這里的代碼,想與公司業務管理起來,比如根據業務線或者項目生成某個規則的id,上報代碼比較簡單,但是每次都要帶哪些信息,還沒很好的思路,先在這里立一個flag吧,接下來時間里全力補足吧,畢竟這塊東西很多。
結語
前端數據有很多需要處理的地方,而數據的核心分為數據采集打點,數據持久化,數據使用,數據分析。
打點又會區分H5打點與native打點,native由於權限本身,能做的事情更多,但是底層數據收集基本能做到統一。
采集的代碼是其中一部分,但采集的各項數據獲取是另一個更重要的部分,會包含數據設計,各種細節處理,我們下篇文章接着研究,有興趣的同學可關注。
