@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 +93 -94
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
#
|
|
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 @
|
|
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
|
-
|
|
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
|
|
37
|
+
import PageRecorder from '@deepsure/page-replay-sdk';
|
|
38
38
|
|
|
39
|
-
await
|
|
40
|
-
//
|
|
41
|
-
apiKey: 'your_api_key', // API
|
|
42
|
-
projectId: 'proj_12345', //
|
|
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:
|
|
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' //
|
|
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('
|
|
69
|
+
console.log('Session created:', session.sessionId);
|
|
70
70
|
},
|
|
71
71
|
onUploadProgress: (progress) => {
|
|
72
|
-
console.log('
|
|
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,
|
|
88
|
-
- `projectId` (string,
|
|
89
|
-
- `apiBaseUrl` (string) - API
|
|
90
|
-
- `autoStart` (boolean) -
|
|
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
|
-
|
|
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
|
-
|
|
103
|
+
**Returns:** Promise<void>
|
|
104
104
|
|
|
105
105
|
### stop()
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
Stop recording and upload remaining data.
|
|
108
108
|
|
|
109
|
-
|
|
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
|
-
|
|
120
|
+
Get the current session ID.
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
**Returns:** string | null
|
|
123
123
|
|
|
124
124
|
### isRecording()
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
Check if recording is in progress.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
**Returns:** boolean
|
|
129
129
|
|
|
130
|
-
##
|
|
130
|
+
## Use Cases
|
|
131
131
|
|
|
132
|
-
###
|
|
132
|
+
### Use Case 1: Insurance Application Recording
|
|
133
133
|
|
|
134
134
|
```javascript
|
|
135
|
-
|
|
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
|
-
###
|
|
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
|
|
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
|
|
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
|
|
173
|
-
console.log('
|
|
172
|
+
const sessionId = await PageRecorder.stop();
|
|
173
|
+
console.log('Session saved:', sessionId);
|
|
174
174
|
};
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
###
|
|
177
|
+
### Use Case 3: Dynamic Metadata Updates
|
|
178
178
|
|
|
179
179
|
```javascript
|
|
180
|
-
//
|
|
181
|
-
await
|
|
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
|
-
|
|
189
|
+
// User proceeds to next step
|
|
190
|
+
PageRecorder.setMetadata({ step: 'fill_info' });
|
|
191
191
|
|
|
192
|
-
//
|
|
193
|
-
|
|
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
|
-
-
|
|
206
|
-
-
|
|
207
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
package/dist/index.esm.js.map
CHANGED
|
@@ -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
|
|
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":"sDAmZA,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,IAC7CC,EAAW,IAAIC,MAAMJ,EAAMK,SAAW,QAAQR,EAASS,UAG7D,MAFAH,EAASI,KAAOP,EAAMO,KACtBJ,EAASG,OAAST,EAASS,OACrBH,CACR,CAEA,aAAaN,EAASI,MACxB,CAEA,mBAAMO,CAAcC,GAKlB,aAJqBlB,KAAKC,QAAQ,mBAAoB,CACpDkB,OAAQ,OACRC,KAAMC,KAAKC,UAAUJ,MAETA,IAChB,CAEA,kBAAMK,CAAaC,EAAWC,EAAkBC,GAC9C,IACE,aAAa1B,KAAKC,QAAQ,oBAAoBuB,WAAoB,CAChEL,OAAQ,QACRC,KAAMC,KAAKC,UAAU,CACnBK,OAAQF,EACRG,YAAY,EACZF,SAAUA,KAGhB,CAAE,MAAOjB,GAEP,GAAmB,uBAAfA,EAAMO,MAAgD,0BAAfP,EAAMO,KAE/C,OADAa,QAAQC,KAAK,2DACN,KAET,MAAMrB,CACR,CACF,CAEA,qBAAMsB,CAAgBP,EAAWN,GAC/B,IACE,aAAalB,KAAKC,QAAQ,oBAAoBuB,aAAsB,CAClEL,OAAQ,OACRC,KAAMC,KAAKC,UAAUJ,IAEzB,CAAE,MAAOT,GAEP,GAAmB,+BAAfA,EAAMO,KAIR,OAHAa,QAAQG,IAAI,gDACN,IAAIC,QAAQC,GAAWC,WAAWD,EAAS,YAEpClC,KAAKoC,iBAAiBZ,GAGrC,GAAmB,8BAAff,EAAMO,KAER,OADAa,QAAQG,IAAI,mCACChC,KAAKoC,iBAAiBZ,GAGrC,MAAMf,CACR,CACF,CAEA,sBAAM2B,CAAiBZ,GAErB,IACE,aAAaxB,KAAKC,QAAQ,oBAAoBuB,IAAa,CACzDL,OAAQ,OAEZ,CAAE,MAAOV,GAEP,OADAoB,QAAQpB,MAAM,gCAAiCA,GACxC,IACT,CACF,EAIG,MAAC4B,EAAW,IA3ejB,MACE,WAAAxC,GACEG,KAAKsC,OAAS,KACdtC,KAAKwB,UAAY,KACjBxB,KAAKuC,WAAY,EACjBvC,KAAKwC,OAAS,KACdxC,KAAK2B,OAAS,GACd3B,KAAKyC,YAAc,GACnBzC,KAAK0C,YAAc,KACnB1C,KAAK2C,UAAY,KACjB3C,KAAK4C,eAAiB,EACtB5C,KAAK6C,cAAgB,IAAIC,IACzB9C,KAAK+C,YAAc,CACrB,CAKA,UAAMC,CAAKV,GAgCT,OA/BAtC,KAAKsC,OAAS,CACZvC,OAAQuC,EAAOvC,OACfkD,UAAWX,EAAOW,UAClBC,WAAYZ,EAAOY,YAAc,+BACjCC,WAAgC,IAArBb,EAAOa,UAClBC,UAAWd,EAAOc,WAAa,IAC/BC,eAAgBf,EAAOe,gBAAkB,IACzCC,QAAS,CACPC,eAAe,EACfC,iBAAkB,CAChBC,QAAQ,EACRC,OAAO,EACPC,UAAU,KACPrB,EAAOgB,SAASE,qBAElBlB,EAAOgB,SAEZM,SAAUtB,EAAOsB,UAAY,CAAA,EAC7BC,QAASvB,EAAOuB,SAAWhC,QAAQpB,MACnCqD,iBAAkBxB,EAAOwB,kBAAgB,MAAa,GACtDC,iBAAkBzB,EAAOyB,kBAAgB,MAAa,IAIxD/D,KAAK2C,UAAY,IAAI/C,EAAUI,KAAKsC,OAAOY,WAAYlD,KAAKsC,OAAOvC,QAG/DC,KAAKsC,OAAOa,iBACRnD,KAAKgE,QAGNhE,IACT,CAKA,WAAMgE,CAAMC,EAAiB,IAC3B,GAAIjE,KAAKuC,UACPV,QAAQC,KAAK,sCAIf,IAEE,MAAMoC,QAAgBlE,KAAK2C,UAAU1B,cAAc,CACjDgC,UAAWjD,KAAKsC,OAAOW,UACvBW,SAAU,IACL5D,KAAKsC,OAAOsB,YACZK,EACH7D,IAAK+D,OAAOC,SAASC,KACrBC,UAAWC,UAAUD,UACrBE,iBAAkB,GAAGL,OAAOM,OAAOC,SAASP,OAAOM,OAAOE,SAC1DC,WAAW,IAAIC,MAAOC,iBAI1B9E,KAAKwB,UAAY0C,EAAQ1C,UACzBxB,KAAKuC,WAAY,EACjBvC,KAAK2B,OAAS,GACd3B,KAAK+C,YAAc,EAGnB/C,KAAKsC,OAAOwB,iBAAiBI,GAG7BlE,KAAKwC,OAASuC,SAAO,CACnBC,KAAOC,IACLjF,KAAKkF,YAAYD,IAEnBE,iBAAkB,OACfnF,KAAKoF,mBACRC,SAAU,CACRC,WAAW,EACXC,kBAAkB,EAClBC,OAAQ,IACRC,MAAO,QAETC,cAAc,EACdC,cAAc,EACdC,cAAc,IAIhB5F,KAAK6F,mBAELhE,QAAQG,IAAI,qBAAsBhC,KAAKwB,UACzC,CAAE,MAAOf,GAEP,MADAT,KAAKsC,OAAOuB,QAAQ,4BAA6BpD,GAC3CA,CACR,CACF,CAKA,UAAMqF,GACJ,IAAK9F,KAAKuC,UACR,OAGFvC,KAAKuC,WAAY,EAGbvC,KAAKwC,SACPxC,KAAKwC,SACLxC,KAAKwC,OAAS,MAIhBxC,KAAK+F,wBAGC/F,KAAKgG,cAGPhG,KAAK6C,cAAcoD,KAAO,IAC5BpE,QAAQC,KAAK,YAAY9B,KAAK6C,cAAcoD,gDACtCjG,KAAKkG,2BAGL,IAAIjE,QAAQC,GAAWC,WAAWD,EAAS,MAC7ClC,KAAK6C,cAAcoD,KAAO,GAC5BpE,QAAQpB,MAAM,cAAcT,KAAK6C,cAAcoD,oCAK/CjG,KAAK2B,OAAOwE,OAAS,IACvBtE,QAAQC,KAAK,+DACP9B,KAAKgG,eAIb,UACQhG,KAAK2C,UAAUZ,gBAAgB/B,KAAKwB,UAAW,CACnD4E,SAAS,IAAIvB,MAAOC,cACpBuB,WAAYrG,KAAK+C,cAEnBlB,QAAQG,IAAI,qBAAsBhC,KAAKwB,UACzC,CAAE,MAAOf,GACPoB,QAAQpB,MAAM,8BAA+BA,GAC7CT,KAAKsC,OAAOuB,QAAQ,6BAA8BpD,EACpD,CAEA,MAAMe,EAAYxB,KAAKwB,UAKvB,OAJAxB,KAAKwB,UAAY,KACjBxB,KAAK2B,OAAS,GACd3B,KAAK+C,YAAc,EAEZvB,CACT,CAKA,WAAA0D,CAAYD,GACVjF,KAAK2B,OAAO2E,KAAKrB,GAGbjF,KAAK2B,OAAOwE,QAAUnG,KAAKsC,OAAOc,WACpCpD,KAAKgG,aAET,CAKA,iBAAMA,GACJ,GAA2B,IAAvBhG,KAAK2B,OAAOwE,SAAiBnG,KAAKwB,UACpC,OAGF,MAAM+E,EAAiB,IAAIvG,KAAK2B,QAChC3B,KAAK+C,aAAewD,EAAeJ,OACnCnG,KAAK2B,OAAS,GAGd3B,KAAK4C,iBACL,MAAMlB,EAAW1B,KAAK4C,eAEtB,IAEE,MAAMhB,EAAa5B,KAAKwG,eAAeD,GAGjCE,QAAezG,KAAK2C,UAAUpB,aAClCvB,KAAKwB,UACLI,EACAF,GAIE+E,GAAQC,kBAAoBD,EAAOC,iBAAiBP,OAAS,IAC/DtE,QAAQC,KAAK,qCAAsC2E,EAAOC,wBAEpD1G,KAAK2G,sBAAsBF,EAAOC,mBAI1C1G,KAAKsC,OAAOyB,iBAAiB,CAC3BvC,UAAWxB,KAAKwB,UAChBE,SAAUA,EACVkF,SAAUL,EAAeJ,OACzBpF,OAAQ0F,GAAQ1F,QAAU,WAE9B,CAAE,MAAON,GAEPT,KAAK6C,cAAcgE,IAAInF,EAAU,CAC/BC,OAAQ4E,EACRO,QAAS,EACTrG,MAAOA,EAAMK,UAGfe,QAAQpB,MAAM,6BAA6BiB,KAAajB,GACxDT,KAAKsC,OAAOuB,QAAQ,0BAA2BpD,GAG5B,sBAAfA,EAAMO,MAA+C,0BAAfP,EAAMO,MAC9CmB,WAAW,IAAMnC,KAAKkG,qBAAsB,IAEhD,CACF,CAKA,wBAAMA,GACJ,GAAgC,IAA5BlG,KAAK6C,cAAcoD,OAAejG,KAAKwB,UACzC,OAGF,MACMuF,EAAU,GAEhB,IAAK,MAAOrF,EAAUsF,KAAiBhH,KAAK6C,cAAcoE,UACpDD,EAAaF,QAJA,EAKfC,EAAQT,KAAK,CAAE5E,WAAUsF,kBAEzBnF,QAAQC,KAAK,YAAYJ,uCACzB1B,KAAK6C,cAAcqE,OAAOxF,IAI9B,IAAK,MAAMA,SAAEA,EAAQsF,aAAEA,KAAkBD,EACvC,IACE,MAAMnF,EAAa5B,KAAKwG,eAAeQ,EAAarF,cAC9C3B,KAAK2C,UAAUpB,aAAavB,KAAKwB,UAAWI,EAAYF,GAE9DG,QAAQG,IAAI,iCAAiCN,KAC7C1B,KAAK6C,cAAcqE,OAAOxF,EAC5B,CAAE,MAAOjB,GACPuG,EAAaF,UACbjF,QAAQpB,MAAM,6BAA6BiB,cAAqBsF,EAAaF,YAAarG,EAC5F,CAEJ,CAKA,2BAAMkG,CAAsBD,GAC1B,IAAK,MAAMhF,KAAYgF,EAAkB,CACvC,MAAMM,EAAehH,KAAK6C,cAAcsE,IAAIzF,GAC5C,GAAIsF,EACF,IACE,MAAMpF,EAAa5B,KAAKwG,eAAeQ,EAAarF,cAC9C3B,KAAK2C,UAAUpB,aAAavB,KAAKwB,UAAWI,EAAYF,GAE9DG,QAAQG,IAAI,yCAAyCN,KACrD1B,KAAK6C,cAAcqE,OAAOxF,EAC5B,CAAE,MAAOjB,GACPoB,QAAQpB,MAAM,sCAAsCiB,KAAajB,EACnE,MAEAoB,QAAQC,KAAK,qCAAqCJ,yBAEtD,CACF,CAKA,cAAA8E,CAAe7E,GACb,MAAMjB,EAAOW,KAAKC,UAAUK,GACtBC,EAAawF,EAAKC,KAAK3G,GAC7B,OAAO4G,KAAKC,OAAOC,gBAAgB5F,GACrC,CAKA,gBAAAwD,GACE,MAAO,CACLqC,WAAY,WACZlE,cAAevD,KAAKsC,OAAOgB,QAAQC,cACnCC,iBAAkBxD,KAAKsC,OAAOgB,QAAQE,iBACtCkE,WAAY,CAACC,EAAMC,IACV5H,KAAK6H,kBAAkBF,EAAMC,GAG1C,CAKA,iBAAAC,CAAkBF,EAAMC,GACtB,IAAKD,EAAM,OAAOA,EAElB,MAAMG,EAAUH,EAAKI,OAGrB,OAAI/H,KAAKsC,OAAOgB,QAAQE,iBAAiBC,QAAU,wBAAwBuE,KAAKF,GACvEA,EAAQG,QAAQ,sBAAuB,gBAI5CjI,KAAKsC,OAAOgB,QAAQE,iBAAiBE,OAAS,gBAAgBsE,KAAKF,GAC9DA,EAAQG,QAAQ,sBAAuB,YAI5CjI,KAAKsC,OAAOgB,QAAQE,iBAAiBG,UAAY,cAAcqE,KAAKF,GAC/DA,EAAQG,QAAQ,oBAAqB,cAGvCN,CACT,CAKA,gBAAA9B,GACE7F,KAAK0C,YAAcwF,YAAY,KAC7BlI,KAAKgG,eACJhG,KAAKsC,OAAOe,eACjB,CAKA,eAAA0C,GACM/F,KAAK0C,cACPyF,cAAcnI,KAAK0C,aACnB1C,KAAK0C,YAAc,KAEvB,CAKA,WAAA0F,CAAYxE,GACV5D,KAAKsC,OAAOsB,SAAW,IAClB5D,KAAKsC,OAAOsB,YACZA,EAEP,CAKA,YAAAyE,GACE,OAAOrI,KAAKwB,SACd,CAKA,WAAA8G,GACE,OAAOtI,KAAKuC,SACd,GAyGoB,oBAAX4B,SACTA,OAAOoE,aAAelG"}
|
package/dist/index.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(
|
|
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
|
package/dist/index.umd.js.map
CHANGED
|
@@ -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":"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"}
|
|
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