@deepsure/page-replay-sdk 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,29 +1,29 @@
1
- # 金融监管录制系统 JavaScript SDK
1
+ # Page Replay JavaScript SDK
2
2
 
3
- 用于金融场景的会话录制和回溯,支持自动会话管理、增量上传和数据脱敏。
3
+ A JavaScript SDK for page session recording and replay, supporting automatic session management, incremental uploads, and data masking.
4
4
 
5
- ## 安装
5
+ ## Installation
6
6
 
7
- ### CDN 引入
7
+ ### CDN
8
8
 
9
9
  ```html
10
10
  <script src="https://cdn.yourplatform.com/sdk/v1/recorder.js"></script>
11
11
  ```
12
12
 
13
- ### NPM 安装
13
+ ### NPM
14
14
 
15
15
  ```bash
16
- npm install @finance-recorder/sdk
16
+ npm install @deepsure/page-replay-sdk
17
17
  ```
18
18
 
19
- ## 快速开始
19
+ ## Quick Start
20
20
 
21
- ### 基础用法
21
+ ### Basic Usage
22
22
 
23
23
  ```html
24
24
  <script src="https://cdn.yourplatform.com/sdk/v1/recorder.js"></script>
25
25
  <script>
26
- FinanceRecorder.init({
26
+ PageRecorder.init({
27
27
  apiKey: 'your_api_key',
28
28
  projectId: 'proj_12345',
29
29
  autoStart: true
@@ -31,45 +31,45 @@ npm install @finance-recorder/sdk
31
31
  </script>
32
32
  ```
33
33
 
34
- ### 完整配置
34
+ ### Full Configuration
35
35
 
36
36
  ```javascript
37
- import FinanceRecorder from '@finance-recorder/sdk';
37
+ import PageRecorder from '@deepsure/page-replay-sdk';
38
38
 
39
- await FinanceRecorder.init({
40
- // 必需配置
41
- apiKey: 'your_api_key', // API 密钥
42
- projectId: 'proj_12345', // 项目 ID
39
+ await PageRecorder.init({
40
+ // Required configuration
41
+ apiKey: 'your_api_key', // API key
42
+ projectId: 'proj_12345', // Project ID
43
43
 
44
- // 可选配置
45
- apiBaseUrl: 'https://api.yourplatform.com', // API 地址
46
- autoStart: true, // 自动开始录制
47
- chunkSize: 100, // 批量上传大小
48
- uploadInterval: 30000, // 上传间隔(毫秒)
44
+ // Optional configuration
45
+ apiBaseUrl: 'https://api.yourplatform.com', // API base URL
46
+ autoStart: false, // Auto-start recording
47
+ chunkSize: 100, // Batch upload size
48
+ uploadInterval: 30000, // Upload interval (milliseconds)
49
49
 
50
- // 隐私配置
50
+ // Privacy configuration
51
51
  privacy: {
52
- maskAllInputs: false, // 是否屏蔽所有输入
52
+ maskAllInputs: false, // Mask all inputs
53
53
  maskInputOptions: {
54
- idCard: true, // 身份证脱敏
55
- phone: true, // 手机号脱敏
56
- bankCard: true, // 银行卡脱敏
54
+ idCard: true, // Mask ID card numbers
55
+ phone: true, // Mask phone numbers
56
+ bankCard: true, // Mask bank card numbers
57
57
  }
58
58
  },
59
59
 
60
- // 业务元数据
60
+ // Business metadata
61
61
  metadata: {
62
- productType: '车险', // 产品类型
63
- quoteId: 'quote_789', // 报价单号
64
- userId: 'user_123' // 用户 ID
62
+ productType: 'Auto Insurance', // Product type
63
+ quoteId: 'quote_789', // Quote ID
64
+ userId: 'user_123' // User ID
65
65
  },
66
66
 
67
- // 回调函数
67
+ // Callback functions
68
68
  onSessionCreated: (session) => {
69
- console.log('会话已创建:', session.sessionId);
69
+ console.log('Session created:', session.sessionId);
70
70
  },
71
71
  onUploadProgress: (progress) => {
72
- console.log('上传进度:', progress);
72
+ console.log('Upload progress:', progress);
73
73
  },
74
74
  onError: (message, error) => {
75
75
  console.error(message, error);
@@ -77,66 +77,66 @@ await FinanceRecorder.init({
77
77
  });
78
78
  ```
79
79
 
80
- ## API 参考
80
+ ## API Reference
81
81
 
82
82
  ### init(config)
83
83
 
84
- 初始化录制器。
84
+ Initialize the recorder.
85
85
 
86
- **参数:**
87
- - `apiKey` (string, 必需) - API 密钥
88
- - `projectId` (string, 必需) - 项目 ID
89
- - `apiBaseUrl` (string) - API 基础 URL
90
- - `autoStart` (boolean) - 是否自动开始录制,默认 true
91
- - `privacy` (object) - 隐私配置
92
- - `metadata` (object) - 业务元数据
86
+ **Parameters:**
87
+ - `apiKey` (string, required) - API key
88
+ - `projectId` (string, required) - Project ID
89
+ - `apiBaseUrl` (string) - API base URL
90
+ - `autoStart` (boolean) - Whether to auto-start recording, defaults to true
91
+ - `privacy` (object) - Privacy configuration
92
+ - `metadata` (object) - Business metadata
93
93
 
94
- **返回:** Promise<FinanceRecorder>
94
+ **Returns:** Promise<PageRecorder>
95
95
 
96
96
  ### start(customMetadata?)
97
97
 
98
- 手动开始录制。
98
+ Manually start recording.
99
99
 
100
- **参数:**
101
- - `customMetadata` (object, 可选) - 自定义元数据
100
+ **Parameters:**
101
+ - `customMetadata` (object, optional) - Custom metadata
102
102
 
103
- **返回:** Promise<void>
103
+ **Returns:** Promise<void>
104
104
 
105
105
  ### stop()
106
106
 
107
- 停止录制并上传剩余数据。
107
+ Stop recording and upload remaining data.
108
108
 
109
- **返回:** Promise<string> - 会话 ID
109
+ **Returns:** Promise<string> - Session ID
110
110
 
111
111
  ### setMetadata(metadata)
112
112
 
113
- 更新会话元数据。
113
+ Update session metadata.
114
114
 
115
- **参数:**
116
- - `metadata` (object) - 要添加/更新的元数据
115
+ **Parameters:**
116
+ - `metadata` (object) - Metadata to add/update
117
117
 
118
118
  ### getSessionId()
119
119
 
120
- 获取当前会话 ID
120
+ Get the current session ID.
121
121
 
122
- **返回:** string | null
122
+ **Returns:** string | null
123
123
 
124
124
  ### isRecording()
125
125
 
126
- 检查是否正在录制。
126
+ Check if recording is in progress.
127
127
 
128
- **返回:** boolean
128
+ **Returns:** boolean
129
129
 
130
- ## 使用场景
130
+ ## Use Cases
131
131
 
132
- ### 场景 1:保险投保流程录制
132
+ ### Use Case 1: Insurance Application Recording
133
133
 
134
134
  ```javascript
135
- FinanceRecorder.init({
135
+ PageRecorder.init({
136
136
  apiKey: 'your_api_key',
137
137
  projectId: 'proj_insurance',
138
138
  metadata: {
139
- productType: '车险',
139
+ productType: 'Auto Insurance',
140
140
  policyNumber: 'POL-2025-001',
141
141
  applicantId: 'user_123'
142
142
  },
@@ -150,35 +150,35 @@ FinanceRecorder.init({
150
150
  });
151
151
  ```
152
152
 
153
- ### 场景 2:客服协助录制
153
+ ### Use Case 2: Customer Support Recording
154
154
 
155
155
  ```javascript
156
- // 仅在客服协助时录制
156
+ // Record only during customer support
157
157
  const startAssist = async () => {
158
- await FinanceRecorder.init({
158
+ await PageRecorder.init({
159
159
  apiKey: 'your_api_key',
160
160
  projectId: 'proj_support',
161
161
  autoStart: false
162
162
  });
163
163
 
164
- // 用户同意后开始录制
165
- await FinanceRecorder.start({
164
+ // Start recording after user consent
165
+ await PageRecorder.start({
166
166
  supportTicketId: 'TICKET-123',
167
167
  agentId: 'agent_456'
168
168
  });
169
169
  };
170
170
 
171
171
  const endAssist = async () => {
172
- const sessionId = await FinanceRecorder.stop();
173
- console.log('会话已保存:', sessionId);
172
+ const sessionId = await PageRecorder.stop();
173
+ console.log('Session saved:', sessionId);
174
174
  };
175
175
  ```
176
176
 
177
- ### 场景 3:动态更新元数据
177
+ ### Use Case 3: Dynamic Metadata Updates
178
178
 
179
179
  ```javascript
180
- // 初始化
181
- await FinanceRecorder.init({
180
+ // Initialize
181
+ await PageRecorder.init({
182
182
  apiKey: 'your_api_key',
183
183
  projectId: 'proj_12345',
184
184
  metadata: {
@@ -186,46 +186,46 @@ await FinanceRecorder.init({
186
186
  }
187
187
  });
188
188
 
189
- // 用户进入下一步
190
- FinanceRecorder.setMetadata({ step: 'fill_info' });
189
+ // User proceeds to next step
190
+ PageRecorder.setMetadata({ step: 'fill_info' });
191
191
 
192
- // 用户提交申请
193
- FinanceRecorder.setMetadata({
192
+ // User submits application
193
+ PageRecorder.setMetadata({
194
194
  step: 'submit',
195
195
  applicationId: 'APP-2025-001'
196
196
  });
197
197
  ```
198
198
 
199
- ## 隐私保护
199
+ ## Privacy Protection
200
200
 
201
- ### 自动脱敏
201
+ ### Automatic Masking
202
202
 
203
- SDK 自动检测并脱敏以下敏感信息:
203
+ The SDK automatically detects and masks the following sensitive information:
204
204
 
205
- - **身份证号**:`110101199001011234` → `110101********1234`
206
- - **手机号**:`13812345678` → `138****5678`
207
- - **银行卡号**:`6222021234567890123` → `622202******123`
205
+ - **ID Card Number**: `110101199001011234` → `110101********1234`
206
+ - **Phone Number**: `13812345678` → `138****5678`
207
+ - **Bank Card Number**: `6222021234567890123` → `622202******123`
208
208
 
209
- ### 手动屏蔽元素
209
+ ### Manual Element Blocking
210
210
 
211
- HTML 中添加 `rr-block` 类名:
211
+ Add the `rr-block` class name in HTML:
212
212
 
213
213
  ```html
214
214
  <div class="rr-block">
215
- 这个区域不会被录制
215
+ This area will not be recorded
216
216
  </div>
217
217
  ```
218
218
 
219
- ## 最佳实践
219
+ ## Best Practices
220
220
 
221
- ### 1. 错误处理
221
+ ### 1. Error Handling
222
222
 
223
223
  ```javascript
224
- FinanceRecorder.init({
224
+ PageRecorder.init({
225
225
  apiKey: 'your_api_key',
226
226
  projectId: 'proj_12345',
227
227
  onError: (message, error) => {
228
- // 上报到错误监控系统
228
+ // Report to error monitoring system
229
229
  Sentry.captureException(error, {
230
230
  tags: { component: 'recorder' },
231
231
  extra: { message }
@@ -234,12 +234,12 @@ FinanceRecorder.init({
234
234
  });
235
235
  ```
236
236
 
237
- ### 2. 性能监控
237
+ ### 2. Performance Monitoring
238
238
 
239
239
  ```javascript
240
240
  const perfObserver = new PerformanceObserver((list) => {
241
241
  for (const entry of list.getEntries()) {
242
- FinanceRecorder.setMetadata({
242
+ PageRecorder.setMetadata({
243
243
  [`perf_${entry.name}`]: entry.duration
244
244
  });
245
245
  }
@@ -248,14 +248,14 @@ const perfObserver = new PerformanceObserver((list) => {
248
248
  perfObserver.observe({ entryTypes: ['navigation', 'resource'] });
249
249
  ```
250
250
 
251
- ### 3. 用户同意
251
+ ### 3. User Consent
252
252
 
253
253
  ```javascript
254
- // 显示用户同意弹窗
254
+ // Show user consent dialog
255
255
  const consent = await showConsentDialog();
256
256
 
257
257
  if (consent) {
258
- await FinanceRecorder.init({
258
+ await PageRecorder.init({
259
259
  apiKey: 'your_api_key',
260
260
  projectId: 'proj_12345',
261
261
  metadata: {
@@ -266,14 +266,13 @@ if (consent) {
266
266
  }
267
267
  ```
268
268
 
269
- ## 浏览器兼容性
269
+ ## Browser Compatibility
270
270
 
271
271
  - Chrome 60+
272
272
  - Firefox 55+
273
273
  - Safari 11+
274
274
  - Edge 79+
275
275
 
276
- ## 许可证
276
+ ## License
277
277
 
278
278
  MIT
279
-
package/dist/index.esm.js CHANGED
@@ -1,2 +1,2 @@
1
- import{record as t}from"rrweb";import s from"pako";class e{constructor(t,s){this.baseUrl=t,this.apiKey=s}async request(t,s={}){const e=`${this.baseUrl}${t}`,i={"Content-Type":"application/json","X-API-Key":this.apiKey,...s.headers},n=await fetch(e,{...s,headers:i});if(!n.ok){const t=await n.json().catch(()=>({}));throw new Error(t.message||`HTTP ${n.status}`)}return await n.json()}async createSession(t){return(await this.request("/api/v1/sessions",{method:"POST",body:JSON.stringify(t)})).data}async appendEvents(t,s){return await this.request(`/api/v1/sessions/${t}/append`,{method:"PATCH",body:JSON.stringify({events:s,compressed:!0})})}async finalizeSession(t,s){return await this.request(`/api/v1/sessions/${t}/finalize`,{method:"POST",body:JSON.stringify(s)})}}const i=new class{constructor(){this.config=null,this.sessionId=null,this.recording=!1,this.stopFn=null,this.events=[],this.uploadQueue=[],this.uploadTimer=null,this.apiClient=null}async init(t){return this.config={apiKey:t.apiKey,projectId:t.projectId,apiBaseUrl:t.apiBaseUrl||"https://api.yourplatform.com",autoStart:!1!==t.autoStart,chunkSize:t.chunkSize||100,uploadInterval:t.uploadInterval||3e4,privacy:{maskAllInputs:!1,maskInputOptions:{idCard:!0,phone:!0,bankCard:!0,...t.privacy?.maskInputOptions},...t.privacy},metadata:t.metadata||{},onError:t.onError||console.error,onSessionCreated:t.onSessionCreated||(()=>{}),onUploadProgress:t.onUploadProgress||(()=>{})},this.apiClient=new e(this.config.apiBaseUrl,this.config.apiKey),this.config.autoStart&&await this.start(),this}async start(s={}){if(this.recording)console.warn("Recording already in progress");else try{const e=await this.apiClient.createSession({projectId:this.config.projectId,metadata:{...this.config.metadata,...s,url:window.location.href,userAgent:navigator.userAgent,screenResolution:`${window.screen.width}x${window.screen.height}`,startTime:(new Date).toISOString()}});this.sessionId=e.sessionId,this.recording=!0,this.events=[],this.config.onSessionCreated(e),this.stopFn=t({emit:t=>{this.handleEvent(t)},checkoutEveryNms:3e5,...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),this.startUploadTimer(),console.log("Recording started:",this.sessionId)}catch(t){throw this.config.onError("Failed to start recording",t),t}}async stop(){if(!this.recording)return;this.recording=!1,this.stopFn&&(this.stopFn(),this.stopFn=null),this.stopUploadTimer(),await this.flushEvents(),await this.apiClient.finalizeSession(this.sessionId,{endTime:(new Date).toISOString(),eventCount:this.events.length}),console.log("Recording stopped:",this.sessionId);const t=this.sessionId;return this.sessionId=null,this.events=[],t}handleEvent(t){this.events.push(t),this.events.length>=this.config.chunkSize&&this.flushEvents()}async flushEvents(){if(0===this.events.length||!this.sessionId)return;const t=[...this.events];this.events=[];try{const s=this.compressEvents(t);await this.apiClient.appendEvents(this.sessionId,s),this.config.onUploadProgress({sessionId:this.sessionId,uploaded:t.length,total:this.events.length+t.length})}catch(s){this.events.unshift(...t),this.config.onError("Failed to upload events",s),setTimeout(()=>this.flushEvents(),5e3)}}compressEvents(t){const e=JSON.stringify(t),i=s.gzip(e);return btoa(String.fromCharCode(...i))}getPrivacyConfig(){return{blockClass:"rr-block",maskAllInputs:this.config.privacy.maskAllInputs,maskInputOptions:this.config.privacy.maskInputOptions,maskTextFn:(t,s)=>this.maskSensitiveData(t,s)}}maskSensitiveData(t,s){if(!t)return t;const e=t.trim();return this.config.privacy.maskInputOptions.idCard&&/^\d{15}|\d{17}[\dXx]$/.test(e)?e.replace(/(\d{6})\d{8}(\d{4})/,"$1********$2"):this.config.privacy.maskInputOptions.phone&&/^1[3-9]\d{9}$/.test(e)?e.replace(/(\d{3})\d{4}(\d{4})/,"$1****$2"):this.config.privacy.maskInputOptions.bankCard&&/^\d{16,19}$/.test(e)?e.replace(/(\d{6})\d+(\d{3})/,"$1******$2"):t}startUploadTimer(){this.uploadTimer=setInterval(()=>{this.flushEvents()},this.config.uploadInterval)}stopUploadTimer(){this.uploadTimer&&(clearInterval(this.uploadTimer),this.uploadTimer=null)}setMetadata(t){this.config.metadata={...this.config.metadata,...t}}getSessionId(){return this.sessionId}isRecording(){return this.recording}};"undefined"!=typeof window&&(window.PageRecorder=i);export{i as default};
1
+ import{record as e}from"rrweb";import s from"pako";class t{constructor(e,s){this.baseUrl=e,this.apiKey=s}async request(e,s={}){const t=`${this.baseUrl}${e}`,i={"Content-Type":"application/json","X-API-Key":this.apiKey,...s.headers},n=await fetch(t,{...s,headers:i});if(!n.ok){const e=await n.json().catch(()=>({})),s=new Error(e.message||`HTTP ${n.status}`);throw s.code=e.code,s.status=n.status,s}return await n.json()}async createSession(e){return(await this.request("/api/v1/sessions",{method:"POST",body:JSON.stringify(e)})).data}async appendEvents(e,s,t){try{return await this.request(`/api/v1/sessions/${e}/append`,{method:"PATCH",body:JSON.stringify({events:s,compressed:!0,sequence:t})})}catch(e){if("SESSION_FINALIZING"===e.code||"SESSION_NOT_RECORDING"===e.code)return console.warn("Session is finalizing or not recording, skipping append"),null;throw e}}async finalizeSession(e,s){try{return await this.request(`/api/v1/sessions/${e}/finalize`,{method:"POST",body:JSON.stringify(s)})}catch(s){if("SESSION_ALREADY_FINALIZING"===s.code)return console.log("Session already finalizing, waiting..."),await new Promise(e=>setTimeout(e,2e3)),await this.getSessionStatus(e);if("SESSION_ALREADY_COMPLETED"===s.code)return console.log("Session already completed"),await this.getSessionStatus(e);throw s}}async getSessionStatus(e){try{return await this.request(`/api/v1/sessions/${e}`,{method:"GET"})}catch(e){return console.error("Failed to get session status:",e),null}}}const i=new class{constructor(){this.config=null,this.sessionId=null,this.recording=!1,this.stopFn=null,this.events=[],this.uploadQueue=[],this.uploadTimer=null,this.apiClient=null,this.appendSequence=0,this.failedAppends=new Map,this.totalEvents=0}async init(e){return this.config={apiKey:e.apiKey,projectId:e.projectId,apiBaseUrl:e.apiBaseUrl||"https://api.yourplatform.com",autoStart:!1!==e.autoStart,chunkSize:e.chunkSize||100,uploadInterval:e.uploadInterval||3e4,privacy:{maskAllInputs:!1,maskInputOptions:{idCard:!0,phone:!0,bankCard:!0,...e.privacy?.maskInputOptions},...e.privacy},metadata:e.metadata||{},onError:e.onError||console.error,onSessionCreated:e.onSessionCreated||(()=>{}),onUploadProgress:e.onUploadProgress||(()=>{})},this.apiClient=new t(this.config.apiBaseUrl,this.config.apiKey),this.config.autoStart&&await this.start(),this}async start(s={}){if(this.recording)console.warn("Recording already in progress");else try{const t=await this.apiClient.createSession({projectId:this.config.projectId,metadata:{...this.config.metadata,...s,url:window.location.href,userAgent:navigator.userAgent,screenResolution:`${window.screen.width}x${window.screen.height}`,startTime:(new Date).toISOString()}});this.sessionId=t.sessionId,this.recording=!0,this.events=[],this.totalEvents=0,this.config.onSessionCreated(t),this.stopFn=e({emit:e=>{this.handleEvent(e)},checkoutEveryNms:3e5,...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),this.startUploadTimer(),console.log("Recording started:",this.sessionId)}catch(e){throw this.config.onError("Failed to start recording",e),e}}async stop(){if(!this.recording)return;this.recording=!1,this.stopFn&&(this.stopFn(),this.stopFn=null),this.stopUploadTimer(),await this.flushEvents(),this.failedAppends.size>0&&(console.warn(`Retrying ${this.failedAppends.size} failed appends before finalize...`),await this.retryFailedAppends(),await new Promise(e=>setTimeout(e,1e3)),this.failedAppends.size>0&&console.error(`Still have ${this.failedAppends.size} failed appends after retry`)),this.events.length>0&&(console.warn("Some events failed to upload, trying one more time..."),await this.flushEvents());try{await this.apiClient.finalizeSession(this.sessionId,{endTime:(new Date).toISOString(),eventCount:this.totalEvents}),console.log("Recording stopped:",this.sessionId)}catch(e){console.error("Failed to finalize session:",e),this.config.onError("Failed to finalize session",e)}const e=this.sessionId;return this.sessionId=null,this.events=[],this.totalEvents=0,e}handleEvent(e){this.events.push(e),this.events.length>=this.config.chunkSize&&this.flushEvents()}async flushEvents(){if(0===this.events.length||!this.sessionId)return;const e=[...this.events];this.totalEvents+=e.length,this.events=[],this.appendSequence++;const s=this.appendSequence;try{const t=this.compressEvents(e),i=await this.apiClient.appendEvents(this.sessionId,t,s);i?.missingSequences&&i.missingSequences.length>0&&(console.warn("Server detected missing sequences:",i.missingSequences),await this.retryMissingSequences(i.missingSequences)),this.config.onUploadProgress({sessionId:this.sessionId,sequence:s,uploaded:e.length,status:i?.status||"success"})}catch(t){this.failedAppends.set(s,{events:e,retries:0,error:t.message}),console.error(`Failed to upload sequence ${s}:`,t),this.config.onError("Failed to upload events",t),"SESSION_NOT_FOUND"!==t.code&&"SESSION_NOT_RECORDING"!==t.code&&setTimeout(()=>this.retryFailedAppends(),2e3)}}async retryFailedAppends(){if(0===this.failedAppends.size||!this.sessionId)return;const e=[];for(const[s,t]of this.failedAppends.entries())t.retries<3?e.push({sequence:s,failedAppend:t}):(console.warn(`Sequence ${s} failed after 3 retries, giving up`),this.failedAppends.delete(s));for(const{sequence:s,failedAppend:t}of e)try{const e=this.compressEvents(t.events);await this.apiClient.appendEvents(this.sessionId,e,s),console.log(`Successfully retried sequence ${s}`),this.failedAppends.delete(s)}catch(e){t.retries++,console.error(`Retry failed for sequence ${s} (attempt ${t.retries}):`,e)}}async retryMissingSequences(e){for(const s of e){const e=this.failedAppends.get(s);if(e)try{const t=this.compressEvents(e.events);await this.apiClient.appendEvents(this.sessionId,t,s),console.log(`Successfully re-sent missing sequence ${s}`),this.failedAppends.delete(s)}catch(e){console.error(`Failed to re-send missing sequence ${s}:`,e)}else console.warn(`Server requested missing sequence ${s}, but no record found`)}}compressEvents(e){const t=JSON.stringify(e),i=s.gzip(t);return btoa(String.fromCharCode(...i))}getPrivacyConfig(){return{blockClass:"rr-block",maskAllInputs:this.config.privacy.maskAllInputs,maskInputOptions:this.config.privacy.maskInputOptions,maskTextFn:(e,s)=>this.maskSensitiveData(e,s)}}maskSensitiveData(e,s){if(!e)return e;const t=e.trim();return this.config.privacy.maskInputOptions.idCard&&/^\d{15}|\d{17}[\dXx]$/.test(t)?t.replace(/(\d{6})\d{8}(\d{4})/,"$1********$2"):this.config.privacy.maskInputOptions.phone&&/^1[3-9]\d{9}$/.test(t)?t.replace(/(\d{3})\d{4}(\d{4})/,"$1****$2"):this.config.privacy.maskInputOptions.bankCard&&/^\d{16,19}$/.test(t)?t.replace(/(\d{6})\d+(\d{3})/,"$1******$2"):e}startUploadTimer(){this.uploadTimer=setInterval(()=>{this.flushEvents()},this.config.uploadInterval)}stopUploadTimer(){this.uploadTimer&&(clearInterval(this.uploadTimer),this.uploadTimer=null)}setMetadata(e){this.config.metadata={...this.config.metadata,...e}}getSessionId(){return this.sessionId}isRecording(){return this.recording}};"undefined"!=typeof window&&(window.PageRecorder=i);export{i as default};
2
2
  //# sourceMappingURL=index.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":["../src/index.js"],"sourcesContent":["/**\n * 页面录制系统 JavaScript SDK\n * 用于页面会话录制和回溯\n */\n\nimport { record } from 'rrweb';\nimport pako from 'pako';\n\nclass PageRecorder {\n constructor() {\n this.config = null;\n this.sessionId = null;\n this.recording = false;\n this.stopFn = null;\n this.events = [];\n this.uploadQueue = [];\n this.uploadTimer = null;\n this.apiClient = null;\n }\n\n /**\n * 初始化录制器\n */\n async init(config) {\n this.config = {\n apiKey: config.apiKey,\n projectId: config.projectId,\n apiBaseUrl: config.apiBaseUrl || 'https://api.yourplatform.com',\n autoStart: config.autoStart !== false,\n chunkSize: config.chunkSize || 100,\n uploadInterval: config.uploadInterval || 30000, // 30秒\n privacy: {\n maskAllInputs: false,\n maskInputOptions: {\n idCard: true,\n phone: true,\n bankCard: true,\n ...config.privacy?.maskInputOptions,\n },\n ...config.privacy,\n },\n metadata: config.metadata || {},\n onError: config.onError || console.error,\n onSessionCreated: config.onSessionCreated || (() => {}),\n onUploadProgress: config.onUploadProgress || (() => {}),\n };\n\n // 初始化 API 客户端\n this.apiClient = new APIClient(this.config.apiBaseUrl, this.config.apiKey);\n\n // 自动开始录制\n if (this.config.autoStart) {\n await this.start();\n }\n\n return this;\n }\n\n /**\n * 开始录制\n */\n async start(customMetadata = {}) {\n if (this.recording) {\n console.warn('Recording already in progress');\n return;\n }\n\n try {\n // 创建会话\n const session = await this.apiClient.createSession({\n projectId: this.config.projectId,\n metadata: {\n ...this.config.metadata,\n ...customMetadata,\n url: window.location.href,\n userAgent: navigator.userAgent,\n screenResolution: `${window.screen.width}x${window.screen.height}`,\n startTime: new Date().toISOString(),\n },\n });\n\n this.sessionId = session.sessionId;\n this.recording = true;\n this.events = [];\n\n // 通知会话创建\n this.config.onSessionCreated(session);\n\n // 开始 rrweb 录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\n checkoutEveryNms: 5 * 60 * 1000,\n ...this.getPrivacyConfig(),\n sampling: {\n mousemove: true,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n recordCanvas: false,\n inlineImages: false,\n collectFonts: false,\n });\n\n // 启动定时上传\n this.startUploadTimer();\n\n console.log('Recording started:', this.sessionId);\n } catch (error) {\n this.config.onError('Failed to start recording', error);\n throw error;\n }\n }\n\n /**\n * 停止录制\n */\n async stop() {\n if (!this.recording) {\n return;\n }\n\n this.recording = false;\n\n // 停止 rrweb 录制\n if (this.stopFn) {\n this.stopFn();\n this.stopFn = null;\n }\n\n // 停止定时上传\n this.stopUploadTimer();\n\n // 上传剩余事件\n await this.flushEvents();\n\n // 完成会话\n await this.apiClient.finalizeSession(this.sessionId, {\n endTime: new Date().toISOString(),\n eventCount: this.events.length,\n });\n\n console.log('Recording stopped:', this.sessionId);\n\n const sessionId = this.sessionId;\n this.sessionId = null;\n this.events = [];\n\n return sessionId;\n }\n\n /**\n * 处理录制事件\n */\n handleEvent(event) {\n this.events.push(event);\n\n // 批量上传\n if (this.events.length >= this.config.chunkSize) {\n this.flushEvents();\n }\n }\n\n /**\n * 上传事件\n */\n async flushEvents() {\n if (this.events.length === 0 || !this.sessionId) {\n return;\n }\n\n const eventsToUpload = [...this.events];\n this.events = [];\n\n try {\n // 压缩数据\n const compressed = this.compressEvents(eventsToUpload);\n\n // 上传\n await this.apiClient.appendEvents(this.sessionId, compressed);\n\n // 通知上传进度\n this.config.onUploadProgress({\n sessionId: this.sessionId,\n uploaded: eventsToUpload.length,\n total: this.events.length + eventsToUpload.length,\n });\n } catch (error) {\n // 上传失败,放回队列\n this.events.unshift(...eventsToUpload);\n this.config.onError('Failed to upload events', error);\n\n // 重试\n setTimeout(() => this.flushEvents(), 5000);\n }\n }\n\n /**\n * 压缩事件数据\n */\n compressEvents(events) {\n const json = JSON.stringify(events);\n const compressed = pako.gzip(json);\n return btoa(String.fromCharCode(...compressed));\n }\n\n /**\n * 获取隐私配置\n */\n getPrivacyConfig() {\n return {\n blockClass: 'rr-block',\n maskAllInputs: this.config.privacy.maskAllInputs,\n maskInputOptions: this.config.privacy.maskInputOptions,\n maskTextFn: (text, element) => {\n return this.maskSensitiveData(text, element);\n },\n };\n }\n\n /**\n * 脱敏敏感数据\n */\n maskSensitiveData(text, element) {\n if (!text) return text;\n\n const trimmed = text.trim();\n\n // 身份证\n if (this.config.privacy.maskInputOptions.idCard && /^\\d{15}|\\d{17}[\\dXx]$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d{8}(\\d{4})/, '$1********$2');\n }\n\n // 手机号\n if (this.config.privacy.maskInputOptions.phone && /^1[3-9]\\d{9}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{3})\\d{4}(\\d{4})/, '$1****$2');\n }\n\n // 银行卡\n if (this.config.privacy.maskInputOptions.bankCard && /^\\d{16,19}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d+(\\d{3})/, '$1******$2');\n }\n\n return text;\n }\n\n /**\n * 启动定时上传\n */\n startUploadTimer() {\n this.uploadTimer = setInterval(() => {\n this.flushEvents();\n }, this.config.uploadInterval);\n }\n\n /**\n * 停止定时上传\n */\n stopUploadTimer() {\n if (this.uploadTimer) {\n clearInterval(this.uploadTimer);\n this.uploadTimer = null;\n }\n }\n\n /**\n * 添加自定义元数据\n */\n setMetadata(metadata) {\n this.config.metadata = {\n ...this.config.metadata,\n ...metadata,\n };\n }\n\n /**\n * 获取当前会话 ID\n */\n getSessionId() {\n return this.sessionId;\n }\n\n /**\n * 是否正在录制\n */\n isRecording() {\n return this.recording;\n }\n}\n\n/**\n * API 客户端\n */\nclass APIClient {\n constructor(baseUrl, apiKey) {\n this.baseUrl = baseUrl;\n this.apiKey = apiKey;\n }\n\n async request(endpoint, options = {}) {\n const url = `${this.baseUrl}${endpoint}`;\n const headers = {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n ...options.headers,\n };\n\n const response = await fetch(url, {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.message || `HTTP ${response.status}`);\n }\n\n return await response.json();\n }\n\n async createSession(data) {\n const result = await this.request('/api/v1/sessions', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n return result.data;\n }\n\n async appendEvents(sessionId, compressedEvents) {\n return await this.request(`/api/v1/sessions/${sessionId}/append`, {\n method: 'PATCH',\n body: JSON.stringify({\n events: compressedEvents,\n compressed: true,\n }),\n });\n }\n\n async finalizeSession(sessionId, data) {\n return await this.request(`/api/v1/sessions/${sessionId}/finalize`, {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n}\n\n// 导出单例\nconst recorder = new PageRecorder();\n\n// 全局对象\nif (typeof window !== 'undefined') {\n window.PageRecorder = recorder;\n}\n\nexport default recorder;\n\n"],"names":["APIClient","constructor","baseUrl","apiKey","this","request","endpoint","options","url","headers","response","fetch","ok","error","json","catch","Error","message","status","createSession","data","method","body","JSON","stringify","appendEvents","sessionId","compressedEvents","events","compressed","finalizeSession","recorder","config","recording","stopFn","uploadQueue","uploadTimer","apiClient","init","projectId","apiBaseUrl","autoStart","chunkSize","uploadInterval","privacy","maskAllInputs","maskInputOptions","idCard","phone","bankCard","metadata","onError","console","onSessionCreated","onUploadProgress","start","customMetadata","warn","session","window","location","href","userAgent","navigator","screenResolution","screen","width","height","startTime","Date","toISOString","record","emit","event","handleEvent","checkoutEveryNms","getPrivacyConfig","sampling","mousemove","mouseInteraction","scroll","input","recordCanvas","inlineImages","collectFonts","startUploadTimer","log","stop","stopUploadTimer","flushEvents","endTime","eventCount","length","push","eventsToUpload","compressEvents","uploaded","total","unshift","setTimeout","pako","gzip","btoa","String","fromCharCode","blockClass","maskTextFn","text","element","maskSensitiveData","trimmed","trim","test","replace","setInterval","clearInterval","setMetadata","getSessionId","isRecording","PageRecorder"],"mappings":"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"}
1
+ {"version":3,"file":"index.esm.js","sources":["../src/index.js"],"sourcesContent":["/**\n * 页面录制系统 JavaScript SDK\n * 用于页面会话录制和回溯\n */\n\nimport { record } from 'rrweb';\nimport pako from 'pako';\n\nclass PageRecorder {\n constructor() {\n this.config = null;\n this.sessionId = null;\n this.recording = false;\n this.stopFn = null;\n this.events = [];\n this.uploadQueue = [];\n this.uploadTimer = null;\n this.apiClient = null;\n this.appendSequence = 0; // 序列号计数器\n this.failedAppends = new Map(); // 失败的序列号记录 {sequence: {events, retries}}\n this.totalEvents = 0; // 累计事件总数\n }\n\n /**\n * 初始化录制器\n */\n async init(config) {\n this.config = {\n apiKey: config.apiKey,\n projectId: config.projectId,\n apiBaseUrl: config.apiBaseUrl || 'https://api.yourplatform.com',\n autoStart: config.autoStart !== false,\n chunkSize: config.chunkSize || 100,\n uploadInterval: config.uploadInterval || 30000, // 30秒\n privacy: {\n maskAllInputs: false,\n maskInputOptions: {\n idCard: true,\n phone: true,\n bankCard: true,\n ...config.privacy?.maskInputOptions,\n },\n ...config.privacy,\n },\n metadata: config.metadata || {},\n onError: config.onError || console.error,\n onSessionCreated: config.onSessionCreated || (() => {}),\n onUploadProgress: config.onUploadProgress || (() => {}),\n };\n\n // 初始化 API 客户端\n this.apiClient = new APIClient(this.config.apiBaseUrl, this.config.apiKey);\n\n // 自动开始录制\n if (this.config.autoStart) {\n await this.start();\n }\n\n return this;\n }\n\n /**\n * 开始录制\n */\n async start(customMetadata = {}) {\n if (this.recording) {\n console.warn('Recording already in progress');\n return;\n }\n\n try {\n // 创建会话\n const session = await this.apiClient.createSession({\n projectId: this.config.projectId,\n metadata: {\n ...this.config.metadata,\n ...customMetadata,\n url: window.location.href,\n userAgent: navigator.userAgent,\n screenResolution: `${window.screen.width}x${window.screen.height}`,\n startTime: new Date().toISOString(),\n },\n });\n\n this.sessionId = session.sessionId;\n this.recording = true;\n this.events = [];\n this.totalEvents = 0; // 重置累计计数\n\n // 通知会话创建\n this.config.onSessionCreated(session);\n\n // 开始 rrweb 录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\n checkoutEveryNms: 5 * 60 * 1000,\n ...this.getPrivacyConfig(),\n sampling: {\n mousemove: true,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n recordCanvas: false,\n inlineImages: false,\n collectFonts: false,\n });\n\n // 启动定时上传\n this.startUploadTimer();\n\n console.log('Recording started:', this.sessionId);\n } catch (error) {\n this.config.onError('Failed to start recording', error);\n throw error;\n }\n }\n\n /**\n * 停止录制\n */\n async stop() {\n if (!this.recording) {\n return;\n }\n\n this.recording = false;\n\n // 停止 rrweb 录制\n if (this.stopFn) {\n this.stopFn();\n this.stopFn = null;\n }\n\n // 停止定时上传\n this.stopUploadTimer();\n\n // 上传剩余事件并等待所有 pending 请求完成\n await this.flushEvents();\n\n // 等待失败的序列号重传完成\n if (this.failedAppends.size > 0) {\n console.warn(`Retrying ${this.failedAppends.size} failed appends before finalize...`);\n await this.retryFailedAppends();\n \n // 再给一次机会\n await new Promise(resolve => setTimeout(resolve, 1000));\n if (this.failedAppends.size > 0) {\n console.error(`Still have ${this.failedAppends.size} failed appends after retry`);\n }\n }\n\n // 确认队列为空\n if (this.events.length > 0) {\n console.warn('Some events failed to upload, trying one more time...');\n await this.flushEvents();\n }\n\n // 完成会话\n try {\n await this.apiClient.finalizeSession(this.sessionId, {\n endTime: new Date().toISOString(),\n eventCount: this.totalEvents, // 使用累计值而非 this.events.length\n });\n console.log('Recording stopped:', this.sessionId);\n } catch (error) {\n console.error('Failed to finalize session:', error);\n this.config.onError('Failed to finalize session', error);\n }\n\n const sessionId = this.sessionId;\n this.sessionId = null;\n this.events = [];\n this.totalEvents = 0; // 清空累计计数\n\n return sessionId;\n }\n\n /**\n * 处理录制事件\n */\n handleEvent(event) {\n this.events.push(event);\n\n // 批量上传\n if (this.events.length >= this.config.chunkSize) {\n this.flushEvents();\n }\n }\n\n /**\n * 上传事件(带序列号)\n */\n async flushEvents() {\n if (this.events.length === 0 || !this.sessionId) {\n return;\n }\n\n const eventsToUpload = [...this.events];\n this.totalEvents += eventsToUpload.length; // 累加事件数\n this.events = [];\n\n // 分配序列号\n this.appendSequence++;\n const sequence = this.appendSequence;\n\n try {\n // 压缩数据\n const compressed = this.compressEvents(eventsToUpload);\n\n // 上传(带序列号)\n const result = await this.apiClient.appendEvents(\n this.sessionId, \n compressed,\n sequence\n );\n\n // 检查是否需要重传\n if (result?.missingSequences && result.missingSequences.length > 0) {\n console.warn('Server detected missing sequences:', result.missingSequences);\n // 重传缺失的序列号\n await this.retryMissingSequences(result.missingSequences);\n }\n\n // 通知上传进度\n this.config.onUploadProgress({\n sessionId: this.sessionId,\n sequence: sequence,\n uploaded: eventsToUpload.length,\n status: result?.status || 'success',\n });\n } catch (error) {\n // 记录失败的序列号,供重传\n this.failedAppends.set(sequence, {\n events: eventsToUpload,\n retries: 0,\n error: error.message,\n });\n \n console.error(`Failed to upload sequence ${sequence}:`, error);\n this.config.onError('Failed to upload events', error);\n \n // 如果不是严重错误,稍后重试\n if (error.code !== 'SESSION_NOT_FOUND' && error.code !== 'SESSION_NOT_RECORDING') {\n setTimeout(() => this.retryFailedAppends(), 2000);\n }\n }\n }\n\n /**\n * 重传失败的序列号\n */\n async retryFailedAppends() {\n if (this.failedAppends.size === 0 || !this.sessionId) {\n return;\n }\n\n const maxRetries = 3;\n const toRetry = [];\n\n for (const [sequence, failedAppend] of this.failedAppends.entries()) {\n if (failedAppend.retries < maxRetries) {\n toRetry.push({ sequence, failedAppend });\n } else {\n console.warn(`Sequence ${sequence} failed after ${maxRetries} retries, giving up`);\n this.failedAppends.delete(sequence);\n }\n }\n\n for (const { sequence, failedAppend } of toRetry) {\n try {\n const compressed = this.compressEvents(failedAppend.events);\n await this.apiClient.appendEvents(this.sessionId, compressed, sequence);\n \n console.log(`Successfully retried sequence ${sequence}`);\n this.failedAppends.delete(sequence);\n } catch (error) {\n failedAppend.retries++;\n console.error(`Retry failed for sequence ${sequence} (attempt ${failedAppend.retries}):`, error);\n }\n }\n }\n\n /**\n * 重传服务器检测到的缺失序列号\n */\n async retryMissingSequences(missingSequences) {\n for (const sequence of missingSequences) {\n const failedAppend = this.failedAppends.get(sequence);\n if (failedAppend) {\n try {\n const compressed = this.compressEvents(failedAppend.events);\n await this.apiClient.appendEvents(this.sessionId, compressed, sequence);\n \n console.log(`Successfully re-sent missing sequence ${sequence}`);\n this.failedAppends.delete(sequence);\n } catch (error) {\n console.error(`Failed to re-send missing sequence ${sequence}:`, error);\n }\n } else {\n console.warn(`Server requested missing sequence ${sequence}, but no record found`);\n }\n }\n }\n\n /**\n * 压缩事件数据\n */\n compressEvents(events) {\n const json = JSON.stringify(events);\n const compressed = pako.gzip(json);\n return btoa(String.fromCharCode(...compressed));\n }\n\n /**\n * 获取隐私配置\n */\n getPrivacyConfig() {\n return {\n blockClass: 'rr-block',\n maskAllInputs: this.config.privacy.maskAllInputs,\n maskInputOptions: this.config.privacy.maskInputOptions,\n maskTextFn: (text, element) => {\n return this.maskSensitiveData(text, element);\n },\n };\n }\n\n /**\n * 脱敏敏感数据\n */\n maskSensitiveData(text, element) {\n if (!text) return text;\n\n const trimmed = text.trim();\n\n // 身份证\n if (this.config.privacy.maskInputOptions.idCard && /^\\d{15}|\\d{17}[\\dXx]$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d{8}(\\d{4})/, '$1********$2');\n }\n\n // 手机号\n if (this.config.privacy.maskInputOptions.phone && /^1[3-9]\\d{9}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{3})\\d{4}(\\d{4})/, '$1****$2');\n }\n\n // 银行卡\n if (this.config.privacy.maskInputOptions.bankCard && /^\\d{16,19}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d+(\\d{3})/, '$1******$2');\n }\n\n return text;\n }\n\n /**\n * 启动定时上传\n */\n startUploadTimer() {\n this.uploadTimer = setInterval(() => {\n this.flushEvents();\n }, this.config.uploadInterval);\n }\n\n /**\n * 停止定时上传\n */\n stopUploadTimer() {\n if (this.uploadTimer) {\n clearInterval(this.uploadTimer);\n this.uploadTimer = null;\n }\n }\n\n /**\n * 添加自定义元数据\n */\n setMetadata(metadata) {\n this.config.metadata = {\n ...this.config.metadata,\n ...metadata,\n };\n }\n\n /**\n * 获取当前会话 ID\n */\n getSessionId() {\n return this.sessionId;\n }\n\n /**\n * 是否正在录制\n */\n isRecording() {\n return this.recording;\n }\n}\n\n/**\n * API 客户端\n */\nclass APIClient {\n constructor(baseUrl, apiKey) {\n this.baseUrl = baseUrl;\n this.apiKey = apiKey;\n }\n\n async request(endpoint, options = {}) {\n const url = `${this.baseUrl}${endpoint}`;\n const headers = {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n ...options.headers,\n };\n\n const response = await fetch(url, {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n const errorObj = new Error(error.message || `HTTP ${response.status}`);\n errorObj.code = error.code;\n errorObj.status = response.status;\n throw errorObj;\n }\n\n return await response.json();\n }\n\n async createSession(data) {\n const result = await this.request('/api/v1/sessions', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n return result.data;\n }\n\n async appendEvents(sessionId, compressedEvents, sequence) {\n try {\n return await this.request(`/api/v1/sessions/${sessionId}/append`, {\n method: 'PATCH',\n body: JSON.stringify({\n events: compressedEvents,\n compressed: true,\n sequence: sequence, // 新增序列号\n }),\n });\n } catch (error) {\n // 如果会话正在 finalize,不重试\n if (error.code === 'SESSION_FINALIZING' || error.code === 'SESSION_NOT_RECORDING') {\n console.warn('Session is finalizing or not recording, skipping append');\n return null;\n }\n throw error;\n }\n }\n\n async finalizeSession(sessionId, data) {\n try {\n return await this.request(`/api/v1/sessions/${sessionId}/finalize`, {\n method: 'POST',\n body: JSON.stringify(data),\n });\n } catch (error) {\n // 如果会话已经在 finalize 或已完成,等待并获取状态\n if (error.code === 'SESSION_ALREADY_FINALIZING') {\n console.log('Session already finalizing, waiting...');\n await new Promise(resolve => setTimeout(resolve, 2000));\n // 尝试再次获取状态\n return await this.getSessionStatus(sessionId);\n }\n \n if (error.code === 'SESSION_ALREADY_COMPLETED') {\n console.log('Session already completed');\n return await this.getSessionStatus(sessionId);\n }\n \n throw error;\n }\n }\n\n async getSessionStatus(sessionId) {\n // 获取会话状态(假设有这个端点)\n try {\n return await this.request(`/api/v1/sessions/${sessionId}`, {\n method: 'GET',\n });\n } catch (error) {\n console.error('Failed to get session status:', error);\n return null;\n }\n }\n}\n\n// 导出单例\nconst recorder = new PageRecorder();\n\n// 全局对象\nif (typeof window !== 'undefined') {\n window.PageRecorder = recorder;\n}\n\nexport default recorder;\n\n"],"names":["APIClient","constructor","baseUrl","apiKey","this","request","endpoint","options","url","headers","response","fetch","ok","error","json","catch","errorObj","Error","message","status","code","createSession","data","method","body","JSON","stringify","appendEvents","sessionId","compressedEvents","sequence","events","compressed","console","warn","finalizeSession","log","Promise","resolve","setTimeout","getSessionStatus","recorder","config","recording","stopFn","uploadQueue","uploadTimer","apiClient","appendSequence","failedAppends","Map","totalEvents","init","projectId","apiBaseUrl","autoStart","chunkSize","uploadInterval","privacy","maskAllInputs","maskInputOptions","idCard","phone","bankCard","metadata","onError","onSessionCreated","onUploadProgress","start","customMetadata","session","window","location","href","userAgent","navigator","screenResolution","screen","width","height","startTime","Date","toISOString","record","emit","event","handleEvent","checkoutEveryNms","getPrivacyConfig","sampling","mousemove","mouseInteraction","scroll","input","recordCanvas","inlineImages","collectFonts","startUploadTimer","stop","stopUploadTimer","flushEvents","size","retryFailedAppends","length","endTime","eventCount","push","eventsToUpload","compressEvents","result","missingSequences","retryMissingSequences","uploaded","set","retries","toRetry","failedAppend","entries","delete","get","pako","gzip","btoa","String","fromCharCode","blockClass","maskTextFn","text","element","maskSensitiveData","trimmed","trim","test","replace","setInterval","clearInterval","setMetadata","getSessionId","isRecording","PageRecorder"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var t=require("rrweb"),s=require("pako");class e{constructor(t,s){this.baseUrl=t,this.apiKey=s}async request(t,s={}){const e=`${this.baseUrl}${t}`,i={"Content-Type":"application/json","X-API-Key":this.apiKey,...s.headers},n=await fetch(e,{...s,headers:i});if(!n.ok){const t=await n.json().catch(()=>({}));throw new Error(t.message||`HTTP ${n.status}`)}return await n.json()}async createSession(t){return(await this.request("/api/v1/sessions",{method:"POST",body:JSON.stringify(t)})).data}async appendEvents(t,s){return await this.request(`/api/v1/sessions/${t}/append`,{method:"PATCH",body:JSON.stringify({events:s,compressed:!0})})}async finalizeSession(t,s){return await this.request(`/api/v1/sessions/${t}/finalize`,{method:"POST",body:JSON.stringify(s)})}}const i=new class{constructor(){this.config=null,this.sessionId=null,this.recording=!1,this.stopFn=null,this.events=[],this.uploadQueue=[],this.uploadTimer=null,this.apiClient=null}async init(t){return this.config={apiKey:t.apiKey,projectId:t.projectId,apiBaseUrl:t.apiBaseUrl||"https://api.yourplatform.com",autoStart:!1!==t.autoStart,chunkSize:t.chunkSize||100,uploadInterval:t.uploadInterval||3e4,privacy:{maskAllInputs:!1,maskInputOptions:{idCard:!0,phone:!0,bankCard:!0,...t.privacy?.maskInputOptions},...t.privacy},metadata:t.metadata||{},onError:t.onError||console.error,onSessionCreated:t.onSessionCreated||(()=>{}),onUploadProgress:t.onUploadProgress||(()=>{})},this.apiClient=new e(this.config.apiBaseUrl,this.config.apiKey),this.config.autoStart&&await this.start(),this}async start(s={}){if(this.recording)console.warn("Recording already in progress");else try{const e=await this.apiClient.createSession({projectId:this.config.projectId,metadata:{...this.config.metadata,...s,url:window.location.href,userAgent:navigator.userAgent,screenResolution:`${window.screen.width}x${window.screen.height}`,startTime:(new Date).toISOString()}});this.sessionId=e.sessionId,this.recording=!0,this.events=[],this.config.onSessionCreated(e),this.stopFn=t.record({emit:t=>{this.handleEvent(t)},checkoutEveryNms:3e5,...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),this.startUploadTimer(),console.log("Recording started:",this.sessionId)}catch(t){throw this.config.onError("Failed to start recording",t),t}}async stop(){if(!this.recording)return;this.recording=!1,this.stopFn&&(this.stopFn(),this.stopFn=null),this.stopUploadTimer(),await this.flushEvents(),await this.apiClient.finalizeSession(this.sessionId,{endTime:(new Date).toISOString(),eventCount:this.events.length}),console.log("Recording stopped:",this.sessionId);const t=this.sessionId;return this.sessionId=null,this.events=[],t}handleEvent(t){this.events.push(t),this.events.length>=this.config.chunkSize&&this.flushEvents()}async flushEvents(){if(0===this.events.length||!this.sessionId)return;const t=[...this.events];this.events=[];try{const s=this.compressEvents(t);await this.apiClient.appendEvents(this.sessionId,s),this.config.onUploadProgress({sessionId:this.sessionId,uploaded:t.length,total:this.events.length+t.length})}catch(s){this.events.unshift(...t),this.config.onError("Failed to upload events",s),setTimeout(()=>this.flushEvents(),5e3)}}compressEvents(t){const e=JSON.stringify(t),i=s.gzip(e);return btoa(String.fromCharCode(...i))}getPrivacyConfig(){return{blockClass:"rr-block",maskAllInputs:this.config.privacy.maskAllInputs,maskInputOptions:this.config.privacy.maskInputOptions,maskTextFn:(t,s)=>this.maskSensitiveData(t,s)}}maskSensitiveData(t,s){if(!t)return t;const e=t.trim();return this.config.privacy.maskInputOptions.idCard&&/^\d{15}|\d{17}[\dXx]$/.test(e)?e.replace(/(\d{6})\d{8}(\d{4})/,"$1********$2"):this.config.privacy.maskInputOptions.phone&&/^1[3-9]\d{9}$/.test(e)?e.replace(/(\d{3})\d{4}(\d{4})/,"$1****$2"):this.config.privacy.maskInputOptions.bankCard&&/^\d{16,19}$/.test(e)?e.replace(/(\d{6})\d+(\d{3})/,"$1******$2"):t}startUploadTimer(){this.uploadTimer=setInterval(()=>{this.flushEvents()},this.config.uploadInterval)}stopUploadTimer(){this.uploadTimer&&(clearInterval(this.uploadTimer),this.uploadTimer=null)}setMetadata(t){this.config.metadata={...this.config.metadata,...t}}getSessionId(){return this.sessionId}isRecording(){return this.recording}};"undefined"!=typeof window&&(window.PageRecorder=i),module.exports=i;
1
+ "use strict";var e=require("rrweb"),s=require("pako");class t{constructor(e,s){this.baseUrl=e,this.apiKey=s}async request(e,s={}){const t=`${this.baseUrl}${e}`,i={"Content-Type":"application/json","X-API-Key":this.apiKey,...s.headers},n=await fetch(t,{...s,headers:i});if(!n.ok){const e=await n.json().catch(()=>({})),s=new Error(e.message||`HTTP ${n.status}`);throw s.code=e.code,s.status=n.status,s}return await n.json()}async createSession(e){return(await this.request("/api/v1/sessions",{method:"POST",body:JSON.stringify(e)})).data}async appendEvents(e,s,t){try{return await this.request(`/api/v1/sessions/${e}/append`,{method:"PATCH",body:JSON.stringify({events:s,compressed:!0,sequence:t})})}catch(e){if("SESSION_FINALIZING"===e.code||"SESSION_NOT_RECORDING"===e.code)return console.warn("Session is finalizing or not recording, skipping append"),null;throw e}}async finalizeSession(e,s){try{return await this.request(`/api/v1/sessions/${e}/finalize`,{method:"POST",body:JSON.stringify(s)})}catch(s){if("SESSION_ALREADY_FINALIZING"===s.code)return console.log("Session already finalizing, waiting..."),await new Promise(e=>setTimeout(e,2e3)),await this.getSessionStatus(e);if("SESSION_ALREADY_COMPLETED"===s.code)return console.log("Session already completed"),await this.getSessionStatus(e);throw s}}async getSessionStatus(e){try{return await this.request(`/api/v1/sessions/${e}`,{method:"GET"})}catch(e){return console.error("Failed to get session status:",e),null}}}const i=new class{constructor(){this.config=null,this.sessionId=null,this.recording=!1,this.stopFn=null,this.events=[],this.uploadQueue=[],this.uploadTimer=null,this.apiClient=null,this.appendSequence=0,this.failedAppends=new Map,this.totalEvents=0}async init(e){return this.config={apiKey:e.apiKey,projectId:e.projectId,apiBaseUrl:e.apiBaseUrl||"https://api.yourplatform.com",autoStart:!1!==e.autoStart,chunkSize:e.chunkSize||100,uploadInterval:e.uploadInterval||3e4,privacy:{maskAllInputs:!1,maskInputOptions:{idCard:!0,phone:!0,bankCard:!0,...e.privacy?.maskInputOptions},...e.privacy},metadata:e.metadata||{},onError:e.onError||console.error,onSessionCreated:e.onSessionCreated||(()=>{}),onUploadProgress:e.onUploadProgress||(()=>{})},this.apiClient=new t(this.config.apiBaseUrl,this.config.apiKey),this.config.autoStart&&await this.start(),this}async start(s={}){if(this.recording)console.warn("Recording already in progress");else try{const t=await this.apiClient.createSession({projectId:this.config.projectId,metadata:{...this.config.metadata,...s,url:window.location.href,userAgent:navigator.userAgent,screenResolution:`${window.screen.width}x${window.screen.height}`,startTime:(new Date).toISOString()}});this.sessionId=t.sessionId,this.recording=!0,this.events=[],this.totalEvents=0,this.config.onSessionCreated(t),this.stopFn=e.record({emit:e=>{this.handleEvent(e)},checkoutEveryNms:3e5,...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),this.startUploadTimer(),console.log("Recording started:",this.sessionId)}catch(e){throw this.config.onError("Failed to start recording",e),e}}async stop(){if(!this.recording)return;this.recording=!1,this.stopFn&&(this.stopFn(),this.stopFn=null),this.stopUploadTimer(),await this.flushEvents(),this.failedAppends.size>0&&(console.warn(`Retrying ${this.failedAppends.size} failed appends before finalize...`),await this.retryFailedAppends(),await new Promise(e=>setTimeout(e,1e3)),this.failedAppends.size>0&&console.error(`Still have ${this.failedAppends.size} failed appends after retry`)),this.events.length>0&&(console.warn("Some events failed to upload, trying one more time..."),await this.flushEvents());try{await this.apiClient.finalizeSession(this.sessionId,{endTime:(new Date).toISOString(),eventCount:this.totalEvents}),console.log("Recording stopped:",this.sessionId)}catch(e){console.error("Failed to finalize session:",e),this.config.onError("Failed to finalize session",e)}const e=this.sessionId;return this.sessionId=null,this.events=[],this.totalEvents=0,e}handleEvent(e){this.events.push(e),this.events.length>=this.config.chunkSize&&this.flushEvents()}async flushEvents(){if(0===this.events.length||!this.sessionId)return;const e=[...this.events];this.totalEvents+=e.length,this.events=[],this.appendSequence++;const s=this.appendSequence;try{const t=this.compressEvents(e),i=await this.apiClient.appendEvents(this.sessionId,t,s);i?.missingSequences&&i.missingSequences.length>0&&(console.warn("Server detected missing sequences:",i.missingSequences),await this.retryMissingSequences(i.missingSequences)),this.config.onUploadProgress({sessionId:this.sessionId,sequence:s,uploaded:e.length,status:i?.status||"success"})}catch(t){this.failedAppends.set(s,{events:e,retries:0,error:t.message}),console.error(`Failed to upload sequence ${s}:`,t),this.config.onError("Failed to upload events",t),"SESSION_NOT_FOUND"!==t.code&&"SESSION_NOT_RECORDING"!==t.code&&setTimeout(()=>this.retryFailedAppends(),2e3)}}async retryFailedAppends(){if(0===this.failedAppends.size||!this.sessionId)return;const e=[];for(const[s,t]of this.failedAppends.entries())t.retries<3?e.push({sequence:s,failedAppend:t}):(console.warn(`Sequence ${s} failed after 3 retries, giving up`),this.failedAppends.delete(s));for(const{sequence:s,failedAppend:t}of e)try{const e=this.compressEvents(t.events);await this.apiClient.appendEvents(this.sessionId,e,s),console.log(`Successfully retried sequence ${s}`),this.failedAppends.delete(s)}catch(e){t.retries++,console.error(`Retry failed for sequence ${s} (attempt ${t.retries}):`,e)}}async retryMissingSequences(e){for(const s of e){const e=this.failedAppends.get(s);if(e)try{const t=this.compressEvents(e.events);await this.apiClient.appendEvents(this.sessionId,t,s),console.log(`Successfully re-sent missing sequence ${s}`),this.failedAppends.delete(s)}catch(e){console.error(`Failed to re-send missing sequence ${s}:`,e)}else console.warn(`Server requested missing sequence ${s}, but no record found`)}}compressEvents(e){const t=JSON.stringify(e),i=s.gzip(t);return btoa(String.fromCharCode(...i))}getPrivacyConfig(){return{blockClass:"rr-block",maskAllInputs:this.config.privacy.maskAllInputs,maskInputOptions:this.config.privacy.maskInputOptions,maskTextFn:(e,s)=>this.maskSensitiveData(e,s)}}maskSensitiveData(e,s){if(!e)return e;const t=e.trim();return this.config.privacy.maskInputOptions.idCard&&/^\d{15}|\d{17}[\dXx]$/.test(t)?t.replace(/(\d{6})\d{8}(\d{4})/,"$1********$2"):this.config.privacy.maskInputOptions.phone&&/^1[3-9]\d{9}$/.test(t)?t.replace(/(\d{3})\d{4}(\d{4})/,"$1****$2"):this.config.privacy.maskInputOptions.bankCard&&/^\d{16,19}$/.test(t)?t.replace(/(\d{6})\d+(\d{3})/,"$1******$2"):e}startUploadTimer(){this.uploadTimer=setInterval(()=>{this.flushEvents()},this.config.uploadInterval)}stopUploadTimer(){this.uploadTimer&&(clearInterval(this.uploadTimer),this.uploadTimer=null)}setMetadata(e){this.config.metadata={...this.config.metadata,...e}}getSessionId(){return this.sessionId}isRecording(){return this.recording}};"undefined"!=typeof window&&(window.PageRecorder=i),module.exports=i;
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["/**\n * 页面录制系统 JavaScript SDK\n * 用于页面会话录制和回溯\n */\n\nimport { record } from 'rrweb';\nimport pako from 'pako';\n\nclass PageRecorder {\n constructor() {\n this.config = null;\n this.sessionId = null;\n this.recording = false;\n this.stopFn = null;\n this.events = [];\n this.uploadQueue = [];\n this.uploadTimer = null;\n this.apiClient = null;\n }\n\n /**\n * 初始化录制器\n */\n async init(config) {\n this.config = {\n apiKey: config.apiKey,\n projectId: config.projectId,\n apiBaseUrl: config.apiBaseUrl || 'https://api.yourplatform.com',\n autoStart: config.autoStart !== false,\n chunkSize: config.chunkSize || 100,\n uploadInterval: config.uploadInterval || 30000, // 30秒\n privacy: {\n maskAllInputs: false,\n maskInputOptions: {\n idCard: true,\n phone: true,\n bankCard: true,\n ...config.privacy?.maskInputOptions,\n },\n ...config.privacy,\n },\n metadata: config.metadata || {},\n onError: config.onError || console.error,\n onSessionCreated: config.onSessionCreated || (() => {}),\n onUploadProgress: config.onUploadProgress || (() => {}),\n };\n\n // 初始化 API 客户端\n this.apiClient = new APIClient(this.config.apiBaseUrl, this.config.apiKey);\n\n // 自动开始录制\n if (this.config.autoStart) {\n await this.start();\n }\n\n return this;\n }\n\n /**\n * 开始录制\n */\n async start(customMetadata = {}) {\n if (this.recording) {\n console.warn('Recording already in progress');\n return;\n }\n\n try {\n // 创建会话\n const session = await this.apiClient.createSession({\n projectId: this.config.projectId,\n metadata: {\n ...this.config.metadata,\n ...customMetadata,\n url: window.location.href,\n userAgent: navigator.userAgent,\n screenResolution: `${window.screen.width}x${window.screen.height}`,\n startTime: new Date().toISOString(),\n },\n });\n\n this.sessionId = session.sessionId;\n this.recording = true;\n this.events = [];\n\n // 通知会话创建\n this.config.onSessionCreated(session);\n\n // 开始 rrweb 录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\n checkoutEveryNms: 5 * 60 * 1000,\n ...this.getPrivacyConfig(),\n sampling: {\n mousemove: true,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n recordCanvas: false,\n inlineImages: false,\n collectFonts: false,\n });\n\n // 启动定时上传\n this.startUploadTimer();\n\n console.log('Recording started:', this.sessionId);\n } catch (error) {\n this.config.onError('Failed to start recording', error);\n throw error;\n }\n }\n\n /**\n * 停止录制\n */\n async stop() {\n if (!this.recording) {\n return;\n }\n\n this.recording = false;\n\n // 停止 rrweb 录制\n if (this.stopFn) {\n this.stopFn();\n this.stopFn = null;\n }\n\n // 停止定时上传\n this.stopUploadTimer();\n\n // 上传剩余事件\n await this.flushEvents();\n\n // 完成会话\n await this.apiClient.finalizeSession(this.sessionId, {\n endTime: new Date().toISOString(),\n eventCount: this.events.length,\n });\n\n console.log('Recording stopped:', this.sessionId);\n\n const sessionId = this.sessionId;\n this.sessionId = null;\n this.events = [];\n\n return sessionId;\n }\n\n /**\n * 处理录制事件\n */\n handleEvent(event) {\n this.events.push(event);\n\n // 批量上传\n if (this.events.length >= this.config.chunkSize) {\n this.flushEvents();\n }\n }\n\n /**\n * 上传事件\n */\n async flushEvents() {\n if (this.events.length === 0 || !this.sessionId) {\n return;\n }\n\n const eventsToUpload = [...this.events];\n this.events = [];\n\n try {\n // 压缩数据\n const compressed = this.compressEvents(eventsToUpload);\n\n // 上传\n await this.apiClient.appendEvents(this.sessionId, compressed);\n\n // 通知上传进度\n this.config.onUploadProgress({\n sessionId: this.sessionId,\n uploaded: eventsToUpload.length,\n total: this.events.length + eventsToUpload.length,\n });\n } catch (error) {\n // 上传失败,放回队列\n this.events.unshift(...eventsToUpload);\n this.config.onError('Failed to upload events', error);\n\n // 重试\n setTimeout(() => this.flushEvents(), 5000);\n }\n }\n\n /**\n * 压缩事件数据\n */\n compressEvents(events) {\n const json = JSON.stringify(events);\n const compressed = pako.gzip(json);\n return btoa(String.fromCharCode(...compressed));\n }\n\n /**\n * 获取隐私配置\n */\n getPrivacyConfig() {\n return {\n blockClass: 'rr-block',\n maskAllInputs: this.config.privacy.maskAllInputs,\n maskInputOptions: this.config.privacy.maskInputOptions,\n maskTextFn: (text, element) => {\n return this.maskSensitiveData(text, element);\n },\n };\n }\n\n /**\n * 脱敏敏感数据\n */\n maskSensitiveData(text, element) {\n if (!text) return text;\n\n const trimmed = text.trim();\n\n // 身份证\n if (this.config.privacy.maskInputOptions.idCard && /^\\d{15}|\\d{17}[\\dXx]$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d{8}(\\d{4})/, '$1********$2');\n }\n\n // 手机号\n if (this.config.privacy.maskInputOptions.phone && /^1[3-9]\\d{9}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{3})\\d{4}(\\d{4})/, '$1****$2');\n }\n\n // 银行卡\n if (this.config.privacy.maskInputOptions.bankCard && /^\\d{16,19}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d+(\\d{3})/, '$1******$2');\n }\n\n return text;\n }\n\n /**\n * 启动定时上传\n */\n startUploadTimer() {\n this.uploadTimer = setInterval(() => {\n this.flushEvents();\n }, this.config.uploadInterval);\n }\n\n /**\n * 停止定时上传\n */\n stopUploadTimer() {\n if (this.uploadTimer) {\n clearInterval(this.uploadTimer);\n this.uploadTimer = null;\n }\n }\n\n /**\n * 添加自定义元数据\n */\n setMetadata(metadata) {\n this.config.metadata = {\n ...this.config.metadata,\n ...metadata,\n };\n }\n\n /**\n * 获取当前会话 ID\n */\n getSessionId() {\n return this.sessionId;\n }\n\n /**\n * 是否正在录制\n */\n isRecording() {\n return this.recording;\n }\n}\n\n/**\n * API 客户端\n */\nclass APIClient {\n constructor(baseUrl, apiKey) {\n this.baseUrl = baseUrl;\n this.apiKey = apiKey;\n }\n\n async request(endpoint, options = {}) {\n const url = `${this.baseUrl}${endpoint}`;\n const headers = {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n ...options.headers,\n };\n\n const response = await fetch(url, {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.message || `HTTP ${response.status}`);\n }\n\n return await response.json();\n }\n\n async createSession(data) {\n const result = await this.request('/api/v1/sessions', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n return result.data;\n }\n\n async appendEvents(sessionId, compressedEvents) {\n return await this.request(`/api/v1/sessions/${sessionId}/append`, {\n method: 'PATCH',\n body: JSON.stringify({\n events: compressedEvents,\n compressed: true,\n }),\n });\n }\n\n async finalizeSession(sessionId, data) {\n return await this.request(`/api/v1/sessions/${sessionId}/finalize`, {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n}\n\n// 导出单例\nconst recorder = new PageRecorder();\n\n// 全局对象\nif (typeof window !== 'undefined') {\n window.PageRecorder = recorder;\n}\n\nexport default recorder;\n\n"],"names":["APIClient","constructor","baseUrl","apiKey","this","request","endpoint","options","url","headers","response","fetch","ok","error","json","catch","Error","message","status","createSession","data","method","body","JSON","stringify","appendEvents","sessionId","compressedEvents","events","compressed","finalizeSession","recorder","config","recording","stopFn","uploadQueue","uploadTimer","apiClient","init","projectId","apiBaseUrl","autoStart","chunkSize","uploadInterval","privacy","maskAllInputs","maskInputOptions","idCard","phone","bankCard","metadata","onError","console","onSessionCreated","onUploadProgress","start","customMetadata","warn","session","window","location","href","userAgent","navigator","screenResolution","screen","width","height","startTime","Date","toISOString","record","emit","event","handleEvent","checkoutEveryNms","getPrivacyConfig","sampling","mousemove","mouseInteraction","scroll","input","recordCanvas","inlineImages","collectFonts","startUploadTimer","log","stop","stopUploadTimer","flushEvents","endTime","eventCount","length","push","eventsToUpload","compressEvents","uploaded","total","unshift","setTimeout","pako","gzip","btoa","String","fromCharCode","blockClass","maskTextFn","text","element","maskSensitiveData","trimmed","trim","test","replace","setInterval","clearInterval","setMetadata","getSessionId","isRecording","PageRecorder"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["/**\n * 页面录制系统 JavaScript SDK\n * 用于页面会话录制和回溯\n */\n\nimport { record } from 'rrweb';\nimport pako from 'pako';\n\nclass PageRecorder {\n constructor() {\n this.config = null;\n this.sessionId = null;\n this.recording = false;\n this.stopFn = null;\n this.events = [];\n this.uploadQueue = [];\n this.uploadTimer = null;\n this.apiClient = null;\n this.appendSequence = 0; // 序列号计数器\n this.failedAppends = new Map(); // 失败的序列号记录 {sequence: {events, retries}}\n this.totalEvents = 0; // 累计事件总数\n }\n\n /**\n * 初始化录制器\n */\n async init(config) {\n this.config = {\n apiKey: config.apiKey,\n projectId: config.projectId,\n apiBaseUrl: config.apiBaseUrl || 'https://api.yourplatform.com',\n autoStart: config.autoStart !== false,\n chunkSize: config.chunkSize || 100,\n uploadInterval: config.uploadInterval || 30000, // 30秒\n privacy: {\n maskAllInputs: false,\n maskInputOptions: {\n idCard: true,\n phone: true,\n bankCard: true,\n ...config.privacy?.maskInputOptions,\n },\n ...config.privacy,\n },\n metadata: config.metadata || {},\n onError: config.onError || console.error,\n onSessionCreated: config.onSessionCreated || (() => {}),\n onUploadProgress: config.onUploadProgress || (() => {}),\n };\n\n // 初始化 API 客户端\n this.apiClient = new APIClient(this.config.apiBaseUrl, this.config.apiKey);\n\n // 自动开始录制\n if (this.config.autoStart) {\n await this.start();\n }\n\n return this;\n }\n\n /**\n * 开始录制\n */\n async start(customMetadata = {}) {\n if (this.recording) {\n console.warn('Recording already in progress');\n return;\n }\n\n try {\n // 创建会话\n const session = await this.apiClient.createSession({\n projectId: this.config.projectId,\n metadata: {\n ...this.config.metadata,\n ...customMetadata,\n url: window.location.href,\n userAgent: navigator.userAgent,\n screenResolution: `${window.screen.width}x${window.screen.height}`,\n startTime: new Date().toISOString(),\n },\n });\n\n this.sessionId = session.sessionId;\n this.recording = true;\n this.events = [];\n this.totalEvents = 0; // 重置累计计数\n\n // 通知会话创建\n this.config.onSessionCreated(session);\n\n // 开始 rrweb 录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\n checkoutEveryNms: 5 * 60 * 1000,\n ...this.getPrivacyConfig(),\n sampling: {\n mousemove: true,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n recordCanvas: false,\n inlineImages: false,\n collectFonts: false,\n });\n\n // 启动定时上传\n this.startUploadTimer();\n\n console.log('Recording started:', this.sessionId);\n } catch (error) {\n this.config.onError('Failed to start recording', error);\n throw error;\n }\n }\n\n /**\n * 停止录制\n */\n async stop() {\n if (!this.recording) {\n return;\n }\n\n this.recording = false;\n\n // 停止 rrweb 录制\n if (this.stopFn) {\n this.stopFn();\n this.stopFn = null;\n }\n\n // 停止定时上传\n this.stopUploadTimer();\n\n // 上传剩余事件并等待所有 pending 请求完成\n await this.flushEvents();\n\n // 等待失败的序列号重传完成\n if (this.failedAppends.size > 0) {\n console.warn(`Retrying ${this.failedAppends.size} failed appends before finalize...`);\n await this.retryFailedAppends();\n \n // 再给一次机会\n await new Promise(resolve => setTimeout(resolve, 1000));\n if (this.failedAppends.size > 0) {\n console.error(`Still have ${this.failedAppends.size} failed appends after retry`);\n }\n }\n\n // 确认队列为空\n if (this.events.length > 0) {\n console.warn('Some events failed to upload, trying one more time...');\n await this.flushEvents();\n }\n\n // 完成会话\n try {\n await this.apiClient.finalizeSession(this.sessionId, {\n endTime: new Date().toISOString(),\n eventCount: this.totalEvents, // 使用累计值而非 this.events.length\n });\n console.log('Recording stopped:', this.sessionId);\n } catch (error) {\n console.error('Failed to finalize session:', error);\n this.config.onError('Failed to finalize session', error);\n }\n\n const sessionId = this.sessionId;\n this.sessionId = null;\n this.events = [];\n this.totalEvents = 0; // 清空累计计数\n\n return sessionId;\n }\n\n /**\n * 处理录制事件\n */\n handleEvent(event) {\n this.events.push(event);\n\n // 批量上传\n if (this.events.length >= this.config.chunkSize) {\n this.flushEvents();\n }\n }\n\n /**\n * 上传事件(带序列号)\n */\n async flushEvents() {\n if (this.events.length === 0 || !this.sessionId) {\n return;\n }\n\n const eventsToUpload = [...this.events];\n this.totalEvents += eventsToUpload.length; // 累加事件数\n this.events = [];\n\n // 分配序列号\n this.appendSequence++;\n const sequence = this.appendSequence;\n\n try {\n // 压缩数据\n const compressed = this.compressEvents(eventsToUpload);\n\n // 上传(带序列号)\n const result = await this.apiClient.appendEvents(\n this.sessionId, \n compressed,\n sequence\n );\n\n // 检查是否需要重传\n if (result?.missingSequences && result.missingSequences.length > 0) {\n console.warn('Server detected missing sequences:', result.missingSequences);\n // 重传缺失的序列号\n await this.retryMissingSequences(result.missingSequences);\n }\n\n // 通知上传进度\n this.config.onUploadProgress({\n sessionId: this.sessionId,\n sequence: sequence,\n uploaded: eventsToUpload.length,\n status: result?.status || 'success',\n });\n } catch (error) {\n // 记录失败的序列号,供重传\n this.failedAppends.set(sequence, {\n events: eventsToUpload,\n retries: 0,\n error: error.message,\n });\n \n console.error(`Failed to upload sequence ${sequence}:`, error);\n this.config.onError('Failed to upload events', error);\n \n // 如果不是严重错误,稍后重试\n if (error.code !== 'SESSION_NOT_FOUND' && error.code !== 'SESSION_NOT_RECORDING') {\n setTimeout(() => this.retryFailedAppends(), 2000);\n }\n }\n }\n\n /**\n * 重传失败的序列号\n */\n async retryFailedAppends() {\n if (this.failedAppends.size === 0 || !this.sessionId) {\n return;\n }\n\n const maxRetries = 3;\n const toRetry = [];\n\n for (const [sequence, failedAppend] of this.failedAppends.entries()) {\n if (failedAppend.retries < maxRetries) {\n toRetry.push({ sequence, failedAppend });\n } else {\n console.warn(`Sequence ${sequence} failed after ${maxRetries} retries, giving up`);\n this.failedAppends.delete(sequence);\n }\n }\n\n for (const { sequence, failedAppend } of toRetry) {\n try {\n const compressed = this.compressEvents(failedAppend.events);\n await this.apiClient.appendEvents(this.sessionId, compressed, sequence);\n \n console.log(`Successfully retried sequence ${sequence}`);\n this.failedAppends.delete(sequence);\n } catch (error) {\n failedAppend.retries++;\n console.error(`Retry failed for sequence ${sequence} (attempt ${failedAppend.retries}):`, error);\n }\n }\n }\n\n /**\n * 重传服务器检测到的缺失序列号\n */\n async retryMissingSequences(missingSequences) {\n for (const sequence of missingSequences) {\n const failedAppend = this.failedAppends.get(sequence);\n if (failedAppend) {\n try {\n const compressed = this.compressEvents(failedAppend.events);\n await this.apiClient.appendEvents(this.sessionId, compressed, sequence);\n \n console.log(`Successfully re-sent missing sequence ${sequence}`);\n this.failedAppends.delete(sequence);\n } catch (error) {\n console.error(`Failed to re-send missing sequence ${sequence}:`, error);\n }\n } else {\n console.warn(`Server requested missing sequence ${sequence}, but no record found`);\n }\n }\n }\n\n /**\n * 压缩事件数据\n */\n compressEvents(events) {\n const json = JSON.stringify(events);\n const compressed = pako.gzip(json);\n return btoa(String.fromCharCode(...compressed));\n }\n\n /**\n * 获取隐私配置\n */\n getPrivacyConfig() {\n return {\n blockClass: 'rr-block',\n maskAllInputs: this.config.privacy.maskAllInputs,\n maskInputOptions: this.config.privacy.maskInputOptions,\n maskTextFn: (text, element) => {\n return this.maskSensitiveData(text, element);\n },\n };\n }\n\n /**\n * 脱敏敏感数据\n */\n maskSensitiveData(text, element) {\n if (!text) return text;\n\n const trimmed = text.trim();\n\n // 身份证\n if (this.config.privacy.maskInputOptions.idCard && /^\\d{15}|\\d{17}[\\dXx]$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d{8}(\\d{4})/, '$1********$2');\n }\n\n // 手机号\n if (this.config.privacy.maskInputOptions.phone && /^1[3-9]\\d{9}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{3})\\d{4}(\\d{4})/, '$1****$2');\n }\n\n // 银行卡\n if (this.config.privacy.maskInputOptions.bankCard && /^\\d{16,19}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d+(\\d{3})/, '$1******$2');\n }\n\n return text;\n }\n\n /**\n * 启动定时上传\n */\n startUploadTimer() {\n this.uploadTimer = setInterval(() => {\n this.flushEvents();\n }, this.config.uploadInterval);\n }\n\n /**\n * 停止定时上传\n */\n stopUploadTimer() {\n if (this.uploadTimer) {\n clearInterval(this.uploadTimer);\n this.uploadTimer = null;\n }\n }\n\n /**\n * 添加自定义元数据\n */\n setMetadata(metadata) {\n this.config.metadata = {\n ...this.config.metadata,\n ...metadata,\n };\n }\n\n /**\n * 获取当前会话 ID\n */\n getSessionId() {\n return this.sessionId;\n }\n\n /**\n * 是否正在录制\n */\n isRecording() {\n return this.recording;\n }\n}\n\n/**\n * API 客户端\n */\nclass APIClient {\n constructor(baseUrl, apiKey) {\n this.baseUrl = baseUrl;\n this.apiKey = apiKey;\n }\n\n async request(endpoint, options = {}) {\n const url = `${this.baseUrl}${endpoint}`;\n const headers = {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n ...options.headers,\n };\n\n const response = await fetch(url, {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n const errorObj = new Error(error.message || `HTTP ${response.status}`);\n errorObj.code = error.code;\n errorObj.status = response.status;\n throw errorObj;\n }\n\n return await response.json();\n }\n\n async createSession(data) {\n const result = await this.request('/api/v1/sessions', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n return result.data;\n }\n\n async appendEvents(sessionId, compressedEvents, sequence) {\n try {\n return await this.request(`/api/v1/sessions/${sessionId}/append`, {\n method: 'PATCH',\n body: JSON.stringify({\n events: compressedEvents,\n compressed: true,\n sequence: sequence, // 新增序列号\n }),\n });\n } catch (error) {\n // 如果会话正在 finalize,不重试\n if (error.code === 'SESSION_FINALIZING' || error.code === 'SESSION_NOT_RECORDING') {\n console.warn('Session is finalizing or not recording, skipping append');\n return null;\n }\n throw error;\n }\n }\n\n async finalizeSession(sessionId, data) {\n try {\n return await this.request(`/api/v1/sessions/${sessionId}/finalize`, {\n method: 'POST',\n body: JSON.stringify(data),\n });\n } catch (error) {\n // 如果会话已经在 finalize 或已完成,等待并获取状态\n if (error.code === 'SESSION_ALREADY_FINALIZING') {\n console.log('Session already finalizing, waiting...');\n await new Promise(resolve => setTimeout(resolve, 2000));\n // 尝试再次获取状态\n return await this.getSessionStatus(sessionId);\n }\n \n if (error.code === 'SESSION_ALREADY_COMPLETED') {\n console.log('Session already completed');\n return await this.getSessionStatus(sessionId);\n }\n \n throw error;\n }\n }\n\n async getSessionStatus(sessionId) {\n // 获取会话状态(假设有这个端点)\n try {\n return await this.request(`/api/v1/sessions/${sessionId}`, {\n method: 'GET',\n });\n } catch (error) {\n console.error('Failed to get session status:', error);\n return null;\n }\n }\n}\n\n// 导出单例\nconst recorder = new PageRecorder();\n\n// 全局对象\nif (typeof window !== 'undefined') {\n window.PageRecorder = recorder;\n}\n\nexport default recorder;\n\n"],"names":["APIClient","constructor","baseUrl","apiKey","this","request","endpoint","options","url","headers","response","fetch","ok","error","json","catch","errorObj","Error","message","status","code","createSession","data","method","body","JSON","stringify","appendEvents","sessionId","compressedEvents","sequence","events","compressed","console","warn","finalizeSession","log","Promise","resolve","setTimeout","getSessionStatus","recorder","config","recording","stopFn","uploadQueue","uploadTimer","apiClient","appendSequence","failedAppends","Map","totalEvents","init","projectId","apiBaseUrl","autoStart","chunkSize","uploadInterval","privacy","maskAllInputs","maskInputOptions","idCard","phone","bankCard","metadata","onError","onSessionCreated","onUploadProgress","start","customMetadata","session","window","location","href","userAgent","navigator","screenResolution","screen","width","height","startTime","Date","toISOString","record","emit","event","handleEvent","checkoutEveryNms","getPrivacyConfig","sampling","mousemove","mouseInteraction","scroll","input","recordCanvas","inlineImages","collectFonts","startUploadTimer","stop","stopUploadTimer","flushEvents","size","retryFailedAppends","length","endTime","eventCount","push","eventsToUpload","compressEvents","result","missingSequences","retryMissingSequences","uploaded","set","retries","toRetry","failedAppend","entries","delete","get","pako","gzip","btoa","String","fromCharCode","blockClass","maskTextFn","text","element","maskSensitiveData","trimmed","trim","test","replace","setInterval","clearInterval","setMetadata","getSessionId","isRecording","PageRecorder"],"mappings":"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"}
package/dist/index.umd.js CHANGED
@@ -1,2 +1,2 @@
1
- !function(t,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s(require("rrweb"),require("pako")):"function"==typeof define&&define.amd?define(["rrweb","pako"],s):(t="undefined"!=typeof globalThis?globalThis:t||self).PageRecorder=s(t.rrweb,t.pako)}(this,function(t,s){"use strict";class e{constructor(t,s){this.baseUrl=t,this.apiKey=s}async request(t,s={}){const e=`${this.baseUrl}${t}`,i={"Content-Type":"application/json","X-API-Key":this.apiKey,...s.headers},n=await fetch(e,{...s,headers:i});if(!n.ok){const t=await n.json().catch(()=>({}));throw new Error(t.message||`HTTP ${n.status}`)}return await n.json()}async createSession(t){return(await this.request("/api/v1/sessions",{method:"POST",body:JSON.stringify(t)})).data}async appendEvents(t,s){return await this.request(`/api/v1/sessions/${t}/append`,{method:"PATCH",body:JSON.stringify({events:s,compressed:!0})})}async finalizeSession(t,s){return await this.request(`/api/v1/sessions/${t}/finalize`,{method:"POST",body:JSON.stringify(s)})}}const i=new class{constructor(){this.config=null,this.sessionId=null,this.recording=!1,this.stopFn=null,this.events=[],this.uploadQueue=[],this.uploadTimer=null,this.apiClient=null}async init(t){return this.config={apiKey:t.apiKey,projectId:t.projectId,apiBaseUrl:t.apiBaseUrl||"https://api.yourplatform.com",autoStart:!1!==t.autoStart,chunkSize:t.chunkSize||100,uploadInterval:t.uploadInterval||3e4,privacy:{maskAllInputs:!1,maskInputOptions:{idCard:!0,phone:!0,bankCard:!0,...t.privacy?.maskInputOptions},...t.privacy},metadata:t.metadata||{},onError:t.onError||console.error,onSessionCreated:t.onSessionCreated||(()=>{}),onUploadProgress:t.onUploadProgress||(()=>{})},this.apiClient=new e(this.config.apiBaseUrl,this.config.apiKey),this.config.autoStart&&await this.start(),this}async start(s={}){if(this.recording)console.warn("Recording already in progress");else try{const e=await this.apiClient.createSession({projectId:this.config.projectId,metadata:{...this.config.metadata,...s,url:window.location.href,userAgent:navigator.userAgent,screenResolution:`${window.screen.width}x${window.screen.height}`,startTime:(new Date).toISOString()}});this.sessionId=e.sessionId,this.recording=!0,this.events=[],this.config.onSessionCreated(e),this.stopFn=t.record({emit:t=>{this.handleEvent(t)},checkoutEveryNms:3e5,...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),this.startUploadTimer(),console.log("Recording started:",this.sessionId)}catch(t){throw this.config.onError("Failed to start recording",t),t}}async stop(){if(!this.recording)return;this.recording=!1,this.stopFn&&(this.stopFn(),this.stopFn=null),this.stopUploadTimer(),await this.flushEvents(),await this.apiClient.finalizeSession(this.sessionId,{endTime:(new Date).toISOString(),eventCount:this.events.length}),console.log("Recording stopped:",this.sessionId);const t=this.sessionId;return this.sessionId=null,this.events=[],t}handleEvent(t){this.events.push(t),this.events.length>=this.config.chunkSize&&this.flushEvents()}async flushEvents(){if(0===this.events.length||!this.sessionId)return;const t=[...this.events];this.events=[];try{const s=this.compressEvents(t);await this.apiClient.appendEvents(this.sessionId,s),this.config.onUploadProgress({sessionId:this.sessionId,uploaded:t.length,total:this.events.length+t.length})}catch(s){this.events.unshift(...t),this.config.onError("Failed to upload events",s),setTimeout(()=>this.flushEvents(),5e3)}}compressEvents(t){const e=JSON.stringify(t),i=s.gzip(e);return btoa(String.fromCharCode(...i))}getPrivacyConfig(){return{blockClass:"rr-block",maskAllInputs:this.config.privacy.maskAllInputs,maskInputOptions:this.config.privacy.maskInputOptions,maskTextFn:(t,s)=>this.maskSensitiveData(t,s)}}maskSensitiveData(t,s){if(!t)return t;const e=t.trim();return this.config.privacy.maskInputOptions.idCard&&/^\d{15}|\d{17}[\dXx]$/.test(e)?e.replace(/(\d{6})\d{8}(\d{4})/,"$1********$2"):this.config.privacy.maskInputOptions.phone&&/^1[3-9]\d{9}$/.test(e)?e.replace(/(\d{3})\d{4}(\d{4})/,"$1****$2"):this.config.privacy.maskInputOptions.bankCard&&/^\d{16,19}$/.test(e)?e.replace(/(\d{6})\d+(\d{3})/,"$1******$2"):t}startUploadTimer(){this.uploadTimer=setInterval(()=>{this.flushEvents()},this.config.uploadInterval)}stopUploadTimer(){this.uploadTimer&&(clearInterval(this.uploadTimer),this.uploadTimer=null)}setMetadata(t){this.config.metadata={...this.config.metadata,...t}}getSessionId(){return this.sessionId}isRecording(){return this.recording}};return"undefined"!=typeof window&&(window.PageRecorder=i),i});
1
+ !function(e,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s(require("rrweb"),require("pako")):"function"==typeof define&&define.amd?define(["rrweb","pako"],s):(e="undefined"!=typeof globalThis?globalThis:e||self).PageRecorder=s(e.rrweb,e.pako)}(this,function(e,s){"use strict";class t{constructor(e,s){this.baseUrl=e,this.apiKey=s}async request(e,s={}){const t=`${this.baseUrl}${e}`,i={"Content-Type":"application/json","X-API-Key":this.apiKey,...s.headers},n=await fetch(t,{...s,headers:i});if(!n.ok){const e=await n.json().catch(()=>({})),s=new Error(e.message||`HTTP ${n.status}`);throw s.code=e.code,s.status=n.status,s}return await n.json()}async createSession(e){return(await this.request("/api/v1/sessions",{method:"POST",body:JSON.stringify(e)})).data}async appendEvents(e,s,t){try{return await this.request(`/api/v1/sessions/${e}/append`,{method:"PATCH",body:JSON.stringify({events:s,compressed:!0,sequence:t})})}catch(e){if("SESSION_FINALIZING"===e.code||"SESSION_NOT_RECORDING"===e.code)return console.warn("Session is finalizing or not recording, skipping append"),null;throw e}}async finalizeSession(e,s){try{return await this.request(`/api/v1/sessions/${e}/finalize`,{method:"POST",body:JSON.stringify(s)})}catch(s){if("SESSION_ALREADY_FINALIZING"===s.code)return console.log("Session already finalizing, waiting..."),await new Promise(e=>setTimeout(e,2e3)),await this.getSessionStatus(e);if("SESSION_ALREADY_COMPLETED"===s.code)return console.log("Session already completed"),await this.getSessionStatus(e);throw s}}async getSessionStatus(e){try{return await this.request(`/api/v1/sessions/${e}`,{method:"GET"})}catch(e){return console.error("Failed to get session status:",e),null}}}const i=new class{constructor(){this.config=null,this.sessionId=null,this.recording=!1,this.stopFn=null,this.events=[],this.uploadQueue=[],this.uploadTimer=null,this.apiClient=null,this.appendSequence=0,this.failedAppends=new Map,this.totalEvents=0}async init(e){return this.config={apiKey:e.apiKey,projectId:e.projectId,apiBaseUrl:e.apiBaseUrl||"https://api.yourplatform.com",autoStart:!1!==e.autoStart,chunkSize:e.chunkSize||100,uploadInterval:e.uploadInterval||3e4,privacy:{maskAllInputs:!1,maskInputOptions:{idCard:!0,phone:!0,bankCard:!0,...e.privacy?.maskInputOptions},...e.privacy},metadata:e.metadata||{},onError:e.onError||console.error,onSessionCreated:e.onSessionCreated||(()=>{}),onUploadProgress:e.onUploadProgress||(()=>{})},this.apiClient=new t(this.config.apiBaseUrl,this.config.apiKey),this.config.autoStart&&await this.start(),this}async start(s={}){if(this.recording)console.warn("Recording already in progress");else try{const t=await this.apiClient.createSession({projectId:this.config.projectId,metadata:{...this.config.metadata,...s,url:window.location.href,userAgent:navigator.userAgent,screenResolution:`${window.screen.width}x${window.screen.height}`,startTime:(new Date).toISOString()}});this.sessionId=t.sessionId,this.recording=!0,this.events=[],this.totalEvents=0,this.config.onSessionCreated(t),this.stopFn=e.record({emit:e=>{this.handleEvent(e)},checkoutEveryNms:3e5,...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),this.startUploadTimer(),console.log("Recording started:",this.sessionId)}catch(e){throw this.config.onError("Failed to start recording",e),e}}async stop(){if(!this.recording)return;this.recording=!1,this.stopFn&&(this.stopFn(),this.stopFn=null),this.stopUploadTimer(),await this.flushEvents(),this.failedAppends.size>0&&(console.warn(`Retrying ${this.failedAppends.size} failed appends before finalize...`),await this.retryFailedAppends(),await new Promise(e=>setTimeout(e,1e3)),this.failedAppends.size>0&&console.error(`Still have ${this.failedAppends.size} failed appends after retry`)),this.events.length>0&&(console.warn("Some events failed to upload, trying one more time..."),await this.flushEvents());try{await this.apiClient.finalizeSession(this.sessionId,{endTime:(new Date).toISOString(),eventCount:this.totalEvents}),console.log("Recording stopped:",this.sessionId)}catch(e){console.error("Failed to finalize session:",e),this.config.onError("Failed to finalize session",e)}const e=this.sessionId;return this.sessionId=null,this.events=[],this.totalEvents=0,e}handleEvent(e){this.events.push(e),this.events.length>=this.config.chunkSize&&this.flushEvents()}async flushEvents(){if(0===this.events.length||!this.sessionId)return;const e=[...this.events];this.totalEvents+=e.length,this.events=[],this.appendSequence++;const s=this.appendSequence;try{const t=this.compressEvents(e),i=await this.apiClient.appendEvents(this.sessionId,t,s);i?.missingSequences&&i.missingSequences.length>0&&(console.warn("Server detected missing sequences:",i.missingSequences),await this.retryMissingSequences(i.missingSequences)),this.config.onUploadProgress({sessionId:this.sessionId,sequence:s,uploaded:e.length,status:i?.status||"success"})}catch(t){this.failedAppends.set(s,{events:e,retries:0,error:t.message}),console.error(`Failed to upload sequence ${s}:`,t),this.config.onError("Failed to upload events",t),"SESSION_NOT_FOUND"!==t.code&&"SESSION_NOT_RECORDING"!==t.code&&setTimeout(()=>this.retryFailedAppends(),2e3)}}async retryFailedAppends(){if(0===this.failedAppends.size||!this.sessionId)return;const e=[];for(const[s,t]of this.failedAppends.entries())t.retries<3?e.push({sequence:s,failedAppend:t}):(console.warn(`Sequence ${s} failed after 3 retries, giving up`),this.failedAppends.delete(s));for(const{sequence:s,failedAppend:t}of e)try{const e=this.compressEvents(t.events);await this.apiClient.appendEvents(this.sessionId,e,s),console.log(`Successfully retried sequence ${s}`),this.failedAppends.delete(s)}catch(e){t.retries++,console.error(`Retry failed for sequence ${s} (attempt ${t.retries}):`,e)}}async retryMissingSequences(e){for(const s of e){const e=this.failedAppends.get(s);if(e)try{const t=this.compressEvents(e.events);await this.apiClient.appendEvents(this.sessionId,t,s),console.log(`Successfully re-sent missing sequence ${s}`),this.failedAppends.delete(s)}catch(e){console.error(`Failed to re-send missing sequence ${s}:`,e)}else console.warn(`Server requested missing sequence ${s}, but no record found`)}}compressEvents(e){const t=JSON.stringify(e),i=s.gzip(t);return btoa(String.fromCharCode(...i))}getPrivacyConfig(){return{blockClass:"rr-block",maskAllInputs:this.config.privacy.maskAllInputs,maskInputOptions:this.config.privacy.maskInputOptions,maskTextFn:(e,s)=>this.maskSensitiveData(e,s)}}maskSensitiveData(e,s){if(!e)return e;const t=e.trim();return this.config.privacy.maskInputOptions.idCard&&/^\d{15}|\d{17}[\dXx]$/.test(t)?t.replace(/(\d{6})\d{8}(\d{4})/,"$1********$2"):this.config.privacy.maskInputOptions.phone&&/^1[3-9]\d{9}$/.test(t)?t.replace(/(\d{3})\d{4}(\d{4})/,"$1****$2"):this.config.privacy.maskInputOptions.bankCard&&/^\d{16,19}$/.test(t)?t.replace(/(\d{6})\d+(\d{3})/,"$1******$2"):e}startUploadTimer(){this.uploadTimer=setInterval(()=>{this.flushEvents()},this.config.uploadInterval)}stopUploadTimer(){this.uploadTimer&&(clearInterval(this.uploadTimer),this.uploadTimer=null)}setMetadata(e){this.config.metadata={...this.config.metadata,...e}}getSessionId(){return this.sessionId}isRecording(){return this.recording}};return"undefined"!=typeof window&&(window.PageRecorder=i),i});
2
2
  //# sourceMappingURL=index.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.umd.js","sources":["../src/index.js"],"sourcesContent":["/**\n * 页面录制系统 JavaScript SDK\n * 用于页面会话录制和回溯\n */\n\nimport { record } from 'rrweb';\nimport pako from 'pako';\n\nclass PageRecorder {\n constructor() {\n this.config = null;\n this.sessionId = null;\n this.recording = false;\n this.stopFn = null;\n this.events = [];\n this.uploadQueue = [];\n this.uploadTimer = null;\n this.apiClient = null;\n }\n\n /**\n * 初始化录制器\n */\n async init(config) {\n this.config = {\n apiKey: config.apiKey,\n projectId: config.projectId,\n apiBaseUrl: config.apiBaseUrl || 'https://api.yourplatform.com',\n autoStart: config.autoStart !== false,\n chunkSize: config.chunkSize || 100,\n uploadInterval: config.uploadInterval || 30000, // 30秒\n privacy: {\n maskAllInputs: false,\n maskInputOptions: {\n idCard: true,\n phone: true,\n bankCard: true,\n ...config.privacy?.maskInputOptions,\n },\n ...config.privacy,\n },\n metadata: config.metadata || {},\n onError: config.onError || console.error,\n onSessionCreated: config.onSessionCreated || (() => {}),\n onUploadProgress: config.onUploadProgress || (() => {}),\n };\n\n // 初始化 API 客户端\n this.apiClient = new APIClient(this.config.apiBaseUrl, this.config.apiKey);\n\n // 自动开始录制\n if (this.config.autoStart) {\n await this.start();\n }\n\n return this;\n }\n\n /**\n * 开始录制\n */\n async start(customMetadata = {}) {\n if (this.recording) {\n console.warn('Recording already in progress');\n return;\n }\n\n try {\n // 创建会话\n const session = await this.apiClient.createSession({\n projectId: this.config.projectId,\n metadata: {\n ...this.config.metadata,\n ...customMetadata,\n url: window.location.href,\n userAgent: navigator.userAgent,\n screenResolution: `${window.screen.width}x${window.screen.height}`,\n startTime: new Date().toISOString(),\n },\n });\n\n this.sessionId = session.sessionId;\n this.recording = true;\n this.events = [];\n\n // 通知会话创建\n this.config.onSessionCreated(session);\n\n // 开始 rrweb 录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\n checkoutEveryNms: 5 * 60 * 1000,\n ...this.getPrivacyConfig(),\n sampling: {\n mousemove: true,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n recordCanvas: false,\n inlineImages: false,\n collectFonts: false,\n });\n\n // 启动定时上传\n this.startUploadTimer();\n\n console.log('Recording started:', this.sessionId);\n } catch (error) {\n this.config.onError('Failed to start recording', error);\n throw error;\n }\n }\n\n /**\n * 停止录制\n */\n async stop() {\n if (!this.recording) {\n return;\n }\n\n this.recording = false;\n\n // 停止 rrweb 录制\n if (this.stopFn) {\n this.stopFn();\n this.stopFn = null;\n }\n\n // 停止定时上传\n this.stopUploadTimer();\n\n // 上传剩余事件\n await this.flushEvents();\n\n // 完成会话\n await this.apiClient.finalizeSession(this.sessionId, {\n endTime: new Date().toISOString(),\n eventCount: this.events.length,\n });\n\n console.log('Recording stopped:', this.sessionId);\n\n const sessionId = this.sessionId;\n this.sessionId = null;\n this.events = [];\n\n return sessionId;\n }\n\n /**\n * 处理录制事件\n */\n handleEvent(event) {\n this.events.push(event);\n\n // 批量上传\n if (this.events.length >= this.config.chunkSize) {\n this.flushEvents();\n }\n }\n\n /**\n * 上传事件\n */\n async flushEvents() {\n if (this.events.length === 0 || !this.sessionId) {\n return;\n }\n\n const eventsToUpload = [...this.events];\n this.events = [];\n\n try {\n // 压缩数据\n const compressed = this.compressEvents(eventsToUpload);\n\n // 上传\n await this.apiClient.appendEvents(this.sessionId, compressed);\n\n // 通知上传进度\n this.config.onUploadProgress({\n sessionId: this.sessionId,\n uploaded: eventsToUpload.length,\n total: this.events.length + eventsToUpload.length,\n });\n } catch (error) {\n // 上传失败,放回队列\n this.events.unshift(...eventsToUpload);\n this.config.onError('Failed to upload events', error);\n\n // 重试\n setTimeout(() => this.flushEvents(), 5000);\n }\n }\n\n /**\n * 压缩事件数据\n */\n compressEvents(events) {\n const json = JSON.stringify(events);\n const compressed = pako.gzip(json);\n return btoa(String.fromCharCode(...compressed));\n }\n\n /**\n * 获取隐私配置\n */\n getPrivacyConfig() {\n return {\n blockClass: 'rr-block',\n maskAllInputs: this.config.privacy.maskAllInputs,\n maskInputOptions: this.config.privacy.maskInputOptions,\n maskTextFn: (text, element) => {\n return this.maskSensitiveData(text, element);\n },\n };\n }\n\n /**\n * 脱敏敏感数据\n */\n maskSensitiveData(text, element) {\n if (!text) return text;\n\n const trimmed = text.trim();\n\n // 身份证\n if (this.config.privacy.maskInputOptions.idCard && /^\\d{15}|\\d{17}[\\dXx]$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d{8}(\\d{4})/, '$1********$2');\n }\n\n // 手机号\n if (this.config.privacy.maskInputOptions.phone && /^1[3-9]\\d{9}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{3})\\d{4}(\\d{4})/, '$1****$2');\n }\n\n // 银行卡\n if (this.config.privacy.maskInputOptions.bankCard && /^\\d{16,19}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d+(\\d{3})/, '$1******$2');\n }\n\n return text;\n }\n\n /**\n * 启动定时上传\n */\n startUploadTimer() {\n this.uploadTimer = setInterval(() => {\n this.flushEvents();\n }, this.config.uploadInterval);\n }\n\n /**\n * 停止定时上传\n */\n stopUploadTimer() {\n if (this.uploadTimer) {\n clearInterval(this.uploadTimer);\n this.uploadTimer = null;\n }\n }\n\n /**\n * 添加自定义元数据\n */\n setMetadata(metadata) {\n this.config.metadata = {\n ...this.config.metadata,\n ...metadata,\n };\n }\n\n /**\n * 获取当前会话 ID\n */\n getSessionId() {\n return this.sessionId;\n }\n\n /**\n * 是否正在录制\n */\n isRecording() {\n return this.recording;\n }\n}\n\n/**\n * API 客户端\n */\nclass APIClient {\n constructor(baseUrl, apiKey) {\n this.baseUrl = baseUrl;\n this.apiKey = apiKey;\n }\n\n async request(endpoint, options = {}) {\n const url = `${this.baseUrl}${endpoint}`;\n const headers = {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n ...options.headers,\n };\n\n const response = await fetch(url, {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.message || `HTTP ${response.status}`);\n }\n\n return await response.json();\n }\n\n async createSession(data) {\n const result = await this.request('/api/v1/sessions', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n return result.data;\n }\n\n async appendEvents(sessionId, compressedEvents) {\n return await this.request(`/api/v1/sessions/${sessionId}/append`, {\n method: 'PATCH',\n body: JSON.stringify({\n events: compressedEvents,\n compressed: true,\n }),\n });\n }\n\n async finalizeSession(sessionId, data) {\n return await this.request(`/api/v1/sessions/${sessionId}/finalize`, {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n}\n\n// 导出单例\nconst recorder = new PageRecorder();\n\n// 全局对象\nif (typeof window !== 'undefined') {\n window.PageRecorder = recorder;\n}\n\nexport default recorder;\n\n"],"names":["APIClient","constructor","baseUrl","apiKey","this","request","endpoint","options","url","headers","response","fetch","ok","error","json","catch","Error","message","status","createSession","data","method","body","JSON","stringify","appendEvents","sessionId","compressedEvents","events","compressed","finalizeSession","recorder","config","recording","stopFn","uploadQueue","uploadTimer","apiClient","init","projectId","apiBaseUrl","autoStart","chunkSize","uploadInterval","privacy","maskAllInputs","maskInputOptions","idCard","phone","bankCard","metadata","onError","console","onSessionCreated","onUploadProgress","start","customMetadata","warn","session","window","location","href","userAgent","navigator","screenResolution","screen","width","height","startTime","Date","toISOString","record","emit","event","handleEvent","checkoutEveryNms","getPrivacyConfig","sampling","mousemove","mouseInteraction","scroll","input","recordCanvas","inlineImages","collectFonts","startUploadTimer","log","stop","stopUploadTimer","flushEvents","endTime","eventCount","length","push","eventsToUpload","compressEvents","uploaded","total","unshift","setTimeout","pako","gzip","btoa","String","fromCharCode","blockClass","maskTextFn","text","element","maskSensitiveData","trimmed","trim","test","replace","setInterval","clearInterval","setMetadata","getSessionId","isRecording","PageRecorder"],"mappings":"8SAuSA,MAAMA,EACJ,WAAAC,CAAYC,EAASC,GACnBC,KAAKF,QAAUA,EACfE,KAAKD,OAASA,CAChB,CAEA,aAAME,CAAQC,EAAUC,EAAU,IAChC,MAAMC,EAAM,GAAGJ,KAAKF,UAAUI,IACxBG,EAAU,CACd,eAAgB,mBAChB,YAAaL,KAAKD,UACfI,EAAQE,SAGPC,QAAiBC,MAAMH,EAAK,IAC7BD,EACHE,YAGF,IAAKC,EAASE,GAAI,CAChB,MAAMC,QAAcH,EAASI,OAAOC,MAAM,KAAA,CAAS,IACnD,MAAM,IAAIC,MAAMH,EAAMI,SAAW,QAAQP,EAASQ,SACpD,CAEA,aAAaR,EAASI,MACxB,CAEA,mBAAMK,CAAcC,GAKlB,aAJqBhB,KAAKC,QAAQ,mBAAoB,CACpDgB,OAAQ,OACRC,KAAMC,KAAKC,UAAUJ,MAETA,IAChB,CAEA,kBAAMK,CAAaC,EAAWC,GAC5B,aAAavB,KAAKC,QAAQ,oBAAoBqB,WAAoB,CAChEL,OAAQ,QACRC,KAAMC,KAAKC,UAAU,CACnBI,OAAQD,EACRE,YAAY,KAGlB,CAEA,qBAAMC,CAAgBJ,EAAWN,GAC/B,aAAahB,KAAKC,QAAQ,oBAAoBqB,aAAsB,CAClEL,OAAQ,OACRC,KAAMC,KAAKC,UAAUJ,IAEzB,EAIG,MAACW,EAAW,IArVjB,MACE,WAAA9B,GACEG,KAAK4B,OAAS,KACd5B,KAAKsB,UAAY,KACjBtB,KAAK6B,WAAY,EACjB7B,KAAK8B,OAAS,KACd9B,KAAKwB,OAAS,GACdxB,KAAK+B,YAAc,GACnB/B,KAAKgC,YAAc,KACnBhC,KAAKiC,UAAY,IACnB,CAKA,UAAMC,CAAKN,GAgCT,OA/BA5B,KAAK4B,OAAS,CACZ7B,OAAQ6B,EAAO7B,OACfoC,UAAWP,EAAOO,UAClBC,WAAYR,EAAOQ,YAAc,+BACjCC,WAAgC,IAArBT,EAAOS,UAClBC,UAAWV,EAAOU,WAAa,IAC/BC,eAAgBX,EAAOW,gBAAkB,IACzCC,QAAS,CACPC,eAAe,EACfC,iBAAkB,CAChBC,QAAQ,EACRC,OAAO,EACPC,UAAU,KACPjB,EAAOY,SAASE,qBAElBd,EAAOY,SAEZM,SAAUlB,EAAOkB,UAAY,CAAA,EAC7BC,QAASnB,EAAOmB,SAAWC,QAAQvC,MACnCwC,iBAAkBrB,EAAOqB,kBAAgB,MAAa,GACtDC,iBAAkBtB,EAAOsB,kBAAgB,MAAa,IAIxDlD,KAAKiC,UAAY,IAAIrC,EAAUI,KAAK4B,OAAOQ,WAAYpC,KAAK4B,OAAO7B,QAG/DC,KAAK4B,OAAOS,iBACRrC,KAAKmD,QAGNnD,IACT,CAKA,WAAMmD,CAAMC,EAAiB,IAC3B,GAAIpD,KAAK6B,UACPmB,QAAQK,KAAK,sCAIf,IAEE,MAAMC,QAAgBtD,KAAKiC,UAAUlB,cAAc,CACjDoB,UAAWnC,KAAK4B,OAAOO,UACvBW,SAAU,IACL9C,KAAK4B,OAAOkB,YACZM,EACHhD,IAAKmD,OAAOC,SAASC,KACrBC,UAAWC,UAAUD,UACrBE,iBAAkB,GAAGL,OAAOM,OAAOC,SAASP,OAAOM,OAAOE,SAC1DC,WAAW,IAAIC,MAAOC,iBAI1BlE,KAAKsB,UAAYgC,EAAQhC,UACzBtB,KAAK6B,WAAY,EACjB7B,KAAKwB,OAAS,GAGdxB,KAAK4B,OAAOqB,iBAAiBK,GAG7BtD,KAAK8B,OAASqC,SAAO,CACnBC,KAAOC,IACLrE,KAAKsE,YAAYD,IAEnBE,iBAAkB,OACfvE,KAAKwE,mBACRC,SAAU,CACRC,WAAW,EACXC,kBAAkB,EAClBC,OAAQ,IACRC,MAAO,QAETC,cAAc,EACdC,cAAc,EACdC,cAAc,IAIhBhF,KAAKiF,mBAELjC,QAAQkC,IAAI,qBAAsBlF,KAAKsB,UACzC,CAAE,MAAOb,GAEP,MADAT,KAAK4B,OAAOmB,QAAQ,4BAA6BtC,GAC3CA,CACR,CACF,CAKA,UAAM0E,GACJ,IAAKnF,KAAK6B,UACR,OAGF7B,KAAK6B,WAAY,EAGb7B,KAAK8B,SACP9B,KAAK8B,SACL9B,KAAK8B,OAAS,MAIhB9B,KAAKoF,wBAGCpF,KAAKqF,oBAGLrF,KAAKiC,UAAUP,gBAAgB1B,KAAKsB,UAAW,CACnDgE,SAAS,IAAIrB,MAAOC,cACpBqB,WAAYvF,KAAKwB,OAAOgE,SAG1BxC,QAAQkC,IAAI,qBAAsBlF,KAAKsB,WAEvC,MAAMA,EAAYtB,KAAKsB,UAIvB,OAHAtB,KAAKsB,UAAY,KACjBtB,KAAKwB,OAAS,GAEPF,CACT,CAKA,WAAAgD,CAAYD,GACVrE,KAAKwB,OAAOiE,KAAKpB,GAGbrE,KAAKwB,OAAOgE,QAAUxF,KAAK4B,OAAOU,WACpCtC,KAAKqF,aAET,CAKA,iBAAMA,GACJ,GAA2B,IAAvBrF,KAAKwB,OAAOgE,SAAiBxF,KAAKsB,UACpC,OAGF,MAAMoE,EAAiB,IAAI1F,KAAKwB,QAChCxB,KAAKwB,OAAS,GAEd,IAEE,MAAMC,EAAazB,KAAK2F,eAAeD,SAGjC1F,KAAKiC,UAAUZ,aAAarB,KAAKsB,UAAWG,GAGlDzB,KAAK4B,OAAOsB,iBAAiB,CAC3B5B,UAAWtB,KAAKsB,UAChBsE,SAAUF,EAAeF,OACzBK,MAAO7F,KAAKwB,OAAOgE,OAASE,EAAeF,QAE/C,CAAE,MAAO/E,GAEPT,KAAKwB,OAAOsE,WAAWJ,GACvB1F,KAAK4B,OAAOmB,QAAQ,0BAA2BtC,GAG/CsF,WAAW,IAAM/F,KAAKqF,cAAe,IACvC,CACF,CAKA,cAAAM,CAAenE,GACb,MAAMd,EAAOS,KAAKC,UAAUI,GACtBC,EAAauE,EAAKC,KAAKvF,GAC7B,OAAOwF,KAAKC,OAAOC,gBAAgB3E,GACrC,CAKA,gBAAA+C,GACE,MAAO,CACL6B,WAAY,WACZ5D,cAAezC,KAAK4B,OAAOY,QAAQC,cACnCC,iBAAkB1C,KAAK4B,OAAOY,QAAQE,iBACtC4D,WAAY,CAACC,EAAMC,IACVxG,KAAKyG,kBAAkBF,EAAMC,GAG1C,CAKA,iBAAAC,CAAkBF,EAAMC,GACtB,IAAKD,EAAM,OAAOA,EAElB,MAAMG,EAAUH,EAAKI,OAGrB,OAAI3G,KAAK4B,OAAOY,QAAQE,iBAAiBC,QAAU,wBAAwBiE,KAAKF,GACvEA,EAAQG,QAAQ,sBAAuB,gBAI5C7G,KAAK4B,OAAOY,QAAQE,iBAAiBE,OAAS,gBAAgBgE,KAAKF,GAC9DA,EAAQG,QAAQ,sBAAuB,YAI5C7G,KAAK4B,OAAOY,QAAQE,iBAAiBG,UAAY,cAAc+D,KAAKF,GAC/DA,EAAQG,QAAQ,oBAAqB,cAGvCN,CACT,CAKA,gBAAAtB,GACEjF,KAAKgC,YAAc8E,YAAY,KAC7B9G,KAAKqF,eACJrF,KAAK4B,OAAOW,eACjB,CAKA,eAAA6C,GACMpF,KAAKgC,cACP+E,cAAc/G,KAAKgC,aACnBhC,KAAKgC,YAAc,KAEvB,CAKA,WAAAgF,CAAYlE,GACV9C,KAAK4B,OAAOkB,SAAW,IAClB9C,KAAK4B,OAAOkB,YACZA,EAEP,CAKA,YAAAmE,GACE,OAAOjH,KAAKsB,SACd,CAKA,WAAA4F,GACE,OAAOlH,KAAK6B,SACd,SA+DoB,oBAAX0B,SACTA,OAAO4D,aAAexF"}
1
+ {"version":3,"file":"index.umd.js","sources":["../src/index.js"],"sourcesContent":["/**\n * 页面录制系统 JavaScript SDK\n * 用于页面会话录制和回溯\n */\n\nimport { record } from 'rrweb';\nimport pako from 'pako';\n\nclass PageRecorder {\n constructor() {\n this.config = null;\n this.sessionId = null;\n this.recording = false;\n this.stopFn = null;\n this.events = [];\n this.uploadQueue = [];\n this.uploadTimer = null;\n this.apiClient = null;\n this.appendSequence = 0; // 序列号计数器\n this.failedAppends = new Map(); // 失败的序列号记录 {sequence: {events, retries}}\n this.totalEvents = 0; // 累计事件总数\n }\n\n /**\n * 初始化录制器\n */\n async init(config) {\n this.config = {\n apiKey: config.apiKey,\n projectId: config.projectId,\n apiBaseUrl: config.apiBaseUrl || 'https://api.yourplatform.com',\n autoStart: config.autoStart !== false,\n chunkSize: config.chunkSize || 100,\n uploadInterval: config.uploadInterval || 30000, // 30秒\n privacy: {\n maskAllInputs: false,\n maskInputOptions: {\n idCard: true,\n phone: true,\n bankCard: true,\n ...config.privacy?.maskInputOptions,\n },\n ...config.privacy,\n },\n metadata: config.metadata || {},\n onError: config.onError || console.error,\n onSessionCreated: config.onSessionCreated || (() => {}),\n onUploadProgress: config.onUploadProgress || (() => {}),\n };\n\n // 初始化 API 客户端\n this.apiClient = new APIClient(this.config.apiBaseUrl, this.config.apiKey);\n\n // 自动开始录制\n if (this.config.autoStart) {\n await this.start();\n }\n\n return this;\n }\n\n /**\n * 开始录制\n */\n async start(customMetadata = {}) {\n if (this.recording) {\n console.warn('Recording already in progress');\n return;\n }\n\n try {\n // 创建会话\n const session = await this.apiClient.createSession({\n projectId: this.config.projectId,\n metadata: {\n ...this.config.metadata,\n ...customMetadata,\n url: window.location.href,\n userAgent: navigator.userAgent,\n screenResolution: `${window.screen.width}x${window.screen.height}`,\n startTime: new Date().toISOString(),\n },\n });\n\n this.sessionId = session.sessionId;\n this.recording = true;\n this.events = [];\n this.totalEvents = 0; // 重置累计计数\n\n // 通知会话创建\n this.config.onSessionCreated(session);\n\n // 开始 rrweb 录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\n checkoutEveryNms: 5 * 60 * 1000,\n ...this.getPrivacyConfig(),\n sampling: {\n mousemove: true,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n recordCanvas: false,\n inlineImages: false,\n collectFonts: false,\n });\n\n // 启动定时上传\n this.startUploadTimer();\n\n console.log('Recording started:', this.sessionId);\n } catch (error) {\n this.config.onError('Failed to start recording', error);\n throw error;\n }\n }\n\n /**\n * 停止录制\n */\n async stop() {\n if (!this.recording) {\n return;\n }\n\n this.recording = false;\n\n // 停止 rrweb 录制\n if (this.stopFn) {\n this.stopFn();\n this.stopFn = null;\n }\n\n // 停止定时上传\n this.stopUploadTimer();\n\n // 上传剩余事件并等待所有 pending 请求完成\n await this.flushEvents();\n\n // 等待失败的序列号重传完成\n if (this.failedAppends.size > 0) {\n console.warn(`Retrying ${this.failedAppends.size} failed appends before finalize...`);\n await this.retryFailedAppends();\n \n // 再给一次机会\n await new Promise(resolve => setTimeout(resolve, 1000));\n if (this.failedAppends.size > 0) {\n console.error(`Still have ${this.failedAppends.size} failed appends after retry`);\n }\n }\n\n // 确认队列为空\n if (this.events.length > 0) {\n console.warn('Some events failed to upload, trying one more time...');\n await this.flushEvents();\n }\n\n // 完成会话\n try {\n await this.apiClient.finalizeSession(this.sessionId, {\n endTime: new Date().toISOString(),\n eventCount: this.totalEvents, // 使用累计值而非 this.events.length\n });\n console.log('Recording stopped:', this.sessionId);\n } catch (error) {\n console.error('Failed to finalize session:', error);\n this.config.onError('Failed to finalize session', error);\n }\n\n const sessionId = this.sessionId;\n this.sessionId = null;\n this.events = [];\n this.totalEvents = 0; // 清空累计计数\n\n return sessionId;\n }\n\n /**\n * 处理录制事件\n */\n handleEvent(event) {\n this.events.push(event);\n\n // 批量上传\n if (this.events.length >= this.config.chunkSize) {\n this.flushEvents();\n }\n }\n\n /**\n * 上传事件(带序列号)\n */\n async flushEvents() {\n if (this.events.length === 0 || !this.sessionId) {\n return;\n }\n\n const eventsToUpload = [...this.events];\n this.totalEvents += eventsToUpload.length; // 累加事件数\n this.events = [];\n\n // 分配序列号\n this.appendSequence++;\n const sequence = this.appendSequence;\n\n try {\n // 压缩数据\n const compressed = this.compressEvents(eventsToUpload);\n\n // 上传(带序列号)\n const result = await this.apiClient.appendEvents(\n this.sessionId, \n compressed,\n sequence\n );\n\n // 检查是否需要重传\n if (result?.missingSequences && result.missingSequences.length > 0) {\n console.warn('Server detected missing sequences:', result.missingSequences);\n // 重传缺失的序列号\n await this.retryMissingSequences(result.missingSequences);\n }\n\n // 通知上传进度\n this.config.onUploadProgress({\n sessionId: this.sessionId,\n sequence: sequence,\n uploaded: eventsToUpload.length,\n status: result?.status || 'success',\n });\n } catch (error) {\n // 记录失败的序列号,供重传\n this.failedAppends.set(sequence, {\n events: eventsToUpload,\n retries: 0,\n error: error.message,\n });\n \n console.error(`Failed to upload sequence ${sequence}:`, error);\n this.config.onError('Failed to upload events', error);\n \n // 如果不是严重错误,稍后重试\n if (error.code !== 'SESSION_NOT_FOUND' && error.code !== 'SESSION_NOT_RECORDING') {\n setTimeout(() => this.retryFailedAppends(), 2000);\n }\n }\n }\n\n /**\n * 重传失败的序列号\n */\n async retryFailedAppends() {\n if (this.failedAppends.size === 0 || !this.sessionId) {\n return;\n }\n\n const maxRetries = 3;\n const toRetry = [];\n\n for (const [sequence, failedAppend] of this.failedAppends.entries()) {\n if (failedAppend.retries < maxRetries) {\n toRetry.push({ sequence, failedAppend });\n } else {\n console.warn(`Sequence ${sequence} failed after ${maxRetries} retries, giving up`);\n this.failedAppends.delete(sequence);\n }\n }\n\n for (const { sequence, failedAppend } of toRetry) {\n try {\n const compressed = this.compressEvents(failedAppend.events);\n await this.apiClient.appendEvents(this.sessionId, compressed, sequence);\n \n console.log(`Successfully retried sequence ${sequence}`);\n this.failedAppends.delete(sequence);\n } catch (error) {\n failedAppend.retries++;\n console.error(`Retry failed for sequence ${sequence} (attempt ${failedAppend.retries}):`, error);\n }\n }\n }\n\n /**\n * 重传服务器检测到的缺失序列号\n */\n async retryMissingSequences(missingSequences) {\n for (const sequence of missingSequences) {\n const failedAppend = this.failedAppends.get(sequence);\n if (failedAppend) {\n try {\n const compressed = this.compressEvents(failedAppend.events);\n await this.apiClient.appendEvents(this.sessionId, compressed, sequence);\n \n console.log(`Successfully re-sent missing sequence ${sequence}`);\n this.failedAppends.delete(sequence);\n } catch (error) {\n console.error(`Failed to re-send missing sequence ${sequence}:`, error);\n }\n } else {\n console.warn(`Server requested missing sequence ${sequence}, but no record found`);\n }\n }\n }\n\n /**\n * 压缩事件数据\n */\n compressEvents(events) {\n const json = JSON.stringify(events);\n const compressed = pako.gzip(json);\n return btoa(String.fromCharCode(...compressed));\n }\n\n /**\n * 获取隐私配置\n */\n getPrivacyConfig() {\n return {\n blockClass: 'rr-block',\n maskAllInputs: this.config.privacy.maskAllInputs,\n maskInputOptions: this.config.privacy.maskInputOptions,\n maskTextFn: (text, element) => {\n return this.maskSensitiveData(text, element);\n },\n };\n }\n\n /**\n * 脱敏敏感数据\n */\n maskSensitiveData(text, element) {\n if (!text) return text;\n\n const trimmed = text.trim();\n\n // 身份证\n if (this.config.privacy.maskInputOptions.idCard && /^\\d{15}|\\d{17}[\\dXx]$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d{8}(\\d{4})/, '$1********$2');\n }\n\n // 手机号\n if (this.config.privacy.maskInputOptions.phone && /^1[3-9]\\d{9}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{3})\\d{4}(\\d{4})/, '$1****$2');\n }\n\n // 银行卡\n if (this.config.privacy.maskInputOptions.bankCard && /^\\d{16,19}$/.test(trimmed)) {\n return trimmed.replace(/(\\d{6})\\d+(\\d{3})/, '$1******$2');\n }\n\n return text;\n }\n\n /**\n * 启动定时上传\n */\n startUploadTimer() {\n this.uploadTimer = setInterval(() => {\n this.flushEvents();\n }, this.config.uploadInterval);\n }\n\n /**\n * 停止定时上传\n */\n stopUploadTimer() {\n if (this.uploadTimer) {\n clearInterval(this.uploadTimer);\n this.uploadTimer = null;\n }\n }\n\n /**\n * 添加自定义元数据\n */\n setMetadata(metadata) {\n this.config.metadata = {\n ...this.config.metadata,\n ...metadata,\n };\n }\n\n /**\n * 获取当前会话 ID\n */\n getSessionId() {\n return this.sessionId;\n }\n\n /**\n * 是否正在录制\n */\n isRecording() {\n return this.recording;\n }\n}\n\n/**\n * API 客户端\n */\nclass APIClient {\n constructor(baseUrl, apiKey) {\n this.baseUrl = baseUrl;\n this.apiKey = apiKey;\n }\n\n async request(endpoint, options = {}) {\n const url = `${this.baseUrl}${endpoint}`;\n const headers = {\n 'Content-Type': 'application/json',\n 'X-API-Key': this.apiKey,\n ...options.headers,\n };\n\n const response = await fetch(url, {\n ...options,\n headers,\n });\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n const errorObj = new Error(error.message || `HTTP ${response.status}`);\n errorObj.code = error.code;\n errorObj.status = response.status;\n throw errorObj;\n }\n\n return await response.json();\n }\n\n async createSession(data) {\n const result = await this.request('/api/v1/sessions', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n return result.data;\n }\n\n async appendEvents(sessionId, compressedEvents, sequence) {\n try {\n return await this.request(`/api/v1/sessions/${sessionId}/append`, {\n method: 'PATCH',\n body: JSON.stringify({\n events: compressedEvents,\n compressed: true,\n sequence: sequence, // 新增序列号\n }),\n });\n } catch (error) {\n // 如果会话正在 finalize,不重试\n if (error.code === 'SESSION_FINALIZING' || error.code === 'SESSION_NOT_RECORDING') {\n console.warn('Session is finalizing or not recording, skipping append');\n return null;\n }\n throw error;\n }\n }\n\n async finalizeSession(sessionId, data) {\n try {\n return await this.request(`/api/v1/sessions/${sessionId}/finalize`, {\n method: 'POST',\n body: JSON.stringify(data),\n });\n } catch (error) {\n // 如果会话已经在 finalize 或已完成,等待并获取状态\n if (error.code === 'SESSION_ALREADY_FINALIZING') {\n console.log('Session already finalizing, waiting...');\n await new Promise(resolve => setTimeout(resolve, 2000));\n // 尝试再次获取状态\n return await this.getSessionStatus(sessionId);\n }\n \n if (error.code === 'SESSION_ALREADY_COMPLETED') {\n console.log('Session already completed');\n return await this.getSessionStatus(sessionId);\n }\n \n throw error;\n }\n }\n\n async getSessionStatus(sessionId) {\n // 获取会话状态(假设有这个端点)\n try {\n return await this.request(`/api/v1/sessions/${sessionId}`, {\n method: 'GET',\n });\n } catch (error) {\n console.error('Failed to get session status:', error);\n return null;\n }\n }\n}\n\n// 导出单例\nconst recorder = new PageRecorder();\n\n// 全局对象\nif (typeof window !== 'undefined') {\n window.PageRecorder = recorder;\n}\n\nexport default recorder;\n\n"],"names":["APIClient","constructor","baseUrl","apiKey","this","request","endpoint","options","url","headers","response","fetch","ok","error","json","catch","errorObj","Error","message","status","code","createSession","data","method","body","JSON","stringify","appendEvents","sessionId","compressedEvents","sequence","events","compressed","console","warn","finalizeSession","log","Promise","resolve","setTimeout","getSessionStatus","recorder","config","recording","stopFn","uploadQueue","uploadTimer","apiClient","appendSequence","failedAppends","Map","totalEvents","init","projectId","apiBaseUrl","autoStart","chunkSize","uploadInterval","privacy","maskAllInputs","maskInputOptions","idCard","phone","bankCard","metadata","onError","onSessionCreated","onUploadProgress","start","customMetadata","session","window","location","href","userAgent","navigator","screenResolution","screen","width","height","startTime","Date","toISOString","record","emit","event","handleEvent","checkoutEveryNms","getPrivacyConfig","sampling","mousemove","mouseInteraction","scroll","input","recordCanvas","inlineImages","collectFonts","startUploadTimer","stop","stopUploadTimer","flushEvents","size","retryFailedAppends","length","endTime","eventCount","push","eventsToUpload","compressEvents","result","missingSequences","retryMissingSequences","uploaded","set","retries","toRetry","failedAppend","entries","delete","get","pako","gzip","btoa","String","fromCharCode","blockClass","maskTextFn","text","element","maskSensitiveData","trimmed","trim","test","replace","setInterval","clearInterval","setMetadata","getSessionId","isRecording","PageRecorder"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepsure/page-replay-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "description": "DeepSure PageReplay SDK",
6
6
  "main": "dist/index.js",
@@ -46,4 +46,3 @@
46
46
  "rrweb": "^2.0.0-alpha.4"
47
47
  }
48
48
  }
49
-