UNPKG

@datanova/browser

Version:

Lightweight browser SDK for event tracking and A/B testing

3 lines 8.41 kB
'use strict';var v=class{constructor(e,t){if(typeof window<"u"?this.storage=e==="localStorage"?window.localStorage:window.sessionStorage:this.storage=null,t)this.prefix=t;else {let r=typeof window<"u"?window.location.origin:"default",i=this.hashString(r).toString(36).slice(-4);this.prefix=`datanova.sdk.browser.${i}.`;}}get(e){if(!this.storage)return null;try{let t=this.storage.getItem(this.prefix+e);return t?JSON.parse(t):null}catch{return null}}set(e,t){if(this.storage)try{this.storage.setItem(this.prefix+e,JSON.stringify(t));}catch{}}remove(e){if(this.storage)try{this.storage.removeItem(this.prefix+e);}catch{}}hashString(e){let t=0;for(let r=0;r<e.length;r++){let i=e.charCodeAt(r);t=(t<<5)-t+i,t=t&t;}return Math.abs(t)}};var o=new v("localStorage");var S=new v("sessionStorage");var f=class{constructor(){this.sessionId=this.getOrCreateSessionId(),this.fingerprint=this.getOrCreateFingerprint();}setUser(e,t){this.userId=e,this.userProperties=t;}clearUser(){this.userId=void 0,this.userProperties=void 0;}overrideContext(e){this.contextOverrides=e;}getContext(){return {...{userId:this.userId,userProperties:this.userProperties,sessionId:this.sessionId,browser:this.getBrowserContext(),library:{name:"@datanova/browser",version:"0.1.0"}},...this.contextOverrides}}getIdentifier(){return this.userId||this.fingerprint||this.sessionId}getBrowserContext(){return typeof window>"u"?{url:"",title:"",referrer:"",path:"",search:"",userAgent:""}:{url:window.location.href,title:document.title,referrer:document.referrer,path:window.location.pathname,search:window.location.search,userAgent:navigator.userAgent}}getOrCreateSessionId(){let e="session_id",t=S.get(e);return t||(t=this.generateId("ss"),S.set(e,t)),t}getOrCreateFingerprint(){let e="fingerprint",t=o.get(e);return t||(t=this.generateId("fp"),o.set(e,t)),t}generateId(e){let t=Date.now().toString(36),r=Math.random().toString(36).substring(2,10);return `${e}-${t}-${r}`}};var c={CLICK:"click",PAGE_VIEW:"pageView",IMPRESSION:"impression",SUBMIT:"submit",CHANGE:"change"},k={SYSTEM:"system"};var u=class{constructor(e,t){this.experimentsService=e;this.tracker=t;}async getVariant(e,t){let r=this.getAssignmentKey(e,t),i=o.get(r),s=await this.experimentsService.getVariant(e,t);if(!i||i.variant!==s){let m={variant:s,assignedAt:Date.now()};o.set(r,m),this.tracker?.track("$variant_assigned",k.SYSTEM,{experiment_id:e,variant:s});}return s}getAssignmentKey(e,t){return `assignment_${e}_${t}`}};function p(n){if(n===null||typeof n!="object")return n;if(n instanceof Date)return new Date(n.getTime());if(Array.isArray(n))return n.map(t=>p(t));if(n instanceof Set)return new Set(Array.from(n).map(t=>p(t)));if(n instanceof Map){let t=new Map;return n.forEach((r,i)=>{t.set(p(i),p(r));}),t}let e={};for(let t in n)Object.prototype.hasOwnProperty.call(n,t)&&(e[t]=p(n[t]));return e}var x=class{constructor(e,t){this.eventsService=e;this.contextManager=t;}track(e,t,r){let i=this.contextManager.getContext(),s={eventName:e,eventType:t,properties:r?p(r):void 0,timestamp:new Date().toISOString(),context:i};this.eventsService.send(s);}};var h=class{constructor(){this.initialized=false;this.contextManager=new f;this.identify=(e,t)=>{this.assertInitialized(),this.contextManager.setUser(e,t);};this.reset=()=>{this.assertInitialized(),this.contextManager.clearUser();};this.track=(e,t,r)=>{this.assertInitialized(),this.assertTracker(),this.tracker.track(e,t,r);};this.trackClick=(e,t)=>{this.assertInitialized(),this.assertTracker(),this.tracker.track(e,c.CLICK,t);};this.trackPageView=(e,t)=>{this.assertInitialized(),this.assertTracker(),this.tracker.track(e,c.PAGE_VIEW,t);};this.trackImpression=(e,t)=>{this.assertInitialized(),this.assertTracker(),this.tracker.track(e,c.IMPRESSION,t);};this.trackSubmit=(e,t)=>{this.assertInitialized(),this.assertTracker(),this.tracker.track(e,c.SUBMIT,t);};this.trackChange=(e,t)=>{this.assertInitialized(),this.assertTracker(),this.tracker.track(e,c.CHANGE,t);};this.getVariant=async e=>{this.assertInitialized(),this.assertExperimentManager();let t=this.contextManager.getIdentifier();return this.experimentManager.getVariant(e,t)};}init(e){if(!this.initialized){if(!e.eventsService&&!e.experimentsService)throw new Error("At least one of 'eventsService' or 'experimentsService' must be provided to initialize the SDK");e.context&&this.contextManager.overrideContext(e.context),e.eventsService&&(this.tracker=new x(e.eventsService,this.contextManager)),e.experimentsService&&(this.experimentManager=new u(e.experimentsService,this.tracker)),this.initialized=true;}}assertInitialized(){if(!this.initialized)throw new Error("Client not initialized. Call init() first.")}assertTracker(){if(!this.tracker)throw new Error("No events service configured. Initialize the client with an events service to use tracking methods.")}assertExperimentManager(){if(!this.experimentManager)throw new Error("No experiments service configured. Initialize the client with an experiments service to use experiment methods.")}};var d=class{constructor(e,t="https://app.datanova.sh/api/v1/e"){this.sdkKey=e;this.endpoint=t;if(!e||!e.startsWith("dn_sdk_"))throw new Error("Invalid API key. Must start with 'dn_sdk_'")}async send(e){try{let t=await fetch(this.endpoint,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.sdkKey}`},body:JSON.stringify(e)});if(!t.ok)throw new Error(`HTTP ${t.status}: ${t.statusText}`)}catch(t){console.error("[Datanova] Failed to send event:",t);}}};var E=class{async send(e){let t={header:"color: #0080ff; font-weight: bold;",label:"color: #666; font-weight: bold;"};console.group(`%c\u{1F4CA} [Datanova] ${e.eventName} (${e.eventType})`,t.header),e.properties&&Object.keys(e.properties).length>0&&console.log("%cProperties:",t.label,e.properties),console.log("%cUser:",t.label,e.context.userId||"Anonymous"),console.log("%cSession:",t.label,e.context.sessionId),console.groupEnd();}};var w=class{async send(e){}};var a={NOT_FOUND:"not-found",ERROR:"error",NOT_IN_TRAFFIC:"not-in-traffic",ASSIGNED:"assigned"},A={[a.ERROR]:60*1e3,[a.NOT_IN_TRAFFIC]:60*60*1e3,[a.NOT_FOUND]:15*60*1e3,[a.ASSIGNED]:7*24*60*60*1e3},l=class{constructor(e){this.sdkKey=e;}async getVariant(e,t){let r=this.storageKey(e,t),i=o.get(r);if(i&&!this.isExpired(i))return i.variant;try{let s=await this.fetchConfig(e);if(!s)return this.saveAssignment({experimentId:e,identifier:t},"control",a.NOT_FOUND);if(this.hash(`${e}:${t}`)%100>=s.trafficAllocation)return this.saveAssignment({experimentId:e,identifier:t},"control",a.NOT_IN_TRAFFIC);let m=this.determineVariant(e,t,s);return this.saveAssignment({experimentId:e,identifier:t},m,a.ASSIGNED)}catch{return this.saveAssignment({experimentId:e,identifier:t},"control",a.ERROR)}}determineVariant(e,t,r){return this.hash(`${e}:${t}:variant`)%100<r.variantAllocation?"variant":"control"}saveAssignment(e,t,r){let{experimentId:i,identifier:s}=e,g={experimentId:i,variant:t,assignedAt:Date.now(),expiresAt:Date.now()+A[r]},m=this.storageKey(i,s);return o.set(m,g),t}isExpired(e){return e.expiresAt?Date.now()>=e.expiresAt:false}async fetchConfig(e){try{let t=await fetch(`https://app.datanova.sh/api/v1/exp/${e}`,{headers:{Authorization:`Bearer ${this.sdkKey}`}});return t.ok?t.json():null}catch{return null}}hash(e){let t=0;for(let r=0;r<e.length;r++){let i=e.charCodeAt(r);t=(t<<5)-t+i,t=t&t;}return Math.abs(t)}storageKey(e,t){return `experiment_${e}_${t}`}};var y=class{constructor(e=.5){this.cache=new Map;this.splitRatio=e;}async getVariant(e,t){let r=`${e}:${t}`,i=this.cache.get(r);if(i)return i;let g=this.hashCode(`${e}:${t}`)%100/100<this.splitRatio?"variant":"control";return this.cache.set(r,g),g}hashCode(e){let t=0;for(let r=0;r<e.length;r++){let i=e.charCodeAt(r);t=(t<<5)-t+i,t=t&t;}return Math.abs(t)}};function T(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,8);return `anon-${n}-${e}`}function I(n){let e=new h;return typeof n=="string"?e.init({eventsService:new d(n),experimentsService:new l(n)}):e.init({eventsService:n.eventsService,experimentsService:n.experimentsService}),e} exports.ConsoleEventsService=E;exports.Datanova=h;exports.DatanovaEventsService=d;exports.DatanovaExperimentsService=l;exports.EventType=c;exports.NoopEventsService=w;exports.RandomExperimentsService=y;exports.createDatanova=I;exports.generateAnonymousId=T;//# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map