Explorar el Código

feat:会话录制

cmy hace 3 días
padre
commit
8bd165cdd9
Se han modificado 4 ficheros con 86 adiciones y 3 borrados
  1. 14 1
      main.html
  2. 6 0
      package.json
  3. 59 2
      src/index.js
  4. 7 0
      webpack.config.js

+ 14 - 1
main.html

@@ -10,8 +10,14 @@
   <button id="btn" data-track>点击我</button>
   <div>操作系统: <span id="os"></span></div>
   <div>用户唯一标识: <span id="userId"></span></div>
+
+  <!-- 会话绘制 -->
+  <button id="startRecording">开始录制</button>
+  <button data-stopRecording>停止录制</button>
+  <button data-replay>回放录制</button>
+  <div id="replay"></div>
 </body>
-<script src="buriedPoint.js"></script>
+<script src="./dist/main.js"></script>
 <script>
   // 展示操作系统和用户唯一标识
   document.addEventListener('DOMContentLoaded', function () {
@@ -23,7 +29,14 @@
   const tracker = new Tracker({ 
     baseUrl: 'http://192.168.3.9:3000/api/log',
     heartbeatInterval: 5000,
+    maxVisitCount: 9999,
+    // enableAutoRecording: false, // 是否自动开始录制
   });
   tracker.init();
+
+  // 开启会话绘制
+  document.getElementById('startRecording').addEventListener('click', function () {
+    tracker.startRecording();
+  });
 </script>
 </html>

+ 6 - 0
package.json

@@ -22,7 +22,13 @@
     "@babel/preset-env": "^7.27.2",
     "babel-loader": "^10.0.0",
     "clean-webpack-plugin": "^4.0.0",
+    "css-loader": "^7.1.2",
+    "style-loader": "^4.0.0",
     "webpack": "^5.99.9",
     "webpack-cli": "^5.1.4"
+  },
+  "dependencies": {
+    "rrweb": "^2.0.0-alpha.4",
+    "rrweb-player": "^1.0.0-alpha.4"
   }
 }

+ 59 - 2
src/index.js

@@ -1,3 +1,6 @@
+import { record } from "rrweb";
+import Replayer from "rrweb-player";
+import "rrweb-player/dist/style.css"; // 必须导入样式
 
 // 通用 JSBridge
 window.JSBridge = {
@@ -157,6 +160,10 @@ class Tracker {
     beforeDestroy: () => {}, // 销毁前的回调函数
     crashTime: 3, // 闪退阈值(几秒内关闭)
     idleTimeout: 300000, // 默认 5 分钟无操作视为离开
+    events: [], // 录制事件列表
+    stopFn: () => {}, // 停止录制函数
+    replayDom: '', // 回放dom
+    enableAutoRecording: true, // 是否开启自动录制
   }
   hasInitialized = false; // 是否已初始化
   initialize(options) {
@@ -178,6 +185,10 @@ class Tracker {
     this.idleTimeout = options.idleTimeout || this.initCofig.idleTimeout; // 默认 5 分钟无操作视为离开
     this.sendDataDebounceTimers = {}; // 【2024-06-09】存储每种事件类型的定时器,用于事件级防抖
     this.debounceTime = options.debounceTime || this.debounceTime || 300; // 【2024-06-09】防抖间隔支持初始化参数传入
+    this.events = options.events || this.initCofig.events; // 录制事件列表
+    this.stopFn = options.stopFn || this.initCofig.stopFn; // 停止录制函数
+    this.replayDom = options.replayDom || document.getElementById('replay'); // 回放dom
+    this.enableAutoRecording = options.enableAutoRecording || this.initCofig.enableAutoRecording; // 是否开启自动录制
 
     window.JSBridge.getUserId((id) => {
       this.userId = id;
@@ -209,6 +220,10 @@ class Tracker {
       this.startHeartbeat();
       console.log(`埋点初始化成功,用户ID: ${this.userId}`);
       this.hasInitialized = true;
+      if(this.enableAutoRecording) {
+        console.log(`开启自动录制`);
+        this.startRecording(); // 开启自动录制
+      }
     }
     if (isBlocked) {
       // restoreBlock();
@@ -288,6 +303,43 @@ class Tracker {
     }
   };
 
+  startRecording() {
+    this.events = []; // 清空之前的记录
+    this.stopFn = record({
+      emit: (event) => { // 使用箭头函数确保 this 正确
+        this.events.push(event);
+      },
+    });
+    console.log("开始录制用户会话");
+  }
+
+  stopRecording() {
+    this.stopFn?.(); // 停止录制
+    const sessionData = JSON.stringify(this.events);
+    localStorage.setItem("rrweb-session", sessionData);
+    console.log("已停止录制,共记录 " + this.events.length + " 个事件");
+    console.log(localStorage.getItem("rrweb-session"));
+  }
+
+  replay() {
+    if (!this.events.length) return;
+    if (!this.replayDom) return;
+
+    this.replayDom.innerHTML = ""; // 清空容器
+
+    // 创建回放器
+    const replayer = new Replayer({
+      target: this.replayDom,
+      props: {
+        events: this.events,
+        width: 800,
+        height: 500,
+      },
+    });
+
+    replayer.play();
+  }
+
   handleClickEvent = (event) => {
     const target = event.target;
     if (target.matches('[data-track]')) {
@@ -300,6 +352,12 @@ class Tracker {
       };
       this.sendData(trackInfo);
     }
+    if (target.matches('[data-stopRecording]')) {
+      this.stopRecording();
+    }
+    if (target.matches('[data-replay]')) {
+      this.replay();
+    }
   };
 
   handleVisibilityChange = () => {
@@ -359,7 +417,6 @@ class Tracker {
       });
     });
   }
-
   destroy() {
     window.removeEventListener('beforeunload', this.handleBeforeUnload);
     window.removeEventListener('click', this.handleClickEvent);
@@ -384,4 +441,4 @@ class Tracker {
 // tracker.init();
 
 // 暴露给全局或者作为模块导出
-window.Tracker = Tracker;
+window.Tracker = Tracker;

+ 7 - 0
webpack.config.js

@@ -16,6 +16,13 @@ module.exports = {
   mode: 'production',
   module: {
     rules: [
+      {
+        test: /\.css$/,
+        use: [
+          'style-loader',
+          'css-loader'
+        ]
+      },
       {
         test: /\.js$/,
         exclude: /node_modules/,