埋點(diǎn)系統(tǒng)與錯(cuò)誤監(jiān)控結(jié)合蛾洛,6爆了

前言

最近雜七雜八的事情比較多温治,難得抽出時(shí)間來(lái)彌補(bǔ)一下之前的系列,欠大家的埋點(diǎn)系列現(xiàn)在開(kāi)始走起來(lái)

為什么需要埋點(diǎn)系統(tǒng)

電影中

前端開(kāi)發(fā)攻城獅開(kāi)開(kāi)心心的 coding戒悠,非常自豪的進(jìn)行了業(yè)務(wù)熬荆、UI 分離開(kāi)發(fā),各種設(shè)計(jì)模式绸狐、算法優(yōu)化輪番上陣卤恳,代碼寫的 Perfect(勞資代碼天下第一),沒(méi)有 BUG寒矿,程序完美突琳,兼容性 No.1,代碼能打能抗質(zhì)量高符相。下班輕松打卡拆融,回家看娃。

現(xiàn)實(shí)中

實(shí)際上啊终,開(kāi)發(fā)環(huán)境與生產(chǎn)環(huán)境并不能等同镜豹,并且測(cè)試的過(guò)程再完善,依然會(huì)有漏測(cè)的情況存在蓝牲√酥考慮到用戶使用客戶端環(huán)境、網(wǎng)絡(luò)環(huán)境等等一系列的不確定因素存在例衍。

所以在開(kāi)發(fā)過(guò)程中一定要記得三大原則(我胡謅的

  1. 沒(méi)有完美的代碼昔期,只有沒(méi)發(fā)現(xiàn)的 BUG
  2. 絕對(duì)不要相信測(cè)試環(huán)境,沒(méi)有一種測(cè)試環(huán)境都涵蓋所有線上情況
  3. 如果線上沒(méi)有一點(diǎn)反饋佛玄,不要懷疑硼一,問(wèn)題應(yīng)該藏得很深、很深

什么是埋點(diǎn)系統(tǒng)

埋點(diǎn)就像城市中的攝像頭翎嫡,從產(chǎn)品的角度考慮欠动,它可以監(jiān)控到用戶在我們產(chǎn)品里的行為軌跡,為產(chǎn)品的迭代惑申、項(xiàng)目的穩(wěn)定提供依據(jù)具伍,WHO、WHEN圈驼、WHERE人芽、HOW、WHAT 是埋點(diǎn)采集數(shù)據(jù)的基礎(chǔ)維度绩脆。

對(duì)前端開(kāi)發(fā)而言萤厅,可以監(jiān)控頁(yè)面資源加載性能橄抹,異常等等惕味,提供了頁(yè)面體驗(yàn)和健康指數(shù),為后續(xù)性能優(yōu)化提供依據(jù)名挥,及時(shí)上報(bào)異常和發(fā)生場(chǎng)景。從而能夠及時(shí)修正問(wèn)題禀倔,提高項(xiàng)目質(zhì)量等榄融。

埋點(diǎn)可以大概分為三類:

  1. 無(wú)痕埋點(diǎn) - 無(wú)差別收集頁(yè)面所有信息包括頁(yè)面進(jìn)出救湖、事件點(diǎn)擊等等,需要進(jìn)行數(shù)據(jù)沖洗才能獲取到有用信息
  2. 可視化埋點(diǎn) - 根據(jù)生成的頁(yè)面結(jié)構(gòu)獲取特定點(diǎn)位鞋既,單獨(dú)埋點(diǎn)分析
  3. 業(yè)務(wù)代碼手動(dòng)埋點(diǎn) - 根據(jù)具體復(fù)雜的業(yè)務(wù)力九,除掉上述兩種不能涵蓋的地方進(jìn)行業(yè)務(wù)代碼埋點(diǎn)
代碼埋點(diǎn) 可視化埋點(diǎn) 無(wú)痕埋點(diǎn)
典型場(chǎng)景 無(wú)痕埋點(diǎn)無(wú)法覆蓋到,比如需要業(yè)務(wù)數(shù)據(jù) 簡(jiǎn)單規(guī)范的頁(yè)面場(chǎng)景 簡(jiǎn)單規(guī)范的頁(yè)面場(chǎng)景邑闺,
優(yōu)勢(shì) 業(yè)務(wù)數(shù)據(jù)明確 開(kāi)發(fā)成本低,運(yùn)營(yíng)人員可直接進(jìn)行相關(guān)埋點(diǎn)配置 無(wú)需配置舒萎,數(shù)據(jù)可回溯
不足 數(shù)據(jù)不可回溯蹭沛,開(kāi)發(fā)成本高 不能關(guān)聯(lián)業(yè)務(wù)數(shù)據(jù),數(shù)據(jù)不可回溯 數(shù)據(jù)量較大摊灭,不能關(guān)聯(lián)業(yè)務(wù)數(shù)據(jù)

大部分情況,我們可以通過(guò)無(wú)痕埋點(diǎn)收集到所有的信息數(shù)據(jù)掏缎,再配合可視化埋點(diǎn)煤杀,能夠具體定位到某一個(gè)點(diǎn)位,這樣大部分的埋點(diǎn)信息都據(jù)此分析出來(lái)沈自。

在特殊情況下,可以多加上業(yè)務(wù)代碼手動(dòng)埋點(diǎn)忌怎,處理一下特別的場(chǎng)景(大部分情況是走強(qiáng)業(yè)務(wù)與正常的點(diǎn)擊,刷新事件無(wú)關(guān)需要上報(bào)的信息)

埋點(diǎn) SDK 開(kāi)發(fā)

埋點(diǎn)數(shù)據(jù)收集分析

  • 事件基本數(shù)據(jù)
    • 事件發(fā)生時(shí)間
    • 發(fā)生時(shí)頁(yè)面信息快照
  • 頁(yè)面
    • 頁(yè)面 PV榴啸,UV
    • 用戶頁(yè)面停留時(shí)長(zhǎng)
    • 頁(yè)面跳轉(zhuǎn)事件
    • 頁(yè)面進(jìn)入后臺(tái)
    • 用戶離開(kāi)頁(yè)面
  • 用戶信息
    • 用戶 uid
    • 用戶設(shè)備指紋
    • 設(shè)備信息
    • ip
    • 定位
  • 用戶操作行為
    • 用戶點(diǎn)擊
      • 點(diǎn)擊目標(biāo)
  • 頁(yè)面 AJAX 請(qǐng)求
    • 請(qǐng)求成功
    • 請(qǐng)求失敗
    • 請(qǐng)求超時(shí)
  • 頁(yè)面報(bào)錯(cuò)
    • 資源加載報(bào)錯(cuò)
    • JS 運(yùn)行報(bào)錯(cuò)
  • 資源加載新性能
  • 圖片
  • 腳本
  • 頁(yè)面加載性能

上面的數(shù)據(jù)通過(guò) 3 個(gè)維度來(lái)定義埋點(diǎn)事件

  • ·LEVEL: 描述埋點(diǎn)數(shù)據(jù)的日志級(jí)別
    • INFO:一些用戶操作鸥印,請(qǐng)求成功,資源加載等等正常的數(shù)據(jù)記錄
    • ERROR: JS報(bào)錯(cuò)辅甥,接口報(bào)錯(cuò)等等錯(cuò)誤類型的數(shù)據(jù)記錄
    • DEBUG: 預(yù)留開(kāi)發(fā)人員通過(guò)手動(dòng)調(diào)用的方式回傳排除bug的數(shù)據(jù)記錄
    • WARN: 預(yù)留開(kāi)發(fā)人員通過(guò)手動(dòng)調(diào)用的方式回傳非正常用戶行為的的數(shù)據(jù)記錄
  • CATEGORY:描述埋點(diǎn)數(shù)據(jù)的分類
    • TRACK: 埋點(diǎn)SDK對(duì)象的生命周期管理整個(gè)埋點(diǎn)數(shù)據(jù)燎竖。
      • WILL_MOUNT:sdk對(duì)象即將初始化加載,生成一個(gè)默認(rèn)ID夏块,跟蹤全部相關(guān)事件
      • DID_MOUNTED:sdk對(duì)象初始化完成纤掸,主要獲取設(shè)備指紋等等的異步操作完成
    • AJAX: AJAX相關(guān)數(shù)據(jù)
    • ERROR:頁(yè)面中的異常相關(guān)數(shù)據(jù)
    • PERFORMANCE: 關(guān)于性能相關(guān)數(shù)據(jù)
    • OPERATION: 用戶操作相關(guān)數(shù)據(jù)
  • EVENT_NAME:具體的事件名稱

根據(jù)上述的維度,我們可以簡(jiǎn)單設(shè)計(jì)如下的架構(gòu)

根據(jù)上圖的架構(gòu)政己,再進(jìn)行下面的具體代碼開(kāi)發(fā)

代理請(qǐng)求

在瀏覽器中現(xiàn)在主要有 2 種請(qǐng)求方式,一個(gè)是 XMLHttpRequest, 一個(gè)是 Fetch掏愁。

代理 XMLHttpRequest

function NewXHR() {
  var realXHR: any = new OldXHR(); // 代理模式里面有提到過(guò)
  realXHR.id = guid()
  const oldSend = realXHR.send;

  realXHR.send = function (body) {
    oldSend.call(this, body)
    //記錄埋點(diǎn)
  }
  realXHR.addEventListener('load', function () {
    //記錄埋點(diǎn)
  }, false);
  realXHR.addEventListener('abort', function () {
    //記錄埋點(diǎn)
  }, false);

  realXHR.addEventListener('error', function () {
    //記錄埋點(diǎn)
  }, false);
  realXHR.addEventListener('timeout', function () {
    //記錄埋點(diǎn)
  }, false);

  return realXHR;
}
復(fù)制代碼

代理 Fetch

 const oldFetch = window.fetch;
  function newFetch(url, init) {
    const fetchObj = {
      url: url,
      method: method,
      body: body,
    }
    ajaxEventTrigger.call(fetchObj, AJAX_START);
    return oldFetch.apply(this, arguments).then(function (response) {
      if (response.ok) {
       //記錄埋點(diǎn)
      } else {
       //上報(bào)錯(cuò)誤
      }
      return response
    }).catch(function (error) {
      fetchObj.error = error
        //記錄埋點(diǎn)      
        throw error
    })
  }
復(fù)制代碼

監(jiān)聽(tīng)頁(yè)面的 PV歇由,UV

在進(jìn)入頁(yè)面時(shí)果港,我們通過(guò)算法生成一個(gè)唯一 session id,作為這次埋點(diǎn)行為的全局 id谢谦,上報(bào)用戶 id萝衩,設(shè)備指紋,設(shè)備信息猩谊。在用戶未登錄的情況下厅各,通過(guò)設(shè)備指紋來(lái)計(jì)算 UV队塘,通過(guò) session id計(jì)算 PV

異常捕獲

異常就是干擾程序的正常流程的不尋常事故

RUNTIME ERROR

JS中可以通過(guò) window.onerrorwindow.addEventListener('error', callback) 捕捉運(yùn)行時(shí)異常憔古,一般使用window.onerror,它兼容性更好。

window.onerror = function(message, url, lineno, columnNo, error) {
    const lowCashMessage = message.toLowerCase()
    if(lowCashMessage.indexOf('script error') > -1) {
      return
    }
    const detail = {
      url: url    
      filename: filename,
      columnNo: columnNo,
      lineno: lineno,
      stack: error.stack,
      message: message
    }
    //記錄埋點(diǎn)
}
復(fù)制代碼

Script Error

在這里我們過(guò)濾了 Script Error, 它產(chǎn)生的原因主要是頁(yè)面中加載的第三方跨域腳本報(bào)錯(cuò)锯梁,比如托管在第三方 CDN 中的 js 腳本焰情。這類問(wèn)題比較難以排查。解決的方法有:

  • 打開(kāi) CORS(Cross Origin Resource Sharing合敦,跨域資源共享),如下步驟

    • <srcipt src="another domain/main.js" cossorigin="anonymous"></script>
    • 修改Access-Control-Allow-Origin: * | 指定域名
  • 使用 try catch

      <script scr="crgt.js"></script> //加載crgt腳本验游,window.crgt = {getUser: () => string}
      try{
          window.crgt.getUser();
      }catch(error) {
          throw error // 輸出正確的錯(cuò)誤堆棧
      }
    復(fù)制代碼
    

Promise reject

js 在異步異常時(shí)無(wú)法通過(guò) onerror 方法捕獲 ,在 Promise 對(duì)象在 reject 時(shí)崔梗,同時(shí)并沒(méi)有進(jìn)行處理時(shí) 會(huì)拋出一個(gè) unhandledrejection 的錯(cuò)誤垒在,并不會(huì)被上述的方法所捕獲,所以需要添加單獨(dú)的處理事件权悟。

window.addEventListener("unhandledrejection", event => {
  throw event.reason
});
復(fù)制代碼

資源加載異常

在瀏覽器中推盛,可以通過(guò) window.addEventListener('error', callback) 的方式監(jiān)聽(tīng)資源加載異常,比如 js 或者 css 腳本文件丟失耘成。

window.addEventListener('error', (event) => {
  if (event.target instanceof HTMLElement) {
    const target = parseDom(event.target, ['src']);
    const detail = {
      target: target,
      path: parseXPath(target),
    }
    //  記錄埋點(diǎn)
  }
}, true)
復(fù)制代碼

監(jiān)聽(tīng)用戶行為

通過(guò) addEventListener click 監(jiān)聽(tīng) click 事件

window.addEventListener('click', (event) => {
    //記錄埋點(diǎn)
}, true)
復(fù)制代碼

在這里通過(guò)組件的 displaName 來(lái)定位元素的位置瘪菌,displaName 表示組件的文件目錄,比如 src/components/Form.js 文件導(dǎo)出的組件 FormItem 通過(guò) babel plugin 自動(dòng)添加屬性 @components/Form.FormItem诵肛,或者使用者主動(dòng)給組件添加 static 屬性 displayName默穴。

頁(yè)面路由變化

  • hashRouter

監(jiān)聽(tīng)頁(yè)面hash變化褪秀,對(duì)hash進(jìn)行解析

window.addEventListener('hashchange', event => {
  const { oldURL, newURL } = event;
  const oldURLObj = url.parseUrl(oldURL);
  const newURLObj = url.parseUrl(newURL);
  const from = oldURLObj.hash && url.parseHash(oldURLObj.hash);
  const to = newURLObj.hash && url.parseHash(newURLObj.hash);
  if(!from && !to ) return;
  // 記錄埋點(diǎn)
})
復(fù)制代碼

監(jiān)聽(tīng)頁(yè)面離開(kāi)

通過(guò) addEventListener beforeunload 監(jiān)聽(tīng)離開(kāi)頁(yè)面事件

window.addEventListener('beforeunload', (event) => {
    //記錄埋點(diǎn)
})
復(fù)制代碼

SDK 架構(gòu)

class Observable {
    constructor(observer) {
        observer(this.emit)
    }
    emit = (data) => {
        this.listeners.forEach(listener => {
            listener(data)
        })
    }
    listeners = [];

    subscribe = (listener) => {
        this.listeners.push(listeners);
        return () => {
            const index = this.listeners.indexOf(listener);
            if(index === -1) {
                return false
            }

            this.listeners.splice(index, 1);
            return true;
        }
     }
}
復(fù)制代碼
const clickObservable = new Observable((emit) => {
    window.addEventListener('click', emit)
})
復(fù)制代碼

然而在處理 ajax媒吗,需要將多種數(shù)據(jù)組合在一起乙埃,需要進(jìn)行 merg 操作,則顯得沒(méi)有那么優(yōu)雅,也很難適應(yīng)后續(xù)復(fù)雜的數(shù)據(jù)流的操作介袜。

const ajaxErrorObservable = new Observable((emit) => {
    window.addEventListener(AJAX_ERROR, emit)
})

const ajaxSuccessObservable = new Observable((emit) => {
    window.addEventListener(AJAX_SUCCESS, emit)
})
const ajaxTimeoutObservable = new Observable((emit) => {
    window.addEventListener(AJAX_TIMEOUT, emit)
})

復(fù)制代碼

可以選擇 RxJS 來(lái)優(yōu)化代碼

export const ajaxError$ = fromEvent(window, 'AJAX_ERROR', true)
export const ajaxSuccess$ = fromEvent(window, 'AJAX_SUCCESS', true)
export const ajaxTimeout$ = fromEvent(window, 'AJAX_TIMEOUT', true)
復(fù)制代碼
ajaxError$.pipe(
    merge(ajaxSuccess$, ajaxTimeout$), 
    map(data=> (data) => ({category: 'ajax', data; data}))
    subscribe(data => console.log(data))
復(fù)制代碼

通過(guò) merge, map 兩個(gè)操作符完成對(duì)數(shù)據(jù)的合并和處理遇伞。

數(shù)據(jù)流

項(xiàng)目結(jié)構(gòu)

  • core
    • event$ 數(shù)據(jù)流合并
    • snapshot 獲取當(dāng)前設(shè)備快照,例如urluserID叫确,router
    • track 埋點(diǎn)類,組合數(shù)據(jù)流和日志飞盆。
  • logger
    • logger 日志類
      • info
      • warn
      • debug
      • error
  • observable
    • ajax
    • beforeUpload
    • opeartion
    • routerChange
    • logger
    • track

參考

結(jié)尾

自建埋點(diǎn)系統(tǒng)是一個(gè)需要前后端一起合作的事情次乓,如果人力不足的情況下,建議使用第三方分析插件城看,例如 Sentry 就能足夠滿足大部分日常使用

但還是建議多了解杏慰,在第三方插件出現(xiàn)不能滿足業(yè)務(wù)需求的時(shí)候,可以頂上缘滥。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末朝扼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子擎颖,更是在濱河造成了極大的恐慌观游,老刑警劉巖肖抱,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件意述,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡荤崇,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門倚喂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)瓣戚,“玉大人,你說(shuō)我怎么就攤上這事舱权÷匦幔” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵鸵贬,是天一觀的道長(zhǎng)脖捻。 經(jīng)常有香客問(wèn)我,道長(zhǎng)地沮,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任周伦,我火速辦了婚禮未荒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘寨腔。我一直安慰自己,他們只是感情好迫卢,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布乾蛤。 她就那樣靜靜地躺著,像睡著了一般家卖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趴樱,一...
    開(kāi)封第一講書人閱讀 52,682評(píng)論 1 312
  • 那天酪捡,我揣著相機(jī)與錄音,去河邊找鬼捺疼。 笑死金刁,一個(gè)胖子當(dāng)著我的面吹牛议薪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斯议,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼哼御,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了恋昼?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤挟炬,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后婿滓,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凸主,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年卿吐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缩挑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谨湘,死狀恐怖芥丧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情续担,我是刑警寧澤,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布乖仇,位于F島的核電站询兴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏诗舰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一蜀铲、第九天 我趴在偏房一處隱蔽的房頂上張望属百。 院中可真熱鬧,春花似錦厌丑、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)睦番。三九已至耍属,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間厚骗,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工夫嗓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留冲秽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓排霉,卻偏偏與公主長(zhǎng)得像民轴,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子后裸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容