<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          還原現(xiàn)場——前端錄制用戶行為技術(shù)方案

          共 11261字,需瀏覽 23分鐘

           ·

          2023-10-16 20:52

          大廠技術(shù)  高級前端  Node進階

          點擊上方 程序員成長指北,關(guān)注公眾號

          回復(fù)1,加入高級Node交流群

          一、問題背景

          目前,在我們的項目中通常會使用各種各樣的埋點和監(jiān)控來收集頁面訪問的信息,例如點擊埋點、PV埋點等,這些埋點數(shù)據(jù)能夠反應(yīng)絕大部分的用戶行為,但是對于一些關(guān)注上下文的使用場景而言這些埋點是不夠的。

          點擊下方“前端圖形”,選擇“設(shè)為星標(biāo)

          第一時間關(guān)注技術(shù)干貨!


          • 對于產(chǎn)品而言,通過點擊或PV埋點來判斷功能的使用情況有時候不夠的,通常需要知道用戶的真實使用路徑以判斷用戶使用是否與功能設(shè)計所預(yù)想的保持一致,從而能夠更好地分析用戶使用情況并進一步優(yōu)化和推廣。

          • 對于開發(fā)而言,當(dāng)我們收到系統(tǒng)異常通知的時候,監(jiān)控系統(tǒng)只能告訴你系統(tǒng)出現(xiàn)了錯誤,但是不能給出錯誤的復(fù)現(xiàn)路徑,對于穩(wěn)定復(fù)現(xiàn)的錯誤而言還好,但對于偶發(fā)錯誤或復(fù)現(xiàn)路徑隱藏較深的場景我們就較難去解決問題。

          • 對于測試而言,用戶反饋線上bug的時候,首先需要知道的就是通過什么操作觸發(fā)了這個問題,有時候用戶自己可能也無法二次復(fù)現(xiàn),這樣的錯誤我們也無法通過埋點的數(shù)據(jù)去推導(dǎo)上下文,因此就會產(chǎn)生較大的溝通成本,在執(zhí)行測試計劃反饋 bug 時同理。

          • ......

          因此,我們需要一種手段來獲取用戶某一時段連續(xù)的操作行為,也就是錄制用戶行為,包括整個會話中的每一個點擊、滑動、輸入等行為,同時支持回放錄制的操作行為,完整且真實地重現(xiàn)用戶行為以幫助我們回溯或分析某些使用場景。

          二、技術(shù)方案

          2.1 視頻錄制

          錄制用戶行為最容易想到的就是將屏幕操作通過視頻的方式錄制下來,目前瀏覽器本身已經(jīng)提供了一套基于音視軌的實時數(shù)據(jù)流傳輸方案 WebRTC(Web Real-Time Communications),在我們的錄屏使用場景主要關(guān)注以下幾個 API:

          • getDisplayMedia() - 提示用戶給予使用媒體輸入的許可從而獲取屏幕的流;

          • MediaRecorder() - 生成對指定的媒體流進行錄制的 MediaRecorder 對象;

          • ondataavailable - 當(dāng) MediaRecorder 將媒體數(shù)據(jù)傳遞到應(yīng)用程序以供使用時將觸發(fā)該事件;

          整體錄制流程如下:

          1. 調(diào)用mediaDevices.getDisplayMedia()由用戶授權(quán)選擇屏幕進行錄制,獲取到數(shù)據(jù)流;

          2. 生成一個new MediaRecorder()對象錄制獲取的屏幕的數(shù)據(jù)流;

          3. 在 MediaRecorder 對象上設(shè)置ondataavailable監(jiān)聽事件用于獲取錄制的 Blob 數(shù)據(jù)。

          html

          復(fù)制代碼

          <template>
          <video ref="playerRef"></video>
          <button @click="handleStart">開啟錄制</button>
          <button @click="handlePause">暫停錄制</button>
          <button @click="handleResume">繼續(xù)錄制</button>
          <button @click="handleStop">結(jié)束錄制</button>
          <button @click="handleReplay">播放錄制</button>
          <button @click="handleReset">重置內(nèi)容</button>
          </template>

          <script lang="ts" setup>
          import { ref, reactive } from 'vue';

          const playerRef = ref();
          const state = reactive({
          mediaRecorder: null as null | MediaRecorder,
          blobs: [] as Blob[],
          });

          // 開始錄制
          const handleStart = async () => {
          const stream = await navigator.mediaDevices.getDisplayMedia();
          state.mediaRecorder = new MediaRecorder(stream, {
          mimeType: 'video/webm',
          });
          state.mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => {
          state.blobs.push(e.data);
          });
          state.mediaRecorder?.start();
          };
          // canvas錄制(特殊處理)
          const handleCanvasRecord = () => {
          const stream = canvas.captureStream(60); // 60 FPS recording
          const recorder = new MediaRecorder(stream, {
          mimeType: 'video/webm;codecs=vp9',
          });
          recorder.ondataavailable = (e) => {
          state.blobs.push(e.data);
          };
          }
          // 暫停錄制
          const handlePause = () => { state.mediaRecorder?.pause() };
          // 繼續(xù)錄制
          const handleResume = () => { state.mediaRecorder?.resume() };
          // 停止錄制
          const handleStop = () => { state.mediaRecorder?.stop() };
          // 播放錄制
          const handleReplay = () => {
          if (state.blobs.length === 0 || !playerRef.value) return;
          const blob = new Blob(state.blobs, { type: 'video/webm' });
          playerRef.value.src = URL.createObjectURL(blob);
          playerRef.value.play();
          };

          const handleReset = () => {
          state.blobs = [];
          state.mediaRecorder = null;
          playerRef.value.src = null;
          };
          const handleDownload = () => {
          if (state.blobs.length === 0) return;
          const blob = new Blob(state.blobs, { type: 'video/webm' });
          const url = URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = url;
          a.style.display = 'none';
          a.download = 'record.webm';
          a.click();
          };
          </script>

          盡管瀏覽器原生提供了這樣既簡單又實用的屏幕錄制解決方案,但在我們實際應(yīng)用場景中仍舊有非常多的問題:

          1. 由用戶感知并控制:通過 WebRTC 提供的 API 所實現(xiàn)的用戶行為錄制在開始錄制前會通過彈窗來讓用戶完成對所需錄制屏幕的授權(quán),所有的錄制行為均由用戶自主控制,這種讓用戶感知到系統(tǒng)錄制的方式對于我們預(yù)期的使用而言是不合適的,我們預(yù)期的錄制行為對于用戶而言應(yīng)該是無感的,這種技術(shù)方案更適用于類似啄木鳥這種反饋系統(tǒng)由用戶主動上報問題的場景或者考試系統(tǒng)屏幕監(jiān)控、在線面試屏幕共享等等。

          2. 錄制數(shù)據(jù)無法脫敏:視頻錄制過程中直接就將整個頁面的內(nèi)容錄制下來,對于一些敏感的數(shù)據(jù)同樣也會直接錄制下來,在錄制的過程中我們無法進行脫敏,這對于一些數(shù)據(jù)安全要求比較高或者涉及用戶隱私的場景就不適用了。

          3. WebRTC 兼容性:在實現(xiàn)錄制過程中使用的幾個 WebRTC API 都具有一定的兼容性要求,不同的瀏覽器的支持情況各不相同,具體可進行相應(yīng)的兼容性查詢。

          2.2 頁面截圖

          眾所周知,視頻是由一幀幀的畫面組合而成的,因此我們可以按照一定時間間隔來截圖的方式保存當(dāng)前頁面快照,然后將快照按照相同的截取速度播放形成視頻就能實現(xiàn)用戶行為錄制了。最常用的截圖方法就是以 html2canvas 庫為代表的 canvas 截圖,我們在使用過程中也發(fā)現(xiàn)了較多問題:

          1. canvas 截圖有較多局限之處,例如無法繪制動畫、樣式錯位、不支持部分CSS樣式等;

          2. 截圖性能開銷較大,可能會導(dǎo)致掉幀,例如我們在嘗試中 css 動畫有非常明顯的卡頓等;

          3. 截圖資源體積大,我們嘗試中截圖時單張圖片體積為200k左右,以24幀來算一分鐘錄制的圖片體積將近300MB,對帶寬和資源存儲都是浪費;

          4. 在需要忽略的元素上增加 data-html2canvas-ignore 屬性或者設(shè)置 ignoreElements 屬性刪除特定元素可以對某些特定數(shù)據(jù)或內(nèi)容進行脫敏,但會直接刪除元素?zé)o法做到“有占位但無內(nèi)容”效果,影響頁面布局。

          html

          復(fù)制代碼

          <template>
          <el-button @click="handleStart">開啟錄制</el-button>
          <el-button @click="handleStop">停止錄制</el-button>
          <el-button @click="handleReplay">播放錄制</el-button>
          <img :src="state.imgs[state.num ?? 0]" />
          </template>

          <script lang="ts" setup>
          import { reactive } from 'vue';
          import html2canvas from 'html2canvas';

          const state = reactive({
          visible: false,
          imgs: [] as string[],
          num: 0,
          recordInterval: null as any,
          replayInterval: null as any,
          });

          const FPS = 30;
          const interval = 1000 / FPS;
          const handleStart = async () => {
          handleReset();
          state.recordInterval = setInterval(() => {
          if (state.imgs.length > 100) {
          handleStop();
          return;
          }
          html2canvas(document.body).then((canvas: any) => {
          const img = canvas.toDataURL();
          state.imgs.push(img);
          });
          }, interval);
          };

          const handleStop = () => {
          state.recordInterval && clearInterval(state.recordInterval);
          };

          const handleReplay = async () => {
          state.recordInterval && clearInterval(state.recordInterval);
          state.num = 0;
          state.visible = true;
          state.replayInterval = setInterval(() => {
          if (state.num >= state.imgs.length - 1) {
          clearInterval(state.replayInterval);
          return;
          }
          state.num++;
          }, interval);
          };

          const handleReset = () => {
          state.imgs = [];
          state.recordInterval = null;
          state.replayInterval = null;
          state.num = 0;
          };
          </script>
          實際內(nèi)容 截圖效果

          2.3 Dom 快照錄制

          每一個瞬間我們看到的頁面都是瀏覽器當(dāng)前渲染的 DOM節(jié)點,那么我們完全可以將 DOM 節(jié)點保存下來,并持續(xù)記錄 DOM 節(jié)點的變化,然后再將記錄的 DOM 節(jié)點數(shù)據(jù)通過瀏覽器渲染回放,這樣即可實現(xiàn)用戶行為錄制的需求。整個思路非常簡單,但具體實現(xiàn)起來是非常復(fù)雜的事情,我們需要考慮 DOM 節(jié)點數(shù)據(jù)如何保存、如何捕獲用戶行為并記錄 DOM 節(jié)點變換和如何將記錄的數(shù)據(jù)在瀏覽器上回放出來等。所幸當(dāng)前社區(qū)已經(jīng)有非常成熟的庫,也就是 rrweb(record and replay the web)??

          rrweb 主要由 3 部分組成:

          1. rrweb-snapshot,包含 snapshot 和 rebuild 兩部分,snapshot 用于將 DOM 及其狀態(tài)轉(zhuǎn)化為可序列化的數(shù)據(jù)結(jié)構(gòu)并添加唯一標(biāo)識,rebuild 是將 snapshot 記錄的數(shù)據(jù)結(jié)構(gòu)重建為對應(yīng) DOM。

          2. rrweb,包含 record 和 replay 兩個功能,record 用于記錄 DOM 中的所有變更,replay 則是將記錄的變更按照對應(yīng)的時間一一重放。

          3. rrweb-player,為 rrweb 提供一套 UI 控件,提供基于 GUI 的暫停、快進、拖拽至任意時間點播放等功能。

          錄制過程

          rrweb 在錄制時會首先進行首屏 DOM 快照,遍歷整個頁面的 DOM Tree 并通過 nodeType 映射轉(zhuǎn)換為 JSON 結(jié)構(gòu)數(shù)據(jù)。針對不同 nodeType 類型的節(jié)點類型的序列化操作具有非常多的細節(jié),如想了解細節(jié)可閱讀這部分源碼。全量快照數(shù)據(jù)示例如下:

          在獲取首屏全量快照之后,我們就需要監(jiān)聽各類變動以獲取增量的數(shù)據(jù),增量改變的數(shù)據(jù)也需要同步轉(zhuǎn)換為 JSON 數(shù)據(jù)進行存儲。對于增量數(shù)據(jù)更新,則是通過 mutationObserver 獲取 DOM 增量變化,以及通過全局事件監(jiān)聽、事件(屬性)代理的方式進行方法(屬性)劫持,并將劫持到的增量變化數(shù)據(jù)存入 JSON 數(shù)據(jù)中。針對不同類型的變動有這不同的監(jiān)聽處理方式,如想了解細節(jié)可閱讀這部分源碼。

          回放過程

          回放主要就是將錄制過程中的全量快照和增量快照進行重建復(fù)原,那么為保證一個安全可靠的環(huán)境在回放時我們不應(yīng)該執(zhí)行被錄制頁面中的 JavaScript,在重建快照的過程中通過 script 標(biāo)簽改寫為 noscript 標(biāo)簽和 dom 重建在 iframe 中并設(shè)置 sandbox 屬性等手段來構(gòu)建安全可靠的沙盒環(huán)境。在沙盒環(huán)境中,首先需要對首屏 DOM 快照進行重建,遍歷 JSON 產(chǎn)物的同時通過自定義 type 映射到不同的節(jié)點構(gòu)建方法以重建首屏 DOM 結(jié)構(gòu),然后 rrweb 內(nèi)部則根據(jù)不同的增量類型調(diào)用不同的函數(shù)在頁面對增量數(shù)據(jù)進行展現(xiàn)。同時,播放時通過錄制產(chǎn)生的時間戳來保證回放順序,通過 Node id 作用至指定的 DOM 節(jié)點,通過 requestAnimationFrame 保證頁面播放流暢度。

          rrweb 實現(xiàn)原理可參考以下相關(guān)資料以及源碼/官方文檔:

          • rrweb:打開 web 頁面錄制與回放的黑盒子 - 知乎

          • 神策數(shù)據(jù)王磊:如何用 JS 實現(xiàn)頁面錄制與回放 - 掘金

          • rrweb錄屏原理淺析 - 掘金

          • rrweb 實現(xiàn)原理介紹 - 微信公眾號

          • rrweb 帶你還原問題現(xiàn)場 - 云音樂大前端

          html

          復(fù)制代碼

          <template>
          <button @click="handleStart">開啟錄制</button>
          <button @click="handleStop">結(jié)束錄制</button>
          <button @click="handleReplay">播放錄制</button>
          <div class="replay" ref="replayRef"></div>
          </template>

          <script lang="ts" setup>
          import { reactive, ref } from 'vue';
          import * as rrweb from 'rrweb';
          import rrwebPlayer from 'rrweb-player';
          import 'rrweb-player/dist/style.css';

          const replayRef = ref();
          const state = reactive({
          events: [] as any[],
          stopFn: null as any,
          });

          const handleStart = () => {
          state.stopFn = rrweb.record({
          emit(event) {
          if (state.events.length > 100) {
          // 當(dāng)事件數(shù)量大于 100 時停止錄制
          handleStop();
          } else {
          state.events.push(event);
          }
          },
          });
          ElMessage('開始錄制');
          };

          const handleStop = () => {
          state.stopFn?.();
          ElMessage('已停止錄制');
          };

          const handleReplay = () => {
          new rrwebPlayer({
          target: replayRef.value, // 可以自定義 DOM 元素
          // 配置項
          props: {
          events: state.events,
          },
          });
          };
          </script>



          從效果上來講,rrweb 錄制內(nèi)容存儲了完整的頁面結(jié)構(gòu)能夠較好地還原頁面的整個操作,并且 rrweb 錄制無損錄制具有較好的清晰度,不像視頻錄制和頁面截圖需要考慮分辨率與產(chǎn)物大小的問題,同時也不像 canvas 截圖一樣對內(nèi)容和樣式有較大的局限性使得部分頁面內(nèi)容無法錄制。

          從性能上來講,rrweb 錄制傳輸?shù)膬?nèi)容為 JSON 數(shù)據(jù)并且只對用戶操作內(nèi)容作增量記錄,當(dāng)頁面靜默的時候不會有額外數(shù)據(jù)的記錄,相比較視頻錄制和頁面截圖而言大大減少了最終產(chǎn)物的體積,減輕了數(shù)據(jù)傳輸?shù)膲毫?,同時也提高了錄制的性能。

          從功能上來講,rrweb 除了基礎(chǔ)的錄制回放功能之外,還具有較好的可擴展性和可操作性:

          • rrweb 錄制得到的產(chǎn)物是 JSON 數(shù)據(jù),還支持將 JSON 數(shù)據(jù)轉(zhuǎn)成視頻,工具rrvideo

          • 其允許通過配置來進行數(shù)據(jù)脫敏,不錄制某些元素或屏蔽某些事件,詳見鏈接

          • 其還提供了存儲優(yōu)化的策略以減少錄制的數(shù)據(jù)量,詳見鏈接

          • 其還支持自定義插件來進行擴展,詳見鏈接

          2.4 方案對比

          對比內(nèi)容 視頻錄制 頁面截圖 Dom 快照錄制
          開源庫 WebRTC 原生支持 html2canvas rrweb
          用戶感知 錄制有感 錄制無感 錄制無感
          產(chǎn)物大小 相對較小
          兼容性 詳見相關(guān) API 兼容性 部分場景內(nèi)容截圖無法顯示 兼容性相對較好
          可操作性 強(支持?jǐn)?shù)據(jù)脫敏/加密等)
          回放清晰度 錄制時決定,有損錄制 錄制時決定,有損錄制 高保真

          ?? Dom快照錄制 - rrweb 庫 是目前最為流行的解決方案,一些商業(yè)化平臺解決方案也都主要基于 rrweb 庫來進行錄制與回放的功能開發(fā)。但是,方案的選擇不是絕對的,在不同的使用場景下選擇合適的方案才是最重要的哦 (^_-)

          三、應(yīng)用場景

          在獲取頁面的錄屏內(nèi)容之后,這只是第一步,更重要的是我們能夠利用這些錄屏信息獲取到什么信息,分析出什么內(nèi)容?

          # 應(yīng)用場景 說明
          1 產(chǎn)品功能分析 產(chǎn)品在功能的上線后僅通過點擊或PV埋點來判斷使用情況是不夠的,更應(yīng)該關(guān)注于一些關(guān)鍵頁面/功能的真實使用場景,通過用戶行為錄制將用戶的操作路徑記錄下來,通過回放種子用戶的操作進行分析或利用算法對使用路徑進行分析能夠更好了解功能設(shè)計的情況,并幫助進一步優(yōu)化。
          2 用戶訪談記錄 產(chǎn)品在對用戶進行訪談時通過整理用戶口述記錄和回放訪談錄音等方式來進行分析和研究,整體訪談的成本較高,信息利用率也較低,而通過用戶行為錄制記錄下來訪談過程中用戶真實的操作記錄能夠更好地幫助產(chǎn)品來回顧訪談用戶的操作習(xí)慣。
          3 問題現(xiàn)場復(fù)現(xiàn) 解決問題第一步就需要復(fù)現(xiàn)問題,但有時候問題的復(fù)現(xiàn)操作是極其隱蔽的或者由于用戶的使用環(huán)境等因素很難定位,通過錄制用戶行為來保存報錯時刻的上下文使得我們能夠更好地了解用戶報錯的操作,最大程度減少溝通和內(nèi)容傳遞的成本。
          4 自動化測試用例 通常自動化測試用例的編寫和維護都由人工手動來完成,成本相對比較高,后期維護也不方便,通過用戶行為錄制將錄制的數(shù)據(jù)加以轉(zhuǎn)換就可以更加快捷方便地進行測試用例的采集,同時也便于管理。
          5 其他 還有案例復(fù)盤、行為監(jiān)控/監(jiān)管、業(yè)務(wù)流程質(zhì)檢...

          四、平臺方案

          Sentry 平臺 提供了錄制與回放功能用于進行分析,其應(yīng)用重點在于查看錯誤或性能問題發(fā)生之前、期間和之后的操作情況,其錄制與回放功能同樣基于 rrweb 庫進行開發(fā)。

          Hotjar 平臺提供了錄制與回放功能用于進行分析,其除了錄制與回放功能外,其提供了頁面熱力圖等分析能力,以幫助用戶更好地了解產(chǎn)品的情況,官方也提供了 Demo 可體驗 。

          其他相關(guān)的商業(yè)化平臺還有LogRocket、FullStory、marker.io等等。

          五、總結(jié)

          目前,用戶行為錄制已經(jīng)在各個場景中廣泛使用,例如用戶調(diào)研、產(chǎn)品分析、Bug回溯、自動化測試和行為監(jiān)控等等。視頻錄制、頁面截圖和 Dom 快照錄制等技術(shù)方案各有優(yōu)劣,在面對不同的使用場景時要選擇合適的技術(shù)方案,但總體而言,以 rrweb 庫為代表的 Dom 快照錄制技術(shù)是目前最為廣泛使用也最具優(yōu)勢的技術(shù)方案,在各種商業(yè)化解決方案中也主要采用該技術(shù)方案或思路來實現(xiàn)。

          參考資料

          • 用戶行為錄制技術(shù)方案 - 掘金

          • 前端錄制回放系統(tǒng)初體驗 - 掘金

          • 瀏覽器錄制方案的調(diào)研和總結(jié) - 知乎

          • 快速入門 WebRTC:屏幕和攝像頭的錄制、回放、下載 - 掘金

          鏈接: https://juejin.cn/user/1521379822015645/posts
          作者:  植物系青年

          Node 社群

              
              


          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。

             “分享、點贊、在看” 支持一下

          瀏覽 2141
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人黄色电影A片 | 色之综合天天综合色天天棕色 | 水多多www视频在线观看高清 | 久久久久久人人人人人人 | 中文字幕国产乱伦 |