@faststats/web 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/analytics-p8pLmlA-.js +1 -0
- package/dist/error-CMM8PYcW.js +3 -0
- package/dist/module.d.ts +11 -15
- package/dist/module.js +1 -1
- package/dist/replay-DeRZXYG3.js +1 -0
- package/dist/types-B5zbhOWK.js +1 -0
- package/dist/web-vitals-DU_i1Ejx.js +1 -0
- package/package.json +6 -3
- package/src/analytics.ts +10 -40
- package/src/error.ts +13 -17
- package/src/replay.ts +25 -24
- package/src/utils/identifiers.ts +8 -1
- package/src/web-vitals.ts +2 -4
- package/dist/error-DtTBFJh_.js +0 -3
- package/dist/replay-ZmimkIBT.js +0 -1
- package/dist/types-E07i1PxI.js +0 -1
- package/dist/web-vitals-CUXDm9A3.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as e,c as t,i as n,n as r,o as i,r as a,s as o,t as s}from"./types-B5zbhOWK.js";let c=null;function l(){return c}function u(e,t){typeof window>`u`||g()||c?.track(e,t??{})}function d(e,t,n){typeof window>`u`||g()||c?.identify(e,t,n??{})}function f(e=!0){typeof window>`u`||g()||c?.logout(e)}function p(e){c?.setConsentMode(e)}function m(){p(`granted`)}function h(){p(`denied`)}function g(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function _(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`}=e,s=n instanceof Blob?n:new Blob([n],{type:r});if(navigator.sendBeacon?.(t,s))return a&&console.log(`${o} ✓ Sent via beacon`),!0;try{let e=await fetch(t,{method:`POST`,body:n,headers:{"Content-Type":r,...i},keepalive:!0}),s=e.ok;return a&&(s?console.log(`${o} ✓ Sent via fetch`):console.warn(`${o} ✗ Failed: ${e.status}`)),s}catch{return a&&console.warn(`${o} ✗ Failed to send`),!1}}function v(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function y(){let e={};if(!location.search)return e;let t=new URLSearchParams(location.search);for(let n of[`utm_source`,`utm_medium`,`utm_campaign`,`utm_term`,`utm_content`]){let r=t.get(n);r&&(e[n]=r)}return e}var b=class{endpoint;debug;started=!1;pageKey=``;navTimer=null;heartbeatTimer=null;scrollDepth=0;pageEntryTime=0;pagePath=``;pageUrl=``;pageHash=``;hasLeftCurrentPage=!1;scrollHandler=null;consentMode;cookielessWhilePending;constructor(e){this.options=e,this.endpoint=e.endpoint??`https://metrics.faststats.dev/v1/web`,this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(g()){this.log(`disabled`);return}c=this,setTimeout(()=>void this.start(),0)}}async start(){if(this.started||typeof window>`u`)return;if(c&&c!==this){this.log(`already started by another instance`);return}if(g()){this.log(`disabled`);return}this.started=!0,c=this,t(this.isCookielessMode());let e=this.options;if(e.errorTracking?.enabled??e.trackErrors){let{default:t}=await import(`./error-CMM8PYcW.js`);new t({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug}).start(),this.log(`error loaded`)}if(e.trackWebVitals){let{default:t}=await import(`./web-vitals-DU_i1Ejx.js`);new t({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,samplingPercentage:s(e.webVitals?.sampling?.percentage)}).start(),this.log(`web-vitals loaded`)}if(e.trackReplay){let{default:t}=await import(`./replay-DeRZXYG3.js`);new t({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,...e.replayOptions}).start(),this.log(`replay loaded`)}this.enterPage(),this.pageview({trigger:`load`}),this.links(),this.trackScroll(),this.startHeartbeat(),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`?this.leavePage():this.startHeartbeat()}),window.addEventListener(`pagehide`,()=>this.leavePage()),window.addEventListener(`popstate`,()=>this.navigate()),e.trackHash&&window.addEventListener(`hashchange`,()=>this.navigate()),this.patch()}pageview(e={}){let t=`${location.pathname}|${this.options.trackHash??!1?location.hash:``}`;t!==this.pageKey&&(this.pageKey=t,this.send(`pageview`,e))}track(e,t={}){this.send(e,t)}identify(e,t,n={}){if(g()||this.isCookielessMode())return;let i=e.trim(),a=t.trim();!i||!a||_({url:this.endpoint.replace(/\/v1\/web$/,`/v1/identify`),data:JSON.stringify({token:this.options.siteKey,identifier:r(!1),externalId:i,email:a,name:n.name?.trim()||void 0,phone:n.phone?.trim()||void 0,avatarUrl:n.avatarUrl?.trim()||void 0,traits:n.traits??{}}),contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] identify`})}logout(e=!0){g()||(e&&i(this.isCookielessMode()),o())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return r(this.isCookielessMode())}getSessionId(){return a()}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,t={}){let n=r(this.isCookielessMode()),i=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:a(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...y(),...t}});this.log(e),_({url:this.endpoint,data:i,contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] ${e}`})}enterPage(){this.pageEntryTime=Date.now(),this.pagePath=location.pathname,this.pageUrl=location.href,this.pageHash=location.hash,this.scrollDepth=0,this.hasLeftCurrentPage=!1}leavePage(){if(this.hasLeftCurrentPage)return;this.hasLeftCurrentPage=!0;let e=Date.now();this.send(`page_leave`,{page:this.pagePath,url:this.pageUrl,time_on_page:e-this.pageEntryTime,scroll_depth:this.scrollDepth,session_duration:e-n()})}trackScroll(){this.scrollHandler&&window.removeEventListener(`scroll`,this.scrollHandler);let e=()=>{let e=document.documentElement,t=document.body,n=window.innerHeight,r=Math.max(e.scrollHeight,t.scrollHeight);if(r<=n){this.scrollDepth=100;return}let i=Math.min(100,Math.round(((window.scrollY||e.scrollTop)+n)/r*100));i>this.scrollDepth&&(this.scrollDepth=i)};this.scrollHandler=e,e(),window.addEventListener(`scroll`,e,{passive:!0})}startHeartbeat(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval(()=>{if(document.visibilityState===`hidden`){clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;return}e()},300*1e3)}navigate(){this.navTimer&&clearTimeout(this.navTimer),this.navTimer=setTimeout(()=>{this.navTimer=null;let e=location.pathname!==this.pagePath,t=(this.options.trackHash??!1)&&location.hash!==this.pageHash;!e&&!t||(this.leavePage(),this.enterPage(),this.trackScroll(),this.pageview({trigger:`navigation`}))},300)}patch(){let e=()=>this.navigate();for(let t of[`pushState`,`replaceState`]){let n=history[t];history[t]=function(...t){let r=n.apply(this,t);return e(),r}}}links(){let e=e=>{let t=v(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};document.addEventListener(`click`,e),document.addEventListener(`auxclick`,e)}};export{f as a,_ as c,g as i,p as l,l as n,m as o,d as r,h as s,b as t,u};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{n as e,r as t}from"./types-B5zbhOWK.js";import{c as n}from"./analytics-p8pLmlA-.js";var r=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;handledErrors=new WeakSet;errorCounts=new Map;flushTimer=null;started=!1;constructor(e){this.siteKey=e.siteKey,this.endpoint=e.endpoint??`https://metrics.faststats.dev/v1/web`,this.debug=e.debug??!1,this.flushInterval=e.flushInterval??5e3,this.maxQueueSize=e.maxQueueSize??50}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.handleErrorEvent),window.addEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer=setInterval(()=>this.flush(),this.flushInterval),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.flush()}),window.addEventListener(`pagehide`,()=>this.flush()),this.debug&&console.log(`[ErrorTracker] Started listening for errors`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.handleErrorEvent),window.removeEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush(),this.debug&&console.log(`[ErrorTracker] Stopped listening for errors`))}handleErrorEvent=e=>{let t=e.error;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate error:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:e.message||(t?.message??`Unknown error`),filename:e.filename||void 0,lineno:e.lineno||void 0,colno:e.colno||void 0,stack:t?.stack||void 0,type:`error`})};handleRejection=e=>{let t=e.reason;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate rejection:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:t instanceof Error?t.message:typeof t==`string`?t:`Unhandled promise rejection`,stack:t instanceof Error?t.stack:void 0,type:`unhandledrejection`})};isExtensionError(e){return!!(e.filename?.startsWith(`chrome-extension://`)||e.stack&&e.stack.split(`
|
|
2
|
+
`).find(e=>e.trim().startsWith(`at `))?.includes(`chrome-extension://`))}parseStack(e){if(e)return e.split(`
|
|
3
|
+
`).map(e=>e.trim()).filter(e=>e.length>0)}async generateErrorHash(e){let t=[e.type,e.message,e.filename??``,e.lineno??``].join(`:`),n=new TextEncoder().encode(t),r=await crypto.subtle.digest(`SHA-256`,n);return`err_${Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}`}async queueError(e){if(this.isExtensionError(e))return;this.debug&&console.log(`[ErrorTracker] Captured error:`,e);let t=await this.generateErrorHash(e),n=this.errorCounts.get(t);if(n)n.count++,this.debug&&console.log(`[ErrorTracker] Incremented count for ${t} to ${n.count}`);else{let n={error:e.type===`unhandledrejection`?`UnhandledRejection`:`Error`,message:e.message,stack:this.parseStack(e.stack)};this.errorCounts.set(t,{entry:n,hash:t,count:1}),this.debug&&console.log(`[ErrorTracker] Queued new error: ${t}`)}this.errorCounts.size>=this.maxQueueSize&&this.flush()}captureError(e){if(this.handledErrors.has(e)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate manual capture:`,e.message);return}this.handledErrors.add(e),this.queueError({message:e.message,stack:e.stack,type:`error`})}flush(){if(this.errorCounts.size===0)return;let r=[];for(let{entry:e,hash:t,count:n}of this.errorCounts.values())r.push({...e,hash:t,count:n});this.errorCounts.clear(),this.debug&&console.log(`[ErrorTracker] Flushing errors:`,r);let i=e(),a=typeof document<`u`?{url:location.href,page:location.pathname,referrer:document.referrer||null}:{url:``,page:``,referrer:null},o=globalThis.__SOURCEMAPS_BUILD__,s=typeof o?.buildId==`string`&&o.buildId.trim().length>0?o.buildId:void 0,c={token:this.siteKey,...i?{userId:i}:{},sessionId:t(),...s?{buildId:s}:{},data:{url:a.url,page:a.page,referrer:a.referrer,title:typeof document<`u`?document.title:``},errors:r},l=JSON.stringify(c);this.debug&&console.log(`[ErrorTracker] Payload:`,l),n({url:this.endpoint,data:l,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{r as default};
|
package/dist/module.d.ts
CHANGED
|
@@ -352,16 +352,6 @@ declare global {
|
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
//#endregion
|
|
355
|
-
//#region src/utils/types.d.ts
|
|
356
|
-
interface SendDataOptions {
|
|
357
|
-
url: string;
|
|
358
|
-
data: string | Blob;
|
|
359
|
-
contentType?: string;
|
|
360
|
-
headers?: Record<string, string>;
|
|
361
|
-
debug?: boolean;
|
|
362
|
-
debugPrefix?: string;
|
|
363
|
-
}
|
|
364
|
-
//#endregion
|
|
365
355
|
//#region src/replay.d.ts
|
|
366
356
|
interface ReplayTrackerOptions {
|
|
367
357
|
siteKey: string;
|
|
@@ -370,6 +360,7 @@ interface ReplayTrackerOptions {
|
|
|
370
360
|
samplingPercentage?: number;
|
|
371
361
|
flushInterval?: number;
|
|
372
362
|
maxEvents?: number;
|
|
363
|
+
minReplayLengthMs?: number;
|
|
373
364
|
sampling?: recordOptions<eventWithTime>["sampling"];
|
|
374
365
|
slimDOMOptions?: SlimDOMOptions;
|
|
375
366
|
maskAllInputs?: boolean;
|
|
@@ -381,9 +372,16 @@ interface ReplayTrackerOptions {
|
|
|
381
372
|
checkoutEveryNms?: number;
|
|
382
373
|
checkoutEveryNth?: number;
|
|
383
374
|
recordConsole?: boolean;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/utils/types.d.ts
|
|
378
|
+
interface SendDataOptions {
|
|
379
|
+
url: string;
|
|
380
|
+
data: string | Blob;
|
|
381
|
+
contentType?: string;
|
|
382
|
+
headers?: Record<string, string>;
|
|
383
|
+
debug?: boolean;
|
|
384
|
+
debugPrefix?: string;
|
|
387
385
|
}
|
|
388
386
|
//#endregion
|
|
389
387
|
//#region src/analytics.d.ts
|
|
@@ -404,11 +402,9 @@ interface ErrorTrackingConfig {
|
|
|
404
402
|
enabled?: boolean;
|
|
405
403
|
}
|
|
406
404
|
interface WebVitalsConfig {
|
|
407
|
-
enabled?: boolean;
|
|
408
405
|
sampling?: SamplingOptions;
|
|
409
406
|
}
|
|
410
407
|
interface SessionReplayConfig {
|
|
411
|
-
enabled?: boolean;
|
|
412
408
|
sampling?: SamplingOptions;
|
|
413
409
|
}
|
|
414
410
|
type ConsentMode = "pending" | "granted" | "denied";
|
package/dist/module.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import"./types-B5zbhOWK.js";import{a as e,c as t,i as n,l as r,n as i,o as a,r as o,s,t as c,u as l}from"./analytics-p8pLmlA-.js";export{c as WebAnalytics,i as getInstance,o as identify,n as isTrackingDisabled,e as logout,a as optIn,s as optOut,t as sendData,r as setConsentMode,l as trackEvent};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as e,r as t,t as n}from"./types-B5zbhOWK.js";import{c as r}from"./analytics-p8pLmlA-.js";import{getRecordConsolePlugin as i}from"@rrweb/rrweb-plugin-console-record";import{record as a}from"rrweb";var o=class{endpoint;siteKey;debug;flushInterval;maxEvents;sampling;slimDOMOptions;maskAllInputs;maskInputOptions;blockClass;blockSelector;maskTextClass;maskTextSelector;checkoutEveryNms;checkoutEveryNth;samplingPercentage;recordConsole;events=[];flushTimer=null;stopRecording=void 0;started=!1;startTime=0;minLengthFlushScheduled=!1;sequenceNumber=0;pendingBatches=[];isFlushing=!1;compressionSupported=!1;sessionSamplingSeed;minReplayLengthMs;constructor(e){this.siteKey=e.siteKey,this.endpoint=e.endpoint?.replace(/\/v1\/web$/,`/v1/replay`)??`https://metrics.faststats.dev/v1/replay`,this.debug=e.debug??!1,this.samplingPercentage=n(e.samplingPercentage),this.flushInterval=e.flushInterval??1e4,this.maxEvents=e.maxEvents??500,this.sampling=e.sampling??{mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},this.slimDOMOptions=e.slimDOMOptions??{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0},this.maskAllInputs=e.maskAllInputs??!0,this.maskInputOptions=e.maskInputOptions??{password:!0,email:!0,tel:!0},this.blockClass=e.blockClass,this.blockSelector=e.blockSelector,this.maskTextClass=e.maskTextClass,this.maskTextSelector=e.maskTextSelector,this.checkoutEveryNms=e.checkoutEveryNms??6e4,this.checkoutEveryNth=e.checkoutEveryNth,this.recordConsole=e.recordConsole??!0,this.minReplayLengthMs=e.minReplayLengthMs??3e3,this.sessionSamplingSeed=Math.random()*100,typeof window<`u`&&(this.compressionSupported=`CompressionStream`in window)}start(){if(this.started||typeof window>`u`||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;this.started=!0,this.startTime=Date.now(),this.debug&&console.log(`[Replay] Recording started`);let e={emit:(e,t)=>this.handleEvent(e,t),sampling:this.sampling,slimDOMOptions:this.slimDOMOptions,maskAllInputs:this.maskAllInputs,checkoutEveryNms:this.checkoutEveryNms};this.maskInputOptions&&(e.maskInputOptions=this.maskInputOptions),this.blockClass&&(e.blockClass=this.blockClass),this.blockSelector&&(e.blockSelector=this.blockSelector),this.maskTextClass&&(e.maskTextClass=this.maskTextClass),this.maskTextSelector&&(e.maskTextSelector=this.maskTextSelector),this.checkoutEveryNth&&(e.checkoutEveryNth=this.checkoutEveryNth),this.recordConsole&&(e.plugins=[i()]),this.stopRecording=a(e),this.flushTimer=setInterval(()=>{this.scheduleFlush()},this.flushInterval),window.addEventListener(`beforeunload`,this.handleUnload),window.addEventListener(`pagehide`,this.handleUnload),document.addEventListener(`visibilitychange`,this.handleVisibilityChange)}stop(){if(this.started){if(this.started=!1,this.debug&&console.log(`[Replay] Recording stopped`),this.stopRecording?.(),this.stopRecording=void 0,this.flushTimer&&=(clearInterval(this.flushTimer),null),window.removeEventListener(`beforeunload`,this.handleUnload),window.removeEventListener(`pagehide`,this.handleUnload),document.removeEventListener(`visibilitychange`,this.handleVisibilityChange),!this.hasReachedMinLength()){this.events=[],this.debug&&console.log(`[Replay] Session too short (${Date.now()-this.startTime}ms), discarding events`);return}this.flush()}}handleEvent(e,t){this.events.push(e),t||this.events.length>=this.maxEvents?this.scheduleFlush():!this.minLengthFlushScheduled&&this.hasReachedMinLength()&&(this.minLengthFlushScheduled=!0,this.scheduleFlush())}hasReachedMinLength(){return this.minReplayLengthMs<=0?!0:Date.now()-this.startTime>=this.minReplayLengthMs}scheduleFlush(){this.isFlushing||this.events.length===0||(typeof window<`u`&&`requestIdleCallback`in window?window.requestIdleCallback(()=>this.flush(),{timeout:2e3}):setTimeout(()=>this.flush(),0))}handleUnload=()=>{this.flush()};handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush()):document.visibilityState===`visible`&&this.started&&(this.flushTimer||=setInterval(()=>{this.scheduleFlush()},this.flushInterval))};async flush(){if(this.events.length===0){this.processPendingBatches();return}if(!this.hasReachedMinLength()){this.debug&&console.log(`[Replay] Too short (${Date.now()-this.startTime}ms), skipping`),this.processPendingBatches();return}if(this.isFlushing)return;this.isFlushing=!0;let n=this.events;this.events=[];let r=e(),i={token:this.siteKey,sessionId:t(),...r?{identifier:r}:{},sequence:this.sequenceNumber++,timestamp:Date.now(),url:window.location.href,events:n};this.debug&&console.log(`[Replay] Sending ${n.length} events (seq: ${i.sequence})`);try{let e,t=!1;if(this.compressionSupported)try{e=await this.compress(JSON.stringify(i)),t=!0}catch{this.debug&&console.warn(`[Replay] Compression failed, using uncompressed`),e=new Blob([JSON.stringify(i)],{type:`application/json`})}else e=new Blob([JSON.stringify(i)],{type:`application/json`});await this.send(e,t)||this.pendingBatches.push({batch:i,isCompressed:t,retries:0})}catch(e){this.debug&&console.warn(`[Replay] Flush error:`,e),this.pendingBatches.push({batch:i,isCompressed:!1,retries:0})}finally{this.isFlushing=!1,this.processPendingBatches()}}async processPendingBatches(){if(this.pendingBatches.length===0)return;let e=this.pendingBatches.shift();if(e){if(e.retries>=3){this.debug&&console.warn(`[Replay] Max retries reached, dropping batch ${e.batch.sequence}`),this.processPendingBatches();return}e.retries++;try{let t;if(e.isCompressed&&this.compressionSupported)try{t=await this.compress(JSON.stringify(e.batch))}catch{t=new Blob([JSON.stringify(e.batch)],{type:`application/json`}),e.isCompressed=!1}else t=new Blob([JSON.stringify(e.batch)],{type:`application/json`});await this.send(t,e.isCompressed)?this.processPendingBatches():(this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries))}catch{this.debug&&console.warn(`[Replay] Retry ${e.retries} failed`),this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries)}}}async compress(e){if(!this.compressionSupported)throw Error(`Compression not supported`);let t=new TextEncoder().encode(e),n=new CompressionStream(`gzip`),r=n.writable.getWriter();r.write(t),r.close();let i=[],a=n.readable.getReader();for(;;){let{done:e,value:t}=await a.read();if(e)break;t&&i.push(t)}let o=i.reduce((e,t)=>e+t.length,0),s=new Uint8Array(o),c=0;for(let e of i)s.set(e,c),c+=e.length;if(this.debug){let e=(s.length/t.length*100).toFixed(1);console.log(`[Replay] Compressed: ${t.length} → ${s.length} bytes (${e}%)`)}return new Blob([s],{type:`application/octet-stream`})}async send(e,t){let n=t?`${this.endpoint}?encoding=gzip`:this.endpoint,i=(e.size/1024).toFixed(1);return r({url:n,data:e,contentType:t?`application/octet-stream`:`application/json`,headers:t?{"Content-Encoding":`gzip`}:void 0,debug:this.debug,debugPrefix:`[Replay] ${i}KB`})??Promise.resolve(!1)}getSessionId(){return t()}};export{o as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
let e=!1;function t(t){e=t}function n(t){return t??e?``:r()}function r(){if(typeof localStorage>`u`)return``;let e=localStorage.getItem(`faststats_anon_id`);if(e)return e;let t=crypto.randomUUID();return localStorage.setItem(`faststats_anon_id`,t),t}function i(e){return e||typeof localStorage>`u`?``:(localStorage.removeItem(`faststats_anon_id`),r())}function a(){if(typeof sessionStorage>`u`)return``;let e=sessionStorage.getItem(`session_id`),t=sessionStorage.getItem(`session_timestamp`);if(e&&t){if(Date.now()-Number.parseInt(t,10)<18e5)return sessionStorage.setItem(`session_timestamp`,Date.now().toString()),e;sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`)}let n=Date.now().toString(),r=crypto.randomUUID();return sessionStorage.setItem(`session_id`,r),sessionStorage.setItem(`session_timestamp`,n),sessionStorage.setItem(`session_start`,n),r}function o(){return typeof sessionStorage>`u`?``:(sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`),a())}function s(){typeof sessionStorage>`u`||sessionStorage.getItem(`session_id`)&&sessionStorage.setItem(`session_timestamp`,Date.now().toString())}function c(){if(typeof sessionStorage>`u`)return Date.now();let e=sessionStorage.getItem(`session_start`);if(e)return Number.parseInt(e,10);let t=sessionStorage.getItem(`session_timestamp`);return t?Number.parseInt(t,10):Date.now()}function l(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}export{s as a,t as c,c as i,n,i as o,a as r,o as s,l as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as e,t}from"./types-B5zbhOWK.js";import{onCLS as n,onFCP as r,onINP as i,onLCP as a,onTTFB as o}from"web-vitals/attribution";var s=class{endpoint;siteKey;debug;samplingPercentage;started=!1;sessionSamplingSeed;metricsMap=new Map;flushed=!1;constructor(e){this.siteKey=e.siteKey,this.endpoint=this.getVitalsEndpoint(e.endpoint??`https://metrics.faststats.dev/v1/web`),this.debug=e.debug??!1,this.samplingPercentage=t(e.samplingPercentage),this.sessionSamplingSeed=Math.random()*100}getVitalsEndpoint(e){let t=new URL(e),n=t.pathname.split(`/`);return n[n.length-1]=`vitals`,t.pathname=n.join(`/`),t.toString()}start(){this.started||typeof window>`u`||(this.started=!0,document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.finalizeAndFlush()}),window.addEventListener(`pagehide`,()=>{this.finalizeAndFlush()}),this.debug&&console.log(`[WebVitals] Tracking started`),n(e=>this.captureMetric(e)),i(e=>this.captureMetric(e)),a(e=>this.captureMetric(e)),r(e=>this.captureMetric(e)),o(e=>this.captureMetric(e)))}captureMetric(e){if(this.flushed||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;let t=e.name,n={id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...e.attribution??{}},r=t===`FCP`||t===`TTFB`;this.metricsMap.set(t,{value:e.value,attributes:n,final:r}),this.debug&&console.log(`[WebVitals] ${t} captured: ${e.value}`+(r?` (final)`:``))}finalizeAndFlush(){if(!(this.flushed||this.metricsMap.size===0)){for(let[e,t]of this.metricsMap.entries())t.final||(t.final=!0,this.debug&&console.log(`[WebVitals] ${e} finalized: ${t.value}`));this.flushWithBeacon()}}buildPayload(){if(this.metricsMap.size===0)return null;let t=Array.from(this.metricsMap.entries()).map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes}));return{body:JSON.stringify({sessionId:e(),vitals:t,metadata:{url:window.location.href}}),count:t.length}}flushWithBeacon(){let e=this.buildPayload();if(e){if(this.flushed=!0,this.debug){let t=Array.from(this.metricsMap.keys()).join(`, `);console.log(`[WebVitals] Sending final metrics (${e.count}): ${t}`)}fetch(this.endpoint,{method:`POST`,body:e.body,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.siteKey}`},keepalive:!0}).catch(()=>{this.debug&&console.warn(`[WebVitals] Failed to send metrics`)})}}};export{s as default};
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststats/web",
|
|
3
|
-
"repository":
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "https://github.com/faststats-dev/web-analytics.git"
|
|
6
|
+
},
|
|
4
7
|
"main": "dist/module.js",
|
|
5
8
|
"module": "dist/module.js",
|
|
6
9
|
"types": "dist/module.d.ts",
|
|
@@ -12,11 +15,11 @@
|
|
|
12
15
|
}
|
|
13
16
|
},
|
|
14
17
|
"type": "module",
|
|
15
|
-
"sideEffects":
|
|
18
|
+
"sideEffects": true,
|
|
16
19
|
"publishConfig": {
|
|
17
20
|
"access": "public"
|
|
18
21
|
},
|
|
19
|
-
"version": "0.1.
|
|
22
|
+
"version": "0.1.2",
|
|
20
23
|
"scripts": {
|
|
21
24
|
"build": "tsdown",
|
|
22
25
|
"dev": "bun run build",
|
package/src/analytics.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
refreshSessionTimestamp,
|
|
7
7
|
resetAnonymousId,
|
|
8
8
|
resetSessionId,
|
|
9
|
+
setCookielessMode,
|
|
9
10
|
} from "./utils/identifiers";
|
|
10
11
|
import {
|
|
11
12
|
normalizeSamplingPercentage,
|
|
@@ -108,13 +109,11 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
function getLinkEl(el: Node | null): HTMLAnchorElement | null {
|
|
111
|
-
while (
|
|
112
|
-
el &&
|
|
113
|
-
!((el as Element).tagName === "A" && (el as HTMLAnchorElement).href)
|
|
114
|
-
) {
|
|
112
|
+
while (el) {
|
|
113
|
+
if (el instanceof HTMLAnchorElement && el.href) return el;
|
|
115
114
|
el = el.parentNode;
|
|
116
115
|
}
|
|
117
|
-
return
|
|
116
|
+
return null;
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
function getUTM(): Record<string, string> {
|
|
@@ -145,12 +144,10 @@ interface ErrorTrackingConfig {
|
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
interface WebVitalsConfig {
|
|
148
|
-
enabled?: boolean;
|
|
149
147
|
sampling?: SamplingOptions;
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
interface SessionReplayConfig {
|
|
153
|
-
enabled?: boolean;
|
|
154
151
|
sampling?: SamplingOptions;
|
|
155
152
|
}
|
|
156
153
|
|
|
@@ -222,9 +219,7 @@ export class WebAnalytics {
|
|
|
222
219
|
return;
|
|
223
220
|
}
|
|
224
221
|
_instance = this;
|
|
225
|
-
|
|
226
|
-
window.requestIdleCallback(() => void this.start());
|
|
227
|
-
else setTimeout(() => void this.start(), 1);
|
|
222
|
+
setTimeout(() => void this.start(), 0);
|
|
228
223
|
}
|
|
229
224
|
|
|
230
225
|
async start(): Promise<void> {
|
|
@@ -240,13 +235,8 @@ export class WebAnalytics {
|
|
|
240
235
|
this.started = true;
|
|
241
236
|
_instance = this;
|
|
242
237
|
|
|
243
|
-
|
|
238
|
+
setCookielessMode(this.isCookielessMode());
|
|
244
239
|
const opts = this.options;
|
|
245
|
-
const hasWebVitalsSampling =
|
|
246
|
-
opts.webVitals?.sampling?.percentage !== undefined;
|
|
247
|
-
const hasReplaySampling =
|
|
248
|
-
opts.replayOptions?.samplingPercentage !== undefined ||
|
|
249
|
-
opts.sessionReplays?.sampling?.percentage !== undefined;
|
|
250
240
|
|
|
251
241
|
if (opts.errorTracking?.enabled ?? opts.trackErrors) {
|
|
252
242
|
const { default: ErrorTracker } = await import("./error");
|
|
@@ -254,22 +244,10 @@ export class WebAnalytics {
|
|
|
254
244
|
siteKey: opts.siteKey,
|
|
255
245
|
endpoint: this.endpoint,
|
|
256
246
|
debug: this.debug,
|
|
257
|
-
getCommonData: () => ({
|
|
258
|
-
url: location.href,
|
|
259
|
-
page: location.pathname,
|
|
260
|
-
referrer: document.referrer || null,
|
|
261
|
-
}),
|
|
262
|
-
getAnonymousId: getAnonymousIdFn,
|
|
263
|
-
getSessionId: getOrCreateSessionId,
|
|
264
|
-
sendData,
|
|
265
247
|
}).start();
|
|
266
248
|
this.log("error loaded");
|
|
267
249
|
}
|
|
268
|
-
if (
|
|
269
|
-
opts.trackWebVitals ??
|
|
270
|
-
opts.webVitals?.enabled ??
|
|
271
|
-
hasWebVitalsSampling
|
|
272
|
-
) {
|
|
250
|
+
if (opts.trackWebVitals) {
|
|
273
251
|
const { default: WebVitalsTracker } = await import("./web-vitals");
|
|
274
252
|
new WebVitalsTracker({
|
|
275
253
|
siteKey: opts.siteKey,
|
|
@@ -278,25 +256,16 @@ export class WebAnalytics {
|
|
|
278
256
|
samplingPercentage: normalizeSamplingPercentage(
|
|
279
257
|
opts.webVitals?.sampling?.percentage,
|
|
280
258
|
),
|
|
281
|
-
getSessionId: getOrCreateSessionId,
|
|
282
259
|
}).start();
|
|
283
260
|
this.log("web-vitals loaded");
|
|
284
261
|
}
|
|
285
|
-
if (opts.
|
|
262
|
+
if (opts.trackReplay) {
|
|
286
263
|
const { default: ReplayTracker } = await import("./replay");
|
|
287
|
-
const replayOpts = opts.replayOptions ?? {};
|
|
288
264
|
new ReplayTracker({
|
|
289
265
|
siteKey: opts.siteKey,
|
|
290
266
|
endpoint: this.endpoint,
|
|
291
267
|
debug: this.debug,
|
|
292
|
-
...
|
|
293
|
-
samplingPercentage: normalizeSamplingPercentage(
|
|
294
|
-
replayOpts.samplingPercentage ??
|
|
295
|
-
opts.sessionReplays?.sampling?.percentage,
|
|
296
|
-
),
|
|
297
|
-
getAnonymousId: getAnonymousIdFn,
|
|
298
|
-
getSessionId: getOrCreateSessionId,
|
|
299
|
-
sendData,
|
|
268
|
+
...opts.replayOptions,
|
|
300
269
|
}).start();
|
|
301
270
|
this.log("replay loaded");
|
|
302
271
|
}
|
|
@@ -375,6 +344,7 @@ export class WebAnalytics {
|
|
|
375
344
|
|
|
376
345
|
setConsentMode(mode: ConsentMode): void {
|
|
377
346
|
this.consentMode = mode;
|
|
347
|
+
setCookielessMode(this.isCookielessMode());
|
|
378
348
|
}
|
|
379
349
|
|
|
380
350
|
optIn(): void {
|
package/src/error.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { sendData } from "./analytics";
|
|
2
|
+
import { getAnonymousId, getOrCreateSessionId } from "./utils/identifiers";
|
|
2
3
|
|
|
3
4
|
export type ErrorEntry = {
|
|
4
5
|
error: string;
|
|
@@ -27,10 +28,6 @@ export interface ErrorTrackingOptions {
|
|
|
27
28
|
debug?: boolean;
|
|
28
29
|
flushInterval?: number;
|
|
29
30
|
maxQueueSize?: number;
|
|
30
|
-
getCommonData?: () => Record<string, unknown>;
|
|
31
|
-
getAnonymousId?: () => string;
|
|
32
|
-
getSessionId?: () => string;
|
|
33
|
-
sendData?: (options: SendDataOptions) => Promise<boolean>;
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
class ErrorTracker {
|
|
@@ -39,10 +36,6 @@ class ErrorTracker {
|
|
|
39
36
|
private readonly debug: boolean;
|
|
40
37
|
private readonly flushInterval: number;
|
|
41
38
|
private readonly maxQueueSize: number;
|
|
42
|
-
private readonly getCommonData: () => Record<string, unknown>;
|
|
43
|
-
private readonly getAnonymousId: () => string;
|
|
44
|
-
private readonly getSessionId: () => string;
|
|
45
|
-
private readonly sendData: (options: SendDataOptions) => Promise<boolean>;
|
|
46
39
|
|
|
47
40
|
private readonly handledErrors = new WeakSet<Error>();
|
|
48
41
|
private readonly errorCounts = new Map<
|
|
@@ -58,10 +51,6 @@ class ErrorTracker {
|
|
|
58
51
|
this.debug = options.debug ?? false;
|
|
59
52
|
this.flushInterval = options.flushInterval ?? 5000;
|
|
60
53
|
this.maxQueueSize = options.maxQueueSize ?? 50;
|
|
61
|
-
this.getCommonData = options.getCommonData ?? (() => ({}));
|
|
62
|
-
this.getAnonymousId = options.getAnonymousId ?? (() => "");
|
|
63
|
-
this.getSessionId = options.getSessionId ?? (() => "");
|
|
64
|
-
this.sendData = options.sendData ?? (async () => false);
|
|
65
54
|
}
|
|
66
55
|
|
|
67
56
|
start(): void {
|
|
@@ -268,8 +257,15 @@ class ErrorTracker {
|
|
|
268
257
|
console.log("[ErrorTracker] Flushing errors:", errors);
|
|
269
258
|
}
|
|
270
259
|
|
|
271
|
-
const
|
|
272
|
-
const
|
|
260
|
+
const identifier = getAnonymousId();
|
|
261
|
+
const commonData =
|
|
262
|
+
typeof document !== "undefined"
|
|
263
|
+
? {
|
|
264
|
+
url: location.href,
|
|
265
|
+
page: location.pathname,
|
|
266
|
+
referrer: document.referrer || null,
|
|
267
|
+
}
|
|
268
|
+
: { url: "", page: "", referrer: null };
|
|
273
269
|
const sourcemapsBuild = (
|
|
274
270
|
globalThis as {
|
|
275
271
|
__SOURCEMAPS_BUILD__?: { buildId?: unknown };
|
|
@@ -284,7 +280,7 @@ class ErrorTracker {
|
|
|
284
280
|
const payload: Record<string, unknown> = {
|
|
285
281
|
token: this.siteKey,
|
|
286
282
|
...(identifier ? { userId: identifier } : {}),
|
|
287
|
-
sessionId:
|
|
283
|
+
sessionId: getOrCreateSessionId(),
|
|
288
284
|
...(buildId ? { buildId } : {}),
|
|
289
285
|
data: {
|
|
290
286
|
url: commonData.url as string,
|
|
@@ -301,7 +297,7 @@ class ErrorTracker {
|
|
|
301
297
|
console.log("[ErrorTracker] Payload:", payloadString);
|
|
302
298
|
}
|
|
303
299
|
|
|
304
|
-
|
|
300
|
+
sendData({
|
|
305
301
|
url: this.endpoint,
|
|
306
302
|
data: payloadString,
|
|
307
303
|
debug: this.debug,
|
package/src/replay.ts
CHANGED
|
@@ -3,10 +3,9 @@ import type { eventWithTime, listenerHandler } from "@rrweb/types";
|
|
|
3
3
|
import { record } from "rrweb";
|
|
4
4
|
import type { recordOptions } from "rrweb/typings/types";
|
|
5
5
|
import type { SlimDOMOptions } from "rrweb-snapshot";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "./utils/types";
|
|
6
|
+
import { sendData } from "./analytics";
|
|
7
|
+
import { getAnonymousId, getOrCreateSessionId } from "./utils/identifiers";
|
|
8
|
+
import { normalizeSamplingPercentage } from "./utils/types";
|
|
10
9
|
|
|
11
10
|
export interface ReplayTrackerOptions {
|
|
12
11
|
siteKey: string;
|
|
@@ -15,6 +14,7 @@ export interface ReplayTrackerOptions {
|
|
|
15
14
|
samplingPercentage?: number;
|
|
16
15
|
flushInterval?: number;
|
|
17
16
|
maxEvents?: number;
|
|
17
|
+
minReplayLengthMs?: number;
|
|
18
18
|
sampling?: recordOptions<eventWithTime>["sampling"];
|
|
19
19
|
slimDOMOptions?: SlimDOMOptions;
|
|
20
20
|
maskAllInputs?: boolean;
|
|
@@ -26,9 +26,6 @@ export interface ReplayTrackerOptions {
|
|
|
26
26
|
checkoutEveryNms?: number;
|
|
27
27
|
checkoutEveryNth?: number;
|
|
28
28
|
recordConsole?: boolean;
|
|
29
|
-
getAnonymousId?: () => string;
|
|
30
|
-
getSessionId?: () => string;
|
|
31
|
-
sendData?: (options: SendDataOptions) => Promise<boolean>;
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
interface PendingBatch {
|
|
@@ -68,15 +65,13 @@ class ReplayTracker {
|
|
|
68
65
|
private stopRecording: listenerHandler | undefined = undefined;
|
|
69
66
|
private started = false;
|
|
70
67
|
private startTime: number = 0;
|
|
68
|
+
private minLengthFlushScheduled = false;
|
|
71
69
|
private sequenceNumber = 0;
|
|
72
70
|
private pendingBatches: PendingBatch[] = [];
|
|
73
71
|
private isFlushing = false;
|
|
74
72
|
private compressionSupported = false;
|
|
75
73
|
private readonly sessionSamplingSeed: number;
|
|
76
|
-
private readonly minReplayLengthMs
|
|
77
|
-
private readonly _getAnonymousId: () => string;
|
|
78
|
-
private readonly _getSessionId: () => string;
|
|
79
|
-
private readonly sendData: (options: SendDataOptions) => Promise<boolean>;
|
|
74
|
+
private readonly minReplayLengthMs: number;
|
|
80
75
|
|
|
81
76
|
constructor(options: ReplayTrackerOptions) {
|
|
82
77
|
this.siteKey = options.siteKey;
|
|
@@ -120,10 +115,8 @@ class ReplayTracker {
|
|
|
120
115
|
this.checkoutEveryNms = options.checkoutEveryNms ?? 60_000;
|
|
121
116
|
this.checkoutEveryNth = options.checkoutEveryNth;
|
|
122
117
|
this.recordConsole = options.recordConsole ?? true;
|
|
118
|
+
this.minReplayLengthMs = options.minReplayLengthMs ?? 3000;
|
|
123
119
|
this.sessionSamplingSeed = Math.random() * 100;
|
|
124
|
-
this._getAnonymousId = options.getAnonymousId ?? (() => "");
|
|
125
|
-
this._getSessionId = options.getSessionId ?? (() => "");
|
|
126
|
-
this.sendData = options.sendData ?? (async () => false);
|
|
127
120
|
|
|
128
121
|
if (typeof window !== "undefined") {
|
|
129
122
|
this.compressionSupported = "CompressionStream" in window;
|
|
@@ -210,12 +203,11 @@ class ReplayTracker {
|
|
|
210
203
|
this.handleVisibilityChange,
|
|
211
204
|
);
|
|
212
205
|
|
|
213
|
-
|
|
214
|
-
if (elapsedTime < this.minReplayLengthMs) {
|
|
206
|
+
if (!this.hasReachedMinLength()) {
|
|
215
207
|
this.events = [];
|
|
216
208
|
if (this.debug) {
|
|
217
209
|
console.log(
|
|
218
|
-
`[Replay] Session too short (${
|
|
210
|
+
`[Replay] Session too short (${Date.now() - this.startTime}ms), discarding events`,
|
|
219
211
|
);
|
|
220
212
|
}
|
|
221
213
|
return;
|
|
@@ -231,9 +223,17 @@ class ReplayTracker {
|
|
|
231
223
|
this.scheduleFlush();
|
|
232
224
|
} else if (this.events.length >= this.maxEvents) {
|
|
233
225
|
this.scheduleFlush();
|
|
226
|
+
} else if (!this.minLengthFlushScheduled && this.hasReachedMinLength()) {
|
|
227
|
+
this.minLengthFlushScheduled = true;
|
|
228
|
+
this.scheduleFlush();
|
|
234
229
|
}
|
|
235
230
|
}
|
|
236
231
|
|
|
232
|
+
private hasReachedMinLength(): boolean {
|
|
233
|
+
if (this.minReplayLengthMs <= 0) return true;
|
|
234
|
+
return Date.now() - this.startTime >= this.minReplayLengthMs;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
237
|
private scheduleFlush(): void {
|
|
238
238
|
if (this.isFlushing || this.events.length === 0) return;
|
|
239
239
|
|
|
@@ -270,10 +270,11 @@ class ReplayTracker {
|
|
|
270
270
|
return;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
|
|
274
|
-
if (elapsedTime < this.minReplayLengthMs) {
|
|
273
|
+
if (!this.hasReachedMinLength()) {
|
|
275
274
|
if (this.debug) {
|
|
276
|
-
console.log(
|
|
275
|
+
console.log(
|
|
276
|
+
`[Replay] Too short (${Date.now() - this.startTime}ms), skipping`,
|
|
277
|
+
);
|
|
277
278
|
}
|
|
278
279
|
this.processPendingBatches();
|
|
279
280
|
return;
|
|
@@ -285,10 +286,10 @@ class ReplayTracker {
|
|
|
285
286
|
const eventsToSend = this.events;
|
|
286
287
|
this.events = [];
|
|
287
288
|
|
|
288
|
-
const anonymousId =
|
|
289
|
+
const anonymousId = getAnonymousId();
|
|
289
290
|
const batch = {
|
|
290
291
|
token: this.siteKey,
|
|
291
|
-
sessionId:
|
|
292
|
+
sessionId: getOrCreateSessionId(),
|
|
292
293
|
...(anonymousId ? { identifier: anonymousId } : {}),
|
|
293
294
|
sequence: this.sequenceNumber++,
|
|
294
295
|
timestamp: Date.now(),
|
|
@@ -446,7 +447,7 @@ class ReplayTracker {
|
|
|
446
447
|
const sizeKB = (data.size / 1024).toFixed(1);
|
|
447
448
|
|
|
448
449
|
return (
|
|
449
|
-
|
|
450
|
+
sendData({
|
|
450
451
|
url,
|
|
451
452
|
data,
|
|
452
453
|
contentType: isCompressed
|
|
@@ -460,7 +461,7 @@ class ReplayTracker {
|
|
|
460
461
|
}
|
|
461
462
|
|
|
462
463
|
getSessionId(): string | undefined {
|
|
463
|
-
return
|
|
464
|
+
return getOrCreateSessionId();
|
|
464
465
|
}
|
|
465
466
|
}
|
|
466
467
|
|
package/src/utils/identifiers.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
2
2
|
|
|
3
|
+
let cookielessMode = false;
|
|
4
|
+
|
|
5
|
+
export function setCookielessMode(mode: boolean): void {
|
|
6
|
+
cookielessMode = mode;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
export function getAnonymousId(cookieless?: boolean): string {
|
|
4
|
-
|
|
10
|
+
const useCookieless = cookieless ?? cookielessMode;
|
|
11
|
+
if (useCookieless) return "";
|
|
5
12
|
return getOrCreateAnonymousId();
|
|
6
13
|
}
|
|
7
14
|
|
package/src/web-vitals.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
onLCP,
|
|
7
7
|
onTTFB,
|
|
8
8
|
} from "web-vitals/attribution";
|
|
9
|
+
import { getOrCreateSessionId } from "./utils/identifiers";
|
|
9
10
|
import { normalizeSamplingPercentage } from "./utils/types";
|
|
10
11
|
|
|
11
12
|
export interface WebVitalsOptions {
|
|
@@ -13,7 +14,6 @@ export interface WebVitalsOptions {
|
|
|
13
14
|
endpoint?: string;
|
|
14
15
|
debug?: boolean;
|
|
15
16
|
samplingPercentage?: number;
|
|
16
|
-
getSessionId?: () => string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
type MetricName = "CLS" | "INP" | "LCP" | "FCP" | "TTFB";
|
|
@@ -34,7 +34,6 @@ class WebVitalsTracker {
|
|
|
34
34
|
private readonly sessionSamplingSeed: number;
|
|
35
35
|
private readonly metricsMap = new Map<MetricName, MetricState>();
|
|
36
36
|
private flushed = false;
|
|
37
|
-
private readonly getSessionId: () => string;
|
|
38
37
|
|
|
39
38
|
constructor(options: WebVitalsOptions) {
|
|
40
39
|
this.siteKey = options.siteKey;
|
|
@@ -46,7 +45,6 @@ class WebVitalsTracker {
|
|
|
46
45
|
options.samplingPercentage,
|
|
47
46
|
);
|
|
48
47
|
this.sessionSamplingSeed = Math.random() * 100;
|
|
49
|
-
this.getSessionId = options.getSessionId ?? (() => "");
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
private getVitalsEndpoint(baseEndpoint: string): string {
|
|
@@ -149,7 +147,7 @@ class WebVitalsTracker {
|
|
|
149
147
|
|
|
150
148
|
return {
|
|
151
149
|
body: JSON.stringify({
|
|
152
|
-
sessionId:
|
|
150
|
+
sessionId: getOrCreateSessionId(),
|
|
153
151
|
vitals,
|
|
154
152
|
metadata: {
|
|
155
153
|
url: window.location.href,
|
package/dist/error-DtTBFJh_.js
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
var e=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;getCommonData;getAnonymousId;getSessionId;sendData;handledErrors=new WeakSet;errorCounts=new Map;flushTimer=null;started=!1;constructor(e){this.siteKey=e.siteKey,this.endpoint=e.endpoint??`https://metrics.faststats.dev/v1/web`,this.debug=e.debug??!1,this.flushInterval=e.flushInterval??5e3,this.maxQueueSize=e.maxQueueSize??50,this.getCommonData=e.getCommonData??(()=>({})),this.getAnonymousId=e.getAnonymousId??(()=>``),this.getSessionId=e.getSessionId??(()=>``),this.sendData=e.sendData??(async()=>!1)}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.handleErrorEvent),window.addEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer=setInterval(()=>this.flush(),this.flushInterval),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.flush()}),window.addEventListener(`pagehide`,()=>this.flush()),this.debug&&console.log(`[ErrorTracker] Started listening for errors`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.handleErrorEvent),window.removeEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush(),this.debug&&console.log(`[ErrorTracker] Stopped listening for errors`))}handleErrorEvent=e=>{let t=e.error;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate error:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:e.message||(t?.message??`Unknown error`),filename:e.filename||void 0,lineno:e.lineno||void 0,colno:e.colno||void 0,stack:t?.stack||void 0,type:`error`})};handleRejection=e=>{let t=e.reason;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate rejection:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:t instanceof Error?t.message:typeof t==`string`?t:`Unhandled promise rejection`,stack:t instanceof Error?t.stack:void 0,type:`unhandledrejection`})};isExtensionError(e){return!!(e.filename?.startsWith(`chrome-extension://`)||e.stack&&e.stack.split(`
|
|
2
|
-
`).find(e=>e.trim().startsWith(`at `))?.includes(`chrome-extension://`))}parseStack(e){if(e)return e.split(`
|
|
3
|
-
`).map(e=>e.trim()).filter(e=>e.length>0)}async generateErrorHash(e){let t=[e.type,e.message,e.filename??``,e.lineno??``].join(`:`),n=new TextEncoder().encode(t),r=await crypto.subtle.digest(`SHA-256`,n);return`err_${Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}`}async queueError(e){if(this.isExtensionError(e))return;this.debug&&console.log(`[ErrorTracker] Captured error:`,e);let t=await this.generateErrorHash(e),n=this.errorCounts.get(t);if(n)n.count++,this.debug&&console.log(`[ErrorTracker] Incremented count for ${t} to ${n.count}`);else{let n={error:e.type===`unhandledrejection`?`UnhandledRejection`:`Error`,message:e.message,stack:this.parseStack(e.stack)};this.errorCounts.set(t,{entry:n,hash:t,count:1}),this.debug&&console.log(`[ErrorTracker] Queued new error: ${t}`)}this.errorCounts.size>=this.maxQueueSize&&this.flush()}captureError(e){if(this.handledErrors.has(e)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate manual capture:`,e.message);return}this.handledErrors.add(e),this.queueError({message:e.message,stack:e.stack,type:`error`})}flush(){if(this.errorCounts.size===0)return;let e=[];for(let{entry:t,hash:n,count:r}of this.errorCounts.values())e.push({...t,hash:n,count:r});this.errorCounts.clear(),this.debug&&console.log(`[ErrorTracker] Flushing errors:`,e);let t=this.getCommonData(),n=this.getAnonymousId(),r=globalThis.__SOURCEMAPS_BUILD__,i=typeof r?.buildId==`string`&&r.buildId.trim().length>0?r.buildId:void 0,a={token:this.siteKey,...n?{userId:n}:{},sessionId:this.getSessionId(),...i?{buildId:i}:{},data:{url:t.url,page:t.page,referrer:t.referrer,title:typeof document<`u`?document.title:``},errors:e},o=JSON.stringify(a);this.debug&&console.log(`[ErrorTracker] Payload:`,o),this.sendData({url:this.endpoint,data:o,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{e as default};
|
package/dist/replay-ZmimkIBT.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./types-E07i1PxI.js";import{getRecordConsolePlugin as t}from"@rrweb/rrweb-plugin-console-record";import{record as n}from"rrweb";var r=class{endpoint;siteKey;debug;flushInterval;maxEvents;sampling;slimDOMOptions;maskAllInputs;maskInputOptions;blockClass;blockSelector;maskTextClass;maskTextSelector;checkoutEveryNms;checkoutEveryNth;samplingPercentage;recordConsole;events=[];flushTimer=null;stopRecording=void 0;started=!1;startTime=0;sequenceNumber=0;pendingBatches=[];isFlushing=!1;compressionSupported=!1;sessionSamplingSeed;minReplayLengthMs=3e3;_getAnonymousId;_getSessionId;sendData;constructor(t){this.siteKey=t.siteKey,this.endpoint=t.endpoint?.replace(/\/v1\/web$/,`/v1/replay`)??`https://metrics.faststats.dev/v1/replay`,this.debug=t.debug??!1,this.samplingPercentage=e(t.samplingPercentage),this.flushInterval=t.flushInterval??1e4,this.maxEvents=t.maxEvents??500,this.sampling=t.sampling??{mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},this.slimDOMOptions=t.slimDOMOptions??{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0},this.maskAllInputs=t.maskAllInputs??!0,this.maskInputOptions=t.maskInputOptions??{password:!0,email:!0,tel:!0},this.blockClass=t.blockClass,this.blockSelector=t.blockSelector,this.maskTextClass=t.maskTextClass,this.maskTextSelector=t.maskTextSelector,this.checkoutEveryNms=t.checkoutEveryNms??6e4,this.checkoutEveryNth=t.checkoutEveryNth,this.recordConsole=t.recordConsole??!0,this.sessionSamplingSeed=Math.random()*100,this._getAnonymousId=t.getAnonymousId??(()=>``),this._getSessionId=t.getSessionId??(()=>``),this.sendData=t.sendData??(async()=>!1),typeof window<`u`&&(this.compressionSupported=`CompressionStream`in window)}start(){if(this.started||typeof window>`u`||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;this.started=!0,this.startTime=Date.now(),this.debug&&console.log(`[Replay] Recording started`);let e={emit:(e,t)=>this.handleEvent(e,t),sampling:this.sampling,slimDOMOptions:this.slimDOMOptions,maskAllInputs:this.maskAllInputs,checkoutEveryNms:this.checkoutEveryNms};this.maskInputOptions&&(e.maskInputOptions=this.maskInputOptions),this.blockClass&&(e.blockClass=this.blockClass),this.blockSelector&&(e.blockSelector=this.blockSelector),this.maskTextClass&&(e.maskTextClass=this.maskTextClass),this.maskTextSelector&&(e.maskTextSelector=this.maskTextSelector),this.checkoutEveryNth&&(e.checkoutEveryNth=this.checkoutEveryNth),this.recordConsole&&(e.plugins=[t()]),this.stopRecording=n(e),this.flushTimer=setInterval(()=>{this.scheduleFlush()},this.flushInterval),window.addEventListener(`beforeunload`,this.handleUnload),window.addEventListener(`pagehide`,this.handleUnload),document.addEventListener(`visibilitychange`,this.handleVisibilityChange)}stop(){if(!this.started)return;this.started=!1,this.debug&&console.log(`[Replay] Recording stopped`),this.stopRecording?.(),this.stopRecording=void 0,this.flushTimer&&=(clearInterval(this.flushTimer),null),window.removeEventListener(`beforeunload`,this.handleUnload),window.removeEventListener(`pagehide`,this.handleUnload),document.removeEventListener(`visibilitychange`,this.handleVisibilityChange);let e=Date.now()-this.startTime;if(e<this.minReplayLengthMs){this.events=[],this.debug&&console.log(`[Replay] Session too short (${e}ms), discarding events`);return}this.flush()}handleEvent(e,t){this.events.push(e),(t||this.events.length>=this.maxEvents)&&this.scheduleFlush()}scheduleFlush(){this.isFlushing||this.events.length===0||(typeof window<`u`&&`requestIdleCallback`in window?window.requestIdleCallback(()=>this.flush(),{timeout:2e3}):setTimeout(()=>this.flush(),0))}handleUnload=()=>{this.flush()};handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush()):document.visibilityState===`visible`&&this.started&&(this.flushTimer||=setInterval(()=>{this.scheduleFlush()},this.flushInterval))};async flush(){if(this.events.length===0){this.processPendingBatches();return}let e=Date.now()-this.startTime;if(e<this.minReplayLengthMs){this.debug&&console.log(`[Replay] Too short (${e}ms), skipping`),this.processPendingBatches();return}if(this.isFlushing)return;this.isFlushing=!0;let t=this.events;this.events=[];let n=this._getAnonymousId(),r={token:this.siteKey,sessionId:this._getSessionId(),...n?{identifier:n}:{},sequence:this.sequenceNumber++,timestamp:Date.now(),url:window.location.href,events:t};this.debug&&console.log(`[Replay] Sending ${t.length} events (seq: ${r.sequence})`);try{let e,t=!1;if(this.compressionSupported)try{e=await this.compress(JSON.stringify(r)),t=!0}catch{this.debug&&console.warn(`[Replay] Compression failed, using uncompressed`),e=new Blob([JSON.stringify(r)],{type:`application/json`})}else e=new Blob([JSON.stringify(r)],{type:`application/json`});await this.send(e,t)||this.pendingBatches.push({batch:r,isCompressed:t,retries:0})}catch(e){this.debug&&console.warn(`[Replay] Flush error:`,e),this.pendingBatches.push({batch:r,isCompressed:!1,retries:0})}finally{this.isFlushing=!1,this.processPendingBatches()}}async processPendingBatches(){if(this.pendingBatches.length===0)return;let e=this.pendingBatches.shift();if(e){if(e.retries>=3){this.debug&&console.warn(`[Replay] Max retries reached, dropping batch ${e.batch.sequence}`),this.processPendingBatches();return}e.retries++;try{let t;if(e.isCompressed&&this.compressionSupported)try{t=await this.compress(JSON.stringify(e.batch))}catch{t=new Blob([JSON.stringify(e.batch)],{type:`application/json`}),e.isCompressed=!1}else t=new Blob([JSON.stringify(e.batch)],{type:`application/json`});await this.send(t,e.isCompressed)?this.processPendingBatches():(this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries))}catch{this.debug&&console.warn(`[Replay] Retry ${e.retries} failed`),this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries)}}}async compress(e){if(!this.compressionSupported)throw Error(`Compression not supported`);let t=new TextEncoder().encode(e),n=new CompressionStream(`gzip`),r=n.writable.getWriter();r.write(t),r.close();let i=[],a=n.readable.getReader();for(;;){let{done:e,value:t}=await a.read();if(e)break;t&&i.push(t)}let o=i.reduce((e,t)=>e+t.length,0),s=new Uint8Array(o),c=0;for(let e of i)s.set(e,c),c+=e.length;if(this.debug){let e=(s.length/t.length*100).toFixed(1);console.log(`[Replay] Compressed: ${t.length} → ${s.length} bytes (${e}%)`)}return new Blob([s],{type:`application/octet-stream`})}async send(e,t){let n=t?`${this.endpoint}?encoding=gzip`:this.endpoint,r=(e.size/1024).toFixed(1);return this.sendData({url:n,data:e,contentType:t?`application/octet-stream`:`application/json`,headers:t?{"Content-Encoding":`gzip`}:void 0,debug:this.debug,debugPrefix:`[Replay] ${r}KB`})??Promise.resolve(!1)}getSessionId(){return this._getSessionId()}};export{r as default};
|
package/dist/types-E07i1PxI.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
function e(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}export{e as t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./types-E07i1PxI.js";import{onCLS as t,onFCP as n,onINP as r,onLCP as i,onTTFB as a}from"web-vitals/attribution";var o=class{endpoint;siteKey;debug;samplingPercentage;started=!1;sessionSamplingSeed;metricsMap=new Map;flushed=!1;getSessionId;constructor(t){this.siteKey=t.siteKey,this.endpoint=this.getVitalsEndpoint(t.endpoint??`https://metrics.faststats.dev/v1/web`),this.debug=t.debug??!1,this.samplingPercentage=e(t.samplingPercentage),this.sessionSamplingSeed=Math.random()*100,this.getSessionId=t.getSessionId??(()=>``)}getVitalsEndpoint(e){let t=new URL(e),n=t.pathname.split(`/`);return n[n.length-1]=`vitals`,t.pathname=n.join(`/`),t.toString()}start(){this.started||typeof window>`u`||(this.started=!0,document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.finalizeAndFlush()}),window.addEventListener(`pagehide`,()=>{this.finalizeAndFlush()}),this.debug&&console.log(`[WebVitals] Tracking started`),t(e=>this.captureMetric(e)),r(e=>this.captureMetric(e)),i(e=>this.captureMetric(e)),n(e=>this.captureMetric(e)),a(e=>this.captureMetric(e)))}captureMetric(e){if(this.flushed||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;let t=e.name,n={id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...e.attribution??{}},r=t===`FCP`||t===`TTFB`;this.metricsMap.set(t,{value:e.value,attributes:n,final:r}),this.debug&&console.log(`[WebVitals] ${t} captured: ${e.value}`+(r?` (final)`:``))}finalizeAndFlush(){if(!(this.flushed||this.metricsMap.size===0)){for(let[e,t]of this.metricsMap.entries())t.final||(t.final=!0,this.debug&&console.log(`[WebVitals] ${e} finalized: ${t.value}`));this.flushWithBeacon()}}buildPayload(){if(this.metricsMap.size===0)return null;let e=Array.from(this.metricsMap.entries()).map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes}));return{body:JSON.stringify({sessionId:this.getSessionId(),vitals:e,metadata:{url:window.location.href}}),count:e.length}}flushWithBeacon(){let e=this.buildPayload();if(e){if(this.flushed=!0,this.debug){let t=Array.from(this.metricsMap.keys()).join(`, `);console.log(`[WebVitals] Sending final metrics (${e.count}): ${t}`)}fetch(this.endpoint,{method:`POST`,body:e.body,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.siteKey}`},keepalive:!0}).catch(()=>{this.debug&&console.warn(`[WebVitals] Failed to send metrics`)})}}};export{o as default};
|