欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

你知道該如何捕獲js報(bào)錯前的用戶行為嗎

 更新時間:2023年06月07日 11:31:42   作者:尤水就下  
這篇文章主要給大家介紹了該如何捕獲js報(bào)錯前的用戶行為的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下

拋出問題

我們知道線上環(huán)境復(fù)雜多變,不像本地測試的時候那樣順利,經(jīng)常會有各種雜七雜八的問題,要想主動高效的定位這些異常,就得接入監(jiān)控系統(tǒng)啦。作為前端的我們應(yīng)該或多或少都有所了解,大概就是監(jiān)聽各種 error 事件,然后整理下數(shù)據(jù)并上報(bào),比如下面這樣????:

const handleError = e => {
    // ...
    report();
};
const handleRejection = e => {
    // ...
    report();
};
window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', handleRejection);

但是有時候這些錯誤并不是那么直觀,也不好復(fù)現(xiàn)??,所以要是我們能夠捕獲到異常發(fā)生時的一些上下文信息就好了。??。。。那,這個上下文是指啥呢,讓我們先看看下面這張圖(參考自sentry):

從上圖中可以看出在發(fā)生報(bào)錯之前,用戶進(jìn)行了兩次頁面跳轉(zhuǎn),兩次 xhr 請求,并且進(jìn)行了幾次點(diǎn)擊操作。于是乎,我們就可以腦補(bǔ)出用戶大概的一個行為路徑了,這樣也許就能復(fù)現(xiàn) bug 了,聽起來是不是還有點(diǎn)意思??。所以本篇文章主要就是講解一下這個東西是怎么實(shí)現(xiàn)的。

具體思路

收集什么

因?yàn)閯傞_始很容易一頭霧水,所以我們先把上面的示意圖進(jìn)行一個簡單的轉(zhuǎn)化,它的本質(zhì)就是個普通的數(shù)組,只不過每條數(shù)據(jù)的類型會有所不同,就像下面這樣????:

[
    { "type": "dom", "timestamp": "2023-05-27T06:37:41.307522Z", "level": "info", "message": "a.product-thumbnail-item", "category": "ui.click", "data": null },
    { "type": "dom", "timestamp": "2023-05-27T06:37:41.692138Z", "level": "info", "message": null, "category": "navigation", "data": { "from": "/shop/", "to": "/shop/products/plant-mood-planter/" } },
    { "type": "dom", "timestamp": "2023-05-27T06:37:42.076753Z", "level": "info", "message": "button.add-to-cart", "category": "ui.click", "data": null },
    { "type": "http", "timestamp": "2023-05-27T06:37:42.461368Z", "level": "info", "message": null, "category": "xhr", "data": { "method": "POST", "status_code": 200, "url": "/api/0/cart/" } },
    { "type": "dom", "timestamp": "2023-05-27T06:37:42.845984Z", "level": "info", "message": "a#view-cart", "category": "ui.click", "data": null },
    { "type": "dom", "timestamp": "2023-05-27T06:37:43.230599Z", "level": "info", "message": null, "category": "navigation", "data": { "from": "/shop/products/plant-mood-planter/","to": "/shop/checkout/" } },
    { "type": "dom", "timestamp": "2023-05-27T06:37:43.615215Z", "level": "info", "message": "input#zipcode", "category": "ui.click", "data": null },
    { "type": "dom", "timestamp": "2023-05-27T06:37:43.999830Z", "level": "info", "message": "button#calculate-shipping", "category": "ui.click", "data": null },
    { "type": "http", "timestamp": "2023-05-27T06:37:44.384445Z", "level": "info", "message": null, "category": "xhr", "data": { "method": "POST", "status_code": 200, "url": "/api/0/cart/update-shipping/" } },
    { "type": "dom", "timestamp": "2023-05-27T06:37:44.769061Z", "level": "info", "message": "input#card-name", "category": "ui.click", "data": null },
    { "type": "dom", "timestamp": "2023-05-27T06:37:45.153677Z", "level": "info", "message": "input#card-number", "category": "ui.click", "data": null },
    { "type": "dom", "timestamp": "2023-05-27T06:37:45.538292Z", "level": "info", "message": "input#card-cvv", "category": "ui.click", "data": null },
    { "type": "dom", "timestamp": "2023-05-27T06:37:45.922907Z", "level": "info", "message": "input#submit", "category": "ui.click", "data": null }
]

簡單抽離一下它的基本結(jié)構(gòu),大致如下:

interface Breadcrumb {
  type?: string; // 類型,比如 dom,http
  category?: string; // 具體分類,比如 dom 下面的 click 和 navigation;http 中的 xhr 和 fetch
  message?: string;
  data?: { [key: string]: any };
  level?: string;
  timestamp?: number;
}

有同學(xué)看到 Breadcrumb 這個單詞,心想說這不是面包屑的嗎,怎么取這個名字。其實(shí)它本意就是有跡可循的意思,所以用在這里還是很恰當(dāng)?shù)摹#??面包屑的起源:從前有兩個孩子在森林中走迷了路,為了找回家,他們在路上散落著面包屑,用以標(biāo)記自己的行走路徑,最終成功回到家中。)

可以看到,每條數(shù)據(jù)大體會有類型、數(shù)據(jù)、時間戳等幾個重要的部分組成,顯然不同的類型會對應(yīng)不同的數(shù)據(jù),所以我們只需要知道有哪些類型并記錄相關(guān)信息即可。那怎么確定有哪些類型呢?

想想我們?nèi)绻烙脩魣?bào)錯前的一些信息,肯定不能在報(bào)錯的時候才去記錄,那時候已經(jīng)晚了,并且我們也不確定什么時候會報(bào)錯。所以...所以在一開始就得進(jìn)行收集??,如果報(bào)錯了就把一路以來收集到的信息上報(bào),如果沒報(bào)錯那就不管,這是要先明確的一點(diǎn)。

然后就是確定要收集哪些類型以及怎么收集。這個乍一看也沒什么思緒,好像還得上錄屏。事實(shí)上沒這么麻煩,我們可以先想想一般什么情況下做什么操作會導(dǎo)致 js 報(bào)錯??。經(jīng)過幾秒鐘短暫的思考后,大概可以羅列出以下幾種情況:

  • 頁面跳轉(zhuǎn)
  • 接口調(diào)用后
  • 點(diǎn)擊某個按鈕
  • 鍵盤按下時
  • console 打印的信息
  • 定時器
  • ...(用戶行為 && 瀏覽器行為 && 控制臺行為)

而其中導(dǎo)致報(bào)錯概率最大的主要就是發(fā)送請求和點(diǎn)擊事件,所以接下來會以這兩種情況為例子來看看我們是怎么進(jìn)行收集的??。

初始工作

在此之前,我們先定義一個全局變量,順便簡化一下 interface,就像下面這樣????:

interface Breadcrumb {
  type?: string; // 類型,比如 fetch、click
  data?: { [key: string]: any }; // 類型對應(yīng)的數(shù)據(jù)
  timestamp?: number; // 觸發(fā)時間
}
const breadcrumbs = []; // 所有行為路徑
function addBreadcrumb(breadcrumb) {
    breadcrumbs.push(breadcrumb);
}

之后想要收集的時候只要調(diào)用 addBreadcrumb 方法往 breadcrumbspush 一條條記錄就好啦。

收集 fetch 請求

這里我們就先以收集請求為例進(jìn)行解釋說明??墒墙涌谡埱竽敲炊?,要是在每個接口都手動加上收集的邏輯會很繁瑣,所以就需要自動的對每個請求進(jìn)行處理(有點(diǎn)類似手動埋點(diǎn)和全自動埋點(diǎn))。那怎么進(jìn)行全量處理并且無感知嘞,就是函數(shù)劫持啦(也可以叫 AOP,面向切面編程),前端最常用的魔改手段之一(此招一出,手動變自動)。通常發(fā)送請求有 xhr 和 fetch 兩種 api,這里我們以 fetch 舉例來看看基本的函數(shù)劫持寫法:

const _fetch = window.fetch; // 緩存原來的方法
window.fetch = function(url, options) {
    // 發(fā)送請求前可以做點(diǎn)事
    const result =  _fetch.call(this, url, options); // 執(zhí)行原來的請求邏輯
    // 發(fā)送請求后可以做點(diǎn)事
    return result;
}

想想我們發(fā)送請求的時候需要記錄什么信息呢???好像主要就幾個:接口地址、請求方法、狀態(tài)碼和請求時間?那就先簡單記錄一下它們,就像下面這樣????:

const _fetch = window.fetch;
window.fetch = function(url, options) {
    const breadcrumb = {
        type: 'fetch',
        data: {
            url,
            method: options.method || 'GET',
            startTimestamp: Date.now()
        }
    };
    return _fetch.call(this, url, options).then(response => {
        breadcrumb.data.response = response;
        breadcrumb.data.endTimestamp = Date.now();
        addBreadcrumb(breadcrumb);
        return response;
    }, error => {
        breadcrumb.data.error = error;
        breadcrumb.data.endTimestamp = Date.now();
        addBreadcrumb(breadcrumb);
        throw error;
    });
}

注意到上面的代碼中,我們是在請求返回的時候才添加一條 fetch 面包屑數(shù)據(jù),這是因?yàn)槲覀冃枰故窘涌诘臓顟B(tài)碼或錯誤碼,以及如果我們希望增加一些自定義參數(shù),比如接口中一般會有個 logid 方便后端排查問題,也可以將其帶上,而這些數(shù)據(jù)在發(fā)送請求前是木有的。

此外我們還注意到這里順便記錄了請求發(fā)起和返回的的時間戳,但這并不是添加面包屑的時間戳(雖然和請求返回的時間差不多),并且面包屑的時間戳是每條記錄都有的,所以可以把時間戳的邏輯放在通用方法 addBreadcrumb 里面,就像下面這樣????:

const breadcrumbs = [];
function addBreadcrumb(breadcrumb) {
    breadcrumb.timestamp || (breadcrumb.timestamp = Date.now());
    breadcrumbs.push(breadcrumb);
}

收集點(diǎn)擊事件

有了上面的基本實(shí)踐,接下來我們說說如何記錄點(diǎn)擊事件,看起來也是直接劫持魔改 EventTarget.prototype.addEventListener 這個 api???這當(dāng)然是沒問題的。不過有個更方便的方法,就是利用點(diǎn)擊事件的特殊性,我們直接在 document 上進(jìn)行全局監(jiān)聽即可,一行代碼就能輕松搞定,比如這樣:

document.addEventListener('click', e => {
    console.log(e.target.tagName);
});

這樣一來所有點(diǎn)擊事件都會冒泡到 document 上,也就是所有點(diǎn)擊事件都會觸發(fā)上面那段代碼,接下來只要在回調(diào)里面加上需要的面包屑邏輯即可。那點(diǎn)擊事件需要記錄什么信息呢?好像只需要知道點(diǎn)擊哪個元素就可以了??,沒錯,確實(shí)是這樣。那怎么標(biāo)識這個元素呢,除了基本的標(biāo)簽名外,我們還需要去獲取點(diǎn)擊元素的 class 樣式名(當(dāng)然 id 和屬性選擇器也是要的,這里就是舉個例子),就像下面這樣:

document.addEventListener('click', e => {
    const { tagName, className } = e.target;
    const breadcrumb = {
        type: 'click',
        data: {
            selector: `${tagName.toLowerCase()}.${className.split(' ').join('.')}` // 點(diǎn)擊元素的格式大概長這樣:'tagName#id.class',比如 'button.submit'
        }
    };
    breadcrumbs.push(breadcrumb);
});

這時候就會出現(xiàn)兩個問題,一個問題是有的元素沒有 class 或者同一個 class 的元素有很多個,那也就無法定位出具體是哪個元素被點(diǎn)擊了,為此我們需要把該元素的父元素也記錄下來,然后拼成下面這個樣子:

body > div#app > div.box > ul > li.row.active

這樣一來就基本能定位到是哪個元素了,不過需要一個小小的向上遞歸的過程??。

另一個問題是如果這樣做會不會造成數(shù)據(jù)冗余并且消耗性能。em。。。確實(shí)如此,不過我們只需要簡單限制下向上遞歸的次數(shù)就行了,比如四五次就 OK 了,不用一直遍歷到根元素,這里就簡單貼個代碼實(shí)現(xiàn)(直接 copy 下面的代碼到瀏覽器運(yùn)行就能看到效果??):

function getXpath(ele) {
  const pathArr = [];

  function helper(ele, depth = 5) {
    if (!ele || depth < 1) return;
    const { tagName, className } = ele;
    const selector = `${tagName.toLowerCase()}.${className.split(' ').join('.')}`;
    pathArr.push(selector);

    helper(ele.parentNode, depth - 1);
  }
  
  try {
    helper(ele);
    return pathArr.reverse().join(' > ');
  } catch(e) {
    return '<unknown>'
  }
}
// 如果不想這么麻煩,也可以直接用 element.outerHTML 來表示,舍棄遞歸

不過這樣還是會有問題,比如我們點(diǎn)擊了某個按鈕,按鈕自身阻止了冒泡怎么辦,我們就捕獲不到這個按鈕的點(diǎn)擊事件了。要解決這個問題很簡單,就是將 addEventListener 的第三個參數(shù)設(shè)置為 true 就行了,就像下面這樣????:

document.addEventListener('click', e => {}, true);

就這樣簡單的一個操作我們就把冒泡的過程改成了捕獲,也就是所有點(diǎn)擊事件都會先觸發(fā)我們的回調(diào)。
此外我們還可以做一些優(yōu)化,比如多次點(diǎn)擊可以簡單節(jié)個流,包個 throttle 函數(shù)即可;點(diǎn)擊空白處或者點(diǎn)擊了某個元素但不觸發(fā)事件的(比如純文本)也不進(jìn)行處理,但這個還是比較難辦的,即便可以通過 getEventListeners(ele) 這個方法來判斷某個元素有沒有綁定事件,但是這并不好用,比如我們點(diǎn)擊按鈕內(nèi)部的元素也可以觸發(fā)事件,但是內(nèi)部元素并沒有綁定事件。

小知識:事件的傳播通常有三個階段:捕獲階段、目標(biāo)階段、冒泡階段。在這三個階段中,事件傳播時所攜帶的信息都是相同的,也就是 event 是相同的。

如果你用的是劫持的方式來處理點(diǎn)擊事件你就要注意,同一個事件可能會被出發(fā)多次,所以需要用一個變量保存最近一次觸發(fā)的事件 lastCapturedEvent,然后和當(dāng)前 event 做對比,如果一樣就說明是同樣的事件源,可以跳過。
至此,我們已經(jīng)簡單過了一下兩種面包屑的實(shí)現(xiàn),至于其他情況寫法大同小異,都是對相應(yīng)的 api 進(jìn)行劫持,只是對應(yīng)的數(shù)據(jù)不同罷了,比如頁面跳轉(zhuǎn)的 data 就是 { from, to } 即可。最后我們只需要在發(fā)生報(bào)錯的時候,把當(dāng)前的 breadcrumbs 一起上報(bào)并做個簡單的可視化就行了??。

一些疑問

  • 我可以對面包屑做一些自定義操作嗎?當(dāng)然,我們只需要在 push 或者上報(bào)的時候加個鉤子即可,就像下面這樣????:

    const breadcrumbs = [];
    const beforeAddBreadcrumb = breadcrumb => {
        // do something
    }
    function addBreadcrumb(breadcrumb) {
        breadcrumb.timestamp || (breadcrumb.timestamp = Date.now())
        beforeAddBreadcrumb(breadcrumb);
        breadcrumbs.push(breadcrumb);
    }

    通常在一些需要過濾敏感數(shù)據(jù)的情況下,beforeAddBreadcrumb 這個鉤子就顯得尤為重要,比如海外業(yè)務(wù)。

  • 面包屑數(shù)據(jù)量不會太大嗎? em。。。確實(shí)是會這樣,畢竟我們從頭記到尾,所以可以控制一下 breadcrumbs 的長度,比如控制在 20 條以內(nèi),超出了就把頭部元素刪了,只留下最近的即可;另外還可以控制一下點(diǎn)擊元素的 selector 的長度,當(dāng) xpath 過長時也不繼續(xù)遞歸了。

  • 我。。??梢杂娩浧羻???當(dāng)然,錄屏相對面包屑的腦補(bǔ)畫面來說肯定更加直觀,但是錄屏的成本和大小都遠(yuǎn)高于面包屑,有條件當(dāng)然是允許錄屏的(比如 sentry 會通過 rrweb 提供這個功能)。不過錄屏一般用在保險、審核這種需要留存記錄和證據(jù)的地方,對于監(jiān)控來說倒不是剛需。

小結(jié)

通過本文的簡單介紹,想必你對怎么捕獲報(bào)錯發(fā)生前的行為應(yīng)該有所了解??。當(dāng)然了,這里還得再強(qiáng)調(diào)一下,這個面包屑只是當(dāng)你對報(bào)錯沒有什么頭緒的時候提供一個有跡可循的思路而已,它不是必需品,僅僅是個輔助。

到此這篇關(guān)于該如何捕獲js報(bào)錯前的用戶行為的文章就介紹到這了,更多相關(guān)js報(bào)錯前用戶行為捕獲內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • js實(shí)現(xiàn)搜索欄效果

    js實(shí)現(xiàn)搜索欄效果

    這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)搜索欄效果,以及焦點(diǎn)問題的解決,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-11-11
  • 深入淺析JavaScript系列(13):This? Yes,this!

    深入淺析JavaScript系列(13):This? Yes,this!

    在這篇文章里,我們將討論跟執(zhí)行上下文直接相關(guān)的更多細(xì)節(jié)。討論的主題就是this關(guān)鍵字。實(shí)踐證明,這個主題很難,在不同執(zhí)行上下文中this的確定經(jīng)常會發(fā)生問題
    2016-01-01
  • 深入理解JS DOM事件機(jī)制

    深入理解JS DOM事件機(jī)制

    下面小編就為大家?guī)硪黄钊肜斫釰S DOM事件機(jī)制。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-08-08
  • JavaScript 對象模型 執(zhí)行模型

    JavaScript 對象模型 執(zhí)行模型

    簡單數(shù)值類型: 有Undefined, Null, Boolean, Number和String。注意,描述中的英文單詞在這里僅指數(shù)據(jù)類型的名稱,并不特指JS的全局對象N an, Boolean, Number, String等,它們在概念上的區(qū)別是比較大的。
    2010-10-10
  • 微信小程序全局狀態(tài)的深入講解

    微信小程序全局狀態(tài)的深入講解

    這篇文章主要介紹了微信小程序全局狀態(tài)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-03-03
  • 微信小程序骨架屏的實(shí)現(xiàn)示例

    微信小程序骨架屏的實(shí)現(xiàn)示例

    本文主要介紹了微信小程序骨架屏的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-10-10
  • Javascript調(diào)試之console對象——你不知道的一些小技巧

    Javascript調(diào)試之console對象——你不知道的一些小技巧

    這篇文章主要總結(jié)了console對象的一些有用的方法,非常不錯,具有參考借鑒價值,需要的朋友參考下吧
    2017-07-07
  • JavaScript如何實(shí)現(xiàn)數(shù)組按屬性分組

    JavaScript如何實(shí)現(xiàn)數(shù)組按屬性分組

    在JavaScript中,有多種方法可以對數(shù)組按屬性進(jìn)行分組,這篇文章主要為大家至少介紹了6種常見的方法,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-08-08
  • 動態(tài)為事件添加js代碼示例

    動態(tài)為事件添加js代碼示例

    動態(tài)添加事件的實(shí)現(xiàn)代碼
    2009-02-02
  • javascript獲取重復(fù)次數(shù)最多的字符

    javascript獲取重復(fù)次數(shù)最多的字符

    本文給大家講述的是使用javascript實(shí)現(xiàn)獲取重復(fù)次數(shù)最多的字符,代碼很簡單,有需要的小伙伴可以參考下。
    2015-07-07

最新評論