@tracekit/replay 0.1.1 → 0.2.1

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.cjs CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";var k=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var L=(n,e)=>{for(var t in e)k(n,t,{get:e[t],enumerable:!0})},U=(n,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of _(e))!P.call(n,i)&&i!==t&&k(n,i,{get:()=>e[i],enumerable:!(s=A(e,i))||s.enumerable});return n};var $=n=>U(k({},"__esModule",{value:!0}),n);var j={};L(j,{replayIntegration:()=>K});module.exports=$(j);var p={sessionSampleRate:.1,errorSampleRate:0,unmask:[],idleTimeout:18e5,flushInterval:3e4,maxBufferSize:10485760};function R(n,e,t,s){return n<t?(console.warn(`[TraceKit Replay] ${e} (${n}) is below ${t}, clamping to ${t}`),t):n>s?(console.warn(`[TraceKit Replay] ${e} (${n}) is above ${s}, clamping to ${s}`),s):n}function T(n,e,t){let s=R(n.sessionSampleRate??p.sessionSampleRate,"sessionSampleRate",0,1),i=R(n.errorSampleRate??p.errorSampleRate,"errorSampleRate",0,1);return s+i>1&&(console.warn(`[TraceKit Replay] sessionSampleRate (${s}) + errorSampleRate (${i}) exceeds 1.0, clamping errorSampleRate`),i=1-s),{sessionSampleRate:s,errorSampleRate:i,unmask:n.unmask??p.unmask,idleTimeout:n.idleTimeout??p.idleTimeout,flushInterval:n.flushInterval??p.flushInterval,maxBufferSize:n.maxBufferSize??p.maxBufferSize,apiKey:e,endpoint:t}}var C=require("rrweb");var I=require("rrweb");function O(n){let e=["[data-tracekit-unmask]"];n.length>0&&e.push(...n);let t=e.join(", ");return(s,i)=>{if(!i)return"*".repeat(s.length);try{if(i.matches(t)||i.closest(t))return s}catch{}return"*".repeat(s.length)}}function h(n,e){try{return(0,I.record)({emit:(s,i)=>{e(s,i??!1)},maskTextSelector:"*",maskAllInputs:!0,maskTextFn:O(n.unmask),blockSelector:"img, video, canvas, svg, iframe",recordCanvas:!1,recordCrossOriginIframes:!1,inlineImages:!1,checkoutEveryNms:3e4,sampling:{mousemove:50,mouseInteraction:!0,scroll:150,input:"last"}})??null}catch{return null}}var f=class{constructor(e=6e4){this.events=[];this.maxAgeMs=e}add(e){this.events.push({event:e,timestamp:e.timestamp??Date.now()}),this.evictExpired()}flush(){let e=this.events.map(t=>t.event);return this.events=[],e}clear(){this.events=[]}get size(){return this.events.length}evictExpired(){let e=Date.now()-this.maxAgeMs;for(;this.events.length>0&&this.events[0].timestamp<e;)this.events.shift()}};function w(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID().replace(/-/g,""):Array.from({length:32},()=>Math.floor(Math.random()*16).toString(16)).join("")}function E(n){let e=Math.random();return e<n.sessionSampleRate?"session":e<n.sessionSampleRate+n.errorSampleRate?"buffer":"off"}var m=class{constructor(e){this.eventCallback=null;this.flushCallback=null;this.restartCallback=null;this.pauseCallback=null;this.resumeCallback=null;this.idleTimer=null;this.visibilityHandler=null;this.config=e,this.ringBuffer=new f(6e4);let t=E(e),s=Date.now();this.state={sessionId:w(),mode:t,startedAt:s,lastActivity:s,segmentId:0},this.resetIdleTimer(),this.setupVisibilityListener()}onEvent(e,t){if(this.state.lastActivity=Date.now(),this.resetIdleTimer(),this.state.mode==="session"){if(this.eventCallback)try{this.eventCallback([e])}catch{}}else this.state.mode==="buffer"&&this.ringBuffer.add(e)}onError(){if(this.state.mode==="buffer"&&this.ringBuffer.size>0){let e=this.ringBuffer.flush();if(this.eventCallback)try{this.eventCallback(e)}catch{}this.state.mode="session"}}setEventCallback(e){this.eventCallback=e}setFlushCallback(e){this.flushCallback=e}setRestartCallback(e){this.restartCallback=e}setPauseCallback(e){this.pauseCallback=e}setResumeCallback(e){this.resumeCallback=e}getSessionId(){return this.state.sessionId}getMode(){return this.state.mode}nextSegmentId(){return this.state.segmentId++}getState(){return{...this.state}}flush(){return this.state.mode==="buffer"?this.ringBuffer.flush():[]}resetIdleTimer(){this.idleTimer!==null&&clearTimeout(this.idleTimer),this.idleTimer=setTimeout(()=>{this.handleIdleTimeout()},this.config.idleTimeout)}handleIdleTimeout(){if(this.flushCallback)try{this.flushCallback()}catch{}let e=E(this.config),t=Date.now();if(this.state={sessionId:w(),mode:e,startedAt:t,lastActivity:t,segmentId:0},this.ringBuffer.clear(),this.restartCallback)try{this.restartCallback()}catch{}}setupVisibilityListener(){typeof document>"u"||(this.visibilityHandler=()=>{if(document.visibilityState==="hidden"){if(this.pauseCallback)try{this.pauseCallback()}catch{}}else if(document.visibilityState==="visible"&&(this.state.lastActivity=Date.now(),this.resetIdleTimer(),this.resumeCallback))try{this.resumeCallback()}catch{}},document.addEventListener("visibilitychange",this.visibilityHandler))}destroy(){this.idleTimer!==null&&(clearTimeout(this.idleTimer),this.idleTimer=null),this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null),this.ringBuffer.clear(),this.eventCallback=null,this.flushCallback=null,this.restartCallback=null,this.pauseCallback=null,this.resumeCallback=null}};var x=require("fflate");var M=`
1
+ "use strict";var k=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var L=(i,e)=>{for(var t in e)k(i,t,{get:e[t],enumerable:!0})},U=(i,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of _(e))!P.call(i,n)&&n!==t&&k(i,n,{get:()=>e[n],enumerable:!(s=A(e,n))||s.enumerable});return i};var $=i=>U(k({},"__esModule",{value:!0}),i);var j={};L(j,{replayIntegration:()=>K});module.exports=$(j);var c={sessionSampleRate:.1,errorSampleRate:0,unmask:[],idleTimeout:18e5,flushInterval:3e4,maxBufferSize:24117248,inlineImages:!1,blockMedia:!0};function R(i,e,t,s){return i<t?(console.warn(`[TraceKit Replay] ${e} (${i}) is below ${t}, clamping to ${t}`),t):i>s?(console.warn(`[TraceKit Replay] ${e} (${i}) is above ${s}, clamping to ${s}`),s):i}function I(i,e,t){let s=R(i.sessionSampleRate??c.sessionSampleRate,"sessionSampleRate",0,1),n=R(i.errorSampleRate??c.errorSampleRate,"errorSampleRate",0,1);return s+n>1&&(console.warn(`[TraceKit Replay] sessionSampleRate (${s}) + errorSampleRate (${n}) exceeds 1.0, clamping errorSampleRate`),n=1-s),{sessionSampleRate:s,errorSampleRate:n,unmask:i.unmask??c.unmask,idleTimeout:i.idleTimeout??c.idleTimeout,flushInterval:i.flushInterval??c.flushInterval,maxBufferSize:i.maxBufferSize??c.maxBufferSize,inlineImages:i.inlineImages??c.inlineImages,blockMedia:i.blockMedia??c.blockMedia,apiKey:e,endpoint:t}}var C=require("rrweb");var T=require("rrweb");function O(i){let e=["[data-tracekit-unmask]"];i.length>0&&e.push(...i);let t=e.join(", ");return(s,n)=>{if(!n)return"*".repeat(s.length);try{if(n.matches(t)||n.closest(t))return s}catch{}return"*".repeat(s.length)}}function h(i,e){try{return(0,T.record)({emit:(s,n)=>{e(s,n??!1)},maskTextSelector:"*",maskAllInputs:!0,maskTextFn:O(i.unmask),...i.blockMedia?{blockSelector:"img, video, canvas, svg, iframe"}:{},recordCanvas:!1,recordCrossOriginIframes:!1,inlineImages:i.inlineImages,checkoutEveryNms:3e4,sampling:{mousemove:50,mouseInteraction:!0,scroll:150,input:"last"}})??null}catch{return null}}var f=class{constructor(e=6e4){this.events=[];this.maxAgeMs=e}add(e){this.events.push({event:e,timestamp:e.timestamp??Date.now()}),this.evictExpired()}flush(){let e=this.events.map(t=>t.event);return this.events=[],e}clear(){this.events=[]}get size(){return this.events.length}evictExpired(){let e=Date.now()-this.maxAgeMs;for(;this.events.length>0&&this.events[0].timestamp<e;)this.events.shift()}};function w(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID().replace(/-/g,""):Array.from({length:32},()=>Math.floor(Math.random()*16).toString(16)).join("")}function E(i){let e=Math.random();return e<i.sessionSampleRate?"session":e<i.sessionSampleRate+i.errorSampleRate?"buffer":"off"}var m=class{constructor(e){this.eventCallback=null;this.flushCallback=null;this.restartCallback=null;this.pauseCallback=null;this.resumeCallback=null;this.idleTimer=null;this.visibilityHandler=null;this.config=e,this.ringBuffer=new f(6e4);let t=E(e),s=Date.now();this.state={sessionId:w(),mode:t,startedAt:s,lastActivity:s,segmentId:0},this.resetIdleTimer(),this.setupVisibilityListener()}onEvent(e,t){if(this.state.lastActivity=Date.now(),this.resetIdleTimer(),this.state.mode==="session"){if(this.eventCallback)try{this.eventCallback([e])}catch{}}else this.state.mode==="buffer"&&this.ringBuffer.add(e)}onError(){if(this.state.mode==="buffer"&&this.ringBuffer.size>0){let e=this.ringBuffer.flush();if(this.eventCallback)try{this.eventCallback(e)}catch{}this.state.mode="session"}}setEventCallback(e){this.eventCallback=e}setFlushCallback(e){this.flushCallback=e}setRestartCallback(e){this.restartCallback=e}setPauseCallback(e){this.pauseCallback=e}setResumeCallback(e){this.resumeCallback=e}getSessionId(){return this.state.sessionId}getMode(){return this.state.mode}nextSegmentId(){return this.state.segmentId++}getState(){return{...this.state}}flush(){return this.state.mode==="buffer"?this.ringBuffer.flush():[]}resetIdleTimer(){this.idleTimer!==null&&clearTimeout(this.idleTimer),this.idleTimer=setTimeout(()=>{this.handleIdleTimeout()},this.config.idleTimeout)}handleIdleTimeout(){if(this.flushCallback)try{this.flushCallback()}catch{}let e=E(this.config),t=Date.now();if(this.state={sessionId:w(),mode:e,startedAt:t,lastActivity:t,segmentId:0},this.ringBuffer.clear(),this.restartCallback)try{this.restartCallback()}catch{}}setupVisibilityListener(){typeof document>"u"||(this.visibilityHandler=()=>{if(document.visibilityState==="hidden"){if(this.pauseCallback)try{this.pauseCallback()}catch{}}else if(document.visibilityState==="visible"&&(this.state.lastActivity=Date.now(),this.resetIdleTimer(),this.resumeCallback))try{this.resumeCallback()}catch{}},document.addEventListener("visibilitychange",this.visibilityHandler))}destroy(){this.idleTimer!==null&&(clearTimeout(this.idleTimer),this.idleTimer=null),this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null),this.ringBuffer.clear(),this.eventCallback=null,this.flushCallback=null,this.restartCallback=null,this.pauseCallback=null,this.resumeCallback=null}};var x=require("fflate");var M=`
2
2
  self.onmessage = function(e) {
3
3
  try {
4
4
  var data = e.data;
@@ -46,5 +46,5 @@ self.onmessage = function(e) {
46
46
  self.postMessage({ error: (err && err.message) || 'Unknown compression error', segmentId: e.data.segmentId });
47
47
  }
48
48
  };
49
- `;var g=class{constructor(){this.worker=null;this.pendingCallbacks=new Map;this.useMainThread=!1;this.initWorker()}initWorker(){try{let e=new Blob([M],{type:"application/javascript"}),t=URL.createObjectURL(e);this.worker=new Worker(t),URL.revokeObjectURL(t),this.worker.onmessage=s=>{let{compressed:i,segmentId:o,originalSize:c,error:a}=s.data,l=this.pendingCallbacks.get(o);if(l){if(this.pendingCallbacks.delete(o),a){console.warn("[TraceKit Replay] Worker compression failed, falling back to main thread:",a),this.useMainThread=!0,this.compressMainThread(l.events).then(l.resolve).catch(l.reject);return}l.resolve({compressed:i,originalSize:c})}},this.worker.onerror=()=>{console.warn("[TraceKit Replay] Web Worker failed to initialize. Using main-thread compression."),this.useMainThread=!0,this.worker=null}}catch{console.warn("[TraceKit Replay] Cannot create Web Worker (CSP?). Using main-thread compression."),this.useMainThread=!0}}async compress(e,t){return this.useMainThread||!this.worker?this.compressMainThread(e):new Promise((s,i)=>{let o={resolve:s,reject:i,events:e};this.pendingCallbacks.set(t,o),this.worker.postMessage({events:e,segmentId:t})})}async compressMainThread(e){let t=JSON.stringify(e),s=new TextEncoder().encode(t);return{compressed:(0,x.gzipSync)(s,{level:6}),originalSize:s.length}}destroy(){this.worker&&(this.worker.terminate(),this.worker=null),this.pendingCallbacks.clear()}};var F=require("fflate"),z=[1e3,2e3,4e3],b=3,H=65536,v=class{constructor(e,t){this.pendingEvents=[];this.flushTimer=null;this.bufferSize=0;this.sessionIdFn=null;this.segmentIdFn=null;this.replayTypeFn=null;this.visibilityHandler=null;this.config=e,this.compressionWorker=t}start(e,t,s){this.sessionIdFn=e,this.segmentIdFn=t,this.replayTypeFn=s,this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.flushInterval),typeof document<"u"&&(this.visibilityHandler=()=>{document.visibilityState==="hidden"&&this.flushSync()},document.addEventListener("visibilitychange",this.visibilityHandler))}stop(){this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null)}destroy(){this.stop(),this.pendingEvents=[],this.bufferSize=0}addEvent(e){try{let t=JSON.stringify(e).length;for(this.pendingEvents.push(e),this.bufferSize+=t;this.bufferSize>this.config.maxBufferSize&&this.pendingEvents.length>1;){let s=this.pendingEvents.shift();if(s)try{this.bufferSize-=JSON.stringify(s).length}catch{}this.config.maxBufferSize>0&&console.warn("[TraceKit Replay] Buffer exceeded maxBufferSize, dropping oldest events")}}catch{}}addEvents(e){for(let t of e)this.addEvent(t)}async flush(){if(this.pendingEvents.length===0)return;let e=this.pendingEvents;this.pendingEvents=[],this.bufferSize=0;try{let t=this.sessionIdFn?this.sessionIdFn():"",s=this.segmentIdFn?this.segmentIdFn():0;if(!t)return;let{compressed:i,originalSize:o}=await this.compressionWorker.compress(e,s);await this.uploadWithRetry(t,s,i,o)}catch{}}flushSync(){if(this.pendingEvents.length!==0)try{let e=this.pendingEvents;this.pendingEvents=[],this.bufferSize=0;let t=this.sessionIdFn?this.sessionIdFn():"",s=this.segmentIdFn?this.segmentIdFn():0,i=this.replayTypeFn?this.replayTypeFn():"session";if(!t)return;let o=JSON.stringify(e),c=new TextEncoder().encode(o),a=(0,F.gzipSync)(c,{level:6}),l=`${this.config.endpoint}/api/replays/${t}/chunks?api_key=${encodeURIComponent(this.config.apiKey)}&segment_id=${s}&original_size=${c.length}&replay_type=${i}`,u=new Blob([a],{type:"application/octet-stream"});if(a.byteLength<=H&&typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(l,u))return;typeof fetch<"u"&&fetch(l,{method:"POST",headers:{"Content-Type":"application/octet-stream"},body:u,keepalive:!0}).catch(()=>{})}catch{}}async uploadWithRetry(e,t,s,i){let o=`${this.config.endpoint}/api/replays/${e}/chunks`,c=this.replayTypeFn?this.replayTypeFn():"session";for(let a=0;a<b;a++)try{let l=await fetch(o,{method:"POST",headers:{"Content-Type":"application/octet-stream","X-API-Key":this.config.apiKey,"X-Segment-Id":String(t),"X-Original-Size":String(i),"X-Replay-Type":c},body:s,keepalive:!0});if(l.ok||l.status===400||l.status===401||l.status===413)return;a<b-1&&await this.delay(z[a])}catch{a<b-1&&await this.delay(z[a])}}delay(e){return new Promise(t=>setTimeout(t,e))}};function K(n={}){let e=null,t=null,s=null,i=null,o=null;return{name:"replay",install(a){try{let l=a.getConfig();if(o=T(n,l.apiKey,l.endpoint),e=new m(o),e.getMode()==="off")return;t=new g,s=new v(o,t),e.setEventCallback(r=>{for(let d of r)s.addEvent(d)});let u=a.captureException.bind(a);a.captureException=function(r,d){let y=e?.getSessionId()??"",S=a.getScope();y&&S.setTag("replay_id",y);let W=u(r,d);return y&&S.setTag("replay_id",""),e&&e.onError(),W},a.getScope().onBreadcrumb(r=>{try{if(r.type==="http"||r.category?.startsWith("fetch")||r.category?.startsWith("xhr"))C.record.addCustomEvent("network-request",{method:r.data?.method||"GET",url:r.data?.url||r.message||"",status:r.data?.status_code,duration:r.data?.duration,error:r.data?.error,traceparent:r.data?.traceparent});else if(r.type==="console"||r.category?.startsWith("console.")){let d={level:r.category?.replace("console.","")||"log",message:r.message||""};r.data&&Object.keys(r.data).length>0&&(d.data=r.data),(r.category==="console.error"||r.category==="console.warn")&&r.data?.stack&&(d.stack=r.data.stack),C.record.addCustomEvent("console-log",d)}}catch{}}),e.setFlushCallback(()=>{s?.flush().catch(()=>{})}),e.setRestartCallback(()=>{i&&i(),i=h(o,(r,d)=>{e?.onEvent(r,d)})}),e.setPauseCallback(()=>{i&&(i(),i=null),s?.flushSync()}),e.setResumeCallback(()=>{i=h(o,(r,d)=>{e?.onEvent(r,d)})}),s.start(()=>e.getSessionId(),()=>e.nextSegmentId(),()=>e.getMode()==="buffer"?"buffer":"session"),i=h(o,(r,d)=>{e.onEvent(r,d)})}catch(l){console.warn("[TraceKit Replay] Failed to initialize replay recording:",l)}},teardown(){try{i&&(i(),i=null),s?.destroy(),t?.destroy(),e?.destroy(),e=null,t=null,s=null,o=null}catch{}},flush(){try{s?.flush().catch(()=>{})}catch{}},getSessionId(){return e?.getSessionId()??""}}}
49
+ `;var g=class{constructor(){this.worker=null;this.pendingCallbacks=new Map;this.useMainThread=!1;this.initWorker()}initWorker(){try{let e=new Blob([M],{type:"application/javascript"}),t=URL.createObjectURL(e);this.worker=new Worker(t),URL.revokeObjectURL(t),this.worker.onmessage=s=>{let{compressed:n,segmentId:o,originalSize:p,error:a}=s.data,l=this.pendingCallbacks.get(o);if(l){if(this.pendingCallbacks.delete(o),a){console.warn("[TraceKit Replay] Worker compression failed, falling back to main thread:",a),this.useMainThread=!0,this.compressMainThread(l.events).then(l.resolve).catch(l.reject);return}l.resolve({compressed:n,originalSize:p})}},this.worker.onerror=()=>{console.warn("[TraceKit Replay] Web Worker failed to initialize. Using main-thread compression."),this.useMainThread=!0,this.worker=null}}catch{console.warn("[TraceKit Replay] Cannot create Web Worker (CSP?). Using main-thread compression."),this.useMainThread=!0}}async compress(e,t){return this.useMainThread||!this.worker?this.compressMainThread(e):new Promise((s,n)=>{let o={resolve:s,reject:n,events:e};this.pendingCallbacks.set(t,o),this.worker.postMessage({events:e,segmentId:t})})}async compressMainThread(e){let t=JSON.stringify(e),s=new TextEncoder().encode(t);return{compressed:(0,x.gzipSync)(s,{level:6}),originalSize:s.length}}destroy(){this.worker&&(this.worker.terminate(),this.worker=null),this.pendingCallbacks.clear()}};var F=require("fflate"),z=[1e3,2e3,4e3],b=3,H=65536,v=class{constructor(e,t){this.pendingEvents=[];this.flushTimer=null;this.bufferSize=0;this.sessionIdFn=null;this.segmentIdFn=null;this.replayTypeFn=null;this.visibilityHandler=null;this.config=e,this.compressionWorker=t}start(e,t,s){this.sessionIdFn=e,this.segmentIdFn=t,this.replayTypeFn=s,this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.flushInterval),typeof document<"u"&&(this.visibilityHandler=()=>{document.visibilityState==="hidden"&&this.flushSync()},document.addEventListener("visibilitychange",this.visibilityHandler))}stop(){this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null)}destroy(){this.stop(),this.pendingEvents=[],this.bufferSize=0}addEvent(e){try{let t=JSON.stringify(e).length;for(this.pendingEvents.push(e),this.bufferSize+=t;this.bufferSize>this.config.maxBufferSize&&this.pendingEvents.length>1;){let s=this.pendingEvents.shift();if(s)try{this.bufferSize-=JSON.stringify(s).length}catch{}this.config.maxBufferSize>0&&console.warn("[TraceKit Replay] Buffer exceeded maxBufferSize, dropping oldest events")}}catch{}}addEvents(e){for(let t of e)this.addEvent(t)}async flush(){if(this.pendingEvents.length===0)return;let e=this.pendingEvents;this.pendingEvents=[],this.bufferSize=0;try{let t=this.sessionIdFn?this.sessionIdFn():"",s=this.segmentIdFn?this.segmentIdFn():0;if(!t)return;let{compressed:n,originalSize:o}=await this.compressionWorker.compress(e,s);await this.uploadWithRetry(t,s,n,o)}catch{}}flushSync(){if(this.pendingEvents.length!==0)try{let e=this.pendingEvents;this.pendingEvents=[],this.bufferSize=0;let t=this.sessionIdFn?this.sessionIdFn():"",s=this.segmentIdFn?this.segmentIdFn():0,n=this.replayTypeFn?this.replayTypeFn():"session";if(!t)return;let o=JSON.stringify(e),p=new TextEncoder().encode(o),a=(0,F.gzipSync)(p,{level:6}),l=`${this.config.endpoint}/api/replays/${t}/chunks?api_key=${encodeURIComponent(this.config.apiKey)}&segment_id=${s}&original_size=${p.length}&replay_type=${n}`,u=new Blob([a],{type:"application/octet-stream"});if(a.byteLength<=H&&typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(l,u))return;typeof fetch<"u"&&fetch(l,{method:"POST",headers:{"Content-Type":"application/octet-stream"},body:u,keepalive:!0}).catch(()=>{})}catch{}}async uploadWithRetry(e,t,s,n){let o=`${this.config.endpoint}/api/replays/${e}/chunks`,p=this.replayTypeFn?this.replayTypeFn():"session";for(let a=0;a<b;a++)try{let l=await fetch(o,{method:"POST",headers:{"Content-Type":"application/octet-stream","X-API-Key":this.config.apiKey,"X-Segment-Id":String(t),"X-Original-Size":String(n),"X-Replay-Type":p},body:s,keepalive:!0});if(l.ok||l.status===400||l.status===401||l.status===413)return;a<b-1&&await this.delay(z[a])}catch{a<b-1&&await this.delay(z[a])}}delay(e){return new Promise(t=>setTimeout(t,e))}};function K(i={}){let e=null,t=null,s=null,n=null,o=null;return{name:"replay",install(a){try{let l=a.getConfig();if(o=I(i,l.apiKey,l.endpoint),e=new m(o),e.getMode()==="off")return;t=new g,s=new v(o,t),e.setEventCallback(r=>{for(let d of r)s.addEvent(d)});let u=a.captureException.bind(a);a.captureException=function(r,d){let y=e?.getSessionId()??"",S=a.getScope();y&&S.setTag("replay_id",y);let W=u(r,d);return y&&S.setTag("replay_id",""),e&&e.onError(),W},a.getScope().onBreadcrumb(r=>{try{if(r.type==="http"||r.category?.startsWith("fetch")||r.category?.startsWith("xhr"))C.record.addCustomEvent("network-request",{method:r.data?.method||"GET",url:r.data?.url||r.message||"",status:r.data?.status_code,duration:r.data?.duration,error:r.data?.error,traceparent:r.data?.traceparent});else if(r.type==="console"||r.category?.startsWith("console.")){let d={level:r.category?.replace("console.","")||"log",message:r.message||""};r.data&&Object.keys(r.data).length>0&&(d.data=r.data),(r.category==="console.error"||r.category==="console.warn")&&r.data?.stack&&(d.stack=r.data.stack),C.record.addCustomEvent("console-log",d)}}catch{}}),e.setFlushCallback(()=>{s?.flush().catch(()=>{})}),e.setRestartCallback(()=>{n&&n(),n=h(o,(r,d)=>{e?.onEvent(r,d)})}),e.setPauseCallback(()=>{n&&(n(),n=null),s?.flushSync()}),e.setResumeCallback(()=>{n=h(o,(r,d)=>{e?.onEvent(r,d)})}),s.start(()=>e.getSessionId(),()=>e.nextSegmentId(),()=>e.getMode()==="buffer"?"buffer":"session"),n=h(o,(r,d)=>{e.onEvent(r,d)})}catch(l){console.warn("[TraceKit Replay] Failed to initialize replay recording:",l)}},teardown(){try{n&&(n(),n=null),s?.destroy(),t?.destroy(),e?.destroy(),e=null,t=null,s=null,o=null}catch{}},flush(){try{s?.flush().catch(()=>{})}catch{}},getSessionId(){return e?.getSessionId()??""}}}
50
50
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/recorder.ts","../src/buffer.ts","../src/session.ts","../src/compression.ts","../src/worker.ts","../src/transport.ts"],"sourcesContent":["/**\n * TraceKit Replay - Public API\n * @package @tracekit/replay\n *\n * Entry point for the session replay addon.\n * Provides replayIntegration() factory that returns an Integration object\n * compatible with @tracekit/browser's addons system.\n *\n * Usage:\n * import { init } from '@tracekit/browser';\n * import { replayIntegration } from '@tracekit/replay';\n * init({ apiKey: 'key', addons: [replayIntegration()] });\n *\n * The integration wires together:\n * recorder -> session manager -> compression worker -> transport\n *\n * Recording starts immediately when the integration is installed via init().\n * Manual control: flush() forces an upload, getSessionId() returns the current ID.\n */\n\nimport type { ReplayConfig, ResolvedReplayConfig, Integration } from './types';\nimport { resolveReplayConfig } from './config';\nimport { record } from 'rrweb';\nimport { startRecording } from './recorder';\nimport { SessionManager } from './session';\nimport { CompressionWorker } from './compression';\nimport { ReplayTransport } from './transport';\n\n/**\n * Create a session replay integration for @tracekit/browser.\n *\n * @param config - Optional replay configuration overrides\n * @returns Integration object with flush() and getSessionId() manual control methods\n */\nexport function replayIntegration(\n config: ReplayConfig = {},\n): Integration & { flush(): void; getSessionId(): string } {\n let session: SessionManager | null = null;\n let compressionWorker: CompressionWorker | null = null;\n let transport: ReplayTransport | null = null;\n let stopRecording: (() => void) | null = null;\n let resolvedConfig: ResolvedReplayConfig | null = null;\n\n const integration: Integration & { flush(): void; getSessionId(): string } = {\n name: 'replay',\n\n /**\n * Install the replay integration into the BrowserClient.\n * Creates the full recording pipeline and starts recording immediately.\n *\n * ALL code is wrapped in try/catch -- if replay fails to initialize,\n * it MUST NOT break the host app or the core browser SDK.\n */\n install(client: any): void {\n try {\n // Resolve config with client's apiKey and endpoint\n const clientConfig = client.getConfig();\n resolvedConfig = resolveReplayConfig(config, clientConfig.apiKey, clientConfig.endpoint);\n\n // Create session manager (makes sampling decision)\n session = new SessionManager(resolvedConfig);\n\n // If mode is 'off', don't set up recording pipeline\n if (session.getMode() === 'off') {\n return;\n }\n\n // Create compression worker (Web Worker with main-thread fallback)\n compressionWorker = new CompressionWorker();\n\n // Create transport (30-second flush interval, retry, sendBeacon fallback)\n transport = new ReplayTransport(resolvedConfig, compressionWorker);\n\n // Wire session -> transport: events flow from session to transport\n session.setEventCallback((events: any[]) => {\n for (const event of events) {\n transport!.addEvent(event);\n }\n });\n\n // Wire error notification: hook into captureException to detect errors\n // for ring buffer flush. Also injects replay_id as a tag on the error event\n // so the playback UI can link errors to their replay session.\n // Uses setTag (not setExtra) so replay_id appears as a direct span attribute\n // in OTLP output -- extras would prefix it as \"extra.replay_id\".\n const originalCaptureException = client.captureException.bind(client);\n client.captureException = function (error: Error, context?: any): string {\n const replayId = session?.getSessionId() ?? '';\n const scope = client.getScope();\n if (replayId) {\n scope.setTag('replay_id', replayId);\n }\n const result = originalCaptureException(error, context);\n if (replayId) {\n scope.setTag('replay_id', '');\n }\n if (session) {\n session.onError();\n }\n return result;\n };\n\n // Bridge breadcrumbs to rrweb custom events for playback sidebar tabs.\n // Network requests become 'network-request' events, console output becomes\n // 'console-log' events -- both timestamped in the rrweb event stream.\n const scope = client.getScope();\n scope.onBreadcrumb((crumb: any) => {\n try {\n if (crumb.type === 'http' || crumb.category?.startsWith('fetch') || crumb.category?.startsWith('xhr')) {\n record.addCustomEvent('network-request', {\n method: crumb.data?.method || 'GET',\n url: crumb.data?.url || crumb.message || '',\n status: crumb.data?.status_code,\n duration: crumb.data?.duration,\n error: crumb.data?.error,\n traceparent: crumb.data?.traceparent,\n });\n } else if (crumb.type === 'console' || crumb.category?.startsWith('console.')) {\n // Console tab requires expandable objects and stack traces for errors\n const payload: Record<string, unknown> = {\n level: crumb.category?.replace('console.', '') || 'log',\n message: crumb.message || '',\n };\n // Include structured data from console args (objects, arrays)\n if (crumb.data && Object.keys(crumb.data).length > 0) {\n payload.data = crumb.data;\n }\n // Include stack trace for error-level console entries\n if ((crumb.category === 'console.error' || crumb.category === 'console.warn') && crumb.data?.stack) {\n payload.stack = crumb.data.stack;\n }\n record.addCustomEvent('console-log', payload);\n }\n } catch {\n // Never crash the host app\n }\n });\n\n // Wire idle timeout: flush pending events when session goes idle\n session.setFlushCallback(() => {\n transport?.flush().catch(() => {\n // Never crash on flush errors\n });\n });\n\n // Wire idle timeout restart: stop recording, start fresh with new snapshot\n session.setRestartCallback(() => {\n if (stopRecording) {\n stopRecording();\n }\n stopRecording = startRecording(resolvedConfig!, (event, isCheckout) => {\n session?.onEvent(event, isCheckout);\n });\n });\n\n // Wire visibility pause: stop recording and flush sync on tab hide\n session.setPauseCallback(() => {\n if (stopRecording) {\n stopRecording();\n stopRecording = null;\n }\n transport?.flushSync();\n });\n\n // Wire visibility resume: restart recording on tab show\n session.setResumeCallback(() => {\n stopRecording = startRecording(resolvedConfig!, (event, isCheckout) => {\n session?.onEvent(event, isCheckout);\n });\n });\n\n // Start transport (30-second flush interval)\n transport.start(\n () => session!.getSessionId(),\n () => session!.nextSegmentId(),\n () => (session!.getMode() === 'buffer' ? 'buffer' : 'session'),\n );\n\n // Start recording immediately (LOCKED: recording starts on init())\n stopRecording = startRecording(resolvedConfig, (event, isCheckout) => {\n session!.onEvent(event, isCheckout);\n });\n } catch (err) {\n console.warn('[TraceKit Replay] Failed to initialize replay recording:', err);\n }\n },\n\n /**\n * Teardown: clean up all resources.\n * Called when the BrowserClient is destroyed.\n */\n teardown(): void {\n try {\n if (stopRecording) {\n stopRecording();\n stopRecording = null;\n }\n transport?.destroy();\n compressionWorker?.destroy();\n session?.destroy();\n session = null;\n compressionWorker = null;\n transport = null;\n resolvedConfig = null;\n } catch {\n // Ignore teardown errors\n }\n },\n\n /**\n * Manual flush: force an immediate upload of pending events.\n * Useful for ensuring data is sent before a page transition.\n */\n flush(): void {\n try {\n transport?.flush().catch(() => {\n // Never crash\n });\n } catch {\n // Never crash\n }\n },\n\n /**\n * Get the current session ID.\n * Returns empty string if replay is not active.\n * Useful for linking errors to replay sessions (Phase 26).\n */\n getSessionId(): string {\n return session?.getSessionId() ?? '';\n },\n };\n\n return integration;\n}\n\n// Re-export types for consumers\nexport type { ReplayConfig, Integration } from './types';\n","/**\n * TraceKit Replay - Configuration Resolution\n * @package @tracekit/replay\n *\n * Resolves user-provided ReplayConfig with privacy-first defaults.\n * Validates and clamps rate values to valid ranges.\n */\n\nimport type { ReplayConfig, ResolvedReplayConfig } from './types';\n\nconst DEFAULTS = {\n sessionSampleRate: 0.1,\n errorSampleRate: 0.0,\n unmask: [] as string[],\n idleTimeout: 1_800_000, // 30 minutes\n flushInterval: 30_000, // 30 seconds\n maxBufferSize: 10_485_760, // 10MB\n} as const;\n\n/**\n * Clamp a value to the [min, max] range, logging a warning if clamped.\n */\nfunction clampRate(value: number, name: string, min: number, max: number): number {\n if (value < min) {\n console.warn(`[TraceKit Replay] ${name} (${value}) is below ${min}, clamping to ${min}`);\n return min;\n }\n if (value > max) {\n console.warn(`[TraceKit Replay] ${name} (${value}) is above ${max}, clamping to ${max}`);\n return max;\n }\n return value;\n}\n\n/**\n * Resolve user-provided replay config with defaults.\n * Validates sample rates are in [0, 1] and their sum does not exceed 1.0.\n */\nexport function resolveReplayConfig(\n config: ReplayConfig,\n apiKey: string,\n endpoint: string,\n): ResolvedReplayConfig {\n let sessionSampleRate = clampRate(\n config.sessionSampleRate ?? DEFAULTS.sessionSampleRate,\n 'sessionSampleRate',\n 0,\n 1,\n );\n\n let errorSampleRate = clampRate(\n config.errorSampleRate ?? DEFAULTS.errorSampleRate,\n 'errorSampleRate',\n 0,\n 1,\n );\n\n // Ensure combined rate does not exceed 1.0\n if (sessionSampleRate + errorSampleRate > 1.0) {\n console.warn(\n `[TraceKit Replay] sessionSampleRate (${sessionSampleRate}) + errorSampleRate (${errorSampleRate}) exceeds 1.0, clamping errorSampleRate`,\n );\n errorSampleRate = 1.0 - sessionSampleRate;\n }\n\n return {\n sessionSampleRate,\n errorSampleRate,\n unmask: config.unmask ?? DEFAULTS.unmask,\n idleTimeout: config.idleTimeout ?? DEFAULTS.idleTimeout,\n flushInterval: config.flushInterval ?? DEFAULTS.flushInterval,\n maxBufferSize: config.maxBufferSize ?? DEFAULTS.maxBufferSize,\n apiKey,\n endpoint,\n };\n}\n","/**\n * TraceKit Replay - rrweb Recorder Wrapper\n * @package @tracekit/replay\n *\n * Wraps rrweb's record() function with privacy-first defaults:\n * - All text masked with same-length asterisk replacement\n * - All inputs masked\n * - Images, videos, canvas, SVGs, and iframes blocked\n * - Unmasking via CSS selectors and data-tracekit-unmask attribute\n * - Periodic full snapshots every 30s aligned with upload interval\n */\n\nimport { record } from 'rrweb';\nimport type { ResolvedReplayConfig } from './types';\n\n/**\n * Create a maskTextFn that masks all text EXCEPT elements matching\n * unmask selectors or the data-tracekit-unmask attribute.\n *\n * Privacy-first: when in doubt (null element, invalid selector), mask.\n */\nfunction createMaskTextFn(\n unmaskSelectors: string[],\n): (text: string, element: HTMLElement | null) => string {\n // Build combined CSS selector for unmask targets\n const selectorParts = ['[data-tracekit-unmask]'];\n if (unmaskSelectors.length > 0) {\n selectorParts.push(...unmaskSelectors);\n }\n const combinedSelector = selectorParts.join(', ');\n\n return (text: string, element: HTMLElement | null): string => {\n // Privacy-first: mask when element is null (uncertain context)\n if (!element) {\n return '*'.repeat(text.length);\n }\n\n // Check if element (or any ancestor) matches unmask selectors\n try {\n if (element.matches(combinedSelector) || element.closest(combinedSelector)) {\n return text; // Unmask: return original text\n }\n } catch {\n // Invalid selector -- fail safe by masking\n }\n\n // Default: same-length asterisk replacement\n return '*'.repeat(text.length);\n };\n}\n\n/**\n * Start rrweb recording with privacy-first defaults.\n *\n * @param config - Resolved replay configuration\n * @param onEvent - Callback invoked for each rrweb event\n * @returns A stop function to halt recording, or null if recording failed to start\n */\nexport function startRecording(\n config: ResolvedReplayConfig,\n onEvent: (event: unknown, isCheckout: boolean) => void,\n): (() => void) | null {\n try {\n const stopFn = record({\n emit: (event, isCheckout) => {\n onEvent(event, isCheckout ?? false);\n },\n\n // ================================================================\n // Privacy-first settings (LOCKED decisions)\n // ================================================================\n\n // Mask all text by matching all elements\n maskTextSelector: '*',\n\n // Mask all input values\n maskAllInputs: true,\n\n // Custom mask function: same-length asterisk replacement with unmask support\n maskTextFn: createMaskTextFn(config.unmask),\n\n // Block all media and embedded content\n blockSelector: 'img, video, canvas, svg, iframe',\n\n // Do NOT record canvas content\n recordCanvas: false,\n\n // Do NOT record cross-origin iframes\n recordCrossOriginIframes: false,\n\n // Do NOT inline images\n inlineImages: false,\n\n // ================================================================\n // Recording lifecycle\n // ================================================================\n\n // Periodic full snapshot every 30s, aligned with upload interval\n checkoutEveryNms: 30_000,\n\n // Sampling configuration to reduce event volume\n sampling: {\n mousemove: 50,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n });\n\n // rrweb record() returns undefined if it fails to start\n return stopFn ?? null;\n } catch {\n // Never crash the host application\n return null;\n }\n}\n","/**\n * TraceKit Replay - Ring Buffer\n * @package @tracekit/replay\n *\n * Timestamp-based ring buffer for error-mode capture.\n * Maintains exactly 60 seconds of rrweb events, evicting expired\n * entries on every add(). On error, the buffer is flushed and\n * the session switches from 'buffer' to 'session' mode.\n */\n\nexport class RingBuffer {\n private events: Array<{ event: any; timestamp: number }> = [];\n private maxAgeMs: number;\n\n constructor(maxAgeMs: number = 60_000) {\n this.maxAgeMs = maxAgeMs;\n }\n\n /**\n * Add an event to the buffer. Uses `event.timestamp` from rrweb\n * (milliseconds since epoch), falling back to Date.now().\n * Evicts expired entries after each add.\n */\n add(event: any): void {\n this.events.push({ event, timestamp: event.timestamp ?? Date.now() });\n this.evictExpired();\n }\n\n /**\n * Flush all buffered events and clear the buffer.\n * Returns the raw rrweb events (unwrapped from the timestamp envelope).\n */\n flush(): any[] {\n const flushed = this.events.map((e) => e.event);\n this.events = [];\n return flushed;\n }\n\n /**\n * Discard all buffered events.\n */\n clear(): void {\n this.events = [];\n }\n\n /**\n * Number of events currently in the buffer.\n */\n get size(): number {\n return this.events.length;\n }\n\n /**\n * Evict events older than maxAgeMs from the front of the buffer.\n */\n private evictExpired(): void {\n const cutoff = Date.now() - this.maxAgeMs;\n while (this.events.length > 0 && this.events[0].timestamp < cutoff) {\n this.events.shift();\n }\n }\n}\n","/**\n * TraceKit Replay - Session Manager\n * @package @tracekit/replay\n *\n * Core orchestrator for recording lifecycle. Controls which sessions\n * get full recording vs error-only buffer capture, handles idle\n * timeouts with session renewal, and manages visibility-based\n * pause/resume.\n *\n * Sampling bands:\n * [0, sessionSampleRate) -> mode = 'session' (full recording)\n * [sessionSampleRate, session+error) -> mode = 'buffer' (error capture)\n * [session+error, 1.0] -> mode = 'off' (no recording)\n */\n\nimport type { ResolvedReplayConfig, SessionState, ReplayMode } from './types';\nimport { RingBuffer } from './buffer';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a 32-character hex session ID.\n * Prefers crypto.randomUUID() where available, falls back to Math.random().\n */\nfunction generateSessionId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID().replace(/-/g, '');\n }\n // Fallback: random hex string\n return Array.from({ length: 32 }, () =>\n Math.floor(Math.random() * 16).toString(16),\n ).join('');\n}\n\n/**\n * Make a sampling decision based on configured rates.\n */\nfunction decideSamplingMode(config: ResolvedReplayConfig): ReplayMode {\n const rand = Math.random();\n if (rand < config.sessionSampleRate) {\n return 'session';\n }\n if (rand < config.sessionSampleRate + config.errorSampleRate) {\n return 'buffer';\n }\n return 'off';\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\nexport class SessionManager {\n private state: SessionState;\n private config: ResolvedReplayConfig;\n private ringBuffer: RingBuffer;\n\n // Callbacks wired by integration layer\n private eventCallback: ((events: any[]) => void) | null = null;\n private flushCallback: (() => void) | null = null;\n private restartCallback: (() => void) | null = null;\n private pauseCallback: (() => void) | null = null;\n private resumeCallback: (() => void) | null = null;\n\n // Idle timeout handle\n private idleTimer: ReturnType<typeof setTimeout> | null = null;\n\n // Visibility change handler (stored for cleanup)\n private visibilityHandler: (() => void) | null = null;\n\n constructor(config: ResolvedReplayConfig) {\n this.config = config;\n this.ringBuffer = new RingBuffer(60_000);\n\n const mode = decideSamplingMode(config);\n const now = Date.now();\n\n this.state = {\n sessionId: generateSessionId(),\n mode,\n startedAt: now,\n lastActivity: now,\n segmentId: 0,\n };\n\n this.resetIdleTimer();\n this.setupVisibilityListener();\n }\n\n // -------------------------------------------------------------------------\n // Event handling\n // -------------------------------------------------------------------------\n\n /**\n * Process an incoming rrweb event.\n * - session mode: forward immediately via eventCallback\n * - buffer mode: add to ring buffer for error-triggered flush\n * - off mode: discard\n */\n onEvent(event: any, _isCheckout: boolean): void {\n this.state.lastActivity = Date.now();\n this.resetIdleTimer();\n\n if (this.state.mode === 'session') {\n if (this.eventCallback) {\n try {\n this.eventCallback([event]);\n } catch {\n // Never crash the host app\n }\n }\n } else if (this.state.mode === 'buffer') {\n this.ringBuffer.add(event);\n }\n // mode === 'off': discard\n }\n\n /**\n * Handle an error event. For buffer-mode sessions:\n * 1. Flush all buffered events via eventCallback\n * 2. Switch to session mode (continue recording after error)\n *\n * Per LOCKED decision: error buffer operates ONLY for non-sampled\n * sessions in buffer mode.\n */\n onError(): void {\n if (this.state.mode === 'buffer' && this.ringBuffer.size > 0) {\n const events = this.ringBuffer.flush();\n if (this.eventCallback) {\n try {\n this.eventCallback(events);\n } catch {\n // Never crash the host app\n }\n }\n // Switch to full recording mode\n this.state.mode = 'session';\n }\n // session mode or off mode: no-op\n }\n\n // -------------------------------------------------------------------------\n // Callback setters\n // -------------------------------------------------------------------------\n\n /** Set callback that receives events for compression/upload */\n setEventCallback(cb: (events: any[]) => void): void {\n this.eventCallback = cb;\n }\n\n /** Set callback called on idle timeout to flush pending events */\n setFlushCallback(cb: () => void): void {\n this.flushCallback = cb;\n }\n\n /** Set callback called on idle timeout to restart recording with new snapshot */\n setRestartCallback(cb: () => void): void {\n this.restartCallback = cb;\n }\n\n /** Set callback called when tab goes hidden to pause recording */\n setPauseCallback(cb: () => void): void {\n this.pauseCallback = cb;\n }\n\n /** Set callback called when tab becomes visible to resume recording */\n setResumeCallback(cb: () => void): void {\n this.resumeCallback = cb;\n }\n\n // -------------------------------------------------------------------------\n // Accessors\n // -------------------------------------------------------------------------\n\n /** Current session ID */\n getSessionId(): string {\n return this.state.sessionId;\n }\n\n /** Current recording mode */\n getMode(): ReplayMode {\n return this.state.mode;\n }\n\n /** Return and increment segment counter */\n nextSegmentId(): number {\n return this.state.segmentId++;\n }\n\n /** Get full session state */\n getState(): SessionState {\n return { ...this.state };\n }\n\n /**\n * Flush events from the ring buffer (buffer mode).\n * Session mode events are forwarded immediately, so returns [].\n */\n flush(): any[] {\n if (this.state.mode === 'buffer') {\n return this.ringBuffer.flush();\n }\n return [];\n }\n\n // -------------------------------------------------------------------------\n // Idle timeout\n // -------------------------------------------------------------------------\n\n /**\n * Reset the idle timeout. Called on every event and at construction.\n * When the timeout fires:\n * 1. Flush pending events for the old session\n * 2. Generate new session ID + reset state\n * 3. Make new sampling decision\n * 4. Trigger a new full snapshot via restartCallback\n */\n private resetIdleTimer(): void {\n if (this.idleTimer !== null) {\n clearTimeout(this.idleTimer);\n }\n\n this.idleTimer = setTimeout(() => {\n this.handleIdleTimeout();\n }, this.config.idleTimeout);\n }\n\n private handleIdleTimeout(): void {\n // 1. Flush pending events for old session\n if (this.flushCallback) {\n try {\n this.flushCallback();\n } catch {\n // Never crash the host app\n }\n }\n\n // 2. Generate new session ID and reset state\n const mode = decideSamplingMode(this.config);\n const now = Date.now();\n\n this.state = {\n sessionId: generateSessionId(),\n mode,\n startedAt: now,\n lastActivity: now,\n segmentId: 0,\n };\n\n // 3. Clear the ring buffer for the new session\n this.ringBuffer.clear();\n\n // 4. Trigger new full snapshot\n if (this.restartCallback) {\n try {\n this.restartCallback();\n } catch {\n // Never crash the host app\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Visibility handling\n // -------------------------------------------------------------------------\n\n /**\n * Pause recording when tab goes hidden, resume when visible.\n * Per LOCKED decision: recording pauses on hidden, resumes on visible.\n */\n private setupVisibilityListener(): void {\n if (typeof document === 'undefined') {\n return;\n }\n\n this.visibilityHandler = () => {\n if (document.visibilityState === 'hidden') {\n // Pause: flush pending events and stop recording\n if (this.pauseCallback) {\n try {\n this.pauseCallback();\n } catch {\n // Never crash the host app\n }\n }\n } else if (document.visibilityState === 'visible') {\n // Resume: restart recording with new full snapshot\n this.state.lastActivity = Date.now();\n this.resetIdleTimer();\n if (this.resumeCallback) {\n try {\n this.resumeCallback();\n } catch {\n // Never crash the host app\n }\n }\n }\n };\n\n document.addEventListener('visibilitychange', this.visibilityHandler);\n }\n\n // -------------------------------------------------------------------------\n // Cleanup\n // -------------------------------------------------------------------------\n\n /**\n * Tear down the session manager: clear timers, remove listeners, clear buffer.\n */\n destroy(): void {\n if (this.idleTimer !== null) {\n clearTimeout(this.idleTimer);\n this.idleTimer = null;\n }\n\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler);\n this.visibilityHandler = null;\n }\n\n this.ringBuffer.clear();\n\n this.eventCallback = null;\n this.flushCallback = null;\n this.restartCallback = null;\n this.pauseCallback = null;\n this.resumeCallback = null;\n }\n}\n","/**\n * TraceKit Replay - Compression Worker\n * @package @tracekit/replay\n *\n * Compresses rrweb events via an inline Blob URL Web Worker using\n * native CompressionStream (gzip). On worker failure (CSP restriction,\n * CompressionStream unavailable), falls back to fflate gzipSync on the\n * main thread with a console warning.\n *\n * Zero-copy transfer: compressed Uint8Array buffer is transferred from\n * the worker via Transferable to avoid cloning overhead.\n */\n\nimport { gzipSync } from 'fflate';\nimport { WORKER_SCRIPT } from './worker';\n\ninterface PendingCompression {\n resolve: (data: { compressed: Uint8Array; originalSize: number }) => void;\n reject: (err: Error) => void;\n events: any[];\n}\n\nexport class CompressionWorker {\n private worker: Worker | null = null;\n private pendingCallbacks = new Map<number, PendingCompression>();\n private useMainThread = false;\n\n constructor() {\n this.initWorker();\n }\n\n /**\n * Create an inline Blob URL worker from WORKER_SCRIPT.\n * If worker creation fails (e.g. CSP), flag permanent main-thread fallback.\n */\n private initWorker(): void {\n try {\n const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' });\n const url = URL.createObjectURL(blob);\n this.worker = new Worker(url);\n URL.revokeObjectURL(url); // URL can be revoked after worker creation\n\n this.worker.onmessage = (e: MessageEvent) => {\n const { compressed, segmentId, originalSize, error } = e.data;\n const pending = this.pendingCallbacks.get(segmentId);\n if (!pending) return;\n this.pendingCallbacks.delete(segmentId);\n\n if (error) {\n // Worker reported an error (e.g. CompressionStream not available)\n // Fall back to main-thread compression for this and future calls\n console.warn(\n '[TraceKit Replay] Worker compression failed, falling back to main thread:',\n error,\n );\n this.useMainThread = true;\n this.compressMainThread(pending.events).then(pending.resolve).catch(pending.reject);\n return;\n }\n\n pending.resolve({ compressed, originalSize });\n };\n\n this.worker.onerror = () => {\n console.warn(\n '[TraceKit Replay] Web Worker failed to initialize. Using main-thread compression.',\n );\n this.useMainThread = true;\n this.worker = null;\n };\n } catch {\n // CSP or other restriction prevents worker creation\n console.warn(\n '[TraceKit Replay] Cannot create Web Worker (CSP?). Using main-thread compression.',\n );\n this.useMainThread = true;\n }\n }\n\n /**\n * Compress an array of rrweb events.\n * Routes to Web Worker when available, otherwise uses main-thread fflate.\n *\n * @param events - Array of rrweb events to compress\n * @param segmentId - Unique segment ID for correlating worker responses\n * @returns Compressed data with original (uncompressed) size\n */\n async compress(\n events: any[],\n segmentId: number,\n ): Promise<{ compressed: Uint8Array; originalSize: number }> {\n if (this.useMainThread || !this.worker) {\n return this.compressMainThread(events);\n }\n\n return new Promise((resolve, reject) => {\n const entry: PendingCompression = { resolve, reject, events };\n this.pendingCallbacks.set(segmentId, entry);\n this.worker!.postMessage({ events, segmentId });\n });\n }\n\n /**\n * Main-thread fallback using fflate gzipSync.\n * Used when Web Worker is unavailable (CSP) or CompressionStream is missing.\n */\n private async compressMainThread(\n events: any[],\n ): Promise<{ compressed: Uint8Array; originalSize: number }> {\n const json = JSON.stringify(events);\n const encoded = new TextEncoder().encode(json);\n const compressed = gzipSync(encoded, { level: 6 });\n return { compressed, originalSize: encoded.length };\n }\n\n /**\n * Terminate the worker and clean up pending callbacks.\n */\n destroy(): void {\n if (this.worker) {\n this.worker.terminate();\n this.worker = null;\n }\n this.pendingCallbacks.clear();\n }\n}\n","/**\n * TraceKit Replay - Web Worker Compression Script\n * @package @tracekit/replay\n *\n * Inline Web Worker script string using native CompressionStream API.\n * No external dependencies inside the worker -- CompressionStream is\n * baseline available in workers since 2023.\n *\n * If CompressionStream is unavailable, the worker posts an error back\n * and the main thread (CompressionWorker) falls back to fflate gzipSync.\n *\n * Message protocol:\n * IN: { events: any[], segmentId: number }\n * OUT: { compressed: Uint8Array, segmentId: number, originalSize: number }\n * ERR: { error: string, segmentId: number }\n */\n\nexport const WORKER_SCRIPT = `\nself.onmessage = function(e) {\n try {\n var data = e.data;\n var json = JSON.stringify(data.events);\n var encoded = new TextEncoder().encode(json);\n\n if (typeof CompressionStream === 'undefined') {\n self.postMessage({ error: 'CompressionStream not available', segmentId: data.segmentId });\n return;\n }\n\n var cs = new CompressionStream('gzip');\n var writer = cs.writable.getWriter();\n var reader = cs.readable.getReader();\n var chunks = [];\n\n writer.write(encoded);\n writer.close();\n\n function readChunks() {\n reader.read().then(function(result) {\n if (result.done) {\n var totalLen = 0;\n for (var i = 0; i < chunks.length; i++) totalLen += chunks[i].length;\n var compressed = new Uint8Array(totalLen);\n var offset = 0;\n for (var i = 0; i < chunks.length; i++) {\n compressed.set(chunks[i], offset);\n offset += chunks[i].length;\n }\n self.postMessage(\n { compressed: compressed, segmentId: data.segmentId, originalSize: encoded.length },\n [compressed.buffer]\n );\n } else {\n chunks.push(result.value);\n readChunks();\n }\n }).catch(function(err) {\n self.postMessage({ error: err.message, segmentId: data.segmentId });\n });\n }\n readChunks();\n } catch(err) {\n self.postMessage({ error: (err && err.message) || 'Unknown compression error', segmentId: e.data.segmentId });\n }\n};\n`;\n","/**\n * TraceKit Replay - Transport Layer\n * @package @tracekit/replay\n *\n * Uploads compressed replay chunks to the server every 30 seconds.\n * Uses fetch with X-API-Key header for normal uploads, sendBeacon\n * with query-parameter API key for tab-close fallback.\n *\n * Retry: exponential backoff (1s, 2s, 4s) with max 3 attempts.\n * Non-retryable status codes: 400, 401, 413.\n * On final failure: drop the chunk (replay data loss is non-critical).\n *\n * SAFETY: All external calls (fetch, sendBeacon) are wrapped in try/catch.\n * The transport NEVER throws -- replay must never crash the host app.\n */\n\nimport type { ResolvedReplayConfig } from './types';\nimport { gzipSync } from 'fflate';\nimport { CompressionWorker } from './compression';\n\n// Retry delays in milliseconds: 1s, 2s, 4s\nconst RETRY_DELAYS = [1000, 2000, 4000];\nconst MAX_ATTEMPTS = 3;\n\n// sendBeacon has a ~64KB payload limit\nconst BEACON_SIZE_LIMIT = 65536;\n\nexport class ReplayTransport {\n private pendingEvents: any[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private config: ResolvedReplayConfig;\n private compressionWorker: CompressionWorker;\n private bufferSize = 0;\n\n // Getter functions wired by integration layer\n private sessionIdFn: (() => string) | null = null;\n private segmentIdFn: (() => number) | null = null;\n private replayTypeFn: (() => string) | null = null;\n\n // Visibility change handler (stored for cleanup)\n private visibilityHandler: (() => void) | null = null;\n\n constructor(config: ResolvedReplayConfig, compressionWorker: CompressionWorker) {\n this.config = config;\n this.compressionWorker = compressionWorker;\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /**\n * Start the transport: begin 30-second flush interval and register\n * visibilitychange listener for sendBeacon fallback on tab close.\n */\n start(\n getSessionId: () => string,\n nextSegmentId: () => number,\n getReplayType: () => string,\n ): void {\n this.sessionIdFn = getSessionId;\n this.segmentIdFn = nextSegmentId;\n this.replayTypeFn = getReplayType;\n\n // Start periodic flush at config.flushInterval (default 30s)\n this.flushTimer = setInterval(() => {\n this.flush().catch(() => {\n // Never crash -- flush errors are swallowed\n });\n }, this.config.flushInterval);\n\n // Register sendBeacon fallback for tab close\n if (typeof document !== 'undefined') {\n this.visibilityHandler = () => {\n if (document.visibilityState === 'hidden') {\n this.flushSync();\n }\n };\n document.addEventListener('visibilitychange', this.visibilityHandler);\n }\n }\n\n /**\n * Stop the transport: clear flush interval and remove listeners.\n */\n stop(): void {\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler);\n this.visibilityHandler = null;\n }\n }\n\n /**\n * Destroy the transport: stop + clear all pending events.\n */\n destroy(): void {\n this.stop();\n this.pendingEvents = [];\n this.bufferSize = 0;\n }\n\n // ---------------------------------------------------------------------------\n // Event accumulation\n // ---------------------------------------------------------------------------\n\n /**\n * Add a single rrweb event to the pending buffer.\n * If buffer exceeds maxBufferSize, oldest events are dropped.\n */\n addEvent(event: any): void {\n try {\n const eventSize = JSON.stringify(event).length;\n this.pendingEvents.push(event);\n this.bufferSize += eventSize;\n\n // Drop oldest events if buffer exceeds max size\n while (this.bufferSize > this.config.maxBufferSize && this.pendingEvents.length > 1) {\n const dropped = this.pendingEvents.shift();\n if (dropped) {\n try {\n this.bufferSize -= JSON.stringify(dropped).length;\n } catch {\n // Ignore sizing errors on drop\n }\n }\n if (this.config.maxBufferSize > 0) {\n console.warn('[TraceKit Replay] Buffer exceeded maxBufferSize, dropping oldest events');\n }\n }\n } catch {\n // Never crash the host app\n }\n }\n\n /**\n * Add multiple rrweb events at once (used for ring buffer flush-on-error).\n */\n addEvents(events: any[]): void {\n for (const event of events) {\n this.addEvent(event);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Flush (async -- normal upload path)\n // ---------------------------------------------------------------------------\n\n /**\n * Flush pending events: compress via worker and upload via fetch with retry.\n * Returns silently if no events are pending.\n */\n async flush(): Promise<void> {\n if (this.pendingEvents.length === 0) {\n return;\n }\n\n // Swap buffer atomically\n const events = this.pendingEvents;\n this.pendingEvents = [];\n this.bufferSize = 0;\n\n try {\n const sessionId = this.sessionIdFn ? this.sessionIdFn() : '';\n const segmentId = this.segmentIdFn ? this.segmentIdFn() : 0;\n\n if (!sessionId) {\n return; // No session -- discard events\n }\n\n // Compress via worker (or main-thread fallback)\n const { compressed, originalSize } = await this.compressionWorker.compress(events, segmentId);\n\n // Upload with retry\n await this.uploadWithRetry(sessionId, segmentId, compressed, originalSize);\n } catch {\n // Drop chunk on any unexpected error -- replay data loss is acceptable\n }\n }\n\n // ---------------------------------------------------------------------------\n // Flush sync (sendBeacon fallback for tab close)\n // ---------------------------------------------------------------------------\n\n /**\n * Synchronous flush for tab close -- uses sendBeacon.\n * Compresses on main thread with fflate gzipSync (sendBeacon must be sync).\n * Falls back to fetch with keepalive:true if sendBeacon fails.\n * If both fail, data is lost (acceptable for replay).\n */\n flushSync(): void {\n if (this.pendingEvents.length === 0) {\n return;\n }\n\n try {\n const events = this.pendingEvents;\n this.pendingEvents = [];\n this.bufferSize = 0;\n\n const sessionId = this.sessionIdFn ? this.sessionIdFn() : '';\n const segmentId = this.segmentIdFn ? this.segmentIdFn() : 0;\n const replayType = this.replayTypeFn ? this.replayTypeFn() : 'session';\n\n if (!sessionId) {\n return;\n }\n\n // Compress on main thread (sync -- sendBeacon requires sync data)\n const json = JSON.stringify(events);\n const encoded = new TextEncoder().encode(json);\n const compressed = gzipSync(encoded, { level: 6 });\n\n // Build URL with query parameters (sendBeacon cannot set custom headers)\n const url =\n `${this.config.endpoint}/api/replays/${sessionId}/chunks` +\n `?api_key=${encodeURIComponent(this.config.apiKey)}` +\n `&segment_id=${segmentId}` +\n `&original_size=${encoded.length}` +\n `&replay_type=${replayType}`;\n\n // Cast through ArrayBuffer to satisfy TypeScript 5.x Uint8Array<ArrayBufferLike> vs BlobPart\n const blob = new Blob([compressed as unknown as BlobPart], { type: 'application/octet-stream' });\n\n // Try sendBeacon first (works during page unload)\n if (compressed.byteLength <= BEACON_SIZE_LIMIT && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n const sent = navigator.sendBeacon(url, blob);\n if (sent) {\n return;\n }\n }\n\n // Fallback: fetch with keepalive (also has ~64KB limit but is the standard approach)\n if (typeof fetch !== 'undefined') {\n fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n },\n body: blob,\n keepalive: true,\n }).catch(() => {\n // Data loss accepted -- replay is non-critical\n });\n }\n } catch {\n // Never crash the host app on tab close\n }\n }\n\n // ---------------------------------------------------------------------------\n // Upload with retry (exponential backoff)\n // ---------------------------------------------------------------------------\n\n /**\n * Upload compressed chunk via fetch with exponential backoff retry.\n * Retries up to 3 times with delays of 1s, 2s, 4s.\n * Non-retryable status codes (400, 401, 413) abort immediately.\n * On final failure: drop chunk silently.\n */\n private async uploadWithRetry(\n sessionId: string,\n segmentId: number,\n compressed: Uint8Array,\n originalSize: number,\n ): Promise<void> {\n const url = `${this.config.endpoint}/api/replays/${sessionId}/chunks`;\n const replayType = this.replayTypeFn ? this.replayTypeFn() : 'session';\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n 'X-API-Key': this.config.apiKey,\n 'X-Segment-Id': String(segmentId),\n 'X-Original-Size': String(originalSize),\n 'X-Replay-Type': replayType,\n },\n body: compressed as unknown as BodyInit,\n keepalive: true,\n });\n\n if (response.ok) {\n return; // Success\n }\n\n // Non-retryable status codes -- abort immediately\n if (response.status === 400 || response.status === 401 || response.status === 413) {\n return;\n }\n\n // Retryable failure -- wait before next attempt\n if (attempt < MAX_ATTEMPTS - 1) {\n await this.delay(RETRY_DELAYS[attempt]);\n }\n } catch {\n // Network error -- wait before retry\n if (attempt < MAX_ATTEMPTS - 1) {\n await this.delay(RETRY_DELAYS[attempt]);\n }\n }\n }\n\n // All attempts exhausted -- drop chunk (replay data loss is acceptable)\n }\n\n /**\n * Promise-based delay for retry backoff.\n */\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":"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","names":["index_exports","__export","replayIntegration","__toCommonJS","DEFAULTS","clampRate","value","name","min","max","resolveReplayConfig","config","apiKey","endpoint","sessionSampleRate","errorSampleRate","import_rrweb","import_rrweb","createMaskTextFn","unmaskSelectors","selectorParts","combinedSelector","text","element","startRecording","config","onEvent","event","isCheckout","RingBuffer","maxAgeMs","event","flushed","e","cutoff","generateSessionId","decideSamplingMode","config","rand","SessionManager","RingBuffer","mode","now","event","_isCheckout","events","cb","import_fflate","WORKER_SCRIPT","CompressionWorker","blob","WORKER_SCRIPT","url","e","compressed","segmentId","originalSize","error","pending","events","resolve","reject","entry","json","encoded","import_fflate","RETRY_DELAYS","MAX_ATTEMPTS","BEACON_SIZE_LIMIT","ReplayTransport","config","compressionWorker","getSessionId","nextSegmentId","getReplayType","event","eventSize","dropped","events","sessionId","segmentId","compressed","originalSize","replayType","json","encoded","url","blob","attempt","response","ms","resolve","replayIntegration","config","session","compressionWorker","transport","stopRecording","resolvedConfig","client","clientConfig","resolveReplayConfig","SessionManager","CompressionWorker","ReplayTransport","events","event","originalCaptureException","error","context","replayId","scope","result","crumb","payload","startRecording","isCheckout","err"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/recorder.ts","../src/buffer.ts","../src/session.ts","../src/compression.ts","../src/worker.ts","../src/transport.ts"],"sourcesContent":["/**\n * TraceKit Replay - Public API\n * @package @tracekit/replay\n *\n * Entry point for the session replay addon.\n * Provides replayIntegration() factory that returns an Integration object\n * compatible with @tracekit/browser's addons system.\n *\n * Usage:\n * import { init } from '@tracekit/browser';\n * import { replayIntegration } from '@tracekit/replay';\n * init({ apiKey: 'key', addons: [replayIntegration()] });\n *\n * The integration wires together:\n * recorder -> session manager -> compression worker -> transport\n *\n * Recording starts immediately when the integration is installed via init().\n * Manual control: flush() forces an upload, getSessionId() returns the current ID.\n */\n\nimport type { ReplayConfig, ResolvedReplayConfig, Integration } from './types';\nimport { resolveReplayConfig } from './config';\nimport { record } from 'rrweb';\nimport { startRecording } from './recorder';\nimport { SessionManager } from './session';\nimport { CompressionWorker } from './compression';\nimport { ReplayTransport } from './transport';\n\n/**\n * Create a session replay integration for @tracekit/browser.\n *\n * @param config - Optional replay configuration overrides\n * @returns Integration object with flush() and getSessionId() manual control methods\n */\nexport function replayIntegration(\n config: ReplayConfig = {},\n): Integration & { flush(): void; getSessionId(): string } {\n let session: SessionManager | null = null;\n let compressionWorker: CompressionWorker | null = null;\n let transport: ReplayTransport | null = null;\n let stopRecording: (() => void) | null = null;\n let resolvedConfig: ResolvedReplayConfig | null = null;\n\n const integration: Integration & { flush(): void; getSessionId(): string } = {\n name: 'replay',\n\n /**\n * Install the replay integration into the BrowserClient.\n * Creates the full recording pipeline and starts recording immediately.\n *\n * ALL code is wrapped in try/catch -- if replay fails to initialize,\n * it MUST NOT break the host app or the core browser SDK.\n */\n install(client: any): void {\n try {\n // Resolve config with client's apiKey and endpoint\n const clientConfig = client.getConfig();\n resolvedConfig = resolveReplayConfig(config, clientConfig.apiKey, clientConfig.endpoint);\n\n // Create session manager (makes sampling decision)\n session = new SessionManager(resolvedConfig);\n\n // If mode is 'off', don't set up recording pipeline\n if (session.getMode() === 'off') {\n return;\n }\n\n // Create compression worker (Web Worker with main-thread fallback)\n compressionWorker = new CompressionWorker();\n\n // Create transport (30-second flush interval, retry, sendBeacon fallback)\n transport = new ReplayTransport(resolvedConfig, compressionWorker);\n\n // Wire session -> transport: events flow from session to transport\n session.setEventCallback((events: any[]) => {\n for (const event of events) {\n transport!.addEvent(event);\n }\n });\n\n // Wire error notification: hook into captureException to detect errors\n // for ring buffer flush. Also injects replay_id as a tag on the error event\n // so the playback UI can link errors to their replay session.\n // Uses setTag (not setExtra) so replay_id appears as a direct span attribute\n // in OTLP output -- extras would prefix it as \"extra.replay_id\".\n const originalCaptureException = client.captureException.bind(client);\n client.captureException = function (error: Error, context?: any): string {\n const replayId = session?.getSessionId() ?? '';\n const scope = client.getScope();\n if (replayId) {\n scope.setTag('replay_id', replayId);\n }\n const result = originalCaptureException(error, context);\n if (replayId) {\n scope.setTag('replay_id', '');\n }\n if (session) {\n session.onError();\n }\n return result;\n };\n\n // Bridge breadcrumbs to rrweb custom events for playback sidebar tabs.\n // Network requests become 'network-request' events, console output becomes\n // 'console-log' events -- both timestamped in the rrweb event stream.\n const scope = client.getScope();\n scope.onBreadcrumb((crumb: any) => {\n try {\n if (crumb.type === 'http' || crumb.category?.startsWith('fetch') || crumb.category?.startsWith('xhr')) {\n record.addCustomEvent('network-request', {\n method: crumb.data?.method || 'GET',\n url: crumb.data?.url || crumb.message || '',\n status: crumb.data?.status_code,\n duration: crumb.data?.duration,\n error: crumb.data?.error,\n traceparent: crumb.data?.traceparent,\n });\n } else if (crumb.type === 'console' || crumb.category?.startsWith('console.')) {\n // Console tab requires expandable objects and stack traces for errors\n const payload: Record<string, unknown> = {\n level: crumb.category?.replace('console.', '') || 'log',\n message: crumb.message || '',\n };\n // Include structured data from console args (objects, arrays)\n if (crumb.data && Object.keys(crumb.data).length > 0) {\n payload.data = crumb.data;\n }\n // Include stack trace for error-level console entries\n if ((crumb.category === 'console.error' || crumb.category === 'console.warn') && crumb.data?.stack) {\n payload.stack = crumb.data.stack;\n }\n record.addCustomEvent('console-log', payload);\n }\n } catch {\n // Never crash the host app\n }\n });\n\n // Wire idle timeout: flush pending events when session goes idle\n session.setFlushCallback(() => {\n transport?.flush().catch(() => {\n // Never crash on flush errors\n });\n });\n\n // Wire idle timeout restart: stop recording, start fresh with new snapshot\n session.setRestartCallback(() => {\n if (stopRecording) {\n stopRecording();\n }\n stopRecording = startRecording(resolvedConfig!, (event, isCheckout) => {\n session?.onEvent(event, isCheckout);\n });\n });\n\n // Wire visibility pause: stop recording and flush sync on tab hide\n session.setPauseCallback(() => {\n if (stopRecording) {\n stopRecording();\n stopRecording = null;\n }\n transport?.flushSync();\n });\n\n // Wire visibility resume: restart recording on tab show\n session.setResumeCallback(() => {\n stopRecording = startRecording(resolvedConfig!, (event, isCheckout) => {\n session?.onEvent(event, isCheckout);\n });\n });\n\n // Start transport (30-second flush interval)\n transport.start(\n () => session!.getSessionId(),\n () => session!.nextSegmentId(),\n () => (session!.getMode() === 'buffer' ? 'buffer' : 'session'),\n );\n\n // Start recording immediately (LOCKED: recording starts on init())\n stopRecording = startRecording(resolvedConfig, (event, isCheckout) => {\n session!.onEvent(event, isCheckout);\n });\n } catch (err) {\n console.warn('[TraceKit Replay] Failed to initialize replay recording:', err);\n }\n },\n\n /**\n * Teardown: clean up all resources.\n * Called when the BrowserClient is destroyed.\n */\n teardown(): void {\n try {\n if (stopRecording) {\n stopRecording();\n stopRecording = null;\n }\n transport?.destroy();\n compressionWorker?.destroy();\n session?.destroy();\n session = null;\n compressionWorker = null;\n transport = null;\n resolvedConfig = null;\n } catch {\n // Ignore teardown errors\n }\n },\n\n /**\n * Manual flush: force an immediate upload of pending events.\n * Useful for ensuring data is sent before a page transition.\n */\n flush(): void {\n try {\n transport?.flush().catch(() => {\n // Never crash\n });\n } catch {\n // Never crash\n }\n },\n\n /**\n * Get the current session ID.\n * Returns empty string if replay is not active.\n * Useful for linking errors to replay sessions (Phase 26).\n */\n getSessionId(): string {\n return session?.getSessionId() ?? '';\n },\n };\n\n return integration;\n}\n\n// Re-export types for consumers\nexport type { ReplayConfig, Integration } from './types';\n","/**\n * TraceKit Replay - Configuration Resolution\n * @package @tracekit/replay\n *\n * Resolves user-provided ReplayConfig with privacy-first defaults.\n * Validates and clamps rate values to valid ranges.\n */\n\nimport type { ReplayConfig, ResolvedReplayConfig } from './types';\n\nconst DEFAULTS = {\n sessionSampleRate: 0.1,\n errorSampleRate: 0.0,\n unmask: [] as string[],\n idleTimeout: 1_800_000, // 30 minutes\n flushInterval: 30_000, // 30 seconds\n maxBufferSize: 24_117_248, // 23MB\n inlineImages: false,\n blockMedia: true,\n} as const;\n\n/**\n * Clamp a value to the [min, max] range, logging a warning if clamped.\n */\nfunction clampRate(value: number, name: string, min: number, max: number): number {\n if (value < min) {\n console.warn(`[TraceKit Replay] ${name} (${value}) is below ${min}, clamping to ${min}`);\n return min;\n }\n if (value > max) {\n console.warn(`[TraceKit Replay] ${name} (${value}) is above ${max}, clamping to ${max}`);\n return max;\n }\n return value;\n}\n\n/**\n * Resolve user-provided replay config with defaults.\n * Validates sample rates are in [0, 1] and their sum does not exceed 1.0.\n */\nexport function resolveReplayConfig(\n config: ReplayConfig,\n apiKey: string,\n endpoint: string,\n): ResolvedReplayConfig {\n let sessionSampleRate = clampRate(\n config.sessionSampleRate ?? DEFAULTS.sessionSampleRate,\n 'sessionSampleRate',\n 0,\n 1,\n );\n\n let errorSampleRate = clampRate(\n config.errorSampleRate ?? DEFAULTS.errorSampleRate,\n 'errorSampleRate',\n 0,\n 1,\n );\n\n // Ensure combined rate does not exceed 1.0\n if (sessionSampleRate + errorSampleRate > 1.0) {\n console.warn(\n `[TraceKit Replay] sessionSampleRate (${sessionSampleRate}) + errorSampleRate (${errorSampleRate}) exceeds 1.0, clamping errorSampleRate`,\n );\n errorSampleRate = 1.0 - sessionSampleRate;\n }\n\n return {\n sessionSampleRate,\n errorSampleRate,\n unmask: config.unmask ?? DEFAULTS.unmask,\n idleTimeout: config.idleTimeout ?? DEFAULTS.idleTimeout,\n flushInterval: config.flushInterval ?? DEFAULTS.flushInterval,\n maxBufferSize: config.maxBufferSize ?? DEFAULTS.maxBufferSize,\n inlineImages: config.inlineImages ?? DEFAULTS.inlineImages,\n blockMedia: config.blockMedia ?? DEFAULTS.blockMedia,\n apiKey,\n endpoint,\n };\n}\n","/**\n * TraceKit Replay - rrweb Recorder Wrapper\n * @package @tracekit/replay\n *\n * Wraps rrweb's record() function with privacy-first defaults:\n * - All text masked with same-length asterisk replacement\n * - All inputs masked\n * - Images, videos, canvas, SVGs, and iframes blocked\n * - Unmasking via CSS selectors and data-tracekit-unmask attribute\n * - Periodic full snapshots every 30s aligned with upload interval\n */\n\nimport { record } from 'rrweb';\nimport type { ResolvedReplayConfig } from './types';\n\n/**\n * Create a maskTextFn that masks all text EXCEPT elements matching\n * unmask selectors or the data-tracekit-unmask attribute.\n *\n * Privacy-first: when in doubt (null element, invalid selector), mask.\n */\nfunction createMaskTextFn(\n unmaskSelectors: string[],\n): (text: string, element: HTMLElement | null) => string {\n // Build combined CSS selector for unmask targets\n const selectorParts = ['[data-tracekit-unmask]'];\n if (unmaskSelectors.length > 0) {\n selectorParts.push(...unmaskSelectors);\n }\n const combinedSelector = selectorParts.join(', ');\n\n return (text: string, element: HTMLElement | null): string => {\n // Privacy-first: mask when element is null (uncertain context)\n if (!element) {\n return '*'.repeat(text.length);\n }\n\n // Check if element (or any ancestor) matches unmask selectors\n try {\n if (element.matches(combinedSelector) || element.closest(combinedSelector)) {\n return text; // Unmask: return original text\n }\n } catch {\n // Invalid selector -- fail safe by masking\n }\n\n // Default: same-length asterisk replacement\n return '*'.repeat(text.length);\n };\n}\n\n/**\n * Start rrweb recording with privacy-first defaults.\n *\n * @param config - Resolved replay configuration\n * @param onEvent - Callback invoked for each rrweb event\n * @returns A stop function to halt recording, or null if recording failed to start\n */\nexport function startRecording(\n config: ResolvedReplayConfig,\n onEvent: (event: unknown, isCheckout: boolean) => void,\n): (() => void) | null {\n try {\n const stopFn = record({\n emit: (event, isCheckout) => {\n onEvent(event, isCheckout ?? false);\n },\n\n // ================================================================\n // Privacy-first settings (LOCKED decisions)\n // ================================================================\n\n // Mask all text by matching all elements\n maskTextSelector: '*',\n\n // Mask all input values\n maskAllInputs: true,\n\n // Custom mask function: same-length asterisk replacement with unmask support\n maskTextFn: createMaskTextFn(config.unmask),\n\n // Block media elements (configurable, default: true for privacy)\n ...(config.blockMedia ? { blockSelector: 'img, video, canvas, svg, iframe' } : {}),\n\n // Do NOT record canvas content\n recordCanvas: false,\n\n // Do NOT record cross-origin iframes\n recordCrossOriginIframes: false,\n\n // Inline images as base64 (configurable, default: false for privacy/size)\n inlineImages: config.inlineImages,\n\n // ================================================================\n // Recording lifecycle\n // ================================================================\n\n // Periodic full snapshot every 30s, aligned with upload interval\n checkoutEveryNms: 30_000,\n\n // Sampling configuration to reduce event volume\n sampling: {\n mousemove: 50,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n });\n\n // rrweb record() returns undefined if it fails to start\n return stopFn ?? null;\n } catch {\n // Never crash the host application\n return null;\n }\n}\n","/**\n * TraceKit Replay - Ring Buffer\n * @package @tracekit/replay\n *\n * Timestamp-based ring buffer for error-mode capture.\n * Maintains exactly 60 seconds of rrweb events, evicting expired\n * entries on every add(). On error, the buffer is flushed and\n * the session switches from 'buffer' to 'session' mode.\n */\n\nexport class RingBuffer {\n private events: Array<{ event: any; timestamp: number }> = [];\n private maxAgeMs: number;\n\n constructor(maxAgeMs: number = 60_000) {\n this.maxAgeMs = maxAgeMs;\n }\n\n /**\n * Add an event to the buffer. Uses `event.timestamp` from rrweb\n * (milliseconds since epoch), falling back to Date.now().\n * Evicts expired entries after each add.\n */\n add(event: any): void {\n this.events.push({ event, timestamp: event.timestamp ?? Date.now() });\n this.evictExpired();\n }\n\n /**\n * Flush all buffered events and clear the buffer.\n * Returns the raw rrweb events (unwrapped from the timestamp envelope).\n */\n flush(): any[] {\n const flushed = this.events.map((e) => e.event);\n this.events = [];\n return flushed;\n }\n\n /**\n * Discard all buffered events.\n */\n clear(): void {\n this.events = [];\n }\n\n /**\n * Number of events currently in the buffer.\n */\n get size(): number {\n return this.events.length;\n }\n\n /**\n * Evict events older than maxAgeMs from the front of the buffer.\n */\n private evictExpired(): void {\n const cutoff = Date.now() - this.maxAgeMs;\n while (this.events.length > 0 && this.events[0].timestamp < cutoff) {\n this.events.shift();\n }\n }\n}\n","/**\n * TraceKit Replay - Session Manager\n * @package @tracekit/replay\n *\n * Core orchestrator for recording lifecycle. Controls which sessions\n * get full recording vs error-only buffer capture, handles idle\n * timeouts with session renewal, and manages visibility-based\n * pause/resume.\n *\n * Sampling bands:\n * [0, sessionSampleRate) -> mode = 'session' (full recording)\n * [sessionSampleRate, session+error) -> mode = 'buffer' (error capture)\n * [session+error, 1.0] -> mode = 'off' (no recording)\n */\n\nimport type { ResolvedReplayConfig, SessionState, ReplayMode } from './types';\nimport { RingBuffer } from './buffer';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a 32-character hex session ID.\n * Prefers crypto.randomUUID() where available, falls back to Math.random().\n */\nfunction generateSessionId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID().replace(/-/g, '');\n }\n // Fallback: random hex string\n return Array.from({ length: 32 }, () =>\n Math.floor(Math.random() * 16).toString(16),\n ).join('');\n}\n\n/**\n * Make a sampling decision based on configured rates.\n */\nfunction decideSamplingMode(config: ResolvedReplayConfig): ReplayMode {\n const rand = Math.random();\n if (rand < config.sessionSampleRate) {\n return 'session';\n }\n if (rand < config.sessionSampleRate + config.errorSampleRate) {\n return 'buffer';\n }\n return 'off';\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\nexport class SessionManager {\n private state: SessionState;\n private config: ResolvedReplayConfig;\n private ringBuffer: RingBuffer;\n\n // Callbacks wired by integration layer\n private eventCallback: ((events: any[]) => void) | null = null;\n private flushCallback: (() => void) | null = null;\n private restartCallback: (() => void) | null = null;\n private pauseCallback: (() => void) | null = null;\n private resumeCallback: (() => void) | null = null;\n\n // Idle timeout handle\n private idleTimer: ReturnType<typeof setTimeout> | null = null;\n\n // Visibility change handler (stored for cleanup)\n private visibilityHandler: (() => void) | null = null;\n\n constructor(config: ResolvedReplayConfig) {\n this.config = config;\n this.ringBuffer = new RingBuffer(60_000);\n\n const mode = decideSamplingMode(config);\n const now = Date.now();\n\n this.state = {\n sessionId: generateSessionId(),\n mode,\n startedAt: now,\n lastActivity: now,\n segmentId: 0,\n };\n\n this.resetIdleTimer();\n this.setupVisibilityListener();\n }\n\n // -------------------------------------------------------------------------\n // Event handling\n // -------------------------------------------------------------------------\n\n /**\n * Process an incoming rrweb event.\n * - session mode: forward immediately via eventCallback\n * - buffer mode: add to ring buffer for error-triggered flush\n * - off mode: discard\n */\n onEvent(event: any, _isCheckout: boolean): void {\n this.state.lastActivity = Date.now();\n this.resetIdleTimer();\n\n if (this.state.mode === 'session') {\n if (this.eventCallback) {\n try {\n this.eventCallback([event]);\n } catch {\n // Never crash the host app\n }\n }\n } else if (this.state.mode === 'buffer') {\n this.ringBuffer.add(event);\n }\n // mode === 'off': discard\n }\n\n /**\n * Handle an error event. For buffer-mode sessions:\n * 1. Flush all buffered events via eventCallback\n * 2. Switch to session mode (continue recording after error)\n *\n * Per LOCKED decision: error buffer operates ONLY for non-sampled\n * sessions in buffer mode.\n */\n onError(): void {\n if (this.state.mode === 'buffer' && this.ringBuffer.size > 0) {\n const events = this.ringBuffer.flush();\n if (this.eventCallback) {\n try {\n this.eventCallback(events);\n } catch {\n // Never crash the host app\n }\n }\n // Switch to full recording mode\n this.state.mode = 'session';\n }\n // session mode or off mode: no-op\n }\n\n // -------------------------------------------------------------------------\n // Callback setters\n // -------------------------------------------------------------------------\n\n /** Set callback that receives events for compression/upload */\n setEventCallback(cb: (events: any[]) => void): void {\n this.eventCallback = cb;\n }\n\n /** Set callback called on idle timeout to flush pending events */\n setFlushCallback(cb: () => void): void {\n this.flushCallback = cb;\n }\n\n /** Set callback called on idle timeout to restart recording with new snapshot */\n setRestartCallback(cb: () => void): void {\n this.restartCallback = cb;\n }\n\n /** Set callback called when tab goes hidden to pause recording */\n setPauseCallback(cb: () => void): void {\n this.pauseCallback = cb;\n }\n\n /** Set callback called when tab becomes visible to resume recording */\n setResumeCallback(cb: () => void): void {\n this.resumeCallback = cb;\n }\n\n // -------------------------------------------------------------------------\n // Accessors\n // -------------------------------------------------------------------------\n\n /** Current session ID */\n getSessionId(): string {\n return this.state.sessionId;\n }\n\n /** Current recording mode */\n getMode(): ReplayMode {\n return this.state.mode;\n }\n\n /** Return and increment segment counter */\n nextSegmentId(): number {\n return this.state.segmentId++;\n }\n\n /** Get full session state */\n getState(): SessionState {\n return { ...this.state };\n }\n\n /**\n * Flush events from the ring buffer (buffer mode).\n * Session mode events are forwarded immediately, so returns [].\n */\n flush(): any[] {\n if (this.state.mode === 'buffer') {\n return this.ringBuffer.flush();\n }\n return [];\n }\n\n // -------------------------------------------------------------------------\n // Idle timeout\n // -------------------------------------------------------------------------\n\n /**\n * Reset the idle timeout. Called on every event and at construction.\n * When the timeout fires:\n * 1. Flush pending events for the old session\n * 2. Generate new session ID + reset state\n * 3. Make new sampling decision\n * 4. Trigger a new full snapshot via restartCallback\n */\n private resetIdleTimer(): void {\n if (this.idleTimer !== null) {\n clearTimeout(this.idleTimer);\n }\n\n this.idleTimer = setTimeout(() => {\n this.handleIdleTimeout();\n }, this.config.idleTimeout);\n }\n\n private handleIdleTimeout(): void {\n // 1. Flush pending events for old session\n if (this.flushCallback) {\n try {\n this.flushCallback();\n } catch {\n // Never crash the host app\n }\n }\n\n // 2. Generate new session ID and reset state\n const mode = decideSamplingMode(this.config);\n const now = Date.now();\n\n this.state = {\n sessionId: generateSessionId(),\n mode,\n startedAt: now,\n lastActivity: now,\n segmentId: 0,\n };\n\n // 3. Clear the ring buffer for the new session\n this.ringBuffer.clear();\n\n // 4. Trigger new full snapshot\n if (this.restartCallback) {\n try {\n this.restartCallback();\n } catch {\n // Never crash the host app\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Visibility handling\n // -------------------------------------------------------------------------\n\n /**\n * Pause recording when tab goes hidden, resume when visible.\n * Per LOCKED decision: recording pauses on hidden, resumes on visible.\n */\n private setupVisibilityListener(): void {\n if (typeof document === 'undefined') {\n return;\n }\n\n this.visibilityHandler = () => {\n if (document.visibilityState === 'hidden') {\n // Pause: flush pending events and stop recording\n if (this.pauseCallback) {\n try {\n this.pauseCallback();\n } catch {\n // Never crash the host app\n }\n }\n } else if (document.visibilityState === 'visible') {\n // Resume: restart recording with new full snapshot\n this.state.lastActivity = Date.now();\n this.resetIdleTimer();\n if (this.resumeCallback) {\n try {\n this.resumeCallback();\n } catch {\n // Never crash the host app\n }\n }\n }\n };\n\n document.addEventListener('visibilitychange', this.visibilityHandler);\n }\n\n // -------------------------------------------------------------------------\n // Cleanup\n // -------------------------------------------------------------------------\n\n /**\n * Tear down the session manager: clear timers, remove listeners, clear buffer.\n */\n destroy(): void {\n if (this.idleTimer !== null) {\n clearTimeout(this.idleTimer);\n this.idleTimer = null;\n }\n\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler);\n this.visibilityHandler = null;\n }\n\n this.ringBuffer.clear();\n\n this.eventCallback = null;\n this.flushCallback = null;\n this.restartCallback = null;\n this.pauseCallback = null;\n this.resumeCallback = null;\n }\n}\n","/**\n * TraceKit Replay - Compression Worker\n * @package @tracekit/replay\n *\n * Compresses rrweb events via an inline Blob URL Web Worker using\n * native CompressionStream (gzip). On worker failure (CSP restriction,\n * CompressionStream unavailable), falls back to fflate gzipSync on the\n * main thread with a console warning.\n *\n * Zero-copy transfer: compressed Uint8Array buffer is transferred from\n * the worker via Transferable to avoid cloning overhead.\n */\n\nimport { gzipSync } from 'fflate';\nimport { WORKER_SCRIPT } from './worker';\n\ninterface PendingCompression {\n resolve: (data: { compressed: Uint8Array; originalSize: number }) => void;\n reject: (err: Error) => void;\n events: any[];\n}\n\nexport class CompressionWorker {\n private worker: Worker | null = null;\n private pendingCallbacks = new Map<number, PendingCompression>();\n private useMainThread = false;\n\n constructor() {\n this.initWorker();\n }\n\n /**\n * Create an inline Blob URL worker from WORKER_SCRIPT.\n * If worker creation fails (e.g. CSP), flag permanent main-thread fallback.\n */\n private initWorker(): void {\n try {\n const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' });\n const url = URL.createObjectURL(blob);\n this.worker = new Worker(url);\n URL.revokeObjectURL(url); // URL can be revoked after worker creation\n\n this.worker.onmessage = (e: MessageEvent) => {\n const { compressed, segmentId, originalSize, error } = e.data;\n const pending = this.pendingCallbacks.get(segmentId);\n if (!pending) return;\n this.pendingCallbacks.delete(segmentId);\n\n if (error) {\n // Worker reported an error (e.g. CompressionStream not available)\n // Fall back to main-thread compression for this and future calls\n console.warn(\n '[TraceKit Replay] Worker compression failed, falling back to main thread:',\n error,\n );\n this.useMainThread = true;\n this.compressMainThread(pending.events).then(pending.resolve).catch(pending.reject);\n return;\n }\n\n pending.resolve({ compressed, originalSize });\n };\n\n this.worker.onerror = () => {\n console.warn(\n '[TraceKit Replay] Web Worker failed to initialize. Using main-thread compression.',\n );\n this.useMainThread = true;\n this.worker = null;\n };\n } catch {\n // CSP or other restriction prevents worker creation\n console.warn(\n '[TraceKit Replay] Cannot create Web Worker (CSP?). Using main-thread compression.',\n );\n this.useMainThread = true;\n }\n }\n\n /**\n * Compress an array of rrweb events.\n * Routes to Web Worker when available, otherwise uses main-thread fflate.\n *\n * @param events - Array of rrweb events to compress\n * @param segmentId - Unique segment ID for correlating worker responses\n * @returns Compressed data with original (uncompressed) size\n */\n async compress(\n events: any[],\n segmentId: number,\n ): Promise<{ compressed: Uint8Array; originalSize: number }> {\n if (this.useMainThread || !this.worker) {\n return this.compressMainThread(events);\n }\n\n return new Promise((resolve, reject) => {\n const entry: PendingCompression = { resolve, reject, events };\n this.pendingCallbacks.set(segmentId, entry);\n this.worker!.postMessage({ events, segmentId });\n });\n }\n\n /**\n * Main-thread fallback using fflate gzipSync.\n * Used when Web Worker is unavailable (CSP) or CompressionStream is missing.\n */\n private async compressMainThread(\n events: any[],\n ): Promise<{ compressed: Uint8Array; originalSize: number }> {\n const json = JSON.stringify(events);\n const encoded = new TextEncoder().encode(json);\n const compressed = gzipSync(encoded, { level: 6 });\n return { compressed, originalSize: encoded.length };\n }\n\n /**\n * Terminate the worker and clean up pending callbacks.\n */\n destroy(): void {\n if (this.worker) {\n this.worker.terminate();\n this.worker = null;\n }\n this.pendingCallbacks.clear();\n }\n}\n","/**\n * TraceKit Replay - Web Worker Compression Script\n * @package @tracekit/replay\n *\n * Inline Web Worker script string using native CompressionStream API.\n * No external dependencies inside the worker -- CompressionStream is\n * baseline available in workers since 2023.\n *\n * If CompressionStream is unavailable, the worker posts an error back\n * and the main thread (CompressionWorker) falls back to fflate gzipSync.\n *\n * Message protocol:\n * IN: { events: any[], segmentId: number }\n * OUT: { compressed: Uint8Array, segmentId: number, originalSize: number }\n * ERR: { error: string, segmentId: number }\n */\n\nexport const WORKER_SCRIPT = `\nself.onmessage = function(e) {\n try {\n var data = e.data;\n var json = JSON.stringify(data.events);\n var encoded = new TextEncoder().encode(json);\n\n if (typeof CompressionStream === 'undefined') {\n self.postMessage({ error: 'CompressionStream not available', segmentId: data.segmentId });\n return;\n }\n\n var cs = new CompressionStream('gzip');\n var writer = cs.writable.getWriter();\n var reader = cs.readable.getReader();\n var chunks = [];\n\n writer.write(encoded);\n writer.close();\n\n function readChunks() {\n reader.read().then(function(result) {\n if (result.done) {\n var totalLen = 0;\n for (var i = 0; i < chunks.length; i++) totalLen += chunks[i].length;\n var compressed = new Uint8Array(totalLen);\n var offset = 0;\n for (var i = 0; i < chunks.length; i++) {\n compressed.set(chunks[i], offset);\n offset += chunks[i].length;\n }\n self.postMessage(\n { compressed: compressed, segmentId: data.segmentId, originalSize: encoded.length },\n [compressed.buffer]\n );\n } else {\n chunks.push(result.value);\n readChunks();\n }\n }).catch(function(err) {\n self.postMessage({ error: err.message, segmentId: data.segmentId });\n });\n }\n readChunks();\n } catch(err) {\n self.postMessage({ error: (err && err.message) || 'Unknown compression error', segmentId: e.data.segmentId });\n }\n};\n`;\n","/**\n * TraceKit Replay - Transport Layer\n * @package @tracekit/replay\n *\n * Uploads compressed replay chunks to the server every 30 seconds.\n * Uses fetch with X-API-Key header for normal uploads, sendBeacon\n * with query-parameter API key for tab-close fallback.\n *\n * Retry: exponential backoff (1s, 2s, 4s) with max 3 attempts.\n * Non-retryable status codes: 400, 401, 413.\n * On final failure: drop the chunk (replay data loss is non-critical).\n *\n * SAFETY: All external calls (fetch, sendBeacon) are wrapped in try/catch.\n * The transport NEVER throws -- replay must never crash the host app.\n */\n\nimport type { ResolvedReplayConfig } from './types';\nimport { gzipSync } from 'fflate';\nimport { CompressionWorker } from './compression';\n\n// Retry delays in milliseconds: 1s, 2s, 4s\nconst RETRY_DELAYS = [1000, 2000, 4000];\nconst MAX_ATTEMPTS = 3;\n\n// sendBeacon has a ~64KB payload limit\nconst BEACON_SIZE_LIMIT = 65536;\n\nexport class ReplayTransport {\n private pendingEvents: any[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private config: ResolvedReplayConfig;\n private compressionWorker: CompressionWorker;\n private bufferSize = 0;\n\n // Getter functions wired by integration layer\n private sessionIdFn: (() => string) | null = null;\n private segmentIdFn: (() => number) | null = null;\n private replayTypeFn: (() => string) | null = null;\n\n // Visibility change handler (stored for cleanup)\n private visibilityHandler: (() => void) | null = null;\n\n constructor(config: ResolvedReplayConfig, compressionWorker: CompressionWorker) {\n this.config = config;\n this.compressionWorker = compressionWorker;\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /**\n * Start the transport: begin 30-second flush interval and register\n * visibilitychange listener for sendBeacon fallback on tab close.\n */\n start(\n getSessionId: () => string,\n nextSegmentId: () => number,\n getReplayType: () => string,\n ): void {\n this.sessionIdFn = getSessionId;\n this.segmentIdFn = nextSegmentId;\n this.replayTypeFn = getReplayType;\n\n // Start periodic flush at config.flushInterval (default 30s)\n this.flushTimer = setInterval(() => {\n this.flush().catch(() => {\n // Never crash -- flush errors are swallowed\n });\n }, this.config.flushInterval);\n\n // Register sendBeacon fallback for tab close\n if (typeof document !== 'undefined') {\n this.visibilityHandler = () => {\n if (document.visibilityState === 'hidden') {\n this.flushSync();\n }\n };\n document.addEventListener('visibilitychange', this.visibilityHandler);\n }\n }\n\n /**\n * Stop the transport: clear flush interval and remove listeners.\n */\n stop(): void {\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler);\n this.visibilityHandler = null;\n }\n }\n\n /**\n * Destroy the transport: stop + clear all pending events.\n */\n destroy(): void {\n this.stop();\n this.pendingEvents = [];\n this.bufferSize = 0;\n }\n\n // ---------------------------------------------------------------------------\n // Event accumulation\n // ---------------------------------------------------------------------------\n\n /**\n * Add a single rrweb event to the pending buffer.\n * If buffer exceeds maxBufferSize, oldest events are dropped.\n */\n addEvent(event: any): void {\n try {\n const eventSize = JSON.stringify(event).length;\n this.pendingEvents.push(event);\n this.bufferSize += eventSize;\n\n // Drop oldest events if buffer exceeds max size\n while (this.bufferSize > this.config.maxBufferSize && this.pendingEvents.length > 1) {\n const dropped = this.pendingEvents.shift();\n if (dropped) {\n try {\n this.bufferSize -= JSON.stringify(dropped).length;\n } catch {\n // Ignore sizing errors on drop\n }\n }\n if (this.config.maxBufferSize > 0) {\n console.warn('[TraceKit Replay] Buffer exceeded maxBufferSize, dropping oldest events');\n }\n }\n } catch {\n // Never crash the host app\n }\n }\n\n /**\n * Add multiple rrweb events at once (used for ring buffer flush-on-error).\n */\n addEvents(events: any[]): void {\n for (const event of events) {\n this.addEvent(event);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Flush (async -- normal upload path)\n // ---------------------------------------------------------------------------\n\n /**\n * Flush pending events: compress via worker and upload via fetch with retry.\n * Returns silently if no events are pending.\n */\n async flush(): Promise<void> {\n if (this.pendingEvents.length === 0) {\n return;\n }\n\n // Swap buffer atomically\n const events = this.pendingEvents;\n this.pendingEvents = [];\n this.bufferSize = 0;\n\n try {\n const sessionId = this.sessionIdFn ? this.sessionIdFn() : '';\n const segmentId = this.segmentIdFn ? this.segmentIdFn() : 0;\n\n if (!sessionId) {\n return; // No session -- discard events\n }\n\n // Compress via worker (or main-thread fallback)\n const { compressed, originalSize } = await this.compressionWorker.compress(events, segmentId);\n\n // Upload with retry\n await this.uploadWithRetry(sessionId, segmentId, compressed, originalSize);\n } catch {\n // Drop chunk on any unexpected error -- replay data loss is acceptable\n }\n }\n\n // ---------------------------------------------------------------------------\n // Flush sync (sendBeacon fallback for tab close)\n // ---------------------------------------------------------------------------\n\n /**\n * Synchronous flush for tab close -- uses sendBeacon.\n * Compresses on main thread with fflate gzipSync (sendBeacon must be sync).\n * Falls back to fetch with keepalive:true if sendBeacon fails.\n * If both fail, data is lost (acceptable for replay).\n */\n flushSync(): void {\n if (this.pendingEvents.length === 0) {\n return;\n }\n\n try {\n const events = this.pendingEvents;\n this.pendingEvents = [];\n this.bufferSize = 0;\n\n const sessionId = this.sessionIdFn ? this.sessionIdFn() : '';\n const segmentId = this.segmentIdFn ? this.segmentIdFn() : 0;\n const replayType = this.replayTypeFn ? this.replayTypeFn() : 'session';\n\n if (!sessionId) {\n return;\n }\n\n // Compress on main thread (sync -- sendBeacon requires sync data)\n const json = JSON.stringify(events);\n const encoded = new TextEncoder().encode(json);\n const compressed = gzipSync(encoded, { level: 6 });\n\n // Build URL with query parameters (sendBeacon cannot set custom headers)\n const url =\n `${this.config.endpoint}/api/replays/${sessionId}/chunks` +\n `?api_key=${encodeURIComponent(this.config.apiKey)}` +\n `&segment_id=${segmentId}` +\n `&original_size=${encoded.length}` +\n `&replay_type=${replayType}`;\n\n // Cast through ArrayBuffer to satisfy TypeScript 5.x Uint8Array<ArrayBufferLike> vs BlobPart\n const blob = new Blob([compressed as unknown as BlobPart], { type: 'application/octet-stream' });\n\n // Try sendBeacon first (works during page unload)\n if (compressed.byteLength <= BEACON_SIZE_LIMIT && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n const sent = navigator.sendBeacon(url, blob);\n if (sent) {\n return;\n }\n }\n\n // Fallback: fetch with keepalive (also has ~64KB limit but is the standard approach)\n if (typeof fetch !== 'undefined') {\n fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n },\n body: blob,\n keepalive: true,\n }).catch(() => {\n // Data loss accepted -- replay is non-critical\n });\n }\n } catch {\n // Never crash the host app on tab close\n }\n }\n\n // ---------------------------------------------------------------------------\n // Upload with retry (exponential backoff)\n // ---------------------------------------------------------------------------\n\n /**\n * Upload compressed chunk via fetch with exponential backoff retry.\n * Retries up to 3 times with delays of 1s, 2s, 4s.\n * Non-retryable status codes (400, 401, 413) abort immediately.\n * On final failure: drop chunk silently.\n */\n private async uploadWithRetry(\n sessionId: string,\n segmentId: number,\n compressed: Uint8Array,\n originalSize: number,\n ): Promise<void> {\n const url = `${this.config.endpoint}/api/replays/${sessionId}/chunks`;\n const replayType = this.replayTypeFn ? this.replayTypeFn() : 'session';\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n 'X-API-Key': this.config.apiKey,\n 'X-Segment-Id': String(segmentId),\n 'X-Original-Size': String(originalSize),\n 'X-Replay-Type': replayType,\n },\n body: compressed as unknown as BodyInit,\n keepalive: true,\n });\n\n if (response.ok) {\n return; // Success\n }\n\n // Non-retryable status codes -- abort immediately\n if (response.status === 400 || response.status === 401 || response.status === 413) {\n return;\n }\n\n // Retryable failure -- wait before next attempt\n if (attempt < MAX_ATTEMPTS - 1) {\n await this.delay(RETRY_DELAYS[attempt]);\n }\n } catch {\n // Network error -- wait before retry\n if (attempt < MAX_ATTEMPTS - 1) {\n await this.delay(RETRY_DELAYS[attempt]);\n }\n }\n }\n\n // All attempts exhausted -- drop chunk (replay data loss is acceptable)\n }\n\n /**\n * Promise-based delay for retry backoff.\n */\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":"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","names":["index_exports","__export","replayIntegration","__toCommonJS","DEFAULTS","clampRate","value","name","min","max","resolveReplayConfig","config","apiKey","endpoint","sessionSampleRate","errorSampleRate","import_rrweb","import_rrweb","createMaskTextFn","unmaskSelectors","selectorParts","combinedSelector","text","element","startRecording","config","onEvent","event","isCheckout","RingBuffer","maxAgeMs","event","flushed","e","cutoff","generateSessionId","decideSamplingMode","config","rand","SessionManager","RingBuffer","mode","now","event","_isCheckout","events","cb","import_fflate","WORKER_SCRIPT","CompressionWorker","blob","WORKER_SCRIPT","url","e","compressed","segmentId","originalSize","error","pending","events","resolve","reject","entry","json","encoded","import_fflate","RETRY_DELAYS","MAX_ATTEMPTS","BEACON_SIZE_LIMIT","ReplayTransport","config","compressionWorker","getSessionId","nextSegmentId","getReplayType","event","eventSize","dropped","events","sessionId","segmentId","compressed","originalSize","replayType","json","encoded","url","blob","attempt","response","ms","resolve","replayIntegration","config","session","compressionWorker","transport","stopRecording","resolvedConfig","client","clientConfig","resolveReplayConfig","SessionManager","CompressionWorker","ReplayTransport","events","event","originalCaptureException","error","context","replayId","scope","result","crumb","payload","startRecording","isCheckout","err"]}
package/dist/index.d.cts CHANGED
@@ -23,8 +23,21 @@ interface ReplayConfig {
23
23
  idleTimeout?: number;
24
24
  /** Flush interval in ms (default: 30000 = 30s) */
25
25
  flushInterval?: number;
26
- /** Max buffer size in bytes (default: 10485760 = 10MB) */
26
+ /** Max buffer size in bytes (default: 24117248 = 23MB) */
27
27
  maxBufferSize?: number;
28
+ /**
29
+ * Inline images as base64 data URIs in the recording.
30
+ * When true, images are captured and available during replay.
31
+ * Increases payload size. (default: false)
32
+ */
33
+ inlineImages?: boolean;
34
+ /**
35
+ * Block media elements (img, video, canvas, svg, iframe) from recording.
36
+ * When true, these elements are replaced with placeholders.
37
+ * Set to false to capture media element structure (combine with inlineImages
38
+ * to also capture image content). (default: true)
39
+ */
40
+ blockMedia?: boolean;
28
41
  }
29
42
 
30
43
  /**
package/dist/index.d.ts CHANGED
@@ -23,8 +23,21 @@ interface ReplayConfig {
23
23
  idleTimeout?: number;
24
24
  /** Flush interval in ms (default: 30000 = 30s) */
25
25
  flushInterval?: number;
26
- /** Max buffer size in bytes (default: 10485760 = 10MB) */
26
+ /** Max buffer size in bytes (default: 24117248 = 23MB) */
27
27
  maxBufferSize?: number;
28
+ /**
29
+ * Inline images as base64 data URIs in the recording.
30
+ * When true, images are captured and available during replay.
31
+ * Increases payload size. (default: false)
32
+ */
33
+ inlineImages?: boolean;
34
+ /**
35
+ * Block media elements (img, video, canvas, svg, iframe) from recording.
36
+ * When true, these elements are replaced with placeholders.
37
+ * Set to false to capture media element structure (combine with inlineImages
38
+ * to also capture image content). (default: true)
39
+ */
40
+ blockMedia?: boolean;
28
41
  }
29
42
 
30
43
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- var p={sessionSampleRate:.1,errorSampleRate:0,unmask:[],idleTimeout:18e5,flushInterval:3e4,maxBufferSize:10485760};function C(r,e,t,s){return r<t?(console.warn(`[TraceKit Replay] ${e} (${r}) is below ${t}, clamping to ${t}`),t):r>s?(console.warn(`[TraceKit Replay] ${e} (${r}) is above ${s}, clamping to ${s}`),s):r}function S(r,e,t){let s=C(r.sessionSampleRate??p.sessionSampleRate,"sessionSampleRate",0,1),i=C(r.errorSampleRate??p.errorSampleRate,"errorSampleRate",0,1);return s+i>1&&(console.warn(`[TraceKit Replay] sessionSampleRate (${s}) + errorSampleRate (${i}) exceeds 1.0, clamping errorSampleRate`),i=1-s),{sessionSampleRate:s,errorSampleRate:i,unmask:r.unmask??p.unmask,idleTimeout:r.idleTimeout??p.idleTimeout,flushInterval:r.flushInterval??p.flushInterval,maxBufferSize:r.maxBufferSize??p.maxBufferSize,apiKey:e,endpoint:t}}import{record as E}from"rrweb";import{record as z}from"rrweb";function F(r){let e=["[data-tracekit-unmask]"];r.length>0&&e.push(...r);let t=e.join(", ");return(s,i)=>{if(!i)return"*".repeat(s.length);try{if(i.matches(t)||i.closest(t))return s}catch{}return"*".repeat(s.length)}}function h(r,e){try{return z({emit:(s,i)=>{e(s,i??!1)},maskTextSelector:"*",maskAllInputs:!0,maskTextFn:F(r.unmask),blockSelector:"img, video, canvas, svg, iframe",recordCanvas:!1,recordCrossOriginIframes:!1,inlineImages:!1,checkoutEveryNms:3e4,sampling:{mousemove:50,mouseInteraction:!0,scroll:150,input:"last"}})??null}catch{return null}}var f=class{constructor(e=6e4){this.events=[];this.maxAgeMs=e}add(e){this.events.push({event:e,timestamp:e.timestamp??Date.now()}),this.evictExpired()}flush(){let e=this.events.map(t=>t.event);return this.events=[],e}clear(){this.events=[]}get size(){return this.events.length}evictExpired(){let e=Date.now()-this.maxAgeMs;for(;this.events.length>0&&this.events[0].timestamp<e;)this.events.shift()}};function R(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID().replace(/-/g,""):Array.from({length:32},()=>Math.floor(Math.random()*16).toString(16)).join("")}function T(r){let e=Math.random();return e<r.sessionSampleRate?"session":e<r.sessionSampleRate+r.errorSampleRate?"buffer":"off"}var m=class{constructor(e){this.eventCallback=null;this.flushCallback=null;this.restartCallback=null;this.pauseCallback=null;this.resumeCallback=null;this.idleTimer=null;this.visibilityHandler=null;this.config=e,this.ringBuffer=new f(6e4);let t=T(e),s=Date.now();this.state={sessionId:R(),mode:t,startedAt:s,lastActivity:s,segmentId:0},this.resetIdleTimer(),this.setupVisibilityListener()}onEvent(e,t){if(this.state.lastActivity=Date.now(),this.resetIdleTimer(),this.state.mode==="session"){if(this.eventCallback)try{this.eventCallback([e])}catch{}}else this.state.mode==="buffer"&&this.ringBuffer.add(e)}onError(){if(this.state.mode==="buffer"&&this.ringBuffer.size>0){let e=this.ringBuffer.flush();if(this.eventCallback)try{this.eventCallback(e)}catch{}this.state.mode="session"}}setEventCallback(e){this.eventCallback=e}setFlushCallback(e){this.flushCallback=e}setRestartCallback(e){this.restartCallback=e}setPauseCallback(e){this.pauseCallback=e}setResumeCallback(e){this.resumeCallback=e}getSessionId(){return this.state.sessionId}getMode(){return this.state.mode}nextSegmentId(){return this.state.segmentId++}getState(){return{...this.state}}flush(){return this.state.mode==="buffer"?this.ringBuffer.flush():[]}resetIdleTimer(){this.idleTimer!==null&&clearTimeout(this.idleTimer),this.idleTimer=setTimeout(()=>{this.handleIdleTimeout()},this.config.idleTimeout)}handleIdleTimeout(){if(this.flushCallback)try{this.flushCallback()}catch{}let e=T(this.config),t=Date.now();if(this.state={sessionId:R(),mode:e,startedAt:t,lastActivity:t,segmentId:0},this.ringBuffer.clear(),this.restartCallback)try{this.restartCallback()}catch{}}setupVisibilityListener(){typeof document>"u"||(this.visibilityHandler=()=>{if(document.visibilityState==="hidden"){if(this.pauseCallback)try{this.pauseCallback()}catch{}}else if(document.visibilityState==="visible"&&(this.state.lastActivity=Date.now(),this.resetIdleTimer(),this.resumeCallback))try{this.resumeCallback()}catch{}},document.addEventListener("visibilitychange",this.visibilityHandler))}destroy(){this.idleTimer!==null&&(clearTimeout(this.idleTimer),this.idleTimer=null),this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null),this.ringBuffer.clear(),this.eventCallback=null,this.flushCallback=null,this.restartCallback=null,this.pauseCallback=null,this.resumeCallback=null}};import{gzipSync as B}from"fflate";var I=`
1
+ var c={sessionSampleRate:.1,errorSampleRate:0,unmask:[],idleTimeout:18e5,flushInterval:3e4,maxBufferSize:24117248,inlineImages:!1,blockMedia:!0};function C(n,e,t,s){return n<t?(console.warn(`[TraceKit Replay] ${e} (${n}) is below ${t}, clamping to ${t}`),t):n>s?(console.warn(`[TraceKit Replay] ${e} (${n}) is above ${s}, clamping to ${s}`),s):n}function S(n,e,t){let s=C(n.sessionSampleRate??c.sessionSampleRate,"sessionSampleRate",0,1),i=C(n.errorSampleRate??c.errorSampleRate,"errorSampleRate",0,1);return s+i>1&&(console.warn(`[TraceKit Replay] sessionSampleRate (${s}) + errorSampleRate (${i}) exceeds 1.0, clamping errorSampleRate`),i=1-s),{sessionSampleRate:s,errorSampleRate:i,unmask:n.unmask??c.unmask,idleTimeout:n.idleTimeout??c.idleTimeout,flushInterval:n.flushInterval??c.flushInterval,maxBufferSize:n.maxBufferSize??c.maxBufferSize,inlineImages:n.inlineImages??c.inlineImages,blockMedia:n.blockMedia??c.blockMedia,apiKey:e,endpoint:t}}import{record as E}from"rrweb";import{record as z}from"rrweb";function F(n){let e=["[data-tracekit-unmask]"];n.length>0&&e.push(...n);let t=e.join(", ");return(s,i)=>{if(!i)return"*".repeat(s.length);try{if(i.matches(t)||i.closest(t))return s}catch{}return"*".repeat(s.length)}}function h(n,e){try{return z({emit:(s,i)=>{e(s,i??!1)},maskTextSelector:"*",maskAllInputs:!0,maskTextFn:F(n.unmask),...n.blockMedia?{blockSelector:"img, video, canvas, svg, iframe"}:{},recordCanvas:!1,recordCrossOriginIframes:!1,inlineImages:n.inlineImages,checkoutEveryNms:3e4,sampling:{mousemove:50,mouseInteraction:!0,scroll:150,input:"last"}})??null}catch{return null}}var f=class{constructor(e=6e4){this.events=[];this.maxAgeMs=e}add(e){this.events.push({event:e,timestamp:e.timestamp??Date.now()}),this.evictExpired()}flush(){let e=this.events.map(t=>t.event);return this.events=[],e}clear(){this.events=[]}get size(){return this.events.length}evictExpired(){let e=Date.now()-this.maxAgeMs;for(;this.events.length>0&&this.events[0].timestamp<e;)this.events.shift()}};function R(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID().replace(/-/g,""):Array.from({length:32},()=>Math.floor(Math.random()*16).toString(16)).join("")}function I(n){let e=Math.random();return e<n.sessionSampleRate?"session":e<n.sessionSampleRate+n.errorSampleRate?"buffer":"off"}var m=class{constructor(e){this.eventCallback=null;this.flushCallback=null;this.restartCallback=null;this.pauseCallback=null;this.resumeCallback=null;this.idleTimer=null;this.visibilityHandler=null;this.config=e,this.ringBuffer=new f(6e4);let t=I(e),s=Date.now();this.state={sessionId:R(),mode:t,startedAt:s,lastActivity:s,segmentId:0},this.resetIdleTimer(),this.setupVisibilityListener()}onEvent(e,t){if(this.state.lastActivity=Date.now(),this.resetIdleTimer(),this.state.mode==="session"){if(this.eventCallback)try{this.eventCallback([e])}catch{}}else this.state.mode==="buffer"&&this.ringBuffer.add(e)}onError(){if(this.state.mode==="buffer"&&this.ringBuffer.size>0){let e=this.ringBuffer.flush();if(this.eventCallback)try{this.eventCallback(e)}catch{}this.state.mode="session"}}setEventCallback(e){this.eventCallback=e}setFlushCallback(e){this.flushCallback=e}setRestartCallback(e){this.restartCallback=e}setPauseCallback(e){this.pauseCallback=e}setResumeCallback(e){this.resumeCallback=e}getSessionId(){return this.state.sessionId}getMode(){return this.state.mode}nextSegmentId(){return this.state.segmentId++}getState(){return{...this.state}}flush(){return this.state.mode==="buffer"?this.ringBuffer.flush():[]}resetIdleTimer(){this.idleTimer!==null&&clearTimeout(this.idleTimer),this.idleTimer=setTimeout(()=>{this.handleIdleTimeout()},this.config.idleTimeout)}handleIdleTimeout(){if(this.flushCallback)try{this.flushCallback()}catch{}let e=I(this.config),t=Date.now();if(this.state={sessionId:R(),mode:e,startedAt:t,lastActivity:t,segmentId:0},this.ringBuffer.clear(),this.restartCallback)try{this.restartCallback()}catch{}}setupVisibilityListener(){typeof document>"u"||(this.visibilityHandler=()=>{if(document.visibilityState==="hidden"){if(this.pauseCallback)try{this.pauseCallback()}catch{}}else if(document.visibilityState==="visible"&&(this.state.lastActivity=Date.now(),this.resetIdleTimer(),this.resumeCallback))try{this.resumeCallback()}catch{}},document.addEventListener("visibilitychange",this.visibilityHandler))}destroy(){this.idleTimer!==null&&(clearTimeout(this.idleTimer),this.idleTimer=null),this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null),this.ringBuffer.clear(),this.eventCallback=null,this.flushCallback=null,this.restartCallback=null,this.pauseCallback=null,this.resumeCallback=null}};import{gzipSync as B}from"fflate";var T=`
2
2
  self.onmessage = function(e) {
3
3
  try {
4
4
  var data = e.data;
@@ -46,5 +46,5 @@ self.onmessage = function(e) {
46
46
  self.postMessage({ error: (err && err.message) || 'Unknown compression error', segmentId: e.data.segmentId });
47
47
  }
48
48
  };
49
- `;var g=class{constructor(){this.worker=null;this.pendingCallbacks=new Map;this.useMainThread=!1;this.initWorker()}initWorker(){try{let e=new Blob([I],{type:"application/javascript"}),t=URL.createObjectURL(e);this.worker=new Worker(t),URL.revokeObjectURL(t),this.worker.onmessage=s=>{let{compressed:i,segmentId:o,originalSize:c,error:a}=s.data,l=this.pendingCallbacks.get(o);if(l){if(this.pendingCallbacks.delete(o),a){console.warn("[TraceKit Replay] Worker compression failed, falling back to main thread:",a),this.useMainThread=!0,this.compressMainThread(l.events).then(l.resolve).catch(l.reject);return}l.resolve({compressed:i,originalSize:c})}},this.worker.onerror=()=>{console.warn("[TraceKit Replay] Web Worker failed to initialize. Using main-thread compression."),this.useMainThread=!0,this.worker=null}}catch{console.warn("[TraceKit Replay] Cannot create Web Worker (CSP?). Using main-thread compression."),this.useMainThread=!0}}async compress(e,t){return this.useMainThread||!this.worker?this.compressMainThread(e):new Promise((s,i)=>{let o={resolve:s,reject:i,events:e};this.pendingCallbacks.set(t,o),this.worker.postMessage({events:e,segmentId:t})})}async compressMainThread(e){let t=JSON.stringify(e),s=new TextEncoder().encode(t);return{compressed:B(s,{level:6}),originalSize:s.length}}destroy(){this.worker&&(this.worker.terminate(),this.worker=null),this.pendingCallbacks.clear()}};import{gzipSync as W}from"fflate";var w=[1e3,2e3,4e3],k=3,A=65536,v=class{constructor(e,t){this.pendingEvents=[];this.flushTimer=null;this.bufferSize=0;this.sessionIdFn=null;this.segmentIdFn=null;this.replayTypeFn=null;this.visibilityHandler=null;this.config=e,this.compressionWorker=t}start(e,t,s){this.sessionIdFn=e,this.segmentIdFn=t,this.replayTypeFn=s,this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.flushInterval),typeof document<"u"&&(this.visibilityHandler=()=>{document.visibilityState==="hidden"&&this.flushSync()},document.addEventListener("visibilitychange",this.visibilityHandler))}stop(){this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null)}destroy(){this.stop(),this.pendingEvents=[],this.bufferSize=0}addEvent(e){try{let t=JSON.stringify(e).length;for(this.pendingEvents.push(e),this.bufferSize+=t;this.bufferSize>this.config.maxBufferSize&&this.pendingEvents.length>1;){let s=this.pendingEvents.shift();if(s)try{this.bufferSize-=JSON.stringify(s).length}catch{}this.config.maxBufferSize>0&&console.warn("[TraceKit Replay] Buffer exceeded maxBufferSize, dropping oldest events")}}catch{}}addEvents(e){for(let t of e)this.addEvent(t)}async flush(){if(this.pendingEvents.length===0)return;let e=this.pendingEvents;this.pendingEvents=[],this.bufferSize=0;try{let t=this.sessionIdFn?this.sessionIdFn():"",s=this.segmentIdFn?this.segmentIdFn():0;if(!t)return;let{compressed:i,originalSize:o}=await this.compressionWorker.compress(e,s);await this.uploadWithRetry(t,s,i,o)}catch{}}flushSync(){if(this.pendingEvents.length!==0)try{let e=this.pendingEvents;this.pendingEvents=[],this.bufferSize=0;let t=this.sessionIdFn?this.sessionIdFn():"",s=this.segmentIdFn?this.segmentIdFn():0,i=this.replayTypeFn?this.replayTypeFn():"session";if(!t)return;let o=JSON.stringify(e),c=new TextEncoder().encode(o),a=W(c,{level:6}),l=`${this.config.endpoint}/api/replays/${t}/chunks?api_key=${encodeURIComponent(this.config.apiKey)}&segment_id=${s}&original_size=${c.length}&replay_type=${i}`,u=new Blob([a],{type:"application/octet-stream"});if(a.byteLength<=A&&typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(l,u))return;typeof fetch<"u"&&fetch(l,{method:"POST",headers:{"Content-Type":"application/octet-stream"},body:u,keepalive:!0}).catch(()=>{})}catch{}}async uploadWithRetry(e,t,s,i){let o=`${this.config.endpoint}/api/replays/${e}/chunks`,c=this.replayTypeFn?this.replayTypeFn():"session";for(let a=0;a<k;a++)try{let l=await fetch(o,{method:"POST",headers:{"Content-Type":"application/octet-stream","X-API-Key":this.config.apiKey,"X-Segment-Id":String(t),"X-Original-Size":String(i),"X-Replay-Type":c},body:s,keepalive:!0});if(l.ok||l.status===400||l.status===401||l.status===413)return;a<k-1&&await this.delay(w[a])}catch{a<k-1&&await this.delay(w[a])}}delay(e){return new Promise(t=>setTimeout(t,e))}};function Q(r={}){let e=null,t=null,s=null,i=null,o=null;return{name:"replay",install(a){try{let l=a.getConfig();if(o=S(r,l.apiKey,l.endpoint),e=new m(o),e.getMode()==="off")return;t=new g,s=new v(o,t),e.setEventCallback(n=>{for(let d of n)s.addEvent(d)});let u=a.captureException.bind(a);a.captureException=function(n,d){let y=e?.getSessionId()??"",b=a.getScope();y&&b.setTag("replay_id",y);let x=u(n,d);return y&&b.setTag("replay_id",""),e&&e.onError(),x},a.getScope().onBreadcrumb(n=>{try{if(n.type==="http"||n.category?.startsWith("fetch")||n.category?.startsWith("xhr"))E.addCustomEvent("network-request",{method:n.data?.method||"GET",url:n.data?.url||n.message||"",status:n.data?.status_code,duration:n.data?.duration,error:n.data?.error,traceparent:n.data?.traceparent});else if(n.type==="console"||n.category?.startsWith("console.")){let d={level:n.category?.replace("console.","")||"log",message:n.message||""};n.data&&Object.keys(n.data).length>0&&(d.data=n.data),(n.category==="console.error"||n.category==="console.warn")&&n.data?.stack&&(d.stack=n.data.stack),E.addCustomEvent("console-log",d)}}catch{}}),e.setFlushCallback(()=>{s?.flush().catch(()=>{})}),e.setRestartCallback(()=>{i&&i(),i=h(o,(n,d)=>{e?.onEvent(n,d)})}),e.setPauseCallback(()=>{i&&(i(),i=null),s?.flushSync()}),e.setResumeCallback(()=>{i=h(o,(n,d)=>{e?.onEvent(n,d)})}),s.start(()=>e.getSessionId(),()=>e.nextSegmentId(),()=>e.getMode()==="buffer"?"buffer":"session"),i=h(o,(n,d)=>{e.onEvent(n,d)})}catch(l){console.warn("[TraceKit Replay] Failed to initialize replay recording:",l)}},teardown(){try{i&&(i(),i=null),s?.destroy(),t?.destroy(),e?.destroy(),e=null,t=null,s=null,o=null}catch{}},flush(){try{s?.flush().catch(()=>{})}catch{}},getSessionId(){return e?.getSessionId()??""}}}export{Q as replayIntegration};
49
+ `;var g=class{constructor(){this.worker=null;this.pendingCallbacks=new Map;this.useMainThread=!1;this.initWorker()}initWorker(){try{let e=new Blob([T],{type:"application/javascript"}),t=URL.createObjectURL(e);this.worker=new Worker(t),URL.revokeObjectURL(t),this.worker.onmessage=s=>{let{compressed:i,segmentId:o,originalSize:p,error:a}=s.data,l=this.pendingCallbacks.get(o);if(l){if(this.pendingCallbacks.delete(o),a){console.warn("[TraceKit Replay] Worker compression failed, falling back to main thread:",a),this.useMainThread=!0,this.compressMainThread(l.events).then(l.resolve).catch(l.reject);return}l.resolve({compressed:i,originalSize:p})}},this.worker.onerror=()=>{console.warn("[TraceKit Replay] Web Worker failed to initialize. Using main-thread compression."),this.useMainThread=!0,this.worker=null}}catch{console.warn("[TraceKit Replay] Cannot create Web Worker (CSP?). Using main-thread compression."),this.useMainThread=!0}}async compress(e,t){return this.useMainThread||!this.worker?this.compressMainThread(e):new Promise((s,i)=>{let o={resolve:s,reject:i,events:e};this.pendingCallbacks.set(t,o),this.worker.postMessage({events:e,segmentId:t})})}async compressMainThread(e){let t=JSON.stringify(e),s=new TextEncoder().encode(t);return{compressed:B(s,{level:6}),originalSize:s.length}}destroy(){this.worker&&(this.worker.terminate(),this.worker=null),this.pendingCallbacks.clear()}};import{gzipSync as W}from"fflate";var w=[1e3,2e3,4e3],k=3,A=65536,v=class{constructor(e,t){this.pendingEvents=[];this.flushTimer=null;this.bufferSize=0;this.sessionIdFn=null;this.segmentIdFn=null;this.replayTypeFn=null;this.visibilityHandler=null;this.config=e,this.compressionWorker=t}start(e,t,s){this.sessionIdFn=e,this.segmentIdFn=t,this.replayTypeFn=s,this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.flushInterval),typeof document<"u"&&(this.visibilityHandler=()=>{document.visibilityState==="hidden"&&this.flushSync()},document.addEventListener("visibilitychange",this.visibilityHandler))}stop(){this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.visibilityHandler&&typeof document<"u"&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null)}destroy(){this.stop(),this.pendingEvents=[],this.bufferSize=0}addEvent(e){try{let t=JSON.stringify(e).length;for(this.pendingEvents.push(e),this.bufferSize+=t;this.bufferSize>this.config.maxBufferSize&&this.pendingEvents.length>1;){let s=this.pendingEvents.shift();if(s)try{this.bufferSize-=JSON.stringify(s).length}catch{}this.config.maxBufferSize>0&&console.warn("[TraceKit Replay] Buffer exceeded maxBufferSize, dropping oldest events")}}catch{}}addEvents(e){for(let t of e)this.addEvent(t)}async flush(){if(this.pendingEvents.length===0)return;let e=this.pendingEvents;this.pendingEvents=[],this.bufferSize=0;try{let t=this.sessionIdFn?this.sessionIdFn():"",s=this.segmentIdFn?this.segmentIdFn():0;if(!t)return;let{compressed:i,originalSize:o}=await this.compressionWorker.compress(e,s);await this.uploadWithRetry(t,s,i,o)}catch{}}flushSync(){if(this.pendingEvents.length!==0)try{let e=this.pendingEvents;this.pendingEvents=[],this.bufferSize=0;let t=this.sessionIdFn?this.sessionIdFn():"",s=this.segmentIdFn?this.segmentIdFn():0,i=this.replayTypeFn?this.replayTypeFn():"session";if(!t)return;let o=JSON.stringify(e),p=new TextEncoder().encode(o),a=W(p,{level:6}),l=`${this.config.endpoint}/api/replays/${t}/chunks?api_key=${encodeURIComponent(this.config.apiKey)}&segment_id=${s}&original_size=${p.length}&replay_type=${i}`,u=new Blob([a],{type:"application/octet-stream"});if(a.byteLength<=A&&typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(l,u))return;typeof fetch<"u"&&fetch(l,{method:"POST",headers:{"Content-Type":"application/octet-stream"},body:u,keepalive:!0}).catch(()=>{})}catch{}}async uploadWithRetry(e,t,s,i){let o=`${this.config.endpoint}/api/replays/${e}/chunks`,p=this.replayTypeFn?this.replayTypeFn():"session";for(let a=0;a<k;a++)try{let l=await fetch(o,{method:"POST",headers:{"Content-Type":"application/octet-stream","X-API-Key":this.config.apiKey,"X-Segment-Id":String(t),"X-Original-Size":String(i),"X-Replay-Type":p},body:s,keepalive:!0});if(l.ok||l.status===400||l.status===401||l.status===413)return;a<k-1&&await this.delay(w[a])}catch{a<k-1&&await this.delay(w[a])}}delay(e){return new Promise(t=>setTimeout(t,e))}};function Q(n={}){let e=null,t=null,s=null,i=null,o=null;return{name:"replay",install(a){try{let l=a.getConfig();if(o=S(n,l.apiKey,l.endpoint),e=new m(o),e.getMode()==="off")return;t=new g,s=new v(o,t),e.setEventCallback(r=>{for(let d of r)s.addEvent(d)});let u=a.captureException.bind(a);a.captureException=function(r,d){let y=e?.getSessionId()??"",b=a.getScope();y&&b.setTag("replay_id",y);let x=u(r,d);return y&&b.setTag("replay_id",""),e&&e.onError(),x},a.getScope().onBreadcrumb(r=>{try{if(r.type==="http"||r.category?.startsWith("fetch")||r.category?.startsWith("xhr"))E.addCustomEvent("network-request",{method:r.data?.method||"GET",url:r.data?.url||r.message||"",status:r.data?.status_code,duration:r.data?.duration,error:r.data?.error,traceparent:r.data?.traceparent});else if(r.type==="console"||r.category?.startsWith("console.")){let d={level:r.category?.replace("console.","")||"log",message:r.message||""};r.data&&Object.keys(r.data).length>0&&(d.data=r.data),(r.category==="console.error"||r.category==="console.warn")&&r.data?.stack&&(d.stack=r.data.stack),E.addCustomEvent("console-log",d)}}catch{}}),e.setFlushCallback(()=>{s?.flush().catch(()=>{})}),e.setRestartCallback(()=>{i&&i(),i=h(o,(r,d)=>{e?.onEvent(r,d)})}),e.setPauseCallback(()=>{i&&(i(),i=null),s?.flushSync()}),e.setResumeCallback(()=>{i=h(o,(r,d)=>{e?.onEvent(r,d)})}),s.start(()=>e.getSessionId(),()=>e.nextSegmentId(),()=>e.getMode()==="buffer"?"buffer":"session"),i=h(o,(r,d)=>{e.onEvent(r,d)})}catch(l){console.warn("[TraceKit Replay] Failed to initialize replay recording:",l)}},teardown(){try{i&&(i(),i=null),s?.destroy(),t?.destroy(),e?.destroy(),e=null,t=null,s=null,o=null}catch{}},flush(){try{s?.flush().catch(()=>{})}catch{}},getSessionId(){return e?.getSessionId()??""}}}export{Q as replayIntegration};
50
50
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts","../src/index.ts","../src/recorder.ts","../src/buffer.ts","../src/session.ts","../src/compression.ts","../src/worker.ts","../src/transport.ts"],"sourcesContent":["/**\n * TraceKit Replay - Configuration Resolution\n * @package @tracekit/replay\n *\n * Resolves user-provided ReplayConfig with privacy-first defaults.\n * Validates and clamps rate values to valid ranges.\n */\n\nimport type { ReplayConfig, ResolvedReplayConfig } from './types';\n\nconst DEFAULTS = {\n sessionSampleRate: 0.1,\n errorSampleRate: 0.0,\n unmask: [] as string[],\n idleTimeout: 1_800_000, // 30 minutes\n flushInterval: 30_000, // 30 seconds\n maxBufferSize: 10_485_760, // 10MB\n} as const;\n\n/**\n * Clamp a value to the [min, max] range, logging a warning if clamped.\n */\nfunction clampRate(value: number, name: string, min: number, max: number): number {\n if (value < min) {\n console.warn(`[TraceKit Replay] ${name} (${value}) is below ${min}, clamping to ${min}`);\n return min;\n }\n if (value > max) {\n console.warn(`[TraceKit Replay] ${name} (${value}) is above ${max}, clamping to ${max}`);\n return max;\n }\n return value;\n}\n\n/**\n * Resolve user-provided replay config with defaults.\n * Validates sample rates are in [0, 1] and their sum does not exceed 1.0.\n */\nexport function resolveReplayConfig(\n config: ReplayConfig,\n apiKey: string,\n endpoint: string,\n): ResolvedReplayConfig {\n let sessionSampleRate = clampRate(\n config.sessionSampleRate ?? DEFAULTS.sessionSampleRate,\n 'sessionSampleRate',\n 0,\n 1,\n );\n\n let errorSampleRate = clampRate(\n config.errorSampleRate ?? DEFAULTS.errorSampleRate,\n 'errorSampleRate',\n 0,\n 1,\n );\n\n // Ensure combined rate does not exceed 1.0\n if (sessionSampleRate + errorSampleRate > 1.0) {\n console.warn(\n `[TraceKit Replay] sessionSampleRate (${sessionSampleRate}) + errorSampleRate (${errorSampleRate}) exceeds 1.0, clamping errorSampleRate`,\n );\n errorSampleRate = 1.0 - sessionSampleRate;\n }\n\n return {\n sessionSampleRate,\n errorSampleRate,\n unmask: config.unmask ?? DEFAULTS.unmask,\n idleTimeout: config.idleTimeout ?? DEFAULTS.idleTimeout,\n flushInterval: config.flushInterval ?? DEFAULTS.flushInterval,\n maxBufferSize: config.maxBufferSize ?? DEFAULTS.maxBufferSize,\n apiKey,\n endpoint,\n };\n}\n","/**\n * TraceKit Replay - Public API\n * @package @tracekit/replay\n *\n * Entry point for the session replay addon.\n * Provides replayIntegration() factory that returns an Integration object\n * compatible with @tracekit/browser's addons system.\n *\n * Usage:\n * import { init } from '@tracekit/browser';\n * import { replayIntegration } from '@tracekit/replay';\n * init({ apiKey: 'key', addons: [replayIntegration()] });\n *\n * The integration wires together:\n * recorder -> session manager -> compression worker -> transport\n *\n * Recording starts immediately when the integration is installed via init().\n * Manual control: flush() forces an upload, getSessionId() returns the current ID.\n */\n\nimport type { ReplayConfig, ResolvedReplayConfig, Integration } from './types';\nimport { resolveReplayConfig } from './config';\nimport { record } from 'rrweb';\nimport { startRecording } from './recorder';\nimport { SessionManager } from './session';\nimport { CompressionWorker } from './compression';\nimport { ReplayTransport } from './transport';\n\n/**\n * Create a session replay integration for @tracekit/browser.\n *\n * @param config - Optional replay configuration overrides\n * @returns Integration object with flush() and getSessionId() manual control methods\n */\nexport function replayIntegration(\n config: ReplayConfig = {},\n): Integration & { flush(): void; getSessionId(): string } {\n let session: SessionManager | null = null;\n let compressionWorker: CompressionWorker | null = null;\n let transport: ReplayTransport | null = null;\n let stopRecording: (() => void) | null = null;\n let resolvedConfig: ResolvedReplayConfig | null = null;\n\n const integration: Integration & { flush(): void; getSessionId(): string } = {\n name: 'replay',\n\n /**\n * Install the replay integration into the BrowserClient.\n * Creates the full recording pipeline and starts recording immediately.\n *\n * ALL code is wrapped in try/catch -- if replay fails to initialize,\n * it MUST NOT break the host app or the core browser SDK.\n */\n install(client: any): void {\n try {\n // Resolve config with client's apiKey and endpoint\n const clientConfig = client.getConfig();\n resolvedConfig = resolveReplayConfig(config, clientConfig.apiKey, clientConfig.endpoint);\n\n // Create session manager (makes sampling decision)\n session = new SessionManager(resolvedConfig);\n\n // If mode is 'off', don't set up recording pipeline\n if (session.getMode() === 'off') {\n return;\n }\n\n // Create compression worker (Web Worker with main-thread fallback)\n compressionWorker = new CompressionWorker();\n\n // Create transport (30-second flush interval, retry, sendBeacon fallback)\n transport = new ReplayTransport(resolvedConfig, compressionWorker);\n\n // Wire session -> transport: events flow from session to transport\n session.setEventCallback((events: any[]) => {\n for (const event of events) {\n transport!.addEvent(event);\n }\n });\n\n // Wire error notification: hook into captureException to detect errors\n // for ring buffer flush. Also injects replay_id as a tag on the error event\n // so the playback UI can link errors to their replay session.\n // Uses setTag (not setExtra) so replay_id appears as a direct span attribute\n // in OTLP output -- extras would prefix it as \"extra.replay_id\".\n const originalCaptureException = client.captureException.bind(client);\n client.captureException = function (error: Error, context?: any): string {\n const replayId = session?.getSessionId() ?? '';\n const scope = client.getScope();\n if (replayId) {\n scope.setTag('replay_id', replayId);\n }\n const result = originalCaptureException(error, context);\n if (replayId) {\n scope.setTag('replay_id', '');\n }\n if (session) {\n session.onError();\n }\n return result;\n };\n\n // Bridge breadcrumbs to rrweb custom events for playback sidebar tabs.\n // Network requests become 'network-request' events, console output becomes\n // 'console-log' events -- both timestamped in the rrweb event stream.\n const scope = client.getScope();\n scope.onBreadcrumb((crumb: any) => {\n try {\n if (crumb.type === 'http' || crumb.category?.startsWith('fetch') || crumb.category?.startsWith('xhr')) {\n record.addCustomEvent('network-request', {\n method: crumb.data?.method || 'GET',\n url: crumb.data?.url || crumb.message || '',\n status: crumb.data?.status_code,\n duration: crumb.data?.duration,\n error: crumb.data?.error,\n traceparent: crumb.data?.traceparent,\n });\n } else if (crumb.type === 'console' || crumb.category?.startsWith('console.')) {\n // Console tab requires expandable objects and stack traces for errors\n const payload: Record<string, unknown> = {\n level: crumb.category?.replace('console.', '') || 'log',\n message: crumb.message || '',\n };\n // Include structured data from console args (objects, arrays)\n if (crumb.data && Object.keys(crumb.data).length > 0) {\n payload.data = crumb.data;\n }\n // Include stack trace for error-level console entries\n if ((crumb.category === 'console.error' || crumb.category === 'console.warn') && crumb.data?.stack) {\n payload.stack = crumb.data.stack;\n }\n record.addCustomEvent('console-log', payload);\n }\n } catch {\n // Never crash the host app\n }\n });\n\n // Wire idle timeout: flush pending events when session goes idle\n session.setFlushCallback(() => {\n transport?.flush().catch(() => {\n // Never crash on flush errors\n });\n });\n\n // Wire idle timeout restart: stop recording, start fresh with new snapshot\n session.setRestartCallback(() => {\n if (stopRecording) {\n stopRecording();\n }\n stopRecording = startRecording(resolvedConfig!, (event, isCheckout) => {\n session?.onEvent(event, isCheckout);\n });\n });\n\n // Wire visibility pause: stop recording and flush sync on tab hide\n session.setPauseCallback(() => {\n if (stopRecording) {\n stopRecording();\n stopRecording = null;\n }\n transport?.flushSync();\n });\n\n // Wire visibility resume: restart recording on tab show\n session.setResumeCallback(() => {\n stopRecording = startRecording(resolvedConfig!, (event, isCheckout) => {\n session?.onEvent(event, isCheckout);\n });\n });\n\n // Start transport (30-second flush interval)\n transport.start(\n () => session!.getSessionId(),\n () => session!.nextSegmentId(),\n () => (session!.getMode() === 'buffer' ? 'buffer' : 'session'),\n );\n\n // Start recording immediately (LOCKED: recording starts on init())\n stopRecording = startRecording(resolvedConfig, (event, isCheckout) => {\n session!.onEvent(event, isCheckout);\n });\n } catch (err) {\n console.warn('[TraceKit Replay] Failed to initialize replay recording:', err);\n }\n },\n\n /**\n * Teardown: clean up all resources.\n * Called when the BrowserClient is destroyed.\n */\n teardown(): void {\n try {\n if (stopRecording) {\n stopRecording();\n stopRecording = null;\n }\n transport?.destroy();\n compressionWorker?.destroy();\n session?.destroy();\n session = null;\n compressionWorker = null;\n transport = null;\n resolvedConfig = null;\n } catch {\n // Ignore teardown errors\n }\n },\n\n /**\n * Manual flush: force an immediate upload of pending events.\n * Useful for ensuring data is sent before a page transition.\n */\n flush(): void {\n try {\n transport?.flush().catch(() => {\n // Never crash\n });\n } catch {\n // Never crash\n }\n },\n\n /**\n * Get the current session ID.\n * Returns empty string if replay is not active.\n * Useful for linking errors to replay sessions (Phase 26).\n */\n getSessionId(): string {\n return session?.getSessionId() ?? '';\n },\n };\n\n return integration;\n}\n\n// Re-export types for consumers\nexport type { ReplayConfig, Integration } from './types';\n","/**\n * TraceKit Replay - rrweb Recorder Wrapper\n * @package @tracekit/replay\n *\n * Wraps rrweb's record() function with privacy-first defaults:\n * - All text masked with same-length asterisk replacement\n * - All inputs masked\n * - Images, videos, canvas, SVGs, and iframes blocked\n * - Unmasking via CSS selectors and data-tracekit-unmask attribute\n * - Periodic full snapshots every 30s aligned with upload interval\n */\n\nimport { record } from 'rrweb';\nimport type { ResolvedReplayConfig } from './types';\n\n/**\n * Create a maskTextFn that masks all text EXCEPT elements matching\n * unmask selectors or the data-tracekit-unmask attribute.\n *\n * Privacy-first: when in doubt (null element, invalid selector), mask.\n */\nfunction createMaskTextFn(\n unmaskSelectors: string[],\n): (text: string, element: HTMLElement | null) => string {\n // Build combined CSS selector for unmask targets\n const selectorParts = ['[data-tracekit-unmask]'];\n if (unmaskSelectors.length > 0) {\n selectorParts.push(...unmaskSelectors);\n }\n const combinedSelector = selectorParts.join(', ');\n\n return (text: string, element: HTMLElement | null): string => {\n // Privacy-first: mask when element is null (uncertain context)\n if (!element) {\n return '*'.repeat(text.length);\n }\n\n // Check if element (or any ancestor) matches unmask selectors\n try {\n if (element.matches(combinedSelector) || element.closest(combinedSelector)) {\n return text; // Unmask: return original text\n }\n } catch {\n // Invalid selector -- fail safe by masking\n }\n\n // Default: same-length asterisk replacement\n return '*'.repeat(text.length);\n };\n}\n\n/**\n * Start rrweb recording with privacy-first defaults.\n *\n * @param config - Resolved replay configuration\n * @param onEvent - Callback invoked for each rrweb event\n * @returns A stop function to halt recording, or null if recording failed to start\n */\nexport function startRecording(\n config: ResolvedReplayConfig,\n onEvent: (event: unknown, isCheckout: boolean) => void,\n): (() => void) | null {\n try {\n const stopFn = record({\n emit: (event, isCheckout) => {\n onEvent(event, isCheckout ?? false);\n },\n\n // ================================================================\n // Privacy-first settings (LOCKED decisions)\n // ================================================================\n\n // Mask all text by matching all elements\n maskTextSelector: '*',\n\n // Mask all input values\n maskAllInputs: true,\n\n // Custom mask function: same-length asterisk replacement with unmask support\n maskTextFn: createMaskTextFn(config.unmask),\n\n // Block all media and embedded content\n blockSelector: 'img, video, canvas, svg, iframe',\n\n // Do NOT record canvas content\n recordCanvas: false,\n\n // Do NOT record cross-origin iframes\n recordCrossOriginIframes: false,\n\n // Do NOT inline images\n inlineImages: false,\n\n // ================================================================\n // Recording lifecycle\n // ================================================================\n\n // Periodic full snapshot every 30s, aligned with upload interval\n checkoutEveryNms: 30_000,\n\n // Sampling configuration to reduce event volume\n sampling: {\n mousemove: 50,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n });\n\n // rrweb record() returns undefined if it fails to start\n return stopFn ?? null;\n } catch {\n // Never crash the host application\n return null;\n }\n}\n","/**\n * TraceKit Replay - Ring Buffer\n * @package @tracekit/replay\n *\n * Timestamp-based ring buffer for error-mode capture.\n * Maintains exactly 60 seconds of rrweb events, evicting expired\n * entries on every add(). On error, the buffer is flushed and\n * the session switches from 'buffer' to 'session' mode.\n */\n\nexport class RingBuffer {\n private events: Array<{ event: any; timestamp: number }> = [];\n private maxAgeMs: number;\n\n constructor(maxAgeMs: number = 60_000) {\n this.maxAgeMs = maxAgeMs;\n }\n\n /**\n * Add an event to the buffer. Uses `event.timestamp` from rrweb\n * (milliseconds since epoch), falling back to Date.now().\n * Evicts expired entries after each add.\n */\n add(event: any): void {\n this.events.push({ event, timestamp: event.timestamp ?? Date.now() });\n this.evictExpired();\n }\n\n /**\n * Flush all buffered events and clear the buffer.\n * Returns the raw rrweb events (unwrapped from the timestamp envelope).\n */\n flush(): any[] {\n const flushed = this.events.map((e) => e.event);\n this.events = [];\n return flushed;\n }\n\n /**\n * Discard all buffered events.\n */\n clear(): void {\n this.events = [];\n }\n\n /**\n * Number of events currently in the buffer.\n */\n get size(): number {\n return this.events.length;\n }\n\n /**\n * Evict events older than maxAgeMs from the front of the buffer.\n */\n private evictExpired(): void {\n const cutoff = Date.now() - this.maxAgeMs;\n while (this.events.length > 0 && this.events[0].timestamp < cutoff) {\n this.events.shift();\n }\n }\n}\n","/**\n * TraceKit Replay - Session Manager\n * @package @tracekit/replay\n *\n * Core orchestrator for recording lifecycle. Controls which sessions\n * get full recording vs error-only buffer capture, handles idle\n * timeouts with session renewal, and manages visibility-based\n * pause/resume.\n *\n * Sampling bands:\n * [0, sessionSampleRate) -> mode = 'session' (full recording)\n * [sessionSampleRate, session+error) -> mode = 'buffer' (error capture)\n * [session+error, 1.0] -> mode = 'off' (no recording)\n */\n\nimport type { ResolvedReplayConfig, SessionState, ReplayMode } from './types';\nimport { RingBuffer } from './buffer';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a 32-character hex session ID.\n * Prefers crypto.randomUUID() where available, falls back to Math.random().\n */\nfunction generateSessionId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID().replace(/-/g, '');\n }\n // Fallback: random hex string\n return Array.from({ length: 32 }, () =>\n Math.floor(Math.random() * 16).toString(16),\n ).join('');\n}\n\n/**\n * Make a sampling decision based on configured rates.\n */\nfunction decideSamplingMode(config: ResolvedReplayConfig): ReplayMode {\n const rand = Math.random();\n if (rand < config.sessionSampleRate) {\n return 'session';\n }\n if (rand < config.sessionSampleRate + config.errorSampleRate) {\n return 'buffer';\n }\n return 'off';\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\nexport class SessionManager {\n private state: SessionState;\n private config: ResolvedReplayConfig;\n private ringBuffer: RingBuffer;\n\n // Callbacks wired by integration layer\n private eventCallback: ((events: any[]) => void) | null = null;\n private flushCallback: (() => void) | null = null;\n private restartCallback: (() => void) | null = null;\n private pauseCallback: (() => void) | null = null;\n private resumeCallback: (() => void) | null = null;\n\n // Idle timeout handle\n private idleTimer: ReturnType<typeof setTimeout> | null = null;\n\n // Visibility change handler (stored for cleanup)\n private visibilityHandler: (() => void) | null = null;\n\n constructor(config: ResolvedReplayConfig) {\n this.config = config;\n this.ringBuffer = new RingBuffer(60_000);\n\n const mode = decideSamplingMode(config);\n const now = Date.now();\n\n this.state = {\n sessionId: generateSessionId(),\n mode,\n startedAt: now,\n lastActivity: now,\n segmentId: 0,\n };\n\n this.resetIdleTimer();\n this.setupVisibilityListener();\n }\n\n // -------------------------------------------------------------------------\n // Event handling\n // -------------------------------------------------------------------------\n\n /**\n * Process an incoming rrweb event.\n * - session mode: forward immediately via eventCallback\n * - buffer mode: add to ring buffer for error-triggered flush\n * - off mode: discard\n */\n onEvent(event: any, _isCheckout: boolean): void {\n this.state.lastActivity = Date.now();\n this.resetIdleTimer();\n\n if (this.state.mode === 'session') {\n if (this.eventCallback) {\n try {\n this.eventCallback([event]);\n } catch {\n // Never crash the host app\n }\n }\n } else if (this.state.mode === 'buffer') {\n this.ringBuffer.add(event);\n }\n // mode === 'off': discard\n }\n\n /**\n * Handle an error event. For buffer-mode sessions:\n * 1. Flush all buffered events via eventCallback\n * 2. Switch to session mode (continue recording after error)\n *\n * Per LOCKED decision: error buffer operates ONLY for non-sampled\n * sessions in buffer mode.\n */\n onError(): void {\n if (this.state.mode === 'buffer' && this.ringBuffer.size > 0) {\n const events = this.ringBuffer.flush();\n if (this.eventCallback) {\n try {\n this.eventCallback(events);\n } catch {\n // Never crash the host app\n }\n }\n // Switch to full recording mode\n this.state.mode = 'session';\n }\n // session mode or off mode: no-op\n }\n\n // -------------------------------------------------------------------------\n // Callback setters\n // -------------------------------------------------------------------------\n\n /** Set callback that receives events for compression/upload */\n setEventCallback(cb: (events: any[]) => void): void {\n this.eventCallback = cb;\n }\n\n /** Set callback called on idle timeout to flush pending events */\n setFlushCallback(cb: () => void): void {\n this.flushCallback = cb;\n }\n\n /** Set callback called on idle timeout to restart recording with new snapshot */\n setRestartCallback(cb: () => void): void {\n this.restartCallback = cb;\n }\n\n /** Set callback called when tab goes hidden to pause recording */\n setPauseCallback(cb: () => void): void {\n this.pauseCallback = cb;\n }\n\n /** Set callback called when tab becomes visible to resume recording */\n setResumeCallback(cb: () => void): void {\n this.resumeCallback = cb;\n }\n\n // -------------------------------------------------------------------------\n // Accessors\n // -------------------------------------------------------------------------\n\n /** Current session ID */\n getSessionId(): string {\n return this.state.sessionId;\n }\n\n /** Current recording mode */\n getMode(): ReplayMode {\n return this.state.mode;\n }\n\n /** Return and increment segment counter */\n nextSegmentId(): number {\n return this.state.segmentId++;\n }\n\n /** Get full session state */\n getState(): SessionState {\n return { ...this.state };\n }\n\n /**\n * Flush events from the ring buffer (buffer mode).\n * Session mode events are forwarded immediately, so returns [].\n */\n flush(): any[] {\n if (this.state.mode === 'buffer') {\n return this.ringBuffer.flush();\n }\n return [];\n }\n\n // -------------------------------------------------------------------------\n // Idle timeout\n // -------------------------------------------------------------------------\n\n /**\n * Reset the idle timeout. Called on every event and at construction.\n * When the timeout fires:\n * 1. Flush pending events for the old session\n * 2. Generate new session ID + reset state\n * 3. Make new sampling decision\n * 4. Trigger a new full snapshot via restartCallback\n */\n private resetIdleTimer(): void {\n if (this.idleTimer !== null) {\n clearTimeout(this.idleTimer);\n }\n\n this.idleTimer = setTimeout(() => {\n this.handleIdleTimeout();\n }, this.config.idleTimeout);\n }\n\n private handleIdleTimeout(): void {\n // 1. Flush pending events for old session\n if (this.flushCallback) {\n try {\n this.flushCallback();\n } catch {\n // Never crash the host app\n }\n }\n\n // 2. Generate new session ID and reset state\n const mode = decideSamplingMode(this.config);\n const now = Date.now();\n\n this.state = {\n sessionId: generateSessionId(),\n mode,\n startedAt: now,\n lastActivity: now,\n segmentId: 0,\n };\n\n // 3. Clear the ring buffer for the new session\n this.ringBuffer.clear();\n\n // 4. Trigger new full snapshot\n if (this.restartCallback) {\n try {\n this.restartCallback();\n } catch {\n // Never crash the host app\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Visibility handling\n // -------------------------------------------------------------------------\n\n /**\n * Pause recording when tab goes hidden, resume when visible.\n * Per LOCKED decision: recording pauses on hidden, resumes on visible.\n */\n private setupVisibilityListener(): void {\n if (typeof document === 'undefined') {\n return;\n }\n\n this.visibilityHandler = () => {\n if (document.visibilityState === 'hidden') {\n // Pause: flush pending events and stop recording\n if (this.pauseCallback) {\n try {\n this.pauseCallback();\n } catch {\n // Never crash the host app\n }\n }\n } else if (document.visibilityState === 'visible') {\n // Resume: restart recording with new full snapshot\n this.state.lastActivity = Date.now();\n this.resetIdleTimer();\n if (this.resumeCallback) {\n try {\n this.resumeCallback();\n } catch {\n // Never crash the host app\n }\n }\n }\n };\n\n document.addEventListener('visibilitychange', this.visibilityHandler);\n }\n\n // -------------------------------------------------------------------------\n // Cleanup\n // -------------------------------------------------------------------------\n\n /**\n * Tear down the session manager: clear timers, remove listeners, clear buffer.\n */\n destroy(): void {\n if (this.idleTimer !== null) {\n clearTimeout(this.idleTimer);\n this.idleTimer = null;\n }\n\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler);\n this.visibilityHandler = null;\n }\n\n this.ringBuffer.clear();\n\n this.eventCallback = null;\n this.flushCallback = null;\n this.restartCallback = null;\n this.pauseCallback = null;\n this.resumeCallback = null;\n }\n}\n","/**\n * TraceKit Replay - Compression Worker\n * @package @tracekit/replay\n *\n * Compresses rrweb events via an inline Blob URL Web Worker using\n * native CompressionStream (gzip). On worker failure (CSP restriction,\n * CompressionStream unavailable), falls back to fflate gzipSync on the\n * main thread with a console warning.\n *\n * Zero-copy transfer: compressed Uint8Array buffer is transferred from\n * the worker via Transferable to avoid cloning overhead.\n */\n\nimport { gzipSync } from 'fflate';\nimport { WORKER_SCRIPT } from './worker';\n\ninterface PendingCompression {\n resolve: (data: { compressed: Uint8Array; originalSize: number }) => void;\n reject: (err: Error) => void;\n events: any[];\n}\n\nexport class CompressionWorker {\n private worker: Worker | null = null;\n private pendingCallbacks = new Map<number, PendingCompression>();\n private useMainThread = false;\n\n constructor() {\n this.initWorker();\n }\n\n /**\n * Create an inline Blob URL worker from WORKER_SCRIPT.\n * If worker creation fails (e.g. CSP), flag permanent main-thread fallback.\n */\n private initWorker(): void {\n try {\n const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' });\n const url = URL.createObjectURL(blob);\n this.worker = new Worker(url);\n URL.revokeObjectURL(url); // URL can be revoked after worker creation\n\n this.worker.onmessage = (e: MessageEvent) => {\n const { compressed, segmentId, originalSize, error } = e.data;\n const pending = this.pendingCallbacks.get(segmentId);\n if (!pending) return;\n this.pendingCallbacks.delete(segmentId);\n\n if (error) {\n // Worker reported an error (e.g. CompressionStream not available)\n // Fall back to main-thread compression for this and future calls\n console.warn(\n '[TraceKit Replay] Worker compression failed, falling back to main thread:',\n error,\n );\n this.useMainThread = true;\n this.compressMainThread(pending.events).then(pending.resolve).catch(pending.reject);\n return;\n }\n\n pending.resolve({ compressed, originalSize });\n };\n\n this.worker.onerror = () => {\n console.warn(\n '[TraceKit Replay] Web Worker failed to initialize. Using main-thread compression.',\n );\n this.useMainThread = true;\n this.worker = null;\n };\n } catch {\n // CSP or other restriction prevents worker creation\n console.warn(\n '[TraceKit Replay] Cannot create Web Worker (CSP?). Using main-thread compression.',\n );\n this.useMainThread = true;\n }\n }\n\n /**\n * Compress an array of rrweb events.\n * Routes to Web Worker when available, otherwise uses main-thread fflate.\n *\n * @param events - Array of rrweb events to compress\n * @param segmentId - Unique segment ID for correlating worker responses\n * @returns Compressed data with original (uncompressed) size\n */\n async compress(\n events: any[],\n segmentId: number,\n ): Promise<{ compressed: Uint8Array; originalSize: number }> {\n if (this.useMainThread || !this.worker) {\n return this.compressMainThread(events);\n }\n\n return new Promise((resolve, reject) => {\n const entry: PendingCompression = { resolve, reject, events };\n this.pendingCallbacks.set(segmentId, entry);\n this.worker!.postMessage({ events, segmentId });\n });\n }\n\n /**\n * Main-thread fallback using fflate gzipSync.\n * Used when Web Worker is unavailable (CSP) or CompressionStream is missing.\n */\n private async compressMainThread(\n events: any[],\n ): Promise<{ compressed: Uint8Array; originalSize: number }> {\n const json = JSON.stringify(events);\n const encoded = new TextEncoder().encode(json);\n const compressed = gzipSync(encoded, { level: 6 });\n return { compressed, originalSize: encoded.length };\n }\n\n /**\n * Terminate the worker and clean up pending callbacks.\n */\n destroy(): void {\n if (this.worker) {\n this.worker.terminate();\n this.worker = null;\n }\n this.pendingCallbacks.clear();\n }\n}\n","/**\n * TraceKit Replay - Web Worker Compression Script\n * @package @tracekit/replay\n *\n * Inline Web Worker script string using native CompressionStream API.\n * No external dependencies inside the worker -- CompressionStream is\n * baseline available in workers since 2023.\n *\n * If CompressionStream is unavailable, the worker posts an error back\n * and the main thread (CompressionWorker) falls back to fflate gzipSync.\n *\n * Message protocol:\n * IN: { events: any[], segmentId: number }\n * OUT: { compressed: Uint8Array, segmentId: number, originalSize: number }\n * ERR: { error: string, segmentId: number }\n */\n\nexport const WORKER_SCRIPT = `\nself.onmessage = function(e) {\n try {\n var data = e.data;\n var json = JSON.stringify(data.events);\n var encoded = new TextEncoder().encode(json);\n\n if (typeof CompressionStream === 'undefined') {\n self.postMessage({ error: 'CompressionStream not available', segmentId: data.segmentId });\n return;\n }\n\n var cs = new CompressionStream('gzip');\n var writer = cs.writable.getWriter();\n var reader = cs.readable.getReader();\n var chunks = [];\n\n writer.write(encoded);\n writer.close();\n\n function readChunks() {\n reader.read().then(function(result) {\n if (result.done) {\n var totalLen = 0;\n for (var i = 0; i < chunks.length; i++) totalLen += chunks[i].length;\n var compressed = new Uint8Array(totalLen);\n var offset = 0;\n for (var i = 0; i < chunks.length; i++) {\n compressed.set(chunks[i], offset);\n offset += chunks[i].length;\n }\n self.postMessage(\n { compressed: compressed, segmentId: data.segmentId, originalSize: encoded.length },\n [compressed.buffer]\n );\n } else {\n chunks.push(result.value);\n readChunks();\n }\n }).catch(function(err) {\n self.postMessage({ error: err.message, segmentId: data.segmentId });\n });\n }\n readChunks();\n } catch(err) {\n self.postMessage({ error: (err && err.message) || 'Unknown compression error', segmentId: e.data.segmentId });\n }\n};\n`;\n","/**\n * TraceKit Replay - Transport Layer\n * @package @tracekit/replay\n *\n * Uploads compressed replay chunks to the server every 30 seconds.\n * Uses fetch with X-API-Key header for normal uploads, sendBeacon\n * with query-parameter API key for tab-close fallback.\n *\n * Retry: exponential backoff (1s, 2s, 4s) with max 3 attempts.\n * Non-retryable status codes: 400, 401, 413.\n * On final failure: drop the chunk (replay data loss is non-critical).\n *\n * SAFETY: All external calls (fetch, sendBeacon) are wrapped in try/catch.\n * The transport NEVER throws -- replay must never crash the host app.\n */\n\nimport type { ResolvedReplayConfig } from './types';\nimport { gzipSync } from 'fflate';\nimport { CompressionWorker } from './compression';\n\n// Retry delays in milliseconds: 1s, 2s, 4s\nconst RETRY_DELAYS = [1000, 2000, 4000];\nconst MAX_ATTEMPTS = 3;\n\n// sendBeacon has a ~64KB payload limit\nconst BEACON_SIZE_LIMIT = 65536;\n\nexport class ReplayTransport {\n private pendingEvents: any[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private config: ResolvedReplayConfig;\n private compressionWorker: CompressionWorker;\n private bufferSize = 0;\n\n // Getter functions wired by integration layer\n private sessionIdFn: (() => string) | null = null;\n private segmentIdFn: (() => number) | null = null;\n private replayTypeFn: (() => string) | null = null;\n\n // Visibility change handler (stored for cleanup)\n private visibilityHandler: (() => void) | null = null;\n\n constructor(config: ResolvedReplayConfig, compressionWorker: CompressionWorker) {\n this.config = config;\n this.compressionWorker = compressionWorker;\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /**\n * Start the transport: begin 30-second flush interval and register\n * visibilitychange listener for sendBeacon fallback on tab close.\n */\n start(\n getSessionId: () => string,\n nextSegmentId: () => number,\n getReplayType: () => string,\n ): void {\n this.sessionIdFn = getSessionId;\n this.segmentIdFn = nextSegmentId;\n this.replayTypeFn = getReplayType;\n\n // Start periodic flush at config.flushInterval (default 30s)\n this.flushTimer = setInterval(() => {\n this.flush().catch(() => {\n // Never crash -- flush errors are swallowed\n });\n }, this.config.flushInterval);\n\n // Register sendBeacon fallback for tab close\n if (typeof document !== 'undefined') {\n this.visibilityHandler = () => {\n if (document.visibilityState === 'hidden') {\n this.flushSync();\n }\n };\n document.addEventListener('visibilitychange', this.visibilityHandler);\n }\n }\n\n /**\n * Stop the transport: clear flush interval and remove listeners.\n */\n stop(): void {\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler);\n this.visibilityHandler = null;\n }\n }\n\n /**\n * Destroy the transport: stop + clear all pending events.\n */\n destroy(): void {\n this.stop();\n this.pendingEvents = [];\n this.bufferSize = 0;\n }\n\n // ---------------------------------------------------------------------------\n // Event accumulation\n // ---------------------------------------------------------------------------\n\n /**\n * Add a single rrweb event to the pending buffer.\n * If buffer exceeds maxBufferSize, oldest events are dropped.\n */\n addEvent(event: any): void {\n try {\n const eventSize = JSON.stringify(event).length;\n this.pendingEvents.push(event);\n this.bufferSize += eventSize;\n\n // Drop oldest events if buffer exceeds max size\n while (this.bufferSize > this.config.maxBufferSize && this.pendingEvents.length > 1) {\n const dropped = this.pendingEvents.shift();\n if (dropped) {\n try {\n this.bufferSize -= JSON.stringify(dropped).length;\n } catch {\n // Ignore sizing errors on drop\n }\n }\n if (this.config.maxBufferSize > 0) {\n console.warn('[TraceKit Replay] Buffer exceeded maxBufferSize, dropping oldest events');\n }\n }\n } catch {\n // Never crash the host app\n }\n }\n\n /**\n * Add multiple rrweb events at once (used for ring buffer flush-on-error).\n */\n addEvents(events: any[]): void {\n for (const event of events) {\n this.addEvent(event);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Flush (async -- normal upload path)\n // ---------------------------------------------------------------------------\n\n /**\n * Flush pending events: compress via worker and upload via fetch with retry.\n * Returns silently if no events are pending.\n */\n async flush(): Promise<void> {\n if (this.pendingEvents.length === 0) {\n return;\n }\n\n // Swap buffer atomically\n const events = this.pendingEvents;\n this.pendingEvents = [];\n this.bufferSize = 0;\n\n try {\n const sessionId = this.sessionIdFn ? this.sessionIdFn() : '';\n const segmentId = this.segmentIdFn ? this.segmentIdFn() : 0;\n\n if (!sessionId) {\n return; // No session -- discard events\n }\n\n // Compress via worker (or main-thread fallback)\n const { compressed, originalSize } = await this.compressionWorker.compress(events, segmentId);\n\n // Upload with retry\n await this.uploadWithRetry(sessionId, segmentId, compressed, originalSize);\n } catch {\n // Drop chunk on any unexpected error -- replay data loss is acceptable\n }\n }\n\n // ---------------------------------------------------------------------------\n // Flush sync (sendBeacon fallback for tab close)\n // ---------------------------------------------------------------------------\n\n /**\n * Synchronous flush for tab close -- uses sendBeacon.\n * Compresses on main thread with fflate gzipSync (sendBeacon must be sync).\n * Falls back to fetch with keepalive:true if sendBeacon fails.\n * If both fail, data is lost (acceptable for replay).\n */\n flushSync(): void {\n if (this.pendingEvents.length === 0) {\n return;\n }\n\n try {\n const events = this.pendingEvents;\n this.pendingEvents = [];\n this.bufferSize = 0;\n\n const sessionId = this.sessionIdFn ? this.sessionIdFn() : '';\n const segmentId = this.segmentIdFn ? this.segmentIdFn() : 0;\n const replayType = this.replayTypeFn ? this.replayTypeFn() : 'session';\n\n if (!sessionId) {\n return;\n }\n\n // Compress on main thread (sync -- sendBeacon requires sync data)\n const json = JSON.stringify(events);\n const encoded = new TextEncoder().encode(json);\n const compressed = gzipSync(encoded, { level: 6 });\n\n // Build URL with query parameters (sendBeacon cannot set custom headers)\n const url =\n `${this.config.endpoint}/api/replays/${sessionId}/chunks` +\n `?api_key=${encodeURIComponent(this.config.apiKey)}` +\n `&segment_id=${segmentId}` +\n `&original_size=${encoded.length}` +\n `&replay_type=${replayType}`;\n\n // Cast through ArrayBuffer to satisfy TypeScript 5.x Uint8Array<ArrayBufferLike> vs BlobPart\n const blob = new Blob([compressed as unknown as BlobPart], { type: 'application/octet-stream' });\n\n // Try sendBeacon first (works during page unload)\n if (compressed.byteLength <= BEACON_SIZE_LIMIT && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n const sent = navigator.sendBeacon(url, blob);\n if (sent) {\n return;\n }\n }\n\n // Fallback: fetch with keepalive (also has ~64KB limit but is the standard approach)\n if (typeof fetch !== 'undefined') {\n fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n },\n body: blob,\n keepalive: true,\n }).catch(() => {\n // Data loss accepted -- replay is non-critical\n });\n }\n } catch {\n // Never crash the host app on tab close\n }\n }\n\n // ---------------------------------------------------------------------------\n // Upload with retry (exponential backoff)\n // ---------------------------------------------------------------------------\n\n /**\n * Upload compressed chunk via fetch with exponential backoff retry.\n * Retries up to 3 times with delays of 1s, 2s, 4s.\n * Non-retryable status codes (400, 401, 413) abort immediately.\n * On final failure: drop chunk silently.\n */\n private async uploadWithRetry(\n sessionId: string,\n segmentId: number,\n compressed: Uint8Array,\n originalSize: number,\n ): Promise<void> {\n const url = `${this.config.endpoint}/api/replays/${sessionId}/chunks`;\n const replayType = this.replayTypeFn ? this.replayTypeFn() : 'session';\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n 'X-API-Key': this.config.apiKey,\n 'X-Segment-Id': String(segmentId),\n 'X-Original-Size': String(originalSize),\n 'X-Replay-Type': replayType,\n },\n body: compressed as unknown as BodyInit,\n keepalive: true,\n });\n\n if (response.ok) {\n return; // Success\n }\n\n // Non-retryable status codes -- abort immediately\n if (response.status === 400 || response.status === 401 || response.status === 413) {\n return;\n }\n\n // Retryable failure -- wait before next attempt\n if (attempt < MAX_ATTEMPTS - 1) {\n await this.delay(RETRY_DELAYS[attempt]);\n }\n } catch {\n // Network error -- wait before retry\n if (attempt < MAX_ATTEMPTS - 1) {\n await this.delay(RETRY_DELAYS[attempt]);\n }\n }\n }\n\n // All attempts exhausted -- drop chunk (replay data loss is acceptable)\n }\n\n /**\n * Promise-based delay for retry backoff.\n */\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":"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","names":["DEFAULTS","clampRate","value","name","min","max","resolveReplayConfig","config","apiKey","endpoint","sessionSampleRate","errorSampleRate","record","record","createMaskTextFn","unmaskSelectors","selectorParts","combinedSelector","text","element","startRecording","config","onEvent","event","isCheckout","RingBuffer","maxAgeMs","event","flushed","e","cutoff","generateSessionId","decideSamplingMode","config","rand","SessionManager","RingBuffer","mode","now","event","_isCheckout","events","cb","gzipSync","WORKER_SCRIPT","CompressionWorker","blob","WORKER_SCRIPT","url","e","compressed","segmentId","originalSize","error","pending","events","resolve","reject","entry","json","encoded","gzipSync","gzipSync","RETRY_DELAYS","MAX_ATTEMPTS","BEACON_SIZE_LIMIT","ReplayTransport","config","compressionWorker","getSessionId","nextSegmentId","getReplayType","event","eventSize","dropped","events","sessionId","segmentId","compressed","originalSize","replayType","json","encoded","url","blob","attempt","response","ms","resolve","replayIntegration","config","session","compressionWorker","transport","stopRecording","resolvedConfig","client","clientConfig","resolveReplayConfig","SessionManager","CompressionWorker","ReplayTransport","events","event","originalCaptureException","error","context","replayId","scope","result","crumb","record","payload","startRecording","isCheckout","err"]}
1
+ {"version":3,"sources":["../src/config.ts","../src/index.ts","../src/recorder.ts","../src/buffer.ts","../src/session.ts","../src/compression.ts","../src/worker.ts","../src/transport.ts"],"sourcesContent":["/**\n * TraceKit Replay - Configuration Resolution\n * @package @tracekit/replay\n *\n * Resolves user-provided ReplayConfig with privacy-first defaults.\n * Validates and clamps rate values to valid ranges.\n */\n\nimport type { ReplayConfig, ResolvedReplayConfig } from './types';\n\nconst DEFAULTS = {\n sessionSampleRate: 0.1,\n errorSampleRate: 0.0,\n unmask: [] as string[],\n idleTimeout: 1_800_000, // 30 minutes\n flushInterval: 30_000, // 30 seconds\n maxBufferSize: 24_117_248, // 23MB\n inlineImages: false,\n blockMedia: true,\n} as const;\n\n/**\n * Clamp a value to the [min, max] range, logging a warning if clamped.\n */\nfunction clampRate(value: number, name: string, min: number, max: number): number {\n if (value < min) {\n console.warn(`[TraceKit Replay] ${name} (${value}) is below ${min}, clamping to ${min}`);\n return min;\n }\n if (value > max) {\n console.warn(`[TraceKit Replay] ${name} (${value}) is above ${max}, clamping to ${max}`);\n return max;\n }\n return value;\n}\n\n/**\n * Resolve user-provided replay config with defaults.\n * Validates sample rates are in [0, 1] and their sum does not exceed 1.0.\n */\nexport function resolveReplayConfig(\n config: ReplayConfig,\n apiKey: string,\n endpoint: string,\n): ResolvedReplayConfig {\n let sessionSampleRate = clampRate(\n config.sessionSampleRate ?? DEFAULTS.sessionSampleRate,\n 'sessionSampleRate',\n 0,\n 1,\n );\n\n let errorSampleRate = clampRate(\n config.errorSampleRate ?? DEFAULTS.errorSampleRate,\n 'errorSampleRate',\n 0,\n 1,\n );\n\n // Ensure combined rate does not exceed 1.0\n if (sessionSampleRate + errorSampleRate > 1.0) {\n console.warn(\n `[TraceKit Replay] sessionSampleRate (${sessionSampleRate}) + errorSampleRate (${errorSampleRate}) exceeds 1.0, clamping errorSampleRate`,\n );\n errorSampleRate = 1.0 - sessionSampleRate;\n }\n\n return {\n sessionSampleRate,\n errorSampleRate,\n unmask: config.unmask ?? DEFAULTS.unmask,\n idleTimeout: config.idleTimeout ?? DEFAULTS.idleTimeout,\n flushInterval: config.flushInterval ?? DEFAULTS.flushInterval,\n maxBufferSize: config.maxBufferSize ?? DEFAULTS.maxBufferSize,\n inlineImages: config.inlineImages ?? DEFAULTS.inlineImages,\n blockMedia: config.blockMedia ?? DEFAULTS.blockMedia,\n apiKey,\n endpoint,\n };\n}\n","/**\n * TraceKit Replay - Public API\n * @package @tracekit/replay\n *\n * Entry point for the session replay addon.\n * Provides replayIntegration() factory that returns an Integration object\n * compatible with @tracekit/browser's addons system.\n *\n * Usage:\n * import { init } from '@tracekit/browser';\n * import { replayIntegration } from '@tracekit/replay';\n * init({ apiKey: 'key', addons: [replayIntegration()] });\n *\n * The integration wires together:\n * recorder -> session manager -> compression worker -> transport\n *\n * Recording starts immediately when the integration is installed via init().\n * Manual control: flush() forces an upload, getSessionId() returns the current ID.\n */\n\nimport type { ReplayConfig, ResolvedReplayConfig, Integration } from './types';\nimport { resolveReplayConfig } from './config';\nimport { record } from 'rrweb';\nimport { startRecording } from './recorder';\nimport { SessionManager } from './session';\nimport { CompressionWorker } from './compression';\nimport { ReplayTransport } from './transport';\n\n/**\n * Create a session replay integration for @tracekit/browser.\n *\n * @param config - Optional replay configuration overrides\n * @returns Integration object with flush() and getSessionId() manual control methods\n */\nexport function replayIntegration(\n config: ReplayConfig = {},\n): Integration & { flush(): void; getSessionId(): string } {\n let session: SessionManager | null = null;\n let compressionWorker: CompressionWorker | null = null;\n let transport: ReplayTransport | null = null;\n let stopRecording: (() => void) | null = null;\n let resolvedConfig: ResolvedReplayConfig | null = null;\n\n const integration: Integration & { flush(): void; getSessionId(): string } = {\n name: 'replay',\n\n /**\n * Install the replay integration into the BrowserClient.\n * Creates the full recording pipeline and starts recording immediately.\n *\n * ALL code is wrapped in try/catch -- if replay fails to initialize,\n * it MUST NOT break the host app or the core browser SDK.\n */\n install(client: any): void {\n try {\n // Resolve config with client's apiKey and endpoint\n const clientConfig = client.getConfig();\n resolvedConfig = resolveReplayConfig(config, clientConfig.apiKey, clientConfig.endpoint);\n\n // Create session manager (makes sampling decision)\n session = new SessionManager(resolvedConfig);\n\n // If mode is 'off', don't set up recording pipeline\n if (session.getMode() === 'off') {\n return;\n }\n\n // Create compression worker (Web Worker with main-thread fallback)\n compressionWorker = new CompressionWorker();\n\n // Create transport (30-second flush interval, retry, sendBeacon fallback)\n transport = new ReplayTransport(resolvedConfig, compressionWorker);\n\n // Wire session -> transport: events flow from session to transport\n session.setEventCallback((events: any[]) => {\n for (const event of events) {\n transport!.addEvent(event);\n }\n });\n\n // Wire error notification: hook into captureException to detect errors\n // for ring buffer flush. Also injects replay_id as a tag on the error event\n // so the playback UI can link errors to their replay session.\n // Uses setTag (not setExtra) so replay_id appears as a direct span attribute\n // in OTLP output -- extras would prefix it as \"extra.replay_id\".\n const originalCaptureException = client.captureException.bind(client);\n client.captureException = function (error: Error, context?: any): string {\n const replayId = session?.getSessionId() ?? '';\n const scope = client.getScope();\n if (replayId) {\n scope.setTag('replay_id', replayId);\n }\n const result = originalCaptureException(error, context);\n if (replayId) {\n scope.setTag('replay_id', '');\n }\n if (session) {\n session.onError();\n }\n return result;\n };\n\n // Bridge breadcrumbs to rrweb custom events for playback sidebar tabs.\n // Network requests become 'network-request' events, console output becomes\n // 'console-log' events -- both timestamped in the rrweb event stream.\n const scope = client.getScope();\n scope.onBreadcrumb((crumb: any) => {\n try {\n if (crumb.type === 'http' || crumb.category?.startsWith('fetch') || crumb.category?.startsWith('xhr')) {\n record.addCustomEvent('network-request', {\n method: crumb.data?.method || 'GET',\n url: crumb.data?.url || crumb.message || '',\n status: crumb.data?.status_code,\n duration: crumb.data?.duration,\n error: crumb.data?.error,\n traceparent: crumb.data?.traceparent,\n });\n } else if (crumb.type === 'console' || crumb.category?.startsWith('console.')) {\n // Console tab requires expandable objects and stack traces for errors\n const payload: Record<string, unknown> = {\n level: crumb.category?.replace('console.', '') || 'log',\n message: crumb.message || '',\n };\n // Include structured data from console args (objects, arrays)\n if (crumb.data && Object.keys(crumb.data).length > 0) {\n payload.data = crumb.data;\n }\n // Include stack trace for error-level console entries\n if ((crumb.category === 'console.error' || crumb.category === 'console.warn') && crumb.data?.stack) {\n payload.stack = crumb.data.stack;\n }\n record.addCustomEvent('console-log', payload);\n }\n } catch {\n // Never crash the host app\n }\n });\n\n // Wire idle timeout: flush pending events when session goes idle\n session.setFlushCallback(() => {\n transport?.flush().catch(() => {\n // Never crash on flush errors\n });\n });\n\n // Wire idle timeout restart: stop recording, start fresh with new snapshot\n session.setRestartCallback(() => {\n if (stopRecording) {\n stopRecording();\n }\n stopRecording = startRecording(resolvedConfig!, (event, isCheckout) => {\n session?.onEvent(event, isCheckout);\n });\n });\n\n // Wire visibility pause: stop recording and flush sync on tab hide\n session.setPauseCallback(() => {\n if (stopRecording) {\n stopRecording();\n stopRecording = null;\n }\n transport?.flushSync();\n });\n\n // Wire visibility resume: restart recording on tab show\n session.setResumeCallback(() => {\n stopRecording = startRecording(resolvedConfig!, (event, isCheckout) => {\n session?.onEvent(event, isCheckout);\n });\n });\n\n // Start transport (30-second flush interval)\n transport.start(\n () => session!.getSessionId(),\n () => session!.nextSegmentId(),\n () => (session!.getMode() === 'buffer' ? 'buffer' : 'session'),\n );\n\n // Start recording immediately (LOCKED: recording starts on init())\n stopRecording = startRecording(resolvedConfig, (event, isCheckout) => {\n session!.onEvent(event, isCheckout);\n });\n } catch (err) {\n console.warn('[TraceKit Replay] Failed to initialize replay recording:', err);\n }\n },\n\n /**\n * Teardown: clean up all resources.\n * Called when the BrowserClient is destroyed.\n */\n teardown(): void {\n try {\n if (stopRecording) {\n stopRecording();\n stopRecording = null;\n }\n transport?.destroy();\n compressionWorker?.destroy();\n session?.destroy();\n session = null;\n compressionWorker = null;\n transport = null;\n resolvedConfig = null;\n } catch {\n // Ignore teardown errors\n }\n },\n\n /**\n * Manual flush: force an immediate upload of pending events.\n * Useful for ensuring data is sent before a page transition.\n */\n flush(): void {\n try {\n transport?.flush().catch(() => {\n // Never crash\n });\n } catch {\n // Never crash\n }\n },\n\n /**\n * Get the current session ID.\n * Returns empty string if replay is not active.\n * Useful for linking errors to replay sessions (Phase 26).\n */\n getSessionId(): string {\n return session?.getSessionId() ?? '';\n },\n };\n\n return integration;\n}\n\n// Re-export types for consumers\nexport type { ReplayConfig, Integration } from './types';\n","/**\n * TraceKit Replay - rrweb Recorder Wrapper\n * @package @tracekit/replay\n *\n * Wraps rrweb's record() function with privacy-first defaults:\n * - All text masked with same-length asterisk replacement\n * - All inputs masked\n * - Images, videos, canvas, SVGs, and iframes blocked\n * - Unmasking via CSS selectors and data-tracekit-unmask attribute\n * - Periodic full snapshots every 30s aligned with upload interval\n */\n\nimport { record } from 'rrweb';\nimport type { ResolvedReplayConfig } from './types';\n\n/**\n * Create a maskTextFn that masks all text EXCEPT elements matching\n * unmask selectors or the data-tracekit-unmask attribute.\n *\n * Privacy-first: when in doubt (null element, invalid selector), mask.\n */\nfunction createMaskTextFn(\n unmaskSelectors: string[],\n): (text: string, element: HTMLElement | null) => string {\n // Build combined CSS selector for unmask targets\n const selectorParts = ['[data-tracekit-unmask]'];\n if (unmaskSelectors.length > 0) {\n selectorParts.push(...unmaskSelectors);\n }\n const combinedSelector = selectorParts.join(', ');\n\n return (text: string, element: HTMLElement | null): string => {\n // Privacy-first: mask when element is null (uncertain context)\n if (!element) {\n return '*'.repeat(text.length);\n }\n\n // Check if element (or any ancestor) matches unmask selectors\n try {\n if (element.matches(combinedSelector) || element.closest(combinedSelector)) {\n return text; // Unmask: return original text\n }\n } catch {\n // Invalid selector -- fail safe by masking\n }\n\n // Default: same-length asterisk replacement\n return '*'.repeat(text.length);\n };\n}\n\n/**\n * Start rrweb recording with privacy-first defaults.\n *\n * @param config - Resolved replay configuration\n * @param onEvent - Callback invoked for each rrweb event\n * @returns A stop function to halt recording, or null if recording failed to start\n */\nexport function startRecording(\n config: ResolvedReplayConfig,\n onEvent: (event: unknown, isCheckout: boolean) => void,\n): (() => void) | null {\n try {\n const stopFn = record({\n emit: (event, isCheckout) => {\n onEvent(event, isCheckout ?? false);\n },\n\n // ================================================================\n // Privacy-first settings (LOCKED decisions)\n // ================================================================\n\n // Mask all text by matching all elements\n maskTextSelector: '*',\n\n // Mask all input values\n maskAllInputs: true,\n\n // Custom mask function: same-length asterisk replacement with unmask support\n maskTextFn: createMaskTextFn(config.unmask),\n\n // Block media elements (configurable, default: true for privacy)\n ...(config.blockMedia ? { blockSelector: 'img, video, canvas, svg, iframe' } : {}),\n\n // Do NOT record canvas content\n recordCanvas: false,\n\n // Do NOT record cross-origin iframes\n recordCrossOriginIframes: false,\n\n // Inline images as base64 (configurable, default: false for privacy/size)\n inlineImages: config.inlineImages,\n\n // ================================================================\n // Recording lifecycle\n // ================================================================\n\n // Periodic full snapshot every 30s, aligned with upload interval\n checkoutEveryNms: 30_000,\n\n // Sampling configuration to reduce event volume\n sampling: {\n mousemove: 50,\n mouseInteraction: true,\n scroll: 150,\n input: 'last',\n },\n });\n\n // rrweb record() returns undefined if it fails to start\n return stopFn ?? null;\n } catch {\n // Never crash the host application\n return null;\n }\n}\n","/**\n * TraceKit Replay - Ring Buffer\n * @package @tracekit/replay\n *\n * Timestamp-based ring buffer for error-mode capture.\n * Maintains exactly 60 seconds of rrweb events, evicting expired\n * entries on every add(). On error, the buffer is flushed and\n * the session switches from 'buffer' to 'session' mode.\n */\n\nexport class RingBuffer {\n private events: Array<{ event: any; timestamp: number }> = [];\n private maxAgeMs: number;\n\n constructor(maxAgeMs: number = 60_000) {\n this.maxAgeMs = maxAgeMs;\n }\n\n /**\n * Add an event to the buffer. Uses `event.timestamp` from rrweb\n * (milliseconds since epoch), falling back to Date.now().\n * Evicts expired entries after each add.\n */\n add(event: any): void {\n this.events.push({ event, timestamp: event.timestamp ?? Date.now() });\n this.evictExpired();\n }\n\n /**\n * Flush all buffered events and clear the buffer.\n * Returns the raw rrweb events (unwrapped from the timestamp envelope).\n */\n flush(): any[] {\n const flushed = this.events.map((e) => e.event);\n this.events = [];\n return flushed;\n }\n\n /**\n * Discard all buffered events.\n */\n clear(): void {\n this.events = [];\n }\n\n /**\n * Number of events currently in the buffer.\n */\n get size(): number {\n return this.events.length;\n }\n\n /**\n * Evict events older than maxAgeMs from the front of the buffer.\n */\n private evictExpired(): void {\n const cutoff = Date.now() - this.maxAgeMs;\n while (this.events.length > 0 && this.events[0].timestamp < cutoff) {\n this.events.shift();\n }\n }\n}\n","/**\n * TraceKit Replay - Session Manager\n * @package @tracekit/replay\n *\n * Core orchestrator for recording lifecycle. Controls which sessions\n * get full recording vs error-only buffer capture, handles idle\n * timeouts with session renewal, and manages visibility-based\n * pause/resume.\n *\n * Sampling bands:\n * [0, sessionSampleRate) -> mode = 'session' (full recording)\n * [sessionSampleRate, session+error) -> mode = 'buffer' (error capture)\n * [session+error, 1.0] -> mode = 'off' (no recording)\n */\n\nimport type { ResolvedReplayConfig, SessionState, ReplayMode } from './types';\nimport { RingBuffer } from './buffer';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a 32-character hex session ID.\n * Prefers crypto.randomUUID() where available, falls back to Math.random().\n */\nfunction generateSessionId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID().replace(/-/g, '');\n }\n // Fallback: random hex string\n return Array.from({ length: 32 }, () =>\n Math.floor(Math.random() * 16).toString(16),\n ).join('');\n}\n\n/**\n * Make a sampling decision based on configured rates.\n */\nfunction decideSamplingMode(config: ResolvedReplayConfig): ReplayMode {\n const rand = Math.random();\n if (rand < config.sessionSampleRate) {\n return 'session';\n }\n if (rand < config.sessionSampleRate + config.errorSampleRate) {\n return 'buffer';\n }\n return 'off';\n}\n\n// ---------------------------------------------------------------------------\n// SessionManager\n// ---------------------------------------------------------------------------\n\nexport class SessionManager {\n private state: SessionState;\n private config: ResolvedReplayConfig;\n private ringBuffer: RingBuffer;\n\n // Callbacks wired by integration layer\n private eventCallback: ((events: any[]) => void) | null = null;\n private flushCallback: (() => void) | null = null;\n private restartCallback: (() => void) | null = null;\n private pauseCallback: (() => void) | null = null;\n private resumeCallback: (() => void) | null = null;\n\n // Idle timeout handle\n private idleTimer: ReturnType<typeof setTimeout> | null = null;\n\n // Visibility change handler (stored for cleanup)\n private visibilityHandler: (() => void) | null = null;\n\n constructor(config: ResolvedReplayConfig) {\n this.config = config;\n this.ringBuffer = new RingBuffer(60_000);\n\n const mode = decideSamplingMode(config);\n const now = Date.now();\n\n this.state = {\n sessionId: generateSessionId(),\n mode,\n startedAt: now,\n lastActivity: now,\n segmentId: 0,\n };\n\n this.resetIdleTimer();\n this.setupVisibilityListener();\n }\n\n // -------------------------------------------------------------------------\n // Event handling\n // -------------------------------------------------------------------------\n\n /**\n * Process an incoming rrweb event.\n * - session mode: forward immediately via eventCallback\n * - buffer mode: add to ring buffer for error-triggered flush\n * - off mode: discard\n */\n onEvent(event: any, _isCheckout: boolean): void {\n this.state.lastActivity = Date.now();\n this.resetIdleTimer();\n\n if (this.state.mode === 'session') {\n if (this.eventCallback) {\n try {\n this.eventCallback([event]);\n } catch {\n // Never crash the host app\n }\n }\n } else if (this.state.mode === 'buffer') {\n this.ringBuffer.add(event);\n }\n // mode === 'off': discard\n }\n\n /**\n * Handle an error event. For buffer-mode sessions:\n * 1. Flush all buffered events via eventCallback\n * 2. Switch to session mode (continue recording after error)\n *\n * Per LOCKED decision: error buffer operates ONLY for non-sampled\n * sessions in buffer mode.\n */\n onError(): void {\n if (this.state.mode === 'buffer' && this.ringBuffer.size > 0) {\n const events = this.ringBuffer.flush();\n if (this.eventCallback) {\n try {\n this.eventCallback(events);\n } catch {\n // Never crash the host app\n }\n }\n // Switch to full recording mode\n this.state.mode = 'session';\n }\n // session mode or off mode: no-op\n }\n\n // -------------------------------------------------------------------------\n // Callback setters\n // -------------------------------------------------------------------------\n\n /** Set callback that receives events for compression/upload */\n setEventCallback(cb: (events: any[]) => void): void {\n this.eventCallback = cb;\n }\n\n /** Set callback called on idle timeout to flush pending events */\n setFlushCallback(cb: () => void): void {\n this.flushCallback = cb;\n }\n\n /** Set callback called on idle timeout to restart recording with new snapshot */\n setRestartCallback(cb: () => void): void {\n this.restartCallback = cb;\n }\n\n /** Set callback called when tab goes hidden to pause recording */\n setPauseCallback(cb: () => void): void {\n this.pauseCallback = cb;\n }\n\n /** Set callback called when tab becomes visible to resume recording */\n setResumeCallback(cb: () => void): void {\n this.resumeCallback = cb;\n }\n\n // -------------------------------------------------------------------------\n // Accessors\n // -------------------------------------------------------------------------\n\n /** Current session ID */\n getSessionId(): string {\n return this.state.sessionId;\n }\n\n /** Current recording mode */\n getMode(): ReplayMode {\n return this.state.mode;\n }\n\n /** Return and increment segment counter */\n nextSegmentId(): number {\n return this.state.segmentId++;\n }\n\n /** Get full session state */\n getState(): SessionState {\n return { ...this.state };\n }\n\n /**\n * Flush events from the ring buffer (buffer mode).\n * Session mode events are forwarded immediately, so returns [].\n */\n flush(): any[] {\n if (this.state.mode === 'buffer') {\n return this.ringBuffer.flush();\n }\n return [];\n }\n\n // -------------------------------------------------------------------------\n // Idle timeout\n // -------------------------------------------------------------------------\n\n /**\n * Reset the idle timeout. Called on every event and at construction.\n * When the timeout fires:\n * 1. Flush pending events for the old session\n * 2. Generate new session ID + reset state\n * 3. Make new sampling decision\n * 4. Trigger a new full snapshot via restartCallback\n */\n private resetIdleTimer(): void {\n if (this.idleTimer !== null) {\n clearTimeout(this.idleTimer);\n }\n\n this.idleTimer = setTimeout(() => {\n this.handleIdleTimeout();\n }, this.config.idleTimeout);\n }\n\n private handleIdleTimeout(): void {\n // 1. Flush pending events for old session\n if (this.flushCallback) {\n try {\n this.flushCallback();\n } catch {\n // Never crash the host app\n }\n }\n\n // 2. Generate new session ID and reset state\n const mode = decideSamplingMode(this.config);\n const now = Date.now();\n\n this.state = {\n sessionId: generateSessionId(),\n mode,\n startedAt: now,\n lastActivity: now,\n segmentId: 0,\n };\n\n // 3. Clear the ring buffer for the new session\n this.ringBuffer.clear();\n\n // 4. Trigger new full snapshot\n if (this.restartCallback) {\n try {\n this.restartCallback();\n } catch {\n // Never crash the host app\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Visibility handling\n // -------------------------------------------------------------------------\n\n /**\n * Pause recording when tab goes hidden, resume when visible.\n * Per LOCKED decision: recording pauses on hidden, resumes on visible.\n */\n private setupVisibilityListener(): void {\n if (typeof document === 'undefined') {\n return;\n }\n\n this.visibilityHandler = () => {\n if (document.visibilityState === 'hidden') {\n // Pause: flush pending events and stop recording\n if (this.pauseCallback) {\n try {\n this.pauseCallback();\n } catch {\n // Never crash the host app\n }\n }\n } else if (document.visibilityState === 'visible') {\n // Resume: restart recording with new full snapshot\n this.state.lastActivity = Date.now();\n this.resetIdleTimer();\n if (this.resumeCallback) {\n try {\n this.resumeCallback();\n } catch {\n // Never crash the host app\n }\n }\n }\n };\n\n document.addEventListener('visibilitychange', this.visibilityHandler);\n }\n\n // -------------------------------------------------------------------------\n // Cleanup\n // -------------------------------------------------------------------------\n\n /**\n * Tear down the session manager: clear timers, remove listeners, clear buffer.\n */\n destroy(): void {\n if (this.idleTimer !== null) {\n clearTimeout(this.idleTimer);\n this.idleTimer = null;\n }\n\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler);\n this.visibilityHandler = null;\n }\n\n this.ringBuffer.clear();\n\n this.eventCallback = null;\n this.flushCallback = null;\n this.restartCallback = null;\n this.pauseCallback = null;\n this.resumeCallback = null;\n }\n}\n","/**\n * TraceKit Replay - Compression Worker\n * @package @tracekit/replay\n *\n * Compresses rrweb events via an inline Blob URL Web Worker using\n * native CompressionStream (gzip). On worker failure (CSP restriction,\n * CompressionStream unavailable), falls back to fflate gzipSync on the\n * main thread with a console warning.\n *\n * Zero-copy transfer: compressed Uint8Array buffer is transferred from\n * the worker via Transferable to avoid cloning overhead.\n */\n\nimport { gzipSync } from 'fflate';\nimport { WORKER_SCRIPT } from './worker';\n\ninterface PendingCompression {\n resolve: (data: { compressed: Uint8Array; originalSize: number }) => void;\n reject: (err: Error) => void;\n events: any[];\n}\n\nexport class CompressionWorker {\n private worker: Worker | null = null;\n private pendingCallbacks = new Map<number, PendingCompression>();\n private useMainThread = false;\n\n constructor() {\n this.initWorker();\n }\n\n /**\n * Create an inline Blob URL worker from WORKER_SCRIPT.\n * If worker creation fails (e.g. CSP), flag permanent main-thread fallback.\n */\n private initWorker(): void {\n try {\n const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' });\n const url = URL.createObjectURL(blob);\n this.worker = new Worker(url);\n URL.revokeObjectURL(url); // URL can be revoked after worker creation\n\n this.worker.onmessage = (e: MessageEvent) => {\n const { compressed, segmentId, originalSize, error } = e.data;\n const pending = this.pendingCallbacks.get(segmentId);\n if (!pending) return;\n this.pendingCallbacks.delete(segmentId);\n\n if (error) {\n // Worker reported an error (e.g. CompressionStream not available)\n // Fall back to main-thread compression for this and future calls\n console.warn(\n '[TraceKit Replay] Worker compression failed, falling back to main thread:',\n error,\n );\n this.useMainThread = true;\n this.compressMainThread(pending.events).then(pending.resolve).catch(pending.reject);\n return;\n }\n\n pending.resolve({ compressed, originalSize });\n };\n\n this.worker.onerror = () => {\n console.warn(\n '[TraceKit Replay] Web Worker failed to initialize. Using main-thread compression.',\n );\n this.useMainThread = true;\n this.worker = null;\n };\n } catch {\n // CSP or other restriction prevents worker creation\n console.warn(\n '[TraceKit Replay] Cannot create Web Worker (CSP?). Using main-thread compression.',\n );\n this.useMainThread = true;\n }\n }\n\n /**\n * Compress an array of rrweb events.\n * Routes to Web Worker when available, otherwise uses main-thread fflate.\n *\n * @param events - Array of rrweb events to compress\n * @param segmentId - Unique segment ID for correlating worker responses\n * @returns Compressed data with original (uncompressed) size\n */\n async compress(\n events: any[],\n segmentId: number,\n ): Promise<{ compressed: Uint8Array; originalSize: number }> {\n if (this.useMainThread || !this.worker) {\n return this.compressMainThread(events);\n }\n\n return new Promise((resolve, reject) => {\n const entry: PendingCompression = { resolve, reject, events };\n this.pendingCallbacks.set(segmentId, entry);\n this.worker!.postMessage({ events, segmentId });\n });\n }\n\n /**\n * Main-thread fallback using fflate gzipSync.\n * Used when Web Worker is unavailable (CSP) or CompressionStream is missing.\n */\n private async compressMainThread(\n events: any[],\n ): Promise<{ compressed: Uint8Array; originalSize: number }> {\n const json = JSON.stringify(events);\n const encoded = new TextEncoder().encode(json);\n const compressed = gzipSync(encoded, { level: 6 });\n return { compressed, originalSize: encoded.length };\n }\n\n /**\n * Terminate the worker and clean up pending callbacks.\n */\n destroy(): void {\n if (this.worker) {\n this.worker.terminate();\n this.worker = null;\n }\n this.pendingCallbacks.clear();\n }\n}\n","/**\n * TraceKit Replay - Web Worker Compression Script\n * @package @tracekit/replay\n *\n * Inline Web Worker script string using native CompressionStream API.\n * No external dependencies inside the worker -- CompressionStream is\n * baseline available in workers since 2023.\n *\n * If CompressionStream is unavailable, the worker posts an error back\n * and the main thread (CompressionWorker) falls back to fflate gzipSync.\n *\n * Message protocol:\n * IN: { events: any[], segmentId: number }\n * OUT: { compressed: Uint8Array, segmentId: number, originalSize: number }\n * ERR: { error: string, segmentId: number }\n */\n\nexport const WORKER_SCRIPT = `\nself.onmessage = function(e) {\n try {\n var data = e.data;\n var json = JSON.stringify(data.events);\n var encoded = new TextEncoder().encode(json);\n\n if (typeof CompressionStream === 'undefined') {\n self.postMessage({ error: 'CompressionStream not available', segmentId: data.segmentId });\n return;\n }\n\n var cs = new CompressionStream('gzip');\n var writer = cs.writable.getWriter();\n var reader = cs.readable.getReader();\n var chunks = [];\n\n writer.write(encoded);\n writer.close();\n\n function readChunks() {\n reader.read().then(function(result) {\n if (result.done) {\n var totalLen = 0;\n for (var i = 0; i < chunks.length; i++) totalLen += chunks[i].length;\n var compressed = new Uint8Array(totalLen);\n var offset = 0;\n for (var i = 0; i < chunks.length; i++) {\n compressed.set(chunks[i], offset);\n offset += chunks[i].length;\n }\n self.postMessage(\n { compressed: compressed, segmentId: data.segmentId, originalSize: encoded.length },\n [compressed.buffer]\n );\n } else {\n chunks.push(result.value);\n readChunks();\n }\n }).catch(function(err) {\n self.postMessage({ error: err.message, segmentId: data.segmentId });\n });\n }\n readChunks();\n } catch(err) {\n self.postMessage({ error: (err && err.message) || 'Unknown compression error', segmentId: e.data.segmentId });\n }\n};\n`;\n","/**\n * TraceKit Replay - Transport Layer\n * @package @tracekit/replay\n *\n * Uploads compressed replay chunks to the server every 30 seconds.\n * Uses fetch with X-API-Key header for normal uploads, sendBeacon\n * with query-parameter API key for tab-close fallback.\n *\n * Retry: exponential backoff (1s, 2s, 4s) with max 3 attempts.\n * Non-retryable status codes: 400, 401, 413.\n * On final failure: drop the chunk (replay data loss is non-critical).\n *\n * SAFETY: All external calls (fetch, sendBeacon) are wrapped in try/catch.\n * The transport NEVER throws -- replay must never crash the host app.\n */\n\nimport type { ResolvedReplayConfig } from './types';\nimport { gzipSync } from 'fflate';\nimport { CompressionWorker } from './compression';\n\n// Retry delays in milliseconds: 1s, 2s, 4s\nconst RETRY_DELAYS = [1000, 2000, 4000];\nconst MAX_ATTEMPTS = 3;\n\n// sendBeacon has a ~64KB payload limit\nconst BEACON_SIZE_LIMIT = 65536;\n\nexport class ReplayTransport {\n private pendingEvents: any[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private config: ResolvedReplayConfig;\n private compressionWorker: CompressionWorker;\n private bufferSize = 0;\n\n // Getter functions wired by integration layer\n private sessionIdFn: (() => string) | null = null;\n private segmentIdFn: (() => number) | null = null;\n private replayTypeFn: (() => string) | null = null;\n\n // Visibility change handler (stored for cleanup)\n private visibilityHandler: (() => void) | null = null;\n\n constructor(config: ResolvedReplayConfig, compressionWorker: CompressionWorker) {\n this.config = config;\n this.compressionWorker = compressionWorker;\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /**\n * Start the transport: begin 30-second flush interval and register\n * visibilitychange listener for sendBeacon fallback on tab close.\n */\n start(\n getSessionId: () => string,\n nextSegmentId: () => number,\n getReplayType: () => string,\n ): void {\n this.sessionIdFn = getSessionId;\n this.segmentIdFn = nextSegmentId;\n this.replayTypeFn = getReplayType;\n\n // Start periodic flush at config.flushInterval (default 30s)\n this.flushTimer = setInterval(() => {\n this.flush().catch(() => {\n // Never crash -- flush errors are swallowed\n });\n }, this.config.flushInterval);\n\n // Register sendBeacon fallback for tab close\n if (typeof document !== 'undefined') {\n this.visibilityHandler = () => {\n if (document.visibilityState === 'hidden') {\n this.flushSync();\n }\n };\n document.addEventListener('visibilitychange', this.visibilityHandler);\n }\n }\n\n /**\n * Stop the transport: clear flush interval and remove listeners.\n */\n stop(): void {\n if (this.flushTimer !== null) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n if (this.visibilityHandler && typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.visibilityHandler);\n this.visibilityHandler = null;\n }\n }\n\n /**\n * Destroy the transport: stop + clear all pending events.\n */\n destroy(): void {\n this.stop();\n this.pendingEvents = [];\n this.bufferSize = 0;\n }\n\n // ---------------------------------------------------------------------------\n // Event accumulation\n // ---------------------------------------------------------------------------\n\n /**\n * Add a single rrweb event to the pending buffer.\n * If buffer exceeds maxBufferSize, oldest events are dropped.\n */\n addEvent(event: any): void {\n try {\n const eventSize = JSON.stringify(event).length;\n this.pendingEvents.push(event);\n this.bufferSize += eventSize;\n\n // Drop oldest events if buffer exceeds max size\n while (this.bufferSize > this.config.maxBufferSize && this.pendingEvents.length > 1) {\n const dropped = this.pendingEvents.shift();\n if (dropped) {\n try {\n this.bufferSize -= JSON.stringify(dropped).length;\n } catch {\n // Ignore sizing errors on drop\n }\n }\n if (this.config.maxBufferSize > 0) {\n console.warn('[TraceKit Replay] Buffer exceeded maxBufferSize, dropping oldest events');\n }\n }\n } catch {\n // Never crash the host app\n }\n }\n\n /**\n * Add multiple rrweb events at once (used for ring buffer flush-on-error).\n */\n addEvents(events: any[]): void {\n for (const event of events) {\n this.addEvent(event);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Flush (async -- normal upload path)\n // ---------------------------------------------------------------------------\n\n /**\n * Flush pending events: compress via worker and upload via fetch with retry.\n * Returns silently if no events are pending.\n */\n async flush(): Promise<void> {\n if (this.pendingEvents.length === 0) {\n return;\n }\n\n // Swap buffer atomically\n const events = this.pendingEvents;\n this.pendingEvents = [];\n this.bufferSize = 0;\n\n try {\n const sessionId = this.sessionIdFn ? this.sessionIdFn() : '';\n const segmentId = this.segmentIdFn ? this.segmentIdFn() : 0;\n\n if (!sessionId) {\n return; // No session -- discard events\n }\n\n // Compress via worker (or main-thread fallback)\n const { compressed, originalSize } = await this.compressionWorker.compress(events, segmentId);\n\n // Upload with retry\n await this.uploadWithRetry(sessionId, segmentId, compressed, originalSize);\n } catch {\n // Drop chunk on any unexpected error -- replay data loss is acceptable\n }\n }\n\n // ---------------------------------------------------------------------------\n // Flush sync (sendBeacon fallback for tab close)\n // ---------------------------------------------------------------------------\n\n /**\n * Synchronous flush for tab close -- uses sendBeacon.\n * Compresses on main thread with fflate gzipSync (sendBeacon must be sync).\n * Falls back to fetch with keepalive:true if sendBeacon fails.\n * If both fail, data is lost (acceptable for replay).\n */\n flushSync(): void {\n if (this.pendingEvents.length === 0) {\n return;\n }\n\n try {\n const events = this.pendingEvents;\n this.pendingEvents = [];\n this.bufferSize = 0;\n\n const sessionId = this.sessionIdFn ? this.sessionIdFn() : '';\n const segmentId = this.segmentIdFn ? this.segmentIdFn() : 0;\n const replayType = this.replayTypeFn ? this.replayTypeFn() : 'session';\n\n if (!sessionId) {\n return;\n }\n\n // Compress on main thread (sync -- sendBeacon requires sync data)\n const json = JSON.stringify(events);\n const encoded = new TextEncoder().encode(json);\n const compressed = gzipSync(encoded, { level: 6 });\n\n // Build URL with query parameters (sendBeacon cannot set custom headers)\n const url =\n `${this.config.endpoint}/api/replays/${sessionId}/chunks` +\n `?api_key=${encodeURIComponent(this.config.apiKey)}` +\n `&segment_id=${segmentId}` +\n `&original_size=${encoded.length}` +\n `&replay_type=${replayType}`;\n\n // Cast through ArrayBuffer to satisfy TypeScript 5.x Uint8Array<ArrayBufferLike> vs BlobPart\n const blob = new Blob([compressed as unknown as BlobPart], { type: 'application/octet-stream' });\n\n // Try sendBeacon first (works during page unload)\n if (compressed.byteLength <= BEACON_SIZE_LIMIT && typeof navigator !== 'undefined' && navigator.sendBeacon) {\n const sent = navigator.sendBeacon(url, blob);\n if (sent) {\n return;\n }\n }\n\n // Fallback: fetch with keepalive (also has ~64KB limit but is the standard approach)\n if (typeof fetch !== 'undefined') {\n fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n },\n body: blob,\n keepalive: true,\n }).catch(() => {\n // Data loss accepted -- replay is non-critical\n });\n }\n } catch {\n // Never crash the host app on tab close\n }\n }\n\n // ---------------------------------------------------------------------------\n // Upload with retry (exponential backoff)\n // ---------------------------------------------------------------------------\n\n /**\n * Upload compressed chunk via fetch with exponential backoff retry.\n * Retries up to 3 times with delays of 1s, 2s, 4s.\n * Non-retryable status codes (400, 401, 413) abort immediately.\n * On final failure: drop chunk silently.\n */\n private async uploadWithRetry(\n sessionId: string,\n segmentId: number,\n compressed: Uint8Array,\n originalSize: number,\n ): Promise<void> {\n const url = `${this.config.endpoint}/api/replays/${sessionId}/chunks`;\n const replayType = this.replayTypeFn ? this.replayTypeFn() : 'session';\n\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/octet-stream',\n 'X-API-Key': this.config.apiKey,\n 'X-Segment-Id': String(segmentId),\n 'X-Original-Size': String(originalSize),\n 'X-Replay-Type': replayType,\n },\n body: compressed as unknown as BodyInit,\n keepalive: true,\n });\n\n if (response.ok) {\n return; // Success\n }\n\n // Non-retryable status codes -- abort immediately\n if (response.status === 400 || response.status === 401 || response.status === 413) {\n return;\n }\n\n // Retryable failure -- wait before next attempt\n if (attempt < MAX_ATTEMPTS - 1) {\n await this.delay(RETRY_DELAYS[attempt]);\n }\n } catch {\n // Network error -- wait before retry\n if (attempt < MAX_ATTEMPTS - 1) {\n await this.delay(RETRY_DELAYS[attempt]);\n }\n }\n }\n\n // All attempts exhausted -- drop chunk (replay data loss is acceptable)\n }\n\n /**\n * Promise-based delay for retry backoff.\n */\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n"],"mappings":"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","names":["DEFAULTS","clampRate","value","name","min","max","resolveReplayConfig","config","apiKey","endpoint","sessionSampleRate","errorSampleRate","record","record","createMaskTextFn","unmaskSelectors","selectorParts","combinedSelector","text","element","startRecording","config","onEvent","event","isCheckout","RingBuffer","maxAgeMs","event","flushed","e","cutoff","generateSessionId","decideSamplingMode","config","rand","SessionManager","RingBuffer","mode","now","event","_isCheckout","events","cb","gzipSync","WORKER_SCRIPT","CompressionWorker","blob","WORKER_SCRIPT","url","e","compressed","segmentId","originalSize","error","pending","events","resolve","reject","entry","json","encoded","gzipSync","gzipSync","RETRY_DELAYS","MAX_ATTEMPTS","BEACON_SIZE_LIMIT","ReplayTransport","config","compressionWorker","getSessionId","nextSegmentId","getReplayType","event","eventSize","dropped","events","sessionId","segmentId","compressed","originalSize","replayType","json","encoded","url","blob","attempt","response","ms","resolve","replayIntegration","config","session","compressionWorker","transport","stopRecording","resolvedConfig","client","clientConfig","resolveReplayConfig","SessionManager","CompressionWorker","ReplayTransport","events","event","originalCaptureException","error","context","replayId","scope","result","crumb","record","payload","startRecording","isCheckout","err"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tracekit/replay",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "TraceKit Session Replay - Privacy-first session recording with rrweb",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",