@deepsure/page-replay-sdk 1.0.4 → 1.0.6

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/dist/index.esm.js CHANGED
@@ -1,2 +1,2 @@
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){if(0===this.totalEvents){2===e.type||"fullSnapshot"===e.type?console.log("✅ [SDK] First event is full snapshot, good!"):(console.warn("⚠️ [SDK] First event is NOT a full snapshot! Type:",e.type),console.warn("[SDK] This may cause playback issues. Event:",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)),console.log(`✅ [SDK] Uploaded sequence ${s} with ${e.length} events (total: ${this.totalEvents})`),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};
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.firstEventReceived=!1,this.config.onSessionCreated(t),this.stopFn=e({emit:e=>{this.handleEvent(e)},...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),await new Promise(e=>setTimeout(e,100)),(!this.firstEventReceived||this.events.length>0&&2!==this.events[0].type)&&(console.warn("⚠️ [SDK] rrweb did not emit fullSnapshot in time, forcing a manual snapshot..."),this.stopFn&&this.stopFn(),this.events=[],this.totalEvents=0,this.firstEventReceived=!1,await new Promise(e=>setTimeout(e,500)),this.stopFn=e({emit:e=>{this.handleEvent(e)},...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),await new Promise(e=>setTimeout(e,100))),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){if(0===this.totalEvents){2===e.type||"fullSnapshot"===e.type?console.log("✅ [SDK] First event is full snapshot, good!"):(console.warn("⚠️ [SDK] First event is NOT a full snapshot! Type:",e.type),console.warn("[SDK] This may cause playback issues. Event:",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)),console.log(`✅ [SDK] Uploaded sequence ${s} with ${e.length} events (total: ${this.totalEvents})`),this.config.onUploadProgress({sessionId:this.sessionId,sequence:s,uploaded:e.length,status:i?.status||"success"})}catch(t){this.failedAppends.set(s,{events:e,retries:0,error:t.message}),console.error(`Failed to upload sequence ${s}:`,t),this.config.onError("Failed to upload events",t),"SESSION_NOT_FOUND"!==t.code&&"SESSION_NOT_RECORDING"!==t.code&&setTimeout(()=>this.retryFailedAppends(),2e3)}}async retryFailedAppends(){if(0===this.failedAppends.size||!this.sessionId)return;const e=[];for(const[s,t]of this.failedAppends.entries())t.retries<3?e.push({sequence:s,failedAppend:t}):(console.warn(`Sequence ${s} failed after 3 retries, giving up`),this.failedAppends.delete(s));for(const{sequence:s,failedAppend:t}of e)try{const e=this.compressEvents(t.events);await this.apiClient.appendEvents(this.sessionId,e,s),console.log(`Successfully retried sequence ${s}`),this.failedAppends.delete(s)}catch(e){t.retries++,console.error(`Retry failed for sequence ${s} (attempt ${t.retries}):`,e)}}async retryMissingSequences(e){for(const s of e){const e=this.failedAppends.get(s);if(e)try{const t=this.compressEvents(e.events);await this.apiClient.appendEvents(this.sessionId,t,s),console.log(`Successfully re-sent missing sequence ${s}`),this.failedAppends.delete(s)}catch(e){console.error(`Failed to re-send missing sequence ${s}:`,e)}else console.warn(`Server requested missing sequence ${s}, but no record found`)}}compressEvents(e){const t=JSON.stringify(e),i=s.gzip(t);return btoa(String.fromCharCode(...i))}getPrivacyConfig(){return{blockClass:"rr-block",maskAllInputs:this.config.privacy.maskAllInputs,maskInputOptions:this.config.privacy.maskInputOptions,maskTextFn:(e,s)=>this.maskSensitiveData(e,s)}}maskSensitiveData(e,s){if(!e)return e;const t=e.trim();return this.config.privacy.maskInputOptions.idCard&&/^\d{15}|\d{17}[\dXx]$/.test(t)?t.replace(/(\d{6})\d{8}(\d{4})/,"$1********$2"):this.config.privacy.maskInputOptions.phone&&/^1[3-9]\d{9}$/.test(t)?t.replace(/(\d{3})\d{4}(\d{4})/,"$1****$2"):this.config.privacy.maskInputOptions.bankCard&&/^\d{16,19}$/.test(t)?t.replace(/(\d{6})\d+(\d{3})/,"$1******$2"):e}startUploadTimer(){this.uploadTimer=setInterval(()=>{this.flushEvents()},this.config.uploadInterval)}stopUploadTimer(){this.uploadTimer&&(clearInterval(this.uploadTimer),this.uploadTimer=null)}setMetadata(e){this.config.metadata={...this.config.metadata,...e}}getSessionId(){return this.sessionId}isRecording(){return this.recording}};"undefined"!=typeof window&&(window.PageRecorder=i);export{i as default};
2
2
  //# sourceMappingURL=index.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.esm.js","sources":["../src/index.js"],"sourcesContent":["/**\n * 页面录制系统 JavaScript SDK\n * 用于页面会话录制和回溯\n */\n\nimport { record } from 'rrweb';\nimport pako from 'pako';\n\nclass PageRecorder {\n constructor() {\n this.config = null;\n this.sessionId = null;\n this.recording = false;\n this.stopFn = null;\n this.events = [];\n this.uploadQueue = [];\n this.uploadTimer = null;\n this.apiClient = null;\n 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 // 检查第一个事件是否是 fullSnapshot\n if (this.totalEvents === 0) {\n const isFullSnapshot = event.type === 2 || event.type === 'fullSnapshot';\n if (isFullSnapshot) {\n console.log('✅ [SDK] First event is full snapshot, good!');\n } else {\n console.warn('⚠️ [SDK] First event is NOT a full snapshot! Type:', event.type);\n console.warn('[SDK] This may cause playback issues. Event:', event);\n }\n }\n \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 console.log(`✅ [SDK] Uploaded sequence ${sequence} with ${eventsToUpload.length} events (total: ${this.totalEvents})`);\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","type","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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"}
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 this.firstEventReceived = false; // 标记第一个事件\n\n // 通知会话创建\n this.config.onSessionCreated(session);\n\n // 开始 rrweb 录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\n // 移除 checkoutEveryNms,避免在录制过程中重新生成快照\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 // 等待一下确保 rrweb 有时间生成 fullSnapshot\n await new Promise(resolve => setTimeout(resolve, 100));\n\n // 检查是否收到了 fullSnapshot\n if (!this.firstEventReceived || (this.events.length > 0 && this.events[0].type !== 2)) {\n console.warn('⚠️ [SDK] rrweb did not emit fullSnapshot in time, forcing a manual snapshot...');\n \n // 如果没有收到 fullSnapshot,重新启动录制\n if (this.stopFn) {\n this.stopFn();\n }\n \n // 清空事件\n this.events = [];\n this.totalEvents = 0;\n this.firstEventReceived = false;\n \n // 再等待一下确保 DOM 稳定\n await new Promise(resolve => setTimeout(resolve, 500));\n \n // 重新开始录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\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 await new Promise(resolve => setTimeout(resolve, 100));\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 // 检查第一个事件是否是 fullSnapshot\n if (this.totalEvents === 0) {\n const isFullSnapshot = event.type === 2 || event.type === 'fullSnapshot';\n if (isFullSnapshot) {\n console.log('✅ [SDK] First event is full snapshot, good!');\n } else {\n console.warn('⚠️ [SDK] First event is NOT a full snapshot! Type:', event.type);\n console.warn('[SDK] This may cause playback issues. Event:', event);\n }\n }\n \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 console.log(`✅ [SDK] Uploaded sequence ${sequence} with ${eventsToUpload.length} events (total: ${this.totalEvents})`);\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","firstEventReceived","record","emit","event","handleEvent","getPrivacyConfig","sampling","mousemove","mouseInteraction","scroll","input","recordCanvas","inlineImages","collectFonts","length","type","startUploadTimer","stop","stopUploadTimer","flushEvents","size","retryFailedAppends","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 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){if(0===this.totalEvents){2===e.type||"fullSnapshot"===e.type?console.log("✅ [SDK] First event is full snapshot, good!"):(console.warn("⚠️ [SDK] First event is NOT a full snapshot! Type:",e.type),console.warn("[SDK] This may cause playback issues. Event:",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)),console.log(`✅ [SDK] Uploaded sequence ${s} with ${e.length} events (total: ${this.totalEvents})`),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;
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.firstEventReceived=!1,this.config.onSessionCreated(t),this.stopFn=e.record({emit:e=>{this.handleEvent(e)},...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),await new Promise(e=>setTimeout(e,100)),(!this.firstEventReceived||this.events.length>0&&2!==this.events[0].type)&&(console.warn("⚠️ [SDK] rrweb did not emit fullSnapshot in time, forcing a manual snapshot..."),this.stopFn&&this.stopFn(),this.events=[],this.totalEvents=0,this.firstEventReceived=!1,await new Promise(e=>setTimeout(e,500)),this.stopFn=e.record({emit:e=>{this.handleEvent(e)},...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),await new Promise(e=>setTimeout(e,100))),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){if(0===this.totalEvents){2===e.type||"fullSnapshot"===e.type?console.log("✅ [SDK] First event is full snapshot, good!"):(console.warn("⚠️ [SDK] First event is NOT a full snapshot! Type:",e.type),console.warn("[SDK] This may cause playback issues. Event:",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)),console.log(`✅ [SDK] Uploaded sequence ${s} with ${e.length} events (total: ${this.totalEvents})`),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 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 // 检查第一个事件是否是 fullSnapshot\n if (this.totalEvents === 0) {\n const isFullSnapshot = event.type === 2 || event.type === 'fullSnapshot';\n if (isFullSnapshot) {\n console.log('✅ [SDK] First event is full snapshot, good!');\n } else {\n console.warn('⚠️ [SDK] First event is NOT a full snapshot! Type:', event.type);\n console.warn('[SDK] This may cause playback issues. Event:', event);\n }\n }\n \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 console.log(`✅ [SDK] Uploaded sequence ${sequence} with ${eventsToUpload.length} events (total: ${this.totalEvents})`);\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","type","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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"}
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 this.firstEventReceived = false; // 标记第一个事件\n\n // 通知会话创建\n this.config.onSessionCreated(session);\n\n // 开始 rrweb 录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\n // 移除 checkoutEveryNms,避免在录制过程中重新生成快照\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 // 等待一下确保 rrweb 有时间生成 fullSnapshot\n await new Promise(resolve => setTimeout(resolve, 100));\n\n // 检查是否收到了 fullSnapshot\n if (!this.firstEventReceived || (this.events.length > 0 && this.events[0].type !== 2)) {\n console.warn('⚠️ [SDK] rrweb did not emit fullSnapshot in time, forcing a manual snapshot...');\n \n // 如果没有收到 fullSnapshot,重新启动录制\n if (this.stopFn) {\n this.stopFn();\n }\n \n // 清空事件\n this.events = [];\n this.totalEvents = 0;\n this.firstEventReceived = false;\n \n // 再等待一下确保 DOM 稳定\n await new Promise(resolve => setTimeout(resolve, 500));\n \n // 重新开始录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\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 await new Promise(resolve => setTimeout(resolve, 100));\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 // 检查第一个事件是否是 fullSnapshot\n if (this.totalEvents === 0) {\n const isFullSnapshot = event.type === 2 || event.type === 'fullSnapshot';\n if (isFullSnapshot) {\n console.log('✅ [SDK] First event is full snapshot, good!');\n } else {\n console.warn('⚠️ [SDK] First event is NOT a full snapshot! Type:', event.type);\n console.warn('[SDK] This may cause playback issues. Event:', event);\n }\n }\n \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 console.log(`✅ [SDK] Uploaded sequence ${sequence} with ${eventsToUpload.length} events (total: ${this.totalEvents})`);\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","firstEventReceived","record","emit","event","handleEvent","getPrivacyConfig","sampling","mousemove","mouseInteraction","scroll","input","recordCanvas","inlineImages","collectFonts","length","type","startUploadTimer","stop","stopUploadTimer","flushEvents","size","retryFailedAppends","endTime","eventCount","push","eventsToUpload","compressEvents","result","missingSequences","retryMissingSequences","uploaded","set","retries","toRetry","failedAppend","entries","delete","get","pako","gzip","btoa","String","fromCharCode","blockClass","maskTextFn","text","element","maskSensitiveData","trimmed","trim","test","replace","setInterval","clearInterval","setMetadata","getSessionId","isRecording","PageRecorder"],"mappings":"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"}
package/dist/index.umd.js CHANGED
@@ -1,2 +1,2 @@
1
- !function(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){if(0===this.totalEvents){2===e.type||"fullSnapshot"===e.type?console.log("✅ [SDK] First event is full snapshot, good!"):(console.warn("⚠️ [SDK] First event is NOT a full snapshot! Type:",e.type),console.warn("[SDK] This may cause playback issues. Event:",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)),console.log(`✅ [SDK] Uploaded sequence ${s} with ${e.length} events (total: ${this.totalEvents})`),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});
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.firstEventReceived=!1,this.config.onSessionCreated(t),this.stopFn=e.record({emit:e=>{this.handleEvent(e)},...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),await new Promise(e=>setTimeout(e,100)),(!this.firstEventReceived||this.events.length>0&&2!==this.events[0].type)&&(console.warn("⚠️ [SDK] rrweb did not emit fullSnapshot in time, forcing a manual snapshot..."),this.stopFn&&this.stopFn(),this.events=[],this.totalEvents=0,this.firstEventReceived=!1,await new Promise(e=>setTimeout(e,500)),this.stopFn=e.record({emit:e=>{this.handleEvent(e)},...this.getPrivacyConfig(),sampling:{mousemove:!0,mouseInteraction:!0,scroll:150,input:"last"},recordCanvas:!1,inlineImages:!1,collectFonts:!1}),await new Promise(e=>setTimeout(e,100))),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){if(0===this.totalEvents){2===e.type||"fullSnapshot"===e.type?console.log("✅ [SDK] First event is full snapshot, good!"):(console.warn("⚠️ [SDK] First event is NOT a full snapshot! Type:",e.type),console.warn("[SDK] This may cause playback issues. Event:",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)),console.log(`✅ [SDK] Uploaded sequence ${s} with ${e.length} events (total: ${this.totalEvents})`),this.config.onUploadProgress({sessionId:this.sessionId,sequence:s,uploaded:e.length,status:i?.status||"success"})}catch(t){this.failedAppends.set(s,{events:e,retries:0,error:t.message}),console.error(`Failed to upload sequence ${s}:`,t),this.config.onError("Failed to upload events",t),"SESSION_NOT_FOUND"!==t.code&&"SESSION_NOT_RECORDING"!==t.code&&setTimeout(()=>this.retryFailedAppends(),2e3)}}async retryFailedAppends(){if(0===this.failedAppends.size||!this.sessionId)return;const e=[];for(const[s,t]of this.failedAppends.entries())t.retries<3?e.push({sequence:s,failedAppend:t}):(console.warn(`Sequence ${s} failed after 3 retries, giving up`),this.failedAppends.delete(s));for(const{sequence:s,failedAppend:t}of e)try{const e=this.compressEvents(t.events);await this.apiClient.appendEvents(this.sessionId,e,s),console.log(`Successfully retried sequence ${s}`),this.failedAppends.delete(s)}catch(e){t.retries++,console.error(`Retry failed for sequence ${s} (attempt ${t.retries}):`,e)}}async retryMissingSequences(e){for(const s of e){const e=this.failedAppends.get(s);if(e)try{const t=this.compressEvents(e.events);await this.apiClient.appendEvents(this.sessionId,t,s),console.log(`Successfully re-sent missing sequence ${s}`),this.failedAppends.delete(s)}catch(e){console.error(`Failed to re-send missing sequence ${s}:`,e)}else console.warn(`Server requested missing sequence ${s}, but no record found`)}}compressEvents(e){const t=JSON.stringify(e),i=s.gzip(t);return btoa(String.fromCharCode(...i))}getPrivacyConfig(){return{blockClass:"rr-block",maskAllInputs:this.config.privacy.maskAllInputs,maskInputOptions:this.config.privacy.maskInputOptions,maskTextFn:(e,s)=>this.maskSensitiveData(e,s)}}maskSensitiveData(e,s){if(!e)return e;const t=e.trim();return this.config.privacy.maskInputOptions.idCard&&/^\d{15}|\d{17}[\dXx]$/.test(t)?t.replace(/(\d{6})\d{8}(\d{4})/,"$1********$2"):this.config.privacy.maskInputOptions.phone&&/^1[3-9]\d{9}$/.test(t)?t.replace(/(\d{3})\d{4}(\d{4})/,"$1****$2"):this.config.privacy.maskInputOptions.bankCard&&/^\d{16,19}$/.test(t)?t.replace(/(\d{6})\d+(\d{3})/,"$1******$2"):e}startUploadTimer(){this.uploadTimer=setInterval(()=>{this.flushEvents()},this.config.uploadInterval)}stopUploadTimer(){this.uploadTimer&&(clearInterval(this.uploadTimer),this.uploadTimer=null)}setMetadata(e){this.config.metadata={...this.config.metadata,...e}}getSessionId(){return this.sessionId}isRecording(){return this.recording}};return"undefined"!=typeof window&&(window.PageRecorder=i),i});
2
2
  //# sourceMappingURL=index.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.umd.js","sources":["../src/index.js"],"sourcesContent":["/**\n * 页面录制系统 JavaScript SDK\n * 用于页面会话录制和回溯\n */\n\nimport { record } from 'rrweb';\nimport pako from 'pako';\n\nclass PageRecorder {\n constructor() {\n this.config = null;\n this.sessionId = null;\n this.recording = false;\n this.stopFn = null;\n this.events = [];\n this.uploadQueue = [];\n this.uploadTimer = null;\n this.apiClient = null;\n 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 // 检查第一个事件是否是 fullSnapshot\n if (this.totalEvents === 0) {\n const isFullSnapshot = event.type === 2 || event.type === 'fullSnapshot';\n if (isFullSnapshot) {\n console.log('✅ [SDK] First event is full snapshot, good!');\n } else {\n console.warn('⚠️ [SDK] First event is NOT a full snapshot! Type:', event.type);\n console.warn('[SDK] This may cause playback issues. Event:', event);\n }\n }\n \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 console.log(`✅ [SDK] Uploaded sequence ${sequence} with ${eventsToUpload.length} events (total: ${this.totalEvents})`);\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","type","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":"8SAgaA,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,IAxfjB,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,GAEV,GAAyB,IAArBjF,KAAK+C,YAAmB,CACY,IAAfkC,EAAMqB,MAA6B,iBAAfrB,EAAMqB,KAE/CzE,QAAQG,IAAI,gDAEZH,QAAQC,KAAK,qDAAsDmD,EAAMqB,MACzEzE,QAAQC,KAAK,+CAAgDmD,GAEjE,CAEAjF,KAAK2B,OAAO4E,KAAKtB,GAGbjF,KAAK2B,OAAOwE,QAAUnG,KAAKsC,OAAOc,WACpCpD,KAAKgG,aAET,CAKA,iBAAMA,GACJ,GAA2B,IAAvBhG,KAAK2B,OAAOwE,SAAiBnG,KAAKwB,UACpC,OAGF,MAAMgF,EAAiB,IAAIxG,KAAK2B,QAChC3B,KAAK+C,aAAeyD,EAAeL,OACnCnG,KAAK2B,OAAS,GAGd3B,KAAK4C,iBACL,MAAMlB,EAAW1B,KAAK4C,eAEtB,IAEE,MAAMhB,EAAa5B,KAAKyG,eAAeD,GAGjCE,QAAe1G,KAAK2C,UAAUpB,aAClCvB,KAAKwB,UACLI,EACAF,GAIEgF,GAAQC,kBAAoBD,EAAOC,iBAAiBR,OAAS,IAC/DtE,QAAQC,KAAK,qCAAsC4E,EAAOC,wBAEpD3G,KAAK4G,sBAAsBF,EAAOC,mBAG1C9E,QAAQG,IAAI,6BAA6BN,UAAiB8E,EAAeL,yBAAyBnG,KAAK+C,gBAGvG/C,KAAKsC,OAAOyB,iBAAiB,CAC3BvC,UAAWxB,KAAKwB,UAChBE,SAAUA,EACVmF,SAAUL,EAAeL,OACzBpF,OAAQ2F,GAAQ3F,QAAU,WAE9B,CAAE,MAAON,GAEPT,KAAK6C,cAAciE,IAAIpF,EAAU,CAC/BC,OAAQ6E,EACRO,QAAS,EACTtG,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,MACMwF,EAAU,GAEhB,IAAK,MAAOtF,EAAUuF,KAAiBjH,KAAK6C,cAAcqE,UACpDD,EAAaF,QAJA,EAKfC,EAAQT,KAAK,CAAE7E,WAAUuF,kBAEzBpF,QAAQC,KAAK,YAAYJ,uCACzB1B,KAAK6C,cAAcsE,OAAOzF,IAI9B,IAAK,MAAMA,SAAEA,EAAQuF,aAAEA,KAAkBD,EACvC,IACE,MAAMpF,EAAa5B,KAAKyG,eAAeQ,EAAatF,cAC9C3B,KAAK2C,UAAUpB,aAAavB,KAAKwB,UAAWI,EAAYF,GAE9DG,QAAQG,IAAI,iCAAiCN,KAC7C1B,KAAK6C,cAAcsE,OAAOzF,EAC5B,CAAE,MAAOjB,GACPwG,EAAaF,UACblF,QAAQpB,MAAM,6BAA6BiB,cAAqBuF,EAAaF,YAAatG,EAC5F,CAEJ,CAKA,2BAAMmG,CAAsBD,GAC1B,IAAK,MAAMjF,KAAYiF,EAAkB,CACvC,MAAMM,EAAejH,KAAK6C,cAAcuE,IAAI1F,GAC5C,GAAIuF,EACF,IACE,MAAMrF,EAAa5B,KAAKyG,eAAeQ,EAAatF,cAC9C3B,KAAK2C,UAAUpB,aAAavB,KAAKwB,UAAWI,EAAYF,GAE9DG,QAAQG,IAAI,yCAAyCN,KACrD1B,KAAK6C,cAAcsE,OAAOzF,EAC5B,CAAE,MAAOjB,GACPoB,QAAQpB,MAAM,sCAAsCiB,KAAajB,EACnE,MAEAoB,QAAQC,KAAK,qCAAqCJ,yBAEtD,CACF,CAKA,cAAA+E,CAAe9E,GACb,MAAMjB,EAAOW,KAAKC,UAAUK,GACtBC,EAAayF,EAAKC,KAAK5G,GAC7B,OAAO6G,KAAKC,OAAOC,gBAAgB7F,GACrC,CAKA,gBAAAwD,GACE,MAAO,CACLsC,WAAY,WACZnE,cAAevD,KAAKsC,OAAOgB,QAAQC,cACnCC,iBAAkBxD,KAAKsC,OAAOgB,QAAQE,iBACtCmE,WAAY,CAACC,EAAMC,IACV7H,KAAK8H,kBAAkBF,EAAMC,GAG1C,CAKA,iBAAAC,CAAkBF,EAAMC,GACtB,IAAKD,EAAM,OAAOA,EAElB,MAAMG,EAAUH,EAAKI,OAGrB,OAAIhI,KAAKsC,OAAOgB,QAAQE,iBAAiBC,QAAU,wBAAwBwE,KAAKF,GACvEA,EAAQG,QAAQ,sBAAuB,gBAI5ClI,KAAKsC,OAAOgB,QAAQE,iBAAiBE,OAAS,gBAAgBuE,KAAKF,GAC9DA,EAAQG,QAAQ,sBAAuB,YAI5ClI,KAAKsC,OAAOgB,QAAQE,iBAAiBG,UAAY,cAAcsE,KAAKF,GAC/DA,EAAQG,QAAQ,oBAAqB,cAGvCN,CACT,CAKA,gBAAA/B,GACE7F,KAAK0C,YAAcyF,YAAY,KAC7BnI,KAAKgG,eACJhG,KAAKsC,OAAOe,eACjB,CAKA,eAAA0C,GACM/F,KAAK0C,cACP0F,cAAcpI,KAAK0C,aACnB1C,KAAK0C,YAAc,KAEvB,CAKA,WAAA2F,CAAYzE,GACV5D,KAAKsC,OAAOsB,SAAW,IAClB5D,KAAKsC,OAAOsB,YACZA,EAEP,CAKA,YAAA0E,GACE,OAAOtI,KAAKwB,SACd,CAKA,WAAA+G,GACE,OAAOvI,KAAKuC,SACd,SAyGoB,oBAAX4B,SACTA,OAAOqE,aAAenG"}
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 this.firstEventReceived = false; // 标记第一个事件\n\n // 通知会话创建\n this.config.onSessionCreated(session);\n\n // 开始 rrweb 录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\n // 移除 checkoutEveryNms,避免在录制过程中重新生成快照\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 // 等待一下确保 rrweb 有时间生成 fullSnapshot\n await new Promise(resolve => setTimeout(resolve, 100));\n\n // 检查是否收到了 fullSnapshot\n if (!this.firstEventReceived || (this.events.length > 0 && this.events[0].type !== 2)) {\n console.warn('⚠️ [SDK] rrweb did not emit fullSnapshot in time, forcing a manual snapshot...');\n \n // 如果没有收到 fullSnapshot,重新启动录制\n if (this.stopFn) {\n this.stopFn();\n }\n \n // 清空事件\n this.events = [];\n this.totalEvents = 0;\n this.firstEventReceived = false;\n \n // 再等待一下确保 DOM 稳定\n await new Promise(resolve => setTimeout(resolve, 500));\n \n // 重新开始录制\n this.stopFn = record({\n emit: (event) => {\n this.handleEvent(event);\n },\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 await new Promise(resolve => setTimeout(resolve, 100));\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 // 检查第一个事件是否是 fullSnapshot\n if (this.totalEvents === 0) {\n const isFullSnapshot = event.type === 2 || event.type === 'fullSnapshot';\n if (isFullSnapshot) {\n console.log('✅ [SDK] First event is full snapshot, good!');\n } else {\n console.warn('⚠️ [SDK] First event is NOT a full snapshot! Type:', event.type);\n console.warn('[SDK] This may cause playback issues. Event:', event);\n }\n }\n \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 console.log(`✅ [SDK] Uploaded sequence ${sequence} with ${eventsToUpload.length} events (total: ${this.totalEvents})`);\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","firstEventReceived","record","emit","event","handleEvent","getPrivacyConfig","sampling","mousemove","mouseInteraction","scroll","input","recordCanvas","inlineImages","collectFonts","length","type","startUploadTimer","stop","stopUploadTimer","flushEvents","size","retryFailedAppends","endTime","eventCount","push","eventsToUpload","compressEvents","result","missingSequences","retryMissingSequences","uploaded","set","retries","toRetry","failedAppend","entries","delete","get","pako","gzip","btoa","String","fromCharCode","blockClass","maskTextFn","text","element","maskSensitiveData","trimmed","trim","test","replace","setInterval","clearInterval","setMetadata","getSessionId","isRecording","PageRecorder"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepsure/page-replay-sdk",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "type": "module",
5
5
  "description": "DeepSure PageReplay SDK",
6
6
  "main": "dist/index.js",