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

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

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

拋出問(wèn)題

我們知道線上環(huán)境復(fù)雜多變,不像本地測(cè)試的時(shí)候那樣順利,經(jīng)常會(huì)有各種雜七雜八的問(wèn)題,要想主動(dò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);

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

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

具體思路

收集什么

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

[
    { "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ǎn)單抽離一下它的基本結(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 這個(gè)單詞,心想說(shuō)這不是面包屑的嗎,怎么取這個(gè)名字。其實(shí)它本意就是有跡可循的意思,所以用在這里還是很恰當(dāng)?shù)?。??面包屑的起源:從前有兩個(gè)孩子在森林中走迷了路,為了找回家,他們?cè)诼飞仙⒙渲姘?,用以?biāo)記自己的行走路徑,最終成功回到家中。)

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

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

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

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

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

初始工作

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

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

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

收集 fetch 請(qǐng)求

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

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

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

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;
    });
}

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

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

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

收集點(diǎn)擊事件

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

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

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

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

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

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

這樣一來(lái)就基本能定位到是哪個(gè)元素了,不過(guò)需要一個(gè)小小的向上遞歸的過(guò)程??。

另一個(gè)問(wèn)題是如果這樣做會(huì)不會(huì)造成數(shù)據(jù)冗余并且消耗性能。em。。。確實(shí)如此,不過(guò)我們只需要簡(jiǎn)單限制下向上遞歸的次數(shù)就行了,比如四五次就 OK 了,不用一直遍歷到根元素,這里就簡(jiǎn)單貼個(gè)代碼實(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 來(lái)表示,舍棄遞歸

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

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

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

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

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

一些疑問(wèn)

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

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

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

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

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

小結(jié)

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

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

相關(guān)文章

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

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

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

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

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

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

    下面小編就為大家?guī)?lái)一篇深入理解JS DOM事件機(jī)制。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2016-08-08
  • JavaScript 對(duì)象模型 執(zhí)行模型

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

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

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

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

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

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

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

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

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

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

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

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

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

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

最新評(píng)論