@obelism/improve-sdk 1.0.0 → 1.2.0

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/client.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,"__esModule",{value:!0});var i=require("ua-parser-js");let t=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],e=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],s=["mac os","ios","android","windows","unix"],r=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],o=["wearable","mobile","tablet"],n=(i,t)=>!i||Object.entries(i).every(([i,e])=>t[i]===e),a=i=>{if(0===i.length)return null;if(1===i.length)return i[0].slug;let t=Math.random()*i.reduce((i,{split:t})=>i+t,0);return(i.find(({split:i})=>(t-=i)<=0)||i[0]).slug},l="visi",h="https://improve.obelism.studio",u="/api/log",c="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",d=async(i=1e3,t)=>(await new Promise(t=>setTimeout(t,i)),t?.abort(),null);class f{#i;constructor({organizationId:i,environment:t,state:e,config:s,fetchTimeout:r,baseUrl:o}){this.environment="develop",this.#i=null,this.config=null,this._fetchConfig=async i=>{if(this.config)return;if(!this.#i)throw Error("No config fetch setup provided");let t=await ((i=3e3,t,e)=>{let s=new AbortController;return Promise.race([fetch(t,{...e,signal:s.signal}),d(i,s)])})(this.#i.timeout,this.#i.url,i);if(!t||!t.ok)throw Error("Configuration fetch timed-out");return this.config=await t.json(),this.config},this.loadConfig=i=>{this.config=i},this.generateVisitorId=()=>[l,(function(i=5){return i&&"number"==typeof i?Array(i).fill("").reduce(i=>i+=c.charAt(Math.floor(Math.random()*c.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this.validateTestValue=(i,t)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let e=this.config.tests[i];if(!e)throw Error(`No config found for ${i}`);return!!e.options.find(i=>i.slug===t)},this.validateVisitorId=i=>{let t=i.split("_");if(2!==t.length)return!1;let[e,s]=t;return e===l&&26===s.length},this.organizationId=i,this.environment=t,this.state=e,this._baseUrl=o||h,s?this.config=s:this.#i={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:r||3e3}}}let g=i=>{if(!i)return!1;let t=document.cookie.split("; ").find(t=>{let[e]=t.split("=");return i===e});return!!t&&t.split("=")[1]},v=(i,t)=>{let e=new Date;e.setDate(e.getDate()+30),document.cookie=`${i}=${t};path=/;expires=${e.toUTCString()};SameSite=Lax;Secure`};exports.ImproveClientSDK=class extends f{#t;#e;#s;#r;#o;#n;constructor(l){super(l),this.#e=!1,this.#s="",this.#r={},this.#o=`${h}${u}`,this.#n=!0,this.fetchConfig=this._fetchConfig,this.setupVisitor=(n=window.navigator.userAgent)=>{let a=g(this.getVisitorCookieName()),l=a&&this.validateVisitorId(a);this.#e=l,this.#s=l?a:this.generateVisitorId();let h=(n=>{var a;if(!n||"string"!=typeof n)return null;let l=new i.UAParser(n).getResult(),h=(a=l.device.type)&&t.includes(a)?a:"desktop";return{pointer:o.includes(h)?"coarse":"fine",device:h,browser:((i="")=>{if(!i)return"other";let t=i.toLowerCase(),s=e.find(i=>t.includes(i));return s||(r.includes(t)?"social":"other")})(l.browser.name),os:((i="")=>{if(!i)return"unix";let t=i.toLowerCase(),e=s.find(i=>t.includes(i));return e||"unix"})(l.os.name)}})(n);return h?(this.#t=h,v(this.getVisitorCookieName(),this.#s),this.#s):null},this.getFlagValue=i=>{if(!this.config)return null;let t=this.config.flags[i];if(!t||!t.options[0])return null;if(this.#t||this.setupVisitor(),!this.#s||!this.#t)return t.options[0].slug;if(this.#t?.[i])return this.#t[i];if(!n(this.config.audience[t.audience],this.#t))return t.options[0].slug;let e=g(i)||a(t.options);return e?(this.#t[i]=e,v(i,e),e):null},this.getTestValue=i=>{if(!this.config)return null;let t=this.config.tests[i];if(!t)return null;if(this.#t||this.setupVisitor(),!this.#s||!this.#t)return t.defaultValue;if(this.#t?.[i])return this.#t[i];if(!n(this.config.audience[t.audience],this.#t))return t.defaultValue;if(t.allocation<100&&100*Math.random()>t.allocation)return this.#t[i]=t.defaultValue,this.#t?.[i];let e=g(i),s=e&&this.validateTestValue(i,e)?e:a(t.options);return s?(this.#t[i]=s,v(i,s),s):null},this.setAnalyticsUrls=i=>{this.#o=i},this.postAnalytic=(i,t,e)=>{var s;let r;if(!this.config)return null;let o=this.config.tests[i];if(this.#t||this.setupVisitor(),!o||!this.#t||this.#r?.[i]?.[t])return null;let n=this.#r[i]||{};n[t]=!0,this.#r[i]=n;let a=this.#t?.[i]||null;if(a||(a=this.getTestValue(i)||null),!a)return;let l={organizationId:this.organizationId,environment:this.environment,testId:o.id,testValue:a,visitorId:this.#s,pointer:this.#t.pointer,device:this.#t.device,screen:(r=window.innerWidth)<=768?"small":r<=1024?"medium":r<=1200?"large":"huge",browser:this.#t.browser,os:this.#t.os,visitor:this.#e?"recurring":"new",event:t,message:e||""};return this.#n&&(s={event:t,improve:{test:i,variant:a,visitorId:this.#s},_improve:!0},"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push(s))),fetch(this.#o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)})},this.#o=`${this._baseUrl}${u}`,this.#n=l.dataLayer??!0}};
1
+ Object.defineProperty(exports,"__esModule",{value:!0});var t=require("ua-parser-js");let e=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],i=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],r=["mac os","ios","android","windows","unix"],s=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],n=["wearable","mobile","tablet"],o=(t,e)=>!t||Object.entries(t).every(([t,i])=>e[t]===i),a=t=>{if(0===t.length)return null;if(1===t.length)return t[0].slug;let e=Math.random()*t.reduce((t,{split:e})=>t+e,0);return(t.find(({split:t})=>(e-=t)<=0)||t[0]).slug},l="visi",h="https://improve.obelism.studio",u="/api/log",d=t=>new Promise(e=>setTimeout(e,t)),c={timeout:"Configuration request timed out",network:"Configuration request could not reach the server",unauthorized:"Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment","rate-limited":"Configuration request was rate limited (429): too many requests, or the organization usage limit was reached",server:"Configuration request failed with a server error",client:"Configuration request was rejected by the server","invalid-response":"Configuration response could not be parsed as JSON"};class f extends Error{constructor(t,e){super(c[t]),this.name="ImproveFetchError",this.reason=t,this.status=e?.status,this.retryAfterMs=e?.retryAfterMs,e&&"cause"in e&&(this.cause=e.cause)}get isRetryable(){return"rate-limited"===this.reason||"server"===this.reason||"timeout"===this.reason||"network"===this.reason}}let g=t=>429===t?"rate-limited":401===t||403===t?"unauthorized":t>=500?"server":"client",v="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",m=t=>{if("object"!=typeof t||null===t||Array.isArray(t))throw new f("invalid-response");return{name:"string"==typeof t.name?t.name:"",version:"number"==typeof t.version?t.version:0,tests:t.tests??{},flags:t.flags??{},audience:t.audience??{}}},w=(t,e=Date.now())=>{if(!t)return;let i=Number(t);if(Number.isFinite(i))return Math.max(0,Math.round(1e3*i));let r=Date.parse(t);if(!Number.isNaN(r))return Math.max(0,r-e)},p=(t,e,i)=>Math.round(Math.random()*Math.min(i,e*2**t)),y=async(t=1e3,e)=>(await d(t),e?.abort(),null),b=(t=3e3,e,i)=>{let r=new AbortController;return Promise.race([fetch(e,{...i,signal:r.signal}),y(t,r)])};class I{#t;#e;constructor({organizationId:t,environment:e,state:i,config:r,fetchTimeout:s,baseUrl:n,configRetries:o,configRevalidate:a,disableWarnings:u}){this.environment="develop",this.#t=null,this.#e=!1,this.config=null,this._fetchConfig=async t=>{if(this.config)return;if(!this.#t)throw Error("No config fetch setup provided");let{url:e,timeout:i,retries:r,revalidate:s}=this.#t,n={...t};"number"==typeof s&&(n.next={...t?.next,revalidate:s});let o=new f("network");for(let t=0;t<=r;t++){let r;t>0&&await d(o.retryAfterMs??p(t-1,300,3e3));try{r=await b(i,e,n)}catch(t){o=new f(t instanceof Error&&"AbortError"===t.name?"timeout":"network",{cause:t});continue}if(!r){o=new f("timeout");continue}if(r.ok){try{this.config=m(await r.json())}catch(t){o=t instanceof f?t:new f("invalid-response",{cause:t});continue}return this.config}if(!(o=new f(g(r.status),{status:r.status,retryAfterMs:w(r.headers.get("Retry-After"))})).isRetryable)break}throw o},this.loadConfig=t=>{this.config=t},this.generateVisitorId=()=>[l,(function(t=5){return t&&"number"==typeof t?Array(t).fill("").reduce(t=>t+=v.charAt(Math.floor(Math.random()*v.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this._warn=t=>{this.#e||"u">typeof console&&console.warn(`[Improve] ${t}`)},this.validateTestValue=(t,e)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let i=this.config.tests[t];if(!i)throw Error(`No config found for ${t}`);return!!i.options.find(t=>t.slug===e)},this.validateVisitorId=t=>{let e=t.split("_");if(2!==e.length)return!1;let[i,r]=e;return i===l&&26===r.length},this.organizationId=t,this.environment=e,this.state=i,this._baseUrl=n||h,this.#e=u??!1,r?this.config=r:this.#t={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:s||3e3,retries:o??2,revalidate:a}}}let C=t=>{if(!t)return!1;let e=document.cookie.split("; ").find(e=>{let[i]=e.split("=");return t===i});return!!e&&e.split("=")[1]},A=(t,e)=>{let i=new Date;i.setDate(i.getDate()+30),document.cookie=`${t}=${e};path=/;expires=${i.toUTCString()};SameSite=Lax;Secure`},k=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,V=(t,e)=>"string"==typeof t&&t.length>e?t.slice(0,e):t;exports.ImproveClientSDK=class extends I{#i;#r;#s;#n;#o;#a;#l;#h;constructor(l){super(l),this.#r=!1,this.#s="",this.#n={},this.#o=0,this.#a=new Set,this.#l=`${h}${u}`,this.#h=!0,this.fetchConfig=this._fetchConfig,this.setupVisitor=(o=window.navigator.userAgent)=>{let a=C(this.getVisitorCookieName()),l=a&&this.validateVisitorId(a);this.#r=l,this.#s=l?a:this.generateVisitorId();let h=(o=>{var a;if(!o||"string"!=typeof o)return null;let l=new t.UAParser(o).getResult(),h=(a=l.device.type)&&e.includes(a)?a:"desktop";return{pointer:n.includes(h)?"coarse":"fine",device:h,browser:((t="")=>{if(!t)return"other";let e=t.toLowerCase(),r=i.find(t=>e.includes(t));return r||(s.includes(e)?"social":"other")})(l.browser.name),os:((t="")=>{if(!t)return"unix";let e=t.toLowerCase(),i=r.find(t=>e.includes(t));return i||"unix"})(l.os.name)}})(o);return h?(this.#i=h,A(this.getVisitorCookieName(),this.#s),this.#s):null},this.getFlagValue=t=>{if(!this.config)return null;let e=this.config.flags[t];if(!e||!e.options[0])return null;if(this.#i||this.setupVisitor(),!this.#s||!this.#i)return e.options[0].slug;if(this.#i?.[t])return this.#i[t];if(!o(this.config.audience[e.audience],this.#i))return e.options[0].slug;let i=C(t)||a(e.options);return i?(this.#i[t]=i,A(t,i),i):null},this.getTestValue=t=>{if(!this.config)return null;let e=this.config.tests[t];if(!e)return null;if(this.#i||this.setupVisitor(),!this.#s||!this.#i)return e.defaultValue;if(this.#i?.[t])return this.#i[t];if(!o(this.config.audience[e.audience],this.#i))return e.defaultValue;if(e.allocation<100&&100*Math.random()>e.allocation)return this.#i[t]=e.defaultValue,this.#i?.[t];let i=C(t),r=i&&this.validateTestValue(t,i)?i:a(e.options);return r?(this.#i[t]=r,A(t,r),r):null},this.setAnalyticsUrls=t=>{this.#l=t},this.postAnalytic=(t,e,i)=>{let r;if(!this.config||e.startsWith("gtm."))return null;if(k.test(e)||this.#a.has(e)||(this.#a.add(e),this._warn(`Event name "${e}" isn't snake_case. Prefer GA4-style names like "add_to_cart" or "purchase" so your events line up with GA4 / Google Tag Manager. Pass \`disableWarnings: true\` at setup to silence this.`)),Date.now()<this.#o)return null;let{value:s,currency:n,message:o,params:a}="string"==typeof i?{message:i}:i??{},l="number"==typeof s&&Number.isFinite(s),h=a&&"object"==typeof a&&!Array.isArray(a)?a:void 0,u=this.config.tests[t];if(this.#i||this.setupVisitor(),!u||!this.#i||this.#n?.[t]?.[e])return null;let d=this.#n[t]||{};d[e]=!0,this.#n[t]=d;let c=this.#i?.[t]||null;if(c||(c=this.getTestValue(t)||null),!c)return;let f={organizationId:this.organizationId,environment:this.environment,testId:u.id,testValue:c,visitorId:this.#s,pointer:this.#i.pointer,device:this.#i.device,screen:(r=window.innerWidth)<=768?"small":r<=1024?"medium":r<=1200?"large":"huge",browser:this.#i.browser,os:this.#i.os,visitor:this.#r?"recurring":"new",event:V(e,256),message:V(o||"",256),...l?{value:s}:{},...n?{currency:V(n,8)}:{},...h?{params:h}:{}};if(this.#h){var g;h&&"ecommerce"in h&&"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push({ecommerce:null})),g={event:e,improve:{test:t,variant:c,visitorId:this.#s},...l?{value:s}:{},...n?{currency:n}:{},...h??{},_improve:!0},"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push(g))}let v=JSON.stringify(f);"u">typeof TextEncoder&&new TextEncoder().encode(v).length>8192&&this._warn(`Analytic for "${e}" exceeds the 8192-byte limit and will be rejected by the server — trim the \`params\` payload.`);let m=fetch(this.#l,{method:"POST",headers:{"Content-Type":"application/json"},body:v,keepalive:!0});return m.then(t=>{if(t?.status===429){let e=w(t.headers.get("Retry-After"));this.#o=Date.now()+(e??6e4)}}).catch(()=>{}),m},this.#l=`${this._baseUrl}${u}`,this.#h=l.dataLayer??!0}};
2
2
  //# sourceMappingURL=client.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/config/urls.ts","../src/utils/getRandomString.ts","../src/utils/getTimeoutError.ts","../src/utils/delay.ts","../src/base.ts","../src/utils/timeoutFetch.ts","../src/utils/clientCookie.ts","../src/client.ts","../src/utils/pushDataLayer.ts","../src/utils/getScreenSize.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tif (options.length === 0) return null\n\tif (options.length === 1) return options[0].slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || options[0]\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\nexport const ANALYTICS_PATH = `/api/log`\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","import {\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { getRandomString } from './utils/getRandomString'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState\n\n\t#configFetch: ConfigFetch | null = null\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst res = await timeoutFetch(\n\t\t\tthis.#configFetch.timeout,\n\t\t\tthis.#configFetch.url,\n\t\t\tconfig,\n\t\t)\n\t\tif (!res || !res.ok) throw new Error('Configuration fetch timed-out')\n\n\t\tthis.config = await res.json()\n\t\treturn this.config\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","const ROW_DELIMITER = '; '\nconst ENTRY_DELIMITER = '='\n\nexport const getCookie = (name: string) => {\n\tif (!name) return false\n\tconst cookie = document.cookie.split(ROW_DELIMITER).find((row) => {\n\t\tconst [key] = row.split(ENTRY_DELIMITER)\n\t\treturn name === key\n\t})\n\treturn cookie ? cookie.split(ENTRY_DELIMITER)[1] : false\n}\n\nexport const setCookie = (name: string, value: string) => {\n\tconst now = new Date()\n\tnow.setDate(now.getDate() + 30)\n\tdocument.cookie = `${name}=${value};path=/;expires=${now.toUTCString()};SameSite=Lax;Secure`\n}\n","import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { getCookie, setCookie } from './utils/clientCookie'\nimport { ANALYTICS_PATH, BASE_URL } from './config/urls'\nimport { getScreenSize } from './utils/getScreenSize'\nimport { pushDataLayer } from './utils/pushDataLayer'\nimport { ImproveEnvironmentOption, ImproveSetupArgs } from './types'\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\nexport type CreateAnalytic = {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption\n\n\ttestId: string\n\ttestValue: string\n\tvisitorId: string\n\n\tpointer: string\n\tdevice: string\n\tscreen: string\n\tbrowser: string\n\tos: string\n\tvisitor: string\n\n\tevent: string\n\tmessage: string\n}\n\ntype TrackedAnalytics = {\n\t[TestSlug: string]: {\n\t\t[Event: string]: boolean\n\t}\n}\n\nexport class ImproveClientSDK extends BaseImproveSDK {\n\t#visitor: Visitor | undefined\n\t#visitorRecurring: boolean = false\n\n\t#visitorId: string = ''\n\n\t#analytics: TrackedAnalytics = {}\n\n\t#analyticsUrl = `${BASE_URL}${ANALYTICS_PATH}`\n\n\t#dataLayerEnabled: boolean = true\n\n\tfetchConfig = this._fetchConfig\n\n\tconstructor(args: ImproveSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#analyticsUrl = `${this._baseUrl}${ANALYTICS_PATH}`\n\t\tthis.#dataLayerEnabled = args.dataLayer ?? true\n\t}\n\n\tsetupVisitor = (userAgent: string = window.navigator.userAgent) => {\n\t\tconst cookieVisitorId = getCookie(this.getVisitorCookieName())\n\t\tconst validCookieVisitorId =\n\t\t\tcookieVisitorId && this.validateVisitorId(cookieVisitorId)\n\n\t\tthis.#visitorRecurring = validCookieVisitorId\n\t\tthis.#visitorId = validCookieVisitorId\n\t\t\t? cookieVisitorId\n\t\t\t: this.generateVisitorId()\n\n\t\tconst parsedUserAgent = parseUserAgent(userAgent)\n\n\t\tif (!parsedUserAgent) return null\n\n\t\tthis.#visitor = parsedUserAgent\n\n\t\tsetCookie(this.getVisitorCookieName(), this.#visitorId)\n\n\t\treturn this.#visitorId\n\t}\n\n\tgetFlagValue = (flagSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst flagConfig = this.config.flags[flagSlug]\n\n\t\tif (!flagConfig || !flagConfig.options[0]) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!this.#visitorId || !this.#visitor) return flagConfig.options[0].slug\n\t\tif (this.#visitor?.[flagSlug]) return this.#visitor[flagSlug]\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue =\n\t\t\tgetCookie(flagSlug) || getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tthis.#visitor[flagSlug] = flagValue\n\n\t\tsetCookie(flagSlug, flagValue)\n\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!testConfig) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\tif (!this.#visitorId || !this.#visitor) return testConfig.defaultValue\n\t\tif (this.#visitor?.[testSlug]) return this.#visitor[testSlug]\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tthis.#visitor[testSlug] = testConfig.defaultValue\n\t\t\treturn this.#visitor?.[testSlug]\n\t\t}\n\n\t\tconst cookieTestValue = getCookie(testSlug)\n\t\tconst validCookieTestValue =\n\t\t\tcookieTestValue && this.validateTestValue(testSlug, cookieTestValue)\n\t\tconst testValue = validCookieTestValue\n\t\t\t? cookieTestValue\n\t\t\t: getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tthis.#visitor[testSlug] = testValue\n\n\t\tsetCookie(testSlug, testValue)\n\n\t\treturn testValue\n\t}\n\n\tsetAnalyticsUrls = (url: string) => {\n\t\tthis.#analyticsUrl = url\n\t}\n\n\tpostAnalytic = (testSlug: string, event: string, message?: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!testConfig || !this.#visitor || this.#analytics?.[testSlug]?.[event]) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst testSlugAnalytics = this.#analytics[testSlug] || {}\n\t\ttestSlugAnalytics[event] = true\n\t\tthis.#analytics[testSlug] = testSlugAnalytics\n\n\t\tlet testValue = this.#visitor?.[testSlug] || null\n\t\tif (!testValue) {\n\t\t\ttestValue = this.getTestValue(testSlug) || null\n\t\t}\n\n\t\tif (!testValue) return\n\n\t\tconst body: CreateAnalytic = {\n\t\t\torganizationId: this.organizationId,\n\t\t\tenvironment: this.environment,\n\n\t\t\ttestId: testConfig.id,\n\t\t\ttestValue: testValue,\n\t\t\tvisitorId: this.#visitorId,\n\t\t\tpointer: this.#visitor.pointer,\n\t\t\tdevice: this.#visitor.device,\n\t\t\tscreen: getScreenSize(),\n\t\t\tbrowser: this.#visitor.browser,\n\t\t\tos: this.#visitor.os,\n\t\t\tvisitor: this.#visitorRecurring ? 'recurring' : 'new',\n\t\t\tevent: event,\n\t\t\tmessage: message || '',\n\t\t}\n\n\t\tif (this.#dataLayerEnabled) {\n\t\t\tpushDataLayer({\n\t\t\t\tevent,\n\t\t\t\timprove: {\n\t\t\t\t\ttest: testSlug,\n\t\t\t\t\tvariant: testValue,\n\t\t\t\t\tvisitorId: this.#visitorId,\n\t\t\t\t},\n\t\t\t\t_improve: true,\n\t\t\t})\n\t\t}\n\n\t\treturn fetch(this.#analyticsUrl, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify(body),\n\t\t})\n\t}\n}\n","export type ImproveDataLayerEntry = {\n\tevent: string\n\timprove: {\n\t\ttest: string\n\t\tvariant: string\n\t\tvisitorId: string\n\t}\n\t/**\n\t * Marks the entry as originating from Improve so platform-side dataLayer\n\t * importers can ignore it (loop prevention).\n\t */\n\t_improve: true\n}\n\ndeclare global {\n\tinterface Window {\n\t\tdataLayer?: Record<string, unknown>[]\n\t}\n}\n\n/**\n * Mirror an analytic onto the GTM dataLayer (with experiment dimensions) so it\n * can drive Google Tag Manager / Google Ads conversions. No-op outside the\n * browser. Initializes window.dataLayer if GTM hasn't yet.\n */\nexport const pushDataLayer = (entry: ImproveDataLayerEntry) => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push(entry)\n}\n","type SizeOptions = 'small' | 'medium' | 'large' | 'huge'\n\nexport const getScreenSize = (): SizeOptions => {\n\tconst size = window.innerWidth\n\tif (size <= 768) return 'small'\n\tif (size <= 1024) return 'medium'\n\tif (size <= 1200) return 'large'\n\treturn 'huge'\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","length","slug","value","Math","random","reduce","acc","split","match","find","VISITOR_ID_PREFIX","BASE_URL","ANALYTICS_PATH","CHAR_SET","getTimeoutError","timeout","controller","Promise","resolve","setTimeout","abort","BaseImproveSDK","organizationId","environment","state","config","fetchTimeout","baseUrl","_fetchConfig","Error","res","timeoutFetch","url","AbortController","race","fetch","signal","ok","json","loadConfig","generateVisitorId","getRandomString","characters","Array","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","validateTestValue","testName","testValue","testConfig","tests","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl","getCookie","name","cookie","document","row","setCookie","now","Date","setDate","getDate","toUTCString","args","fetchConfig","setupVisitor","userAgent","window","navigator","cookieVisitorId","validCookieVisitorId","parsedUserAgent","parseUserAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType","getFlagValue","flagSlug","flagConfig","flags","flagValue","getTestValue","testSlug","defaultValue","allocation","cookieTestValue","validCookieTestValue","setAnalyticsUrls","postAnalytic","event","message","entry","size","testSlugAnalytics","body","testId","id","visitorId","screen","innerWidth","visitor","improve","test","variant","_improve","dataLayer","push","method","headers","JSON","stringify"],"mappings":"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"}
1
+ {"version":3,"file":"client.cjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/config/urls.ts","../src/utils/delay.ts","../src/utils/errors.ts","../src/utils/getRandomString.ts","../src/utils/normalizeConfig.ts","../src/utils/retry.ts","../src/utils/getTimeoutError.ts","../src/utils/timeoutFetch.ts","../src/base.ts","../src/utils/clientCookie.ts","../src/utils/isSnakeCaseEventName.ts","../src/utils/truncate.ts","../src/client.ts","../src/utils/getScreenSize.ts","../src/utils/pushDataLayer.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tif (options.length === 0) return null\n\tif (options.length === 1) return options[0].slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || options[0]\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n\n/** Default number of retries for a failed config fetch (network / 5xx / 429). */\nexport const CONFIG_RETRY_COUNT = 2\n\n/** Base delay for config-fetch backoff, in ms. */\nexport const CONFIG_RETRY_BASE_DELAY_MS = 300\n\n/** Upper bound for a single config-fetch backoff wait, in ms. */\nexport const CONFIG_RETRY_MAX_DELAY_MS = 3000\n\n/**\n * Max length of a single analytic string field. The backend rejects any\n * field longer than this (varchar(256)) with a 400, so the client caps\n * developer-controlled values (event, message) to avoid silently dropping\n * the event.\n */\nexport const MAX_ANALYTIC_FIELD_LENGTH = 256\n\n/**\n * Max length of the analytic `currency` field. The backend column is\n * `varchar(8)` (ISO 4217 is 3 chars), and a longer value throws at insert —\n * failing the *entire* event, not just the currency — so the client caps it\n * here rather than at the generic 256-char limit.\n */\nexport const MAX_CURRENCY_FIELD_LENGTH = 8\n\n/**\n * Max size of the analytic POST body. The backend rejects anything larger with\n * a 413 (`MAX_BODY_BYTES = 8 * 1024`), so an oversized `params` payload silently\n * loses the event. Kept in sync with the backend limit.\n */\nexport const MAX_ANALYTIC_BODY_BYTES = 8 * 1024\n\n/**\n * How long to stop sending analytics after the backend returns 429 (usage\n * limit / rate limit) without a `Retry-After`. Avoids hammering an org that is\n * already over its quota.\n */\nexport const ANALYTIC_RATE_LIMIT_COOLDOWN_MS = 60_000\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\nexport const ANALYTICS_PATH = `/api/log`\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","export type ImproveFetchErrorReason =\n\t| 'timeout'\n\t| 'network'\n\t| 'unauthorized'\n\t| 'rate-limited'\n\t| 'server'\n\t| 'client'\n\t| 'invalid-response'\n\nconst REASON_MESSAGES: Record<ImproveFetchErrorReason, string> = {\n\ttimeout: 'Configuration request timed out',\n\tnetwork: 'Configuration request could not reach the server',\n\tunauthorized:\n\t\t'Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment',\n\t'rate-limited':\n\t\t'Configuration request was rate limited (429): too many requests, or the organization usage limit was reached',\n\tserver: 'Configuration request failed with a server error',\n\tclient: 'Configuration request was rejected by the server',\n\t'invalid-response': 'Configuration response could not be parsed as JSON',\n}\n\n/**\n * Error thrown by the config fetch layer. Carries the HTTP `status`, a coarse\n * `reason`, and (when the server sent one) the `Retry-After` delay in ms, so\n * callers can distinguish a misconfiguration (unauthorized) from a transient\n * failure (rate-limited/server/timeout) instead of guessing from a string.\n */\nexport class ImproveFetchError extends Error {\n\treason: ImproveFetchErrorReason\n\tstatus?: number\n\tretryAfterMs?: number\n\n\tconstructor(\n\t\treason: ImproveFetchErrorReason,\n\t\toptions?: { status?: number; retryAfterMs?: number; cause?: unknown },\n\t) {\n\t\tsuper(REASON_MESSAGES[reason])\n\t\tthis.name = 'ImproveFetchError'\n\t\tthis.reason = reason\n\t\tthis.status = options?.status\n\t\tthis.retryAfterMs = options?.retryAfterMs\n\t\tif (options && 'cause' in options) {\n\t\t\t;(this as { cause?: unknown }).cause = options.cause\n\t\t}\n\t}\n\n\t/** Whether retrying the request could plausibly succeed. */\n\tget isRetryable() {\n\t\treturn (\n\t\t\tthis.reason === 'rate-limited' ||\n\t\t\tthis.reason === 'server' ||\n\t\t\tthis.reason === 'timeout' ||\n\t\t\tthis.reason === 'network'\n\t\t)\n\t}\n}\n\n/** Map an HTTP status onto a coarse {@link ImproveFetchErrorReason}. */\nexport const getReasonFromStatus = (\n\tstatus: number,\n): ImproveFetchErrorReason => {\n\tif (status === 429) return 'rate-limited'\n\tif (status === 401 || status === 403) return 'unauthorized'\n\tif (status >= 500) return 'server'\n\treturn 'client'\n}\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { ImproveConfiguration } from '../types'\nimport { ImproveFetchError } from './errors'\n\n/**\n * @function normalizeConfig\n * @description Coerce a parsed config response into a well-formed\n * {@link ImproveConfiguration}. The backend returns `200 {}` for an unknown\n * org/environment (and on its own internal error). An empty object is truthy,\n * so it slips past `if (!this.config)` guards and then crashes downstream on\n * `config.tests[slug]` (`Cannot read properties of undefined`). Guaranteeing\n * `tests`/`flags`/`audience` are always objects lets the SDK fail open to\n * default values instead of throwing during render/middleware.\n *\n * Throws {@link ImproveFetchError} `invalid-response` when the body isn't an\n * object at all (e.g. an array, string, or null).\n */\nexport const normalizeConfig = (body: unknown): ImproveConfiguration => {\n\tif (typeof body !== 'object' || body === null || Array.isArray(body)) {\n\t\tthrow new ImproveFetchError('invalid-response')\n\t}\n\n\tconst raw = body as Partial<ImproveConfiguration>\n\n\treturn {\n\t\tname: typeof raw.name === 'string' ? raw.name : '',\n\t\tversion: typeof raw.version === 'number' ? raw.version : 0,\n\t\ttests: raw.tests ?? {},\n\t\tflags: raw.flags ?? {},\n\t\taudience: raw.audience ?? {},\n\t}\n}\n","/**\n * @function parseRetryAfterMs\n * @description Parse an HTTP `Retry-After` header into milliseconds. Supports\n * both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Returns `undefined` when the header is\n * absent or unparseable so the caller can fall back to computed backoff.\n *\n * @param {string | null} [header] - the raw `Retry-After` header value\n * @param {number} [now] - current epoch ms (injectable for testing)\n */\nexport const parseRetryAfterMs = (\n\theader: string | null | undefined,\n\tnow: number = Date.now(),\n): number | undefined => {\n\tif (!header) return undefined\n\n\tconst seconds = Number(header)\n\tif (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1000))\n\n\tconst date = Date.parse(header)\n\tif (Number.isNaN(date)) return undefined\n\n\treturn Math.max(0, date - now)\n}\n\n/**\n * @function getBackoffDelayMs\n * @description Exponential backoff with full jitter, capped at `maxDelay`.\n * Jitter spreads retries so concurrent clients don't stampede the origin in\n * lockstep after a shared failure.\n *\n * @param {number} attempt - zero-based retry attempt index\n * @param {number} baseDelay - base delay in ms\n * @param {number} maxDelay - upper bound in ms\n */\nexport const getBackoffDelayMs = (\n\tattempt: number,\n\tbaseDelay: number,\n\tmaxDelay: number,\n): number => {\n\tconst exponential = Math.min(maxDelay, baseDelay * 2 ** attempt)\n\treturn Math.round(Math.random() * exponential)\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","import {\n\tCONFIG_RETRY_BASE_DELAY_MS,\n\tCONFIG_RETRY_COUNT,\n\tCONFIG_RETRY_MAX_DELAY_MS,\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { delay } from './utils/delay'\nimport { getReasonFromStatus, ImproveFetchError } from './utils/errors'\nimport { getRandomString } from './utils/getRandomString'\nimport { normalizeConfig } from './utils/normalizeConfig'\nimport { getBackoffDelayMs, parseRetryAfterMs } from './utils/retry'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n\tretries: number\n\trevalidate?: number\n}\n\n// Next.js augments RequestInit with a `next` field for its fetch cache.\ntype RequestInitWithNext = RequestInit & {\n\tnext?: { revalidate?: number; tags?: string[] }\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState\n\n\t#configFetch: ConfigFetch | null = null\n\n\t#warningsDisabled: boolean = false\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t\tconfigRetries,\n\t\tconfigRevalidate,\n\t\tdisableWarnings,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\t\tthis.#warningsDisabled = disableWarnings ?? false\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t\tretries: configRetries ?? CONFIG_RETRY_COUNT,\n\t\t\t\trevalidate: configRevalidate,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst { url, timeout, retries, revalidate } = this.#configFetch\n\n\t\t// Opt into the Next.js fetch cache when a revalidate window is configured,\n\t\t// so the datafile is reused across requests instead of re-fetched.\n\t\tconst requestInit: RequestInitWithNext = { ...config }\n\t\tif (typeof revalidate === 'number') {\n\t\t\trequestInit.next = {\n\t\t\t\t...(config as RequestInitWithNext | undefined)?.next,\n\t\t\t\trevalidate,\n\t\t\t}\n\t\t}\n\n\t\t// Seed with a generic network error so an exhausted loop always has a\n\t\t// meaningful, typed error to throw.\n\t\tlet lastError = new ImproveFetchError('network')\n\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\t// Prefer a server-provided Retry-After; otherwise back off with jitter.\n\t\t\t\tawait delay(\n\t\t\t\t\tlastError.retryAfterMs ??\n\t\t\t\t\t\tgetBackoffDelayMs(\n\t\t\t\t\t\t\tattempt - 1,\n\t\t\t\t\t\t\tCONFIG_RETRY_BASE_DELAY_MS,\n\t\t\t\t\t\t\tCONFIG_RETRY_MAX_DELAY_MS,\n\t\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tlet res: Response | null\n\t\t\ttry {\n\t\t\t\tres = await timeoutFetch(timeout, url, requestInit)\n\t\t\t} catch (cause) {\n\t\t\t\t// timeoutFetch aborts on timeout (AbortError) and rejects on\n\t\t\t\t// network-level failures — both are transient and retryable.\n\t\t\t\tconst aborted = cause instanceof Error && cause.name === 'AbortError'\n\t\t\t\tlastError = new ImproveFetchError(aborted ? 'timeout' : 'network', {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// timeoutFetch resolves null when the timeout wins the race.\n\t\t\tif (!res) {\n\t\t\t\tlastError = new ImproveFetchError('timeout')\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (res.ok) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.config = normalizeConfig(await res.json())\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tlastError =\n\t\t\t\t\t\tcause instanceof ImproveFetchError\n\t\t\t\t\t\t\t? cause\n\t\t\t\t\t\t\t: new ImproveFetchError('invalid-response', { cause })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn this.config\n\t\t\t}\n\n\t\t\tlastError = new ImproveFetchError(getReasonFromStatus(res.status), {\n\t\t\t\tstatus: res.status,\n\t\t\t\tretryAfterMs: parseRetryAfterMs(res.headers.get('Retry-After')),\n\t\t\t})\n\n\t\t\t// Auth/validation rejections won't change on retry — fail fast.\n\t\t\tif (!lastError.isRetryable) break\n\t\t}\n\n\t\tthrow lastError\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\t/**\n\t * Emit a development warning, unless warnings were disabled via the\n\t * `disableWarnings` setup option. Centralized so every SDK warning honors the\n\t * same switch. No-op where `console` is unavailable.\n\t */\n\tprotected _warn = (message: string) => {\n\t\tif (this.#warningsDisabled) return\n\t\tif (typeof console === 'undefined') return\n\t\tconsole.warn(`[Improve] ${message}`)\n\t}\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","const ROW_DELIMITER = '; '\nconst ENTRY_DELIMITER = '='\n\nexport const getCookie = (name: string) => {\n\tif (!name) return false\n\tconst cookie = document.cookie.split(ROW_DELIMITER).find((row) => {\n\t\tconst [key] = row.split(ENTRY_DELIMITER)\n\t\treturn name === key\n\t})\n\treturn cookie ? cookie.split(ENTRY_DELIMITER)[1] : false\n}\n\nexport const setCookie = (name: string, value: string) => {\n\tconst now = new Date()\n\tnow.setDate(now.getDate() + 30)\n\tdocument.cookie = `${name}=${value};path=/;expires=${now.toUTCString()};SameSite=Lax;Secure`\n}\n","/**\n * A well-formed `snake_case` event name: starts with a lowercase letter and\n * contains only lowercase letters, digits, and single underscores between\n * segments. Matches the GA4 / GTM event-name convention Improve recommends\n * (`add_to_cart`, `sign_up`, `purchase`).\n */\nconst SNAKE_CASE_EVENT_NAME = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/\n\n/**\n * Whether `event` follows the recommended `snake_case` naming convention.\n * Flags camelCase (`pageView`), PascalCase (`AddToCart`), kebab-case\n * (`add-to-cart`), and names with spaces or trailing/double underscores.\n */\nexport const isSnakeCaseEventName = (event: string): boolean =>\n\tSNAKE_CASE_EVENT_NAME.test(event)\n","/**\n * @function truncate\n * @description Cap a string to `maxLength` characters. Returns non-strings\n * unchanged so it is safe to apply to loosely-typed payload values.\n *\n * @param {string} value - the value to truncate\n * @param {number} maxLength - maximum length to keep\n */\nexport const truncate = (value: string, maxLength: number): string =>\n\ttypeof value === 'string' && value.length > maxLength\n\t\t? value.slice(0, maxLength)\n\t\t: value\n","import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { getCookie, setCookie } from './utils/clientCookie'\nimport {\n\tANALYTIC_RATE_LIMIT_COOLDOWN_MS,\n\tMAX_ANALYTIC_BODY_BYTES,\n\tMAX_ANALYTIC_FIELD_LENGTH,\n\tMAX_CURRENCY_FIELD_LENGTH,\n} from './config/constants'\nimport { ANALYTICS_PATH, BASE_URL } from './config/urls'\nimport { getScreenSize } from './utils/getScreenSize'\nimport { isSnakeCaseEventName } from './utils/isSnakeCaseEventName'\nimport { pushDataLayer, resetDataLayerEcommerce } from './utils/pushDataLayer'\nimport { parseRetryAfterMs } from './utils/retry'\nimport { truncate } from './utils/truncate'\nimport {\n\tImproveAnalyticPayload,\n\tImproveEnvironmentOption,\n\tImproveEventName,\n\tImproveSetupArgs,\n} from './types'\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\nexport type CreateAnalytic = {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption\n\n\ttestId: string\n\ttestValue: string\n\tvisitorId: string\n\n\tpointer: string\n\tdevice: string\n\tscreen: string\n\tbrowser: string\n\tos: string\n\tvisitor: string\n\n\tevent: string\n\tmessage: string\n\n\t/** GA4-aligned numeric value aggregated into revenue / AOV per variant. */\n\tvalue?: number\n\t/** ISO 4217 currency for `value`. */\n\tcurrency?: string\n\t/** Extra event params stored as JSON (e.g. a GA4 `ecommerce` object). */\n\tparams?: Record<string, unknown>\n}\n\ntype TrackedAnalytics = {\n\t[TestSlug: string]: {\n\t\t[Event: string]: boolean\n\t}\n}\n\nexport class ImproveClientSDK extends BaseImproveSDK {\n\t#visitor: Visitor | undefined\n\t#visitorRecurring: boolean = false\n\n\t#visitorId: string = ''\n\n\t#analytics: TrackedAnalytics = {}\n\n\t// Epoch ms until which analytics posting is suspended after a 429.\n\t#rateLimitedUntil: number = 0\n\n\t// Event names already warned about, so the snake_case nudge fires once per\n\t// offending name instead of on every call.\n\t#warnedEventNames = new Set<string>()\n\n\t#analyticsUrl = `${BASE_URL}${ANALYTICS_PATH}`\n\n\t#dataLayerEnabled: boolean = true\n\n\tfetchConfig = this._fetchConfig\n\n\tconstructor(args: ImproveSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#analyticsUrl = `${this._baseUrl}${ANALYTICS_PATH}`\n\t\tthis.#dataLayerEnabled = args.dataLayer ?? true\n\t}\n\n\tsetupVisitor = (userAgent: string = window.navigator.userAgent) => {\n\t\tconst cookieVisitorId = getCookie(this.getVisitorCookieName())\n\t\tconst validCookieVisitorId =\n\t\t\tcookieVisitorId && this.validateVisitorId(cookieVisitorId)\n\n\t\tthis.#visitorRecurring = validCookieVisitorId\n\t\tthis.#visitorId = validCookieVisitorId\n\t\t\t? cookieVisitorId\n\t\t\t: this.generateVisitorId()\n\n\t\tconst parsedUserAgent = parseUserAgent(userAgent)\n\n\t\tif (!parsedUserAgent) return null\n\n\t\tthis.#visitor = parsedUserAgent\n\n\t\tsetCookie(this.getVisitorCookieName(), this.#visitorId)\n\n\t\treturn this.#visitorId\n\t}\n\n\tgetFlagValue = (flagSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst flagConfig = this.config.flags[flagSlug]\n\n\t\tif (!flagConfig || !flagConfig.options[0]) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!this.#visitorId || !this.#visitor) return flagConfig.options[0].slug\n\t\tif (this.#visitor?.[flagSlug]) return this.#visitor[flagSlug]\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue =\n\t\t\tgetCookie(flagSlug) || getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tthis.#visitor[flagSlug] = flagValue\n\n\t\tsetCookie(flagSlug, flagValue)\n\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!testConfig) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\tif (!this.#visitorId || !this.#visitor) return testConfig.defaultValue\n\t\tif (this.#visitor?.[testSlug]) return this.#visitor[testSlug]\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tthis.#visitor[testSlug] = testConfig.defaultValue\n\t\t\treturn this.#visitor?.[testSlug]\n\t\t}\n\n\t\tconst cookieTestValue = getCookie(testSlug)\n\t\tconst validCookieTestValue =\n\t\t\tcookieTestValue && this.validateTestValue(testSlug, cookieTestValue)\n\t\tconst testValue = validCookieTestValue\n\t\t\t? cookieTestValue\n\t\t\t: getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tthis.#visitor[testSlug] = testValue\n\n\t\tsetCookie(testSlug, testValue)\n\n\t\treturn testValue\n\t}\n\n\tsetAnalyticsUrls = (url: string) => {\n\t\tthis.#analyticsUrl = url\n\t}\n\n\tpostAnalytic = (\n\t\ttestSlug: string,\n\t\tevent: ImproveEventName,\n\t\t// Accepts a structured payload, or a plain string as shorthand for the\n\t\t// `message`. Backwards compatible with the previous `message?: string`.\n\t\tpayload?: ImproveAnalyticPayload | string,\n\t) => {\n\t\tif (!this.config) return null\n\n\t\t// GTM reserves the `gtm.*` namespace for its own events; never emit into\n\t\t// it, or the mirrored dataLayer entry would collide with GTM internals.\n\t\tif (event.startsWith('gtm.')) return null\n\n\t\t// Nudge developers toward the snake_case / GA4 naming convention. Warn\n\t\t// once per offending name so it's noticeable without spamming the console;\n\t\t// silence entirely with the `disableWarnings` setup option.\n\t\tif (!isSnakeCaseEventName(event) && !this.#warnedEventNames.has(event)) {\n\t\t\tthis.#warnedEventNames.add(event)\n\t\t\tthis._warn(\n\t\t\t\t`Event name \"${event}\" isn't snake_case. Prefer GA4-style names ` +\n\t\t\t\t\t`like \"add_to_cart\" or \"purchase\" so your events line up with ` +\n\t\t\t\t\t`GA4 / Google Tag Manager. Pass \\`disableWarnings: true\\` at setup ` +\n\t\t\t\t\t`to silence this.`,\n\t\t\t)\n\t\t}\n\n\t\t// Suspended after a 429: skip sending, but don't mark the event as\n\t\t// tracked, so it can still fire once the cooldown elapses.\n\t\tif (Date.now() < this.#rateLimitedUntil) return null\n\n\t\tconst { value, currency, message, params }: ImproveAnalyticPayload =\n\t\t\ttypeof payload === 'string' ? { message: payload } : (payload ?? {})\n\t\tconst hasValue = typeof value === 'number' && Number.isFinite(value)\n\t\t// The backend requires `params` to be a plain (non-array) object and 400s\n\t\t// the whole event otherwise — drop an invalid value rather than lose the\n\t\t// event.\n\t\tconst validParams =\n\t\t\tparams && typeof params === 'object' && !Array.isArray(params)\n\t\t\t\t? params\n\t\t\t\t: undefined\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!testConfig || !this.#visitor || this.#analytics?.[testSlug]?.[event]) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst testSlugAnalytics = this.#analytics[testSlug] || {}\n\t\ttestSlugAnalytics[event] = true\n\t\tthis.#analytics[testSlug] = testSlugAnalytics\n\n\t\tlet testValue = this.#visitor?.[testSlug] || null\n\t\tif (!testValue) {\n\t\t\ttestValue = this.getTestValue(testSlug) || null\n\t\t}\n\n\t\tif (!testValue) return\n\n\t\tconst body: CreateAnalytic = {\n\t\t\torganizationId: this.organizationId,\n\t\t\tenvironment: this.environment,\n\n\t\t\ttestId: testConfig.id,\n\t\t\ttestValue: testValue,\n\t\t\tvisitorId: this.#visitorId,\n\t\t\tpointer: this.#visitor.pointer,\n\t\t\tdevice: this.#visitor.device,\n\t\t\tscreen: getScreenSize(),\n\t\t\tbrowser: this.#visitor.browser,\n\t\t\tos: this.#visitor.os,\n\t\t\tvisitor: this.#visitorRecurring ? 'recurring' : 'new',\n\t\t\t// Cap developer-controlled fields to the backend's varchar(256) limit;\n\t\t\t// an over-length value is otherwise rejected with a 400 and the event\n\t\t\t// is lost.\n\t\t\tevent: truncate(event, MAX_ANALYTIC_FIELD_LENGTH),\n\t\t\tmessage: truncate(message || '', MAX_ANALYTIC_FIELD_LENGTH),\n\t\t\t...(hasValue ? { value } : {}),\n\t\t\t...(currency\n\t\t\t\t? { currency: truncate(currency, MAX_CURRENCY_FIELD_LENGTH) }\n\t\t\t\t: {}),\n\t\t\t...(validParams ? { params: validParams } : {}),\n\t\t}\n\n\t\tif (this.#dataLayerEnabled) {\n\t\t\t// Clear any prior ecommerce object first so its keys don't bleed into\n\t\t\t// this event (GTM/GA4 best practice).\n\t\t\tif (validParams && 'ecommerce' in validParams) resetDataLayerEcommerce()\n\n\t\t\tpushDataLayer({\n\t\t\t\tevent,\n\t\t\t\timprove: {\n\t\t\t\t\ttest: testSlug,\n\t\t\t\t\tvariant: testValue,\n\t\t\t\t\tvisitorId: this.#visitorId,\n\t\t\t\t},\n\t\t\t\t...(hasValue ? { value } : {}),\n\t\t\t\t...(currency ? { currency } : {}),\n\t\t\t\t// Spread custom params (incl. a GA4 `ecommerce` object) to the top\n\t\t\t\t// level so GA4/GTM tags can read them directly.\n\t\t\t\t...(validParams ?? {}),\n\t\t\t\t_improve: true,\n\t\t\t})\n\t\t}\n\n\t\t// Serialize once so we can both measure and send the same bytes.\n\t\tconst serializedBody = JSON.stringify(body)\n\n\t\t// The backend rejects bodies over 8KB with a 413 — realistically only an\n\t\t// oversized `params` (e.g. a large GA4 `ecommerce.items` array) gets there.\n\t\t// Warn so the dropped event isn't silent; still attempt the send in case\n\t\t// the server limit differs.\n\t\tif (\n\t\t\ttypeof TextEncoder !== 'undefined' &&\n\t\t\tnew TextEncoder().encode(serializedBody).length > MAX_ANALYTIC_BODY_BYTES\n\t\t) {\n\t\t\tthis._warn(\n\t\t\t\t`Analytic for \"${event}\" exceeds the ${MAX_ANALYTIC_BODY_BYTES}-byte ` +\n\t\t\t\t\t`limit and will be rejected by the server — trim the \\`params\\` payload.`,\n\t\t\t)\n\t\t}\n\n\t\t// `keepalive` lets the beacon outlive the page: conversion events are\n\t\t// often fired immediately before a navigation/unload, and a normal fetch\n\t\t// would be cancelled with the document. The 8KB body cap is well within\n\t\t// the browser's 64KB keepalive budget.\n\t\tconst request = fetch(this.#analyticsUrl, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: serializedBody,\n\t\t\tkeepalive: true,\n\t\t})\n\n\t\t// Back off when the org hits its usage/rate limit. Honor a server\n\t\t// Retry-After when present, otherwise use a fixed cooldown.\n\t\trequest\n\t\t\t.then((res) => {\n\t\t\t\tif (res?.status === 429) {\n\t\t\t\t\tconst retryAfterMs = parseRetryAfterMs(res.headers.get('Retry-After'))\n\t\t\t\t\tthis.#rateLimitedUntil =\n\t\t\t\t\t\tDate.now() + (retryAfterMs ?? ANALYTIC_RATE_LIMIT_COOLDOWN_MS)\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {})\n\n\t\treturn request\n\t}\n}\n","type SizeOptions = 'small' | 'medium' | 'large' | 'huge'\n\nexport const getScreenSize = (): SizeOptions => {\n\tconst size = window.innerWidth\n\tif (size <= 768) return 'small'\n\tif (size <= 1024) return 'medium'\n\tif (size <= 1200) return 'large'\n\treturn 'huge'\n}\n","import { ImproveEventName } from '../types'\n\nexport type ImproveDataLayerEntry = {\n\tevent: ImproveEventName\n\timprove: {\n\t\ttest: string\n\t\tvariant: string\n\t\tvisitorId: string\n\t}\n\t/** GA4 numeric value / currency, when provided on the analytic. */\n\tvalue?: number\n\tcurrency?: string\n\t/**\n\t * Marks the entry as originating from Improve so platform-side dataLayer\n\t * importers can ignore it (loop prevention).\n\t */\n\t_improve: true\n\t/** Any extra event params (e.g. a GA4 `ecommerce` object) spread onto the entry. */\n\t[param: string]: unknown\n}\n\ndeclare global {\n\tinterface Window {\n\t\tdataLayer?: Record<string, unknown>[]\n\t}\n}\n\n/**\n * Mirror an analytic onto the GTM dataLayer (with experiment dimensions) so it\n * can drive Google Tag Manager / Google Ads conversions. No-op outside the\n * browser. Initializes window.dataLayer if GTM hasn't yet.\n */\nexport const pushDataLayer = (entry: ImproveDataLayerEntry) => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push(entry)\n}\n\n/**\n * Clear the dataLayer `ecommerce` object before pushing an ecommerce event, so\n * leftover keys (old `items`, a stale `coupon`…) from a previous event don't\n * bleed into the next one — the GTM/GA4 recommended reset. No-op outside the\n * browser.\n */\nexport const resetDataLayerEcommerce = () => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push({ ecommerce: null })\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","length","slug","value","Math","random","reduce","acc","split","match","find","VISITOR_ID_PREFIX","BASE_URL","ANALYTICS_PATH","delay","ms","Promise","resolve","setTimeout","REASON_MESSAGES","timeout","network","unauthorized","server","client","ImproveFetchError","Error","reason","name","status","retryAfterMs","cause","isRetryable","getReasonFromStatus","CHAR_SET","normalizeConfig","body","Array","isArray","raw","version","tests","flags","parseRetryAfterMs","header","now","Date","seconds","Number","isFinite","max","round","date","parse","isNaN","getBackoffDelayMs","attempt","baseDelay","maxDelay","min","getTimeoutError","controller","abort","timeoutFetch","url","AbortController","race","fetch","signal","BaseImproveSDK","organizationId","environment","state","config","fetchTimeout","baseUrl","configRetries","configRevalidate","disableWarnings","_fetchConfig","retries","revalidate","requestInit","next","lastError","res","aborted","ok","json","headers","get","loadConfig","generateVisitorId","getRandomString","characters","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","_warn","message","console","warn","validateTestValue","testName","testValue","testConfig","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl","getCookie","cookie","document","row","setCookie","setDate","getDate","toUTCString","SNAKE_CASE_EVENT_NAME","truncate","maxLength","slice","args","Set","fetchConfig","setupVisitor","userAgent","window","navigator","cookieVisitorId","validCookieVisitorId","parsedUserAgent","parseUserAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType","getFlagValue","flagSlug","flagConfig","flagValue","getTestValue","testSlug","defaultValue","allocation","cookieTestValue","validCookieTestValue","setAnalyticsUrls","postAnalytic","event","payload","size","startsWith","test","has","add","currency","params","hasValue","validParams","undefined","testSlugAnalytics","testId","id","visitorId","screen","innerWidth","visitor","entry","dataLayer","push","ecommerce","improve","variant","_improve","serializedBody","JSON","stringify","TextEncoder","encode","request","method","keepalive","then","ANALYTIC_RATE_LIMIT_COOLDOWN_MS","catch"],"mappings":"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"}
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as __types from './types.js';
2
- import { ImproveEnvironmentOption, ImproveTestState, ImproveConfiguration, ImproveSetupArgs } from './types.js';
2
+ import { ImproveEnvironmentOption, ImproveTestState, ImproveConfiguration, ImproveSetupArgs, ImproveEventName, ImproveAnalyticPayload } from './types.js';
3
3
 
4
4
  declare class BaseImproveSDK {
5
5
  #private;
@@ -8,11 +8,17 @@ declare class BaseImproveSDK {
8
8
  state: ImproveTestState;
9
9
  config: ImproveConfiguration | null;
10
10
  _baseUrl: undefined | string;
11
- constructor({ organizationId, environment, state, config, fetchTimeout, baseUrl, }: ImproveSetupArgs);
11
+ constructor({ organizationId, environment, state, config, fetchTimeout, baseUrl, configRetries, configRevalidate, disableWarnings, }: ImproveSetupArgs);
12
12
  _fetchConfig: (config?: RequestInit) => Promise<ImproveConfiguration>;
13
13
  loadConfig: (config: ImproveConfiguration) => void;
14
14
  generateVisitorId: () => string;
15
15
  getVisitorCookieName: () => string;
16
+ /**
17
+ * Emit a development warning, unless warnings were disabled via the
18
+ * `disableWarnings` setup option. Centralized so every SDK warning honors the
19
+ * same switch. No-op where `console` is unavailable.
20
+ */
21
+ protected _warn: (message: string) => void;
16
22
  validateTestValue: (testName: string, testValue: string) => boolean;
17
23
  validateVisitorId: (possibleVisitorId: string) => boolean;
18
24
  }
@@ -31,6 +37,12 @@ type CreateAnalytic = {
31
37
  visitor: string;
32
38
  event: string;
33
39
  message: string;
40
+ /** GA4-aligned numeric value aggregated into revenue / AOV per variant. */
41
+ value?: number;
42
+ /** ISO 4217 currency for `value`. */
43
+ currency?: string;
44
+ /** Extra event params stored as JSON (e.g. a GA4 `ecommerce` object). */
45
+ params?: Record<string, unknown>;
34
46
  };
35
47
  declare class ImproveClientSDK extends BaseImproveSDK {
36
48
  #private;
@@ -40,7 +52,7 @@ declare class ImproveClientSDK extends BaseImproveSDK {
40
52
  getFlagValue: (flagSlug: string) => string;
41
53
  getTestValue: (testSlug: string) => string;
42
54
  setAnalyticsUrls: (url: string) => void;
43
- postAnalytic: (testSlug: string, event: string, message?: string) => Promise<Response>;
55
+ postAnalytic: (testSlug: string, event: ImproveEventName, payload?: ImproveAnalyticPayload | string) => Promise<Response>;
44
56
  }
45
57
 
46
58
  export { ImproveClientSDK };
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sources":["../src/base.ts","../src/client.ts"],"mappings":";;;AACA,cAAc,cAAc;AAC5B;AACA;AACA,iBAAiB,wBAAwB;AACzC,WAAW,gBAAgB;AAC3B,YAAY,oBAAoB;AAChC;AACA,wFAAwF,gBAAgB;AACxG,4BAA4B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACxE,yBAAyB,oBAAoB;AAC7C;AACA;AACA;AACA;AACA;;ACZA,KAAK,cAAc;AACnB;AACA,iBAAiB,wBAAwB;AACzC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,gBAAgB,SAAS,cAAc;AACrD;AACA,2BAA2B,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,oBAAoB;AAC/E,sBAAsB,gBAAgB;AACtC;AACA;AACA;AACA;AACA,yEAAyE,OAAO,CAAC,QAAQ;AACzF;;;;","names":[]}
1
+ {"version":3,"file":"client.d.ts","sources":["../src/base.ts","../src/client.ts"],"mappings":";;;AACA,cAAc,cAAc;AAC5B;AACA;AACA,iBAAiB,wBAAwB;AACzC,WAAW,gBAAgB;AAC3B,YAAY,oBAAoB;AAChC;AACA,0IAA0I,gBAAgB;AAC1J,4BAA4B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACxE,yBAAyB,oBAAoB;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AClBA,KAAK,cAAc;AACnB;AACA,iBAAiB,wBAAwB;AACzC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA,cAAc,gBAAgB,SAAS,cAAc;AACrD;AACA,2BAA2B,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,oBAAoB;AAC/E,sBAAsB,gBAAgB;AACtC;AACA;AACA;AACA;AACA,4CAA4C,gBAAgB,YAAY,sBAAsB,cAAc,OAAO,CAAC,QAAQ;AAC5H;;;;","names":[]}
package/dist/client.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{UAParser as i}from"ua-parser-js";let t=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],e=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],s=["mac os","ios","android","windows","unix"],r=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],o=["wearable","mobile","tablet"],n=(i,t)=>!i||Object.entries(i).every(([i,e])=>t[i]===e),a=i=>{if(0===i.length)return null;if(1===i.length)return i[0].slug;let t=Math.random()*i.reduce((i,{split:t})=>i+t,0);return(i.find(({split:i})=>(t-=i)<=0)||i[0]).slug},l="visi",h="https://improve.obelism.studio",u="/api/log",c="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",d=async(i=1e3,t)=>(await new Promise(t=>setTimeout(t,i)),t?.abort(),null);class f{#i;constructor({organizationId:i,environment:t,state:e,config:s,fetchTimeout:r,baseUrl:o}){this.environment="develop",this.#i=null,this.config=null,this._fetchConfig=async i=>{if(this.config)return;if(!this.#i)throw Error("No config fetch setup provided");let t=await ((i=3e3,t,e)=>{let s=new AbortController;return Promise.race([fetch(t,{...e,signal:s.signal}),d(i,s)])})(this.#i.timeout,this.#i.url,i);if(!t||!t.ok)throw Error("Configuration fetch timed-out");return this.config=await t.json(),this.config},this.loadConfig=i=>{this.config=i},this.generateVisitorId=()=>[l,(function(i=5){return i&&"number"==typeof i?Array(i).fill("").reduce(i=>i+=c.charAt(Math.floor(Math.random()*c.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this.validateTestValue=(i,t)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let e=this.config.tests[i];if(!e)throw Error(`No config found for ${i}`);return!!e.options.find(i=>i.slug===t)},this.validateVisitorId=i=>{let t=i.split("_");if(2!==t.length)return!1;let[e,s]=t;return e===l&&26===s.length},this.organizationId=i,this.environment=t,this.state=e,this._baseUrl=o||h,s?this.config=s:this.#i={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:r||3e3}}}let g=i=>{if(!i)return!1;let t=document.cookie.split("; ").find(t=>{let[e]=t.split("=");return i===e});return!!t&&t.split("=")[1]},v=(i,t)=>{let e=new Date;e.setDate(e.getDate()+30),document.cookie=`${i}=${t};path=/;expires=${e.toUTCString()};SameSite=Lax;Secure`};class p extends f{#t;#e;#s;#r;#o;#n;constructor(l){super(l),this.#e=!1,this.#s="",this.#r={},this.#o=`${h}${u}`,this.#n=!0,this.fetchConfig=this._fetchConfig,this.setupVisitor=(n=window.navigator.userAgent)=>{let a=g(this.getVisitorCookieName()),l=a&&this.validateVisitorId(a);this.#e=l,this.#s=l?a:this.generateVisitorId();let h=(n=>{var a;if(!n||"string"!=typeof n)return null;let l=new i(n).getResult(),h=(a=l.device.type)&&t.includes(a)?a:"desktop";return{pointer:o.includes(h)?"coarse":"fine",device:h,browser:((i="")=>{if(!i)return"other";let t=i.toLowerCase(),s=e.find(i=>t.includes(i));return s||(r.includes(t)?"social":"other")})(l.browser.name),os:((i="")=>{if(!i)return"unix";let t=i.toLowerCase(),e=s.find(i=>t.includes(i));return e||"unix"})(l.os.name)}})(n);return h?(this.#t=h,v(this.getVisitorCookieName(),this.#s),this.#s):null},this.getFlagValue=i=>{if(!this.config)return null;let t=this.config.flags[i];if(!t||!t.options[0])return null;if(this.#t||this.setupVisitor(),!this.#s||!this.#t)return t.options[0].slug;if(this.#t?.[i])return this.#t[i];if(!n(this.config.audience[t.audience],this.#t))return t.options[0].slug;let e=g(i)||a(t.options);return e?(this.#t[i]=e,v(i,e),e):null},this.getTestValue=i=>{if(!this.config)return null;let t=this.config.tests[i];if(!t)return null;if(this.#t||this.setupVisitor(),!this.#s||!this.#t)return t.defaultValue;if(this.#t?.[i])return this.#t[i];if(!n(this.config.audience[t.audience],this.#t))return t.defaultValue;if(t.allocation<100&&100*Math.random()>t.allocation)return this.#t[i]=t.defaultValue,this.#t?.[i];let e=g(i),s=e&&this.validateTestValue(i,e)?e:a(t.options);return s?(this.#t[i]=s,v(i,s),s):null},this.setAnalyticsUrls=i=>{this.#o=i},this.postAnalytic=(i,t,e)=>{var s;let r;if(!this.config)return null;let o=this.config.tests[i];if(this.#t||this.setupVisitor(),!o||!this.#t||this.#r?.[i]?.[t])return null;let n=this.#r[i]||{};n[t]=!0,this.#r[i]=n;let a=this.#t?.[i]||null;if(a||(a=this.getTestValue(i)||null),!a)return;let l={organizationId:this.organizationId,environment:this.environment,testId:o.id,testValue:a,visitorId:this.#s,pointer:this.#t.pointer,device:this.#t.device,screen:(r=window.innerWidth)<=768?"small":r<=1024?"medium":r<=1200?"large":"huge",browser:this.#t.browser,os:this.#t.os,visitor:this.#e?"recurring":"new",event:t,message:e||""};return this.#n&&(s={event:t,improve:{test:i,variant:a,visitorId:this.#s},_improve:!0},"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push(s))),fetch(this.#o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)})},this.#o=`${this._baseUrl}${u}`,this.#n=l.dataLayer??!0}}export{p as ImproveClientSDK};
1
+ import{UAParser as t}from"ua-parser-js";let e=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],i=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],r=["mac os","ios","android","windows","unix"],s=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],n=["wearable","mobile","tablet"],o=(t,e)=>!t||Object.entries(t).every(([t,i])=>e[t]===i),a=t=>{if(0===t.length)return null;if(1===t.length)return t[0].slug;let e=Math.random()*t.reduce((t,{split:e})=>t+e,0);return(t.find(({split:t})=>(e-=t)<=0)||t[0]).slug},l="visi",h="https://improve.obelism.studio",u="/api/log",d=t=>new Promise(e=>setTimeout(e,t)),c={timeout:"Configuration request timed out",network:"Configuration request could not reach the server",unauthorized:"Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment","rate-limited":"Configuration request was rate limited (429): too many requests, or the organization usage limit was reached",server:"Configuration request failed with a server error",client:"Configuration request was rejected by the server","invalid-response":"Configuration response could not be parsed as JSON"};class f extends Error{constructor(t,e){super(c[t]),this.name="ImproveFetchError",this.reason=t,this.status=e?.status,this.retryAfterMs=e?.retryAfterMs,e&&"cause"in e&&(this.cause=e.cause)}get isRetryable(){return"rate-limited"===this.reason||"server"===this.reason||"timeout"===this.reason||"network"===this.reason}}let g=t=>429===t?"rate-limited":401===t||403===t?"unauthorized":t>=500?"server":"client",v="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",m=t=>{if("object"!=typeof t||null===t||Array.isArray(t))throw new f("invalid-response");return{name:"string"==typeof t.name?t.name:"",version:"number"==typeof t.version?t.version:0,tests:t.tests??{},flags:t.flags??{},audience:t.audience??{}}},w=(t,e=Date.now())=>{if(!t)return;let i=Number(t);if(Number.isFinite(i))return Math.max(0,Math.round(1e3*i));let r=Date.parse(t);if(!Number.isNaN(r))return Math.max(0,r-e)},p=(t,e,i)=>Math.round(Math.random()*Math.min(i,e*2**t)),y=async(t=1e3,e)=>(await d(t),e?.abort(),null),b=(t=3e3,e,i)=>{let r=new AbortController;return Promise.race([fetch(e,{...i,signal:r.signal}),y(t,r)])};class I{#t;#e;constructor({organizationId:t,environment:e,state:i,config:r,fetchTimeout:s,baseUrl:n,configRetries:o,configRevalidate:a,disableWarnings:u}){this.environment="develop",this.#t=null,this.#e=!1,this.config=null,this._fetchConfig=async t=>{if(this.config)return;if(!this.#t)throw Error("No config fetch setup provided");let{url:e,timeout:i,retries:r,revalidate:s}=this.#t,n={...t};"number"==typeof s&&(n.next={...t?.next,revalidate:s});let o=new f("network");for(let t=0;t<=r;t++){let r;t>0&&await d(o.retryAfterMs??p(t-1,300,3e3));try{r=await b(i,e,n)}catch(t){o=new f(t instanceof Error&&"AbortError"===t.name?"timeout":"network",{cause:t});continue}if(!r){o=new f("timeout");continue}if(r.ok){try{this.config=m(await r.json())}catch(t){o=t instanceof f?t:new f("invalid-response",{cause:t});continue}return this.config}if(!(o=new f(g(r.status),{status:r.status,retryAfterMs:w(r.headers.get("Retry-After"))})).isRetryable)break}throw o},this.loadConfig=t=>{this.config=t},this.generateVisitorId=()=>[l,(function(t=5){return t&&"number"==typeof t?Array(t).fill("").reduce(t=>t+=v.charAt(Math.floor(Math.random()*v.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this._warn=t=>{this.#e||"u">typeof console&&console.warn(`[Improve] ${t}`)},this.validateTestValue=(t,e)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let i=this.config.tests[t];if(!i)throw Error(`No config found for ${t}`);return!!i.options.find(t=>t.slug===e)},this.validateVisitorId=t=>{let e=t.split("_");if(2!==e.length)return!1;let[i,r]=e;return i===l&&26===r.length},this.organizationId=t,this.environment=e,this.state=i,this._baseUrl=n||h,this.#e=u??!1,r?this.config=r:this.#t={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:s||3e3,retries:o??2,revalidate:a}}}let C=t=>{if(!t)return!1;let e=document.cookie.split("; ").find(e=>{let[i]=e.split("=");return t===i});return!!e&&e.split("=")[1]},k=(t,e)=>{let i=new Date;i.setDate(i.getDate()+30),document.cookie=`${t}=${e};path=/;expires=${i.toUTCString()};SameSite=Lax;Secure`},A=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,V=(t,e)=>"string"==typeof t&&t.length>e?t.slice(0,e):t;class L extends I{#i;#r;#s;#n;#o;#a;#l;#h;constructor(l){super(l),this.#r=!1,this.#s="",this.#n={},this.#o=0,this.#a=new Set,this.#l=`${h}${u}`,this.#h=!0,this.fetchConfig=this._fetchConfig,this.setupVisitor=(o=window.navigator.userAgent)=>{let a=C(this.getVisitorCookieName()),l=a&&this.validateVisitorId(a);this.#r=l,this.#s=l?a:this.generateVisitorId();let h=(o=>{var a;if(!o||"string"!=typeof o)return null;let l=new t(o).getResult(),h=(a=l.device.type)&&e.includes(a)?a:"desktop";return{pointer:n.includes(h)?"coarse":"fine",device:h,browser:((t="")=>{if(!t)return"other";let e=t.toLowerCase(),r=i.find(t=>e.includes(t));return r||(s.includes(e)?"social":"other")})(l.browser.name),os:((t="")=>{if(!t)return"unix";let e=t.toLowerCase(),i=r.find(t=>e.includes(t));return i||"unix"})(l.os.name)}})(o);return h?(this.#i=h,k(this.getVisitorCookieName(),this.#s),this.#s):null},this.getFlagValue=t=>{if(!this.config)return null;let e=this.config.flags[t];if(!e||!e.options[0])return null;if(this.#i||this.setupVisitor(),!this.#s||!this.#i)return e.options[0].slug;if(this.#i?.[t])return this.#i[t];if(!o(this.config.audience[e.audience],this.#i))return e.options[0].slug;let i=C(t)||a(e.options);return i?(this.#i[t]=i,k(t,i),i):null},this.getTestValue=t=>{if(!this.config)return null;let e=this.config.tests[t];if(!e)return null;if(this.#i||this.setupVisitor(),!this.#s||!this.#i)return e.defaultValue;if(this.#i?.[t])return this.#i[t];if(!o(this.config.audience[e.audience],this.#i))return e.defaultValue;if(e.allocation<100&&100*Math.random()>e.allocation)return this.#i[t]=e.defaultValue,this.#i?.[t];let i=C(t),r=i&&this.validateTestValue(t,i)?i:a(e.options);return r?(this.#i[t]=r,k(t,r),r):null},this.setAnalyticsUrls=t=>{this.#l=t},this.postAnalytic=(t,e,i)=>{let r;if(!this.config||e.startsWith("gtm."))return null;if(A.test(e)||this.#a.has(e)||(this.#a.add(e),this._warn(`Event name "${e}" isn't snake_case. Prefer GA4-style names like "add_to_cart" or "purchase" so your events line up with GA4 / Google Tag Manager. Pass \`disableWarnings: true\` at setup to silence this.`)),Date.now()<this.#o)return null;let{value:s,currency:n,message:o,params:a}="string"==typeof i?{message:i}:i??{},l="number"==typeof s&&Number.isFinite(s),h=a&&"object"==typeof a&&!Array.isArray(a)?a:void 0,u=this.config.tests[t];if(this.#i||this.setupVisitor(),!u||!this.#i||this.#n?.[t]?.[e])return null;let d=this.#n[t]||{};d[e]=!0,this.#n[t]=d;let c=this.#i?.[t]||null;if(c||(c=this.getTestValue(t)||null),!c)return;let f={organizationId:this.organizationId,environment:this.environment,testId:u.id,testValue:c,visitorId:this.#s,pointer:this.#i.pointer,device:this.#i.device,screen:(r=window.innerWidth)<=768?"small":r<=1024?"medium":r<=1200?"large":"huge",browser:this.#i.browser,os:this.#i.os,visitor:this.#r?"recurring":"new",event:V(e,256),message:V(o||"",256),...l?{value:s}:{},...n?{currency:V(n,8)}:{},...h?{params:h}:{}};if(this.#h){var g;h&&"ecommerce"in h&&"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push({ecommerce:null})),g={event:e,improve:{test:t,variant:c,visitorId:this.#s},...l?{value:s}:{},...n?{currency:n}:{},...h??{},_improve:!0},"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push(g))}let v=JSON.stringify(f);"u">typeof TextEncoder&&new TextEncoder().encode(v).length>8192&&this._warn(`Analytic for "${e}" exceeds the 8192-byte limit and will be rejected by the server — trim the \`params\` payload.`);let m=fetch(this.#l,{method:"POST",headers:{"Content-Type":"application/json"},body:v,keepalive:!0});return m.then(t=>{if(t?.status===429){let e=w(t.headers.get("Retry-After"));this.#o=Date.now()+(e??6e4)}}).catch(()=>{}),m},this.#l=`${this._baseUrl}${u}`,this.#h=l.dataLayer??!0}}export{L as ImproveClientSDK};
2
2
  //# sourceMappingURL=client.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/config/urls.ts","../src/utils/getRandomString.ts","../src/utils/getTimeoutError.ts","../src/utils/delay.ts","../src/base.ts","../src/utils/timeoutFetch.ts","../src/utils/clientCookie.ts","../src/client.ts","../src/utils/pushDataLayer.ts","../src/utils/getScreenSize.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tif (options.length === 0) return null\n\tif (options.length === 1) return options[0].slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || options[0]\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\nexport const ANALYTICS_PATH = `/api/log`\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","import {\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { getRandomString } from './utils/getRandomString'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState\n\n\t#configFetch: ConfigFetch | null = null\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst res = await timeoutFetch(\n\t\t\tthis.#configFetch.timeout,\n\t\t\tthis.#configFetch.url,\n\t\t\tconfig,\n\t\t)\n\t\tif (!res || !res.ok) throw new Error('Configuration fetch timed-out')\n\n\t\tthis.config = await res.json()\n\t\treturn this.config\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","const ROW_DELIMITER = '; '\nconst ENTRY_DELIMITER = '='\n\nexport const getCookie = (name: string) => {\n\tif (!name) return false\n\tconst cookie = document.cookie.split(ROW_DELIMITER).find((row) => {\n\t\tconst [key] = row.split(ENTRY_DELIMITER)\n\t\treturn name === key\n\t})\n\treturn cookie ? cookie.split(ENTRY_DELIMITER)[1] : false\n}\n\nexport const setCookie = (name: string, value: string) => {\n\tconst now = new Date()\n\tnow.setDate(now.getDate() + 30)\n\tdocument.cookie = `${name}=${value};path=/;expires=${now.toUTCString()};SameSite=Lax;Secure`\n}\n","import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { getCookie, setCookie } from './utils/clientCookie'\nimport { ANALYTICS_PATH, BASE_URL } from './config/urls'\nimport { getScreenSize } from './utils/getScreenSize'\nimport { pushDataLayer } from './utils/pushDataLayer'\nimport { ImproveEnvironmentOption, ImproveSetupArgs } from './types'\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\nexport type CreateAnalytic = {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption\n\n\ttestId: string\n\ttestValue: string\n\tvisitorId: string\n\n\tpointer: string\n\tdevice: string\n\tscreen: string\n\tbrowser: string\n\tos: string\n\tvisitor: string\n\n\tevent: string\n\tmessage: string\n}\n\ntype TrackedAnalytics = {\n\t[TestSlug: string]: {\n\t\t[Event: string]: boolean\n\t}\n}\n\nexport class ImproveClientSDK extends BaseImproveSDK {\n\t#visitor: Visitor | undefined\n\t#visitorRecurring: boolean = false\n\n\t#visitorId: string = ''\n\n\t#analytics: TrackedAnalytics = {}\n\n\t#analyticsUrl = `${BASE_URL}${ANALYTICS_PATH}`\n\n\t#dataLayerEnabled: boolean = true\n\n\tfetchConfig = this._fetchConfig\n\n\tconstructor(args: ImproveSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#analyticsUrl = `${this._baseUrl}${ANALYTICS_PATH}`\n\t\tthis.#dataLayerEnabled = args.dataLayer ?? true\n\t}\n\n\tsetupVisitor = (userAgent: string = window.navigator.userAgent) => {\n\t\tconst cookieVisitorId = getCookie(this.getVisitorCookieName())\n\t\tconst validCookieVisitorId =\n\t\t\tcookieVisitorId && this.validateVisitorId(cookieVisitorId)\n\n\t\tthis.#visitorRecurring = validCookieVisitorId\n\t\tthis.#visitorId = validCookieVisitorId\n\t\t\t? cookieVisitorId\n\t\t\t: this.generateVisitorId()\n\n\t\tconst parsedUserAgent = parseUserAgent(userAgent)\n\n\t\tif (!parsedUserAgent) return null\n\n\t\tthis.#visitor = parsedUserAgent\n\n\t\tsetCookie(this.getVisitorCookieName(), this.#visitorId)\n\n\t\treturn this.#visitorId\n\t}\n\n\tgetFlagValue = (flagSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst flagConfig = this.config.flags[flagSlug]\n\n\t\tif (!flagConfig || !flagConfig.options[0]) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!this.#visitorId || !this.#visitor) return flagConfig.options[0].slug\n\t\tif (this.#visitor?.[flagSlug]) return this.#visitor[flagSlug]\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue =\n\t\t\tgetCookie(flagSlug) || getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tthis.#visitor[flagSlug] = flagValue\n\n\t\tsetCookie(flagSlug, flagValue)\n\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!testConfig) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\tif (!this.#visitorId || !this.#visitor) return testConfig.defaultValue\n\t\tif (this.#visitor?.[testSlug]) return this.#visitor[testSlug]\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tthis.#visitor[testSlug] = testConfig.defaultValue\n\t\t\treturn this.#visitor?.[testSlug]\n\t\t}\n\n\t\tconst cookieTestValue = getCookie(testSlug)\n\t\tconst validCookieTestValue =\n\t\t\tcookieTestValue && this.validateTestValue(testSlug, cookieTestValue)\n\t\tconst testValue = validCookieTestValue\n\t\t\t? cookieTestValue\n\t\t\t: getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tthis.#visitor[testSlug] = testValue\n\n\t\tsetCookie(testSlug, testValue)\n\n\t\treturn testValue\n\t}\n\n\tsetAnalyticsUrls = (url: string) => {\n\t\tthis.#analyticsUrl = url\n\t}\n\n\tpostAnalytic = (testSlug: string, event: string, message?: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!testConfig || !this.#visitor || this.#analytics?.[testSlug]?.[event]) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst testSlugAnalytics = this.#analytics[testSlug] || {}\n\t\ttestSlugAnalytics[event] = true\n\t\tthis.#analytics[testSlug] = testSlugAnalytics\n\n\t\tlet testValue = this.#visitor?.[testSlug] || null\n\t\tif (!testValue) {\n\t\t\ttestValue = this.getTestValue(testSlug) || null\n\t\t}\n\n\t\tif (!testValue) return\n\n\t\tconst body: CreateAnalytic = {\n\t\t\torganizationId: this.organizationId,\n\t\t\tenvironment: this.environment,\n\n\t\t\ttestId: testConfig.id,\n\t\t\ttestValue: testValue,\n\t\t\tvisitorId: this.#visitorId,\n\t\t\tpointer: this.#visitor.pointer,\n\t\t\tdevice: this.#visitor.device,\n\t\t\tscreen: getScreenSize(),\n\t\t\tbrowser: this.#visitor.browser,\n\t\t\tos: this.#visitor.os,\n\t\t\tvisitor: this.#visitorRecurring ? 'recurring' : 'new',\n\t\t\tevent: event,\n\t\t\tmessage: message || '',\n\t\t}\n\n\t\tif (this.#dataLayerEnabled) {\n\t\t\tpushDataLayer({\n\t\t\t\tevent,\n\t\t\t\timprove: {\n\t\t\t\t\ttest: testSlug,\n\t\t\t\t\tvariant: testValue,\n\t\t\t\t\tvisitorId: this.#visitorId,\n\t\t\t\t},\n\t\t\t\t_improve: true,\n\t\t\t})\n\t\t}\n\n\t\treturn fetch(this.#analyticsUrl, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify(body),\n\t\t})\n\t}\n}\n","export type ImproveDataLayerEntry = {\n\tevent: string\n\timprove: {\n\t\ttest: string\n\t\tvariant: string\n\t\tvisitorId: string\n\t}\n\t/**\n\t * Marks the entry as originating from Improve so platform-side dataLayer\n\t * importers can ignore it (loop prevention).\n\t */\n\t_improve: true\n}\n\ndeclare global {\n\tinterface Window {\n\t\tdataLayer?: Record<string, unknown>[]\n\t}\n}\n\n/**\n * Mirror an analytic onto the GTM dataLayer (with experiment dimensions) so it\n * can drive Google Tag Manager / Google Ads conversions. No-op outside the\n * browser. Initializes window.dataLayer if GTM hasn't yet.\n */\nexport const pushDataLayer = (entry: ImproveDataLayerEntry) => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push(entry)\n}\n","type SizeOptions = 'small' | 'medium' | 'large' | 'huge'\n\nexport const getScreenSize = (): SizeOptions => {\n\tconst size = window.innerWidth\n\tif (size <= 768) return 'small'\n\tif (size <= 1024) return 'medium'\n\tif (size <= 1200) return 'large'\n\treturn 'huge'\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","length","slug","value","Math","random","reduce","acc","split","match","find","VISITOR_ID_PREFIX","BASE_URL","ANALYTICS_PATH","CHAR_SET","getTimeoutError","timeout","controller","Promise","resolve","setTimeout","abort","BaseImproveSDK","organizationId","environment","state","config","fetchTimeout","baseUrl","_fetchConfig","Error","res","timeoutFetch","url","AbortController","race","fetch","signal","ok","json","loadConfig","generateVisitorId","getRandomString","characters","Array","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","validateTestValue","testName","testValue","testConfig","tests","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl","getCookie","name","cookie","document","row","setCookie","now","Date","setDate","getDate","toUTCString","ImproveClientSDK","args","fetchConfig","setupVisitor","userAgent","window","navigator","cookieVisitorId","validCookieVisitorId","parsedUserAgent","parseUserAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType","getFlagValue","flagSlug","flagConfig","flags","flagValue","getTestValue","testSlug","defaultValue","allocation","cookieTestValue","validCookieTestValue","setAnalyticsUrls","postAnalytic","event","message","entry","size","testSlugAnalytics","body","testId","id","visitorId","screen","innerWidth","visitor","improve","test","variant","_improve","dataLayer","push","method","headers","JSON","stringify"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/config/urls.ts","../src/utils/delay.ts","../src/utils/errors.ts","../src/utils/getRandomString.ts","../src/utils/normalizeConfig.ts","../src/utils/retry.ts","../src/utils/getTimeoutError.ts","../src/utils/timeoutFetch.ts","../src/base.ts","../src/utils/clientCookie.ts","../src/utils/isSnakeCaseEventName.ts","../src/utils/truncate.ts","../src/client.ts","../src/utils/getScreenSize.ts","../src/utils/pushDataLayer.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tif (options.length === 0) return null\n\tif (options.length === 1) return options[0].slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || options[0]\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n\n/** Default number of retries for a failed config fetch (network / 5xx / 429). */\nexport const CONFIG_RETRY_COUNT = 2\n\n/** Base delay for config-fetch backoff, in ms. */\nexport const CONFIG_RETRY_BASE_DELAY_MS = 300\n\n/** Upper bound for a single config-fetch backoff wait, in ms. */\nexport const CONFIG_RETRY_MAX_DELAY_MS = 3000\n\n/**\n * Max length of a single analytic string field. The backend rejects any\n * field longer than this (varchar(256)) with a 400, so the client caps\n * developer-controlled values (event, message) to avoid silently dropping\n * the event.\n */\nexport const MAX_ANALYTIC_FIELD_LENGTH = 256\n\n/**\n * Max length of the analytic `currency` field. The backend column is\n * `varchar(8)` (ISO 4217 is 3 chars), and a longer value throws at insert —\n * failing the *entire* event, not just the currency — so the client caps it\n * here rather than at the generic 256-char limit.\n */\nexport const MAX_CURRENCY_FIELD_LENGTH = 8\n\n/**\n * Max size of the analytic POST body. The backend rejects anything larger with\n * a 413 (`MAX_BODY_BYTES = 8 * 1024`), so an oversized `params` payload silently\n * loses the event. Kept in sync with the backend limit.\n */\nexport const MAX_ANALYTIC_BODY_BYTES = 8 * 1024\n\n/**\n * How long to stop sending analytics after the backend returns 429 (usage\n * limit / rate limit) without a `Retry-After`. Avoids hammering an org that is\n * already over its quota.\n */\nexport const ANALYTIC_RATE_LIMIT_COOLDOWN_MS = 60_000\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\nexport const ANALYTICS_PATH = `/api/log`\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","export type ImproveFetchErrorReason =\n\t| 'timeout'\n\t| 'network'\n\t| 'unauthorized'\n\t| 'rate-limited'\n\t| 'server'\n\t| 'client'\n\t| 'invalid-response'\n\nconst REASON_MESSAGES: Record<ImproveFetchErrorReason, string> = {\n\ttimeout: 'Configuration request timed out',\n\tnetwork: 'Configuration request could not reach the server',\n\tunauthorized:\n\t\t'Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment',\n\t'rate-limited':\n\t\t'Configuration request was rate limited (429): too many requests, or the organization usage limit was reached',\n\tserver: 'Configuration request failed with a server error',\n\tclient: 'Configuration request was rejected by the server',\n\t'invalid-response': 'Configuration response could not be parsed as JSON',\n}\n\n/**\n * Error thrown by the config fetch layer. Carries the HTTP `status`, a coarse\n * `reason`, and (when the server sent one) the `Retry-After` delay in ms, so\n * callers can distinguish a misconfiguration (unauthorized) from a transient\n * failure (rate-limited/server/timeout) instead of guessing from a string.\n */\nexport class ImproveFetchError extends Error {\n\treason: ImproveFetchErrorReason\n\tstatus?: number\n\tretryAfterMs?: number\n\n\tconstructor(\n\t\treason: ImproveFetchErrorReason,\n\t\toptions?: { status?: number; retryAfterMs?: number; cause?: unknown },\n\t) {\n\t\tsuper(REASON_MESSAGES[reason])\n\t\tthis.name = 'ImproveFetchError'\n\t\tthis.reason = reason\n\t\tthis.status = options?.status\n\t\tthis.retryAfterMs = options?.retryAfterMs\n\t\tif (options && 'cause' in options) {\n\t\t\t;(this as { cause?: unknown }).cause = options.cause\n\t\t}\n\t}\n\n\t/** Whether retrying the request could plausibly succeed. */\n\tget isRetryable() {\n\t\treturn (\n\t\t\tthis.reason === 'rate-limited' ||\n\t\t\tthis.reason === 'server' ||\n\t\t\tthis.reason === 'timeout' ||\n\t\t\tthis.reason === 'network'\n\t\t)\n\t}\n}\n\n/** Map an HTTP status onto a coarse {@link ImproveFetchErrorReason}. */\nexport const getReasonFromStatus = (\n\tstatus: number,\n): ImproveFetchErrorReason => {\n\tif (status === 429) return 'rate-limited'\n\tif (status === 401 || status === 403) return 'unauthorized'\n\tif (status >= 500) return 'server'\n\treturn 'client'\n}\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { ImproveConfiguration } from '../types'\nimport { ImproveFetchError } from './errors'\n\n/**\n * @function normalizeConfig\n * @description Coerce a parsed config response into a well-formed\n * {@link ImproveConfiguration}. The backend returns `200 {}` for an unknown\n * org/environment (and on its own internal error). An empty object is truthy,\n * so it slips past `if (!this.config)` guards and then crashes downstream on\n * `config.tests[slug]` (`Cannot read properties of undefined`). Guaranteeing\n * `tests`/`flags`/`audience` are always objects lets the SDK fail open to\n * default values instead of throwing during render/middleware.\n *\n * Throws {@link ImproveFetchError} `invalid-response` when the body isn't an\n * object at all (e.g. an array, string, or null).\n */\nexport const normalizeConfig = (body: unknown): ImproveConfiguration => {\n\tif (typeof body !== 'object' || body === null || Array.isArray(body)) {\n\t\tthrow new ImproveFetchError('invalid-response')\n\t}\n\n\tconst raw = body as Partial<ImproveConfiguration>\n\n\treturn {\n\t\tname: typeof raw.name === 'string' ? raw.name : '',\n\t\tversion: typeof raw.version === 'number' ? raw.version : 0,\n\t\ttests: raw.tests ?? {},\n\t\tflags: raw.flags ?? {},\n\t\taudience: raw.audience ?? {},\n\t}\n}\n","/**\n * @function parseRetryAfterMs\n * @description Parse an HTTP `Retry-After` header into milliseconds. Supports\n * both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Returns `undefined` when the header is\n * absent or unparseable so the caller can fall back to computed backoff.\n *\n * @param {string | null} [header] - the raw `Retry-After` header value\n * @param {number} [now] - current epoch ms (injectable for testing)\n */\nexport const parseRetryAfterMs = (\n\theader: string | null | undefined,\n\tnow: number = Date.now(),\n): number | undefined => {\n\tif (!header) return undefined\n\n\tconst seconds = Number(header)\n\tif (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1000))\n\n\tconst date = Date.parse(header)\n\tif (Number.isNaN(date)) return undefined\n\n\treturn Math.max(0, date - now)\n}\n\n/**\n * @function getBackoffDelayMs\n * @description Exponential backoff with full jitter, capped at `maxDelay`.\n * Jitter spreads retries so concurrent clients don't stampede the origin in\n * lockstep after a shared failure.\n *\n * @param {number} attempt - zero-based retry attempt index\n * @param {number} baseDelay - base delay in ms\n * @param {number} maxDelay - upper bound in ms\n */\nexport const getBackoffDelayMs = (\n\tattempt: number,\n\tbaseDelay: number,\n\tmaxDelay: number,\n): number => {\n\tconst exponential = Math.min(maxDelay, baseDelay * 2 ** attempt)\n\treturn Math.round(Math.random() * exponential)\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","import {\n\tCONFIG_RETRY_BASE_DELAY_MS,\n\tCONFIG_RETRY_COUNT,\n\tCONFIG_RETRY_MAX_DELAY_MS,\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { delay } from './utils/delay'\nimport { getReasonFromStatus, ImproveFetchError } from './utils/errors'\nimport { getRandomString } from './utils/getRandomString'\nimport { normalizeConfig } from './utils/normalizeConfig'\nimport { getBackoffDelayMs, parseRetryAfterMs } from './utils/retry'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n\tretries: number\n\trevalidate?: number\n}\n\n// Next.js augments RequestInit with a `next` field for its fetch cache.\ntype RequestInitWithNext = RequestInit & {\n\tnext?: { revalidate?: number; tags?: string[] }\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState\n\n\t#configFetch: ConfigFetch | null = null\n\n\t#warningsDisabled: boolean = false\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t\tconfigRetries,\n\t\tconfigRevalidate,\n\t\tdisableWarnings,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\t\tthis.#warningsDisabled = disableWarnings ?? false\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t\tretries: configRetries ?? CONFIG_RETRY_COUNT,\n\t\t\t\trevalidate: configRevalidate,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst { url, timeout, retries, revalidate } = this.#configFetch\n\n\t\t// Opt into the Next.js fetch cache when a revalidate window is configured,\n\t\t// so the datafile is reused across requests instead of re-fetched.\n\t\tconst requestInit: RequestInitWithNext = { ...config }\n\t\tif (typeof revalidate === 'number') {\n\t\t\trequestInit.next = {\n\t\t\t\t...(config as RequestInitWithNext | undefined)?.next,\n\t\t\t\trevalidate,\n\t\t\t}\n\t\t}\n\n\t\t// Seed with a generic network error so an exhausted loop always has a\n\t\t// meaningful, typed error to throw.\n\t\tlet lastError = new ImproveFetchError('network')\n\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\t// Prefer a server-provided Retry-After; otherwise back off with jitter.\n\t\t\t\tawait delay(\n\t\t\t\t\tlastError.retryAfterMs ??\n\t\t\t\t\t\tgetBackoffDelayMs(\n\t\t\t\t\t\t\tattempt - 1,\n\t\t\t\t\t\t\tCONFIG_RETRY_BASE_DELAY_MS,\n\t\t\t\t\t\t\tCONFIG_RETRY_MAX_DELAY_MS,\n\t\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tlet res: Response | null\n\t\t\ttry {\n\t\t\t\tres = await timeoutFetch(timeout, url, requestInit)\n\t\t\t} catch (cause) {\n\t\t\t\t// timeoutFetch aborts on timeout (AbortError) and rejects on\n\t\t\t\t// network-level failures — both are transient and retryable.\n\t\t\t\tconst aborted = cause instanceof Error && cause.name === 'AbortError'\n\t\t\t\tlastError = new ImproveFetchError(aborted ? 'timeout' : 'network', {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// timeoutFetch resolves null when the timeout wins the race.\n\t\t\tif (!res) {\n\t\t\t\tlastError = new ImproveFetchError('timeout')\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (res.ok) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.config = normalizeConfig(await res.json())\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tlastError =\n\t\t\t\t\t\tcause instanceof ImproveFetchError\n\t\t\t\t\t\t\t? cause\n\t\t\t\t\t\t\t: new ImproveFetchError('invalid-response', { cause })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn this.config\n\t\t\t}\n\n\t\t\tlastError = new ImproveFetchError(getReasonFromStatus(res.status), {\n\t\t\t\tstatus: res.status,\n\t\t\t\tretryAfterMs: parseRetryAfterMs(res.headers.get('Retry-After')),\n\t\t\t})\n\n\t\t\t// Auth/validation rejections won't change on retry — fail fast.\n\t\t\tif (!lastError.isRetryable) break\n\t\t}\n\n\t\tthrow lastError\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\t/**\n\t * Emit a development warning, unless warnings were disabled via the\n\t * `disableWarnings` setup option. Centralized so every SDK warning honors the\n\t * same switch. No-op where `console` is unavailable.\n\t */\n\tprotected _warn = (message: string) => {\n\t\tif (this.#warningsDisabled) return\n\t\tif (typeof console === 'undefined') return\n\t\tconsole.warn(`[Improve] ${message}`)\n\t}\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","const ROW_DELIMITER = '; '\nconst ENTRY_DELIMITER = '='\n\nexport const getCookie = (name: string) => {\n\tif (!name) return false\n\tconst cookie = document.cookie.split(ROW_DELIMITER).find((row) => {\n\t\tconst [key] = row.split(ENTRY_DELIMITER)\n\t\treturn name === key\n\t})\n\treturn cookie ? cookie.split(ENTRY_DELIMITER)[1] : false\n}\n\nexport const setCookie = (name: string, value: string) => {\n\tconst now = new Date()\n\tnow.setDate(now.getDate() + 30)\n\tdocument.cookie = `${name}=${value};path=/;expires=${now.toUTCString()};SameSite=Lax;Secure`\n}\n","/**\n * A well-formed `snake_case` event name: starts with a lowercase letter and\n * contains only lowercase letters, digits, and single underscores between\n * segments. Matches the GA4 / GTM event-name convention Improve recommends\n * (`add_to_cart`, `sign_up`, `purchase`).\n */\nconst SNAKE_CASE_EVENT_NAME = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/\n\n/**\n * Whether `event` follows the recommended `snake_case` naming convention.\n * Flags camelCase (`pageView`), PascalCase (`AddToCart`), kebab-case\n * (`add-to-cart`), and names with spaces or trailing/double underscores.\n */\nexport const isSnakeCaseEventName = (event: string): boolean =>\n\tSNAKE_CASE_EVENT_NAME.test(event)\n","/**\n * @function truncate\n * @description Cap a string to `maxLength` characters. Returns non-strings\n * unchanged so it is safe to apply to loosely-typed payload values.\n *\n * @param {string} value - the value to truncate\n * @param {number} maxLength - maximum length to keep\n */\nexport const truncate = (value: string, maxLength: number): string =>\n\ttypeof value === 'string' && value.length > maxLength\n\t\t? value.slice(0, maxLength)\n\t\t: value\n","import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { getCookie, setCookie } from './utils/clientCookie'\nimport {\n\tANALYTIC_RATE_LIMIT_COOLDOWN_MS,\n\tMAX_ANALYTIC_BODY_BYTES,\n\tMAX_ANALYTIC_FIELD_LENGTH,\n\tMAX_CURRENCY_FIELD_LENGTH,\n} from './config/constants'\nimport { ANALYTICS_PATH, BASE_URL } from './config/urls'\nimport { getScreenSize } from './utils/getScreenSize'\nimport { isSnakeCaseEventName } from './utils/isSnakeCaseEventName'\nimport { pushDataLayer, resetDataLayerEcommerce } from './utils/pushDataLayer'\nimport { parseRetryAfterMs } from './utils/retry'\nimport { truncate } from './utils/truncate'\nimport {\n\tImproveAnalyticPayload,\n\tImproveEnvironmentOption,\n\tImproveEventName,\n\tImproveSetupArgs,\n} from './types'\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\nexport type CreateAnalytic = {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption\n\n\ttestId: string\n\ttestValue: string\n\tvisitorId: string\n\n\tpointer: string\n\tdevice: string\n\tscreen: string\n\tbrowser: string\n\tos: string\n\tvisitor: string\n\n\tevent: string\n\tmessage: string\n\n\t/** GA4-aligned numeric value aggregated into revenue / AOV per variant. */\n\tvalue?: number\n\t/** ISO 4217 currency for `value`. */\n\tcurrency?: string\n\t/** Extra event params stored as JSON (e.g. a GA4 `ecommerce` object). */\n\tparams?: Record<string, unknown>\n}\n\ntype TrackedAnalytics = {\n\t[TestSlug: string]: {\n\t\t[Event: string]: boolean\n\t}\n}\n\nexport class ImproveClientSDK extends BaseImproveSDK {\n\t#visitor: Visitor | undefined\n\t#visitorRecurring: boolean = false\n\n\t#visitorId: string = ''\n\n\t#analytics: TrackedAnalytics = {}\n\n\t// Epoch ms until which analytics posting is suspended after a 429.\n\t#rateLimitedUntil: number = 0\n\n\t// Event names already warned about, so the snake_case nudge fires once per\n\t// offending name instead of on every call.\n\t#warnedEventNames = new Set<string>()\n\n\t#analyticsUrl = `${BASE_URL}${ANALYTICS_PATH}`\n\n\t#dataLayerEnabled: boolean = true\n\n\tfetchConfig = this._fetchConfig\n\n\tconstructor(args: ImproveSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#analyticsUrl = `${this._baseUrl}${ANALYTICS_PATH}`\n\t\tthis.#dataLayerEnabled = args.dataLayer ?? true\n\t}\n\n\tsetupVisitor = (userAgent: string = window.navigator.userAgent) => {\n\t\tconst cookieVisitorId = getCookie(this.getVisitorCookieName())\n\t\tconst validCookieVisitorId =\n\t\t\tcookieVisitorId && this.validateVisitorId(cookieVisitorId)\n\n\t\tthis.#visitorRecurring = validCookieVisitorId\n\t\tthis.#visitorId = validCookieVisitorId\n\t\t\t? cookieVisitorId\n\t\t\t: this.generateVisitorId()\n\n\t\tconst parsedUserAgent = parseUserAgent(userAgent)\n\n\t\tif (!parsedUserAgent) return null\n\n\t\tthis.#visitor = parsedUserAgent\n\n\t\tsetCookie(this.getVisitorCookieName(), this.#visitorId)\n\n\t\treturn this.#visitorId\n\t}\n\n\tgetFlagValue = (flagSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst flagConfig = this.config.flags[flagSlug]\n\n\t\tif (!flagConfig || !flagConfig.options[0]) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!this.#visitorId || !this.#visitor) return flagConfig.options[0].slug\n\t\tif (this.#visitor?.[flagSlug]) return this.#visitor[flagSlug]\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue =\n\t\t\tgetCookie(flagSlug) || getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tthis.#visitor[flagSlug] = flagValue\n\n\t\tsetCookie(flagSlug, flagValue)\n\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!testConfig) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\tif (!this.#visitorId || !this.#visitor) return testConfig.defaultValue\n\t\tif (this.#visitor?.[testSlug]) return this.#visitor[testSlug]\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tthis.#visitor[testSlug] = testConfig.defaultValue\n\t\t\treturn this.#visitor?.[testSlug]\n\t\t}\n\n\t\tconst cookieTestValue = getCookie(testSlug)\n\t\tconst validCookieTestValue =\n\t\t\tcookieTestValue && this.validateTestValue(testSlug, cookieTestValue)\n\t\tconst testValue = validCookieTestValue\n\t\t\t? cookieTestValue\n\t\t\t: getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tthis.#visitor[testSlug] = testValue\n\n\t\tsetCookie(testSlug, testValue)\n\n\t\treturn testValue\n\t}\n\n\tsetAnalyticsUrls = (url: string) => {\n\t\tthis.#analyticsUrl = url\n\t}\n\n\tpostAnalytic = (\n\t\ttestSlug: string,\n\t\tevent: ImproveEventName,\n\t\t// Accepts a structured payload, or a plain string as shorthand for the\n\t\t// `message`. Backwards compatible with the previous `message?: string`.\n\t\tpayload?: ImproveAnalyticPayload | string,\n\t) => {\n\t\tif (!this.config) return null\n\n\t\t// GTM reserves the `gtm.*` namespace for its own events; never emit into\n\t\t// it, or the mirrored dataLayer entry would collide with GTM internals.\n\t\tif (event.startsWith('gtm.')) return null\n\n\t\t// Nudge developers toward the snake_case / GA4 naming convention. Warn\n\t\t// once per offending name so it's noticeable without spamming the console;\n\t\t// silence entirely with the `disableWarnings` setup option.\n\t\tif (!isSnakeCaseEventName(event) && !this.#warnedEventNames.has(event)) {\n\t\t\tthis.#warnedEventNames.add(event)\n\t\t\tthis._warn(\n\t\t\t\t`Event name \"${event}\" isn't snake_case. Prefer GA4-style names ` +\n\t\t\t\t\t`like \"add_to_cart\" or \"purchase\" so your events line up with ` +\n\t\t\t\t\t`GA4 / Google Tag Manager. Pass \\`disableWarnings: true\\` at setup ` +\n\t\t\t\t\t`to silence this.`,\n\t\t\t)\n\t\t}\n\n\t\t// Suspended after a 429: skip sending, but don't mark the event as\n\t\t// tracked, so it can still fire once the cooldown elapses.\n\t\tif (Date.now() < this.#rateLimitedUntil) return null\n\n\t\tconst { value, currency, message, params }: ImproveAnalyticPayload =\n\t\t\ttypeof payload === 'string' ? { message: payload } : (payload ?? {})\n\t\tconst hasValue = typeof value === 'number' && Number.isFinite(value)\n\t\t// The backend requires `params` to be a plain (non-array) object and 400s\n\t\t// the whole event otherwise — drop an invalid value rather than lose the\n\t\t// event.\n\t\tconst validParams =\n\t\t\tparams && typeof params === 'object' && !Array.isArray(params)\n\t\t\t\t? params\n\t\t\t\t: undefined\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!testConfig || !this.#visitor || this.#analytics?.[testSlug]?.[event]) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst testSlugAnalytics = this.#analytics[testSlug] || {}\n\t\ttestSlugAnalytics[event] = true\n\t\tthis.#analytics[testSlug] = testSlugAnalytics\n\n\t\tlet testValue = this.#visitor?.[testSlug] || null\n\t\tif (!testValue) {\n\t\t\ttestValue = this.getTestValue(testSlug) || null\n\t\t}\n\n\t\tif (!testValue) return\n\n\t\tconst body: CreateAnalytic = {\n\t\t\torganizationId: this.organizationId,\n\t\t\tenvironment: this.environment,\n\n\t\t\ttestId: testConfig.id,\n\t\t\ttestValue: testValue,\n\t\t\tvisitorId: this.#visitorId,\n\t\t\tpointer: this.#visitor.pointer,\n\t\t\tdevice: this.#visitor.device,\n\t\t\tscreen: getScreenSize(),\n\t\t\tbrowser: this.#visitor.browser,\n\t\t\tos: this.#visitor.os,\n\t\t\tvisitor: this.#visitorRecurring ? 'recurring' : 'new',\n\t\t\t// Cap developer-controlled fields to the backend's varchar(256) limit;\n\t\t\t// an over-length value is otherwise rejected with a 400 and the event\n\t\t\t// is lost.\n\t\t\tevent: truncate(event, MAX_ANALYTIC_FIELD_LENGTH),\n\t\t\tmessage: truncate(message || '', MAX_ANALYTIC_FIELD_LENGTH),\n\t\t\t...(hasValue ? { value } : {}),\n\t\t\t...(currency\n\t\t\t\t? { currency: truncate(currency, MAX_CURRENCY_FIELD_LENGTH) }\n\t\t\t\t: {}),\n\t\t\t...(validParams ? { params: validParams } : {}),\n\t\t}\n\n\t\tif (this.#dataLayerEnabled) {\n\t\t\t// Clear any prior ecommerce object first so its keys don't bleed into\n\t\t\t// this event (GTM/GA4 best practice).\n\t\t\tif (validParams && 'ecommerce' in validParams) resetDataLayerEcommerce()\n\n\t\t\tpushDataLayer({\n\t\t\t\tevent,\n\t\t\t\timprove: {\n\t\t\t\t\ttest: testSlug,\n\t\t\t\t\tvariant: testValue,\n\t\t\t\t\tvisitorId: this.#visitorId,\n\t\t\t\t},\n\t\t\t\t...(hasValue ? { value } : {}),\n\t\t\t\t...(currency ? { currency } : {}),\n\t\t\t\t// Spread custom params (incl. a GA4 `ecommerce` object) to the top\n\t\t\t\t// level so GA4/GTM tags can read them directly.\n\t\t\t\t...(validParams ?? {}),\n\t\t\t\t_improve: true,\n\t\t\t})\n\t\t}\n\n\t\t// Serialize once so we can both measure and send the same bytes.\n\t\tconst serializedBody = JSON.stringify(body)\n\n\t\t// The backend rejects bodies over 8KB with a 413 — realistically only an\n\t\t// oversized `params` (e.g. a large GA4 `ecommerce.items` array) gets there.\n\t\t// Warn so the dropped event isn't silent; still attempt the send in case\n\t\t// the server limit differs.\n\t\tif (\n\t\t\ttypeof TextEncoder !== 'undefined' &&\n\t\t\tnew TextEncoder().encode(serializedBody).length > MAX_ANALYTIC_BODY_BYTES\n\t\t) {\n\t\t\tthis._warn(\n\t\t\t\t`Analytic for \"${event}\" exceeds the ${MAX_ANALYTIC_BODY_BYTES}-byte ` +\n\t\t\t\t\t`limit and will be rejected by the server — trim the \\`params\\` payload.`,\n\t\t\t)\n\t\t}\n\n\t\t// `keepalive` lets the beacon outlive the page: conversion events are\n\t\t// often fired immediately before a navigation/unload, and a normal fetch\n\t\t// would be cancelled with the document. The 8KB body cap is well within\n\t\t// the browser's 64KB keepalive budget.\n\t\tconst request = fetch(this.#analyticsUrl, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: serializedBody,\n\t\t\tkeepalive: true,\n\t\t})\n\n\t\t// Back off when the org hits its usage/rate limit. Honor a server\n\t\t// Retry-After when present, otherwise use a fixed cooldown.\n\t\trequest\n\t\t\t.then((res) => {\n\t\t\t\tif (res?.status === 429) {\n\t\t\t\t\tconst retryAfterMs = parseRetryAfterMs(res.headers.get('Retry-After'))\n\t\t\t\t\tthis.#rateLimitedUntil =\n\t\t\t\t\t\tDate.now() + (retryAfterMs ?? ANALYTIC_RATE_LIMIT_COOLDOWN_MS)\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch(() => {})\n\n\t\treturn request\n\t}\n}\n","type SizeOptions = 'small' | 'medium' | 'large' | 'huge'\n\nexport const getScreenSize = (): SizeOptions => {\n\tconst size = window.innerWidth\n\tif (size <= 768) return 'small'\n\tif (size <= 1024) return 'medium'\n\tif (size <= 1200) return 'large'\n\treturn 'huge'\n}\n","import { ImproveEventName } from '../types'\n\nexport type ImproveDataLayerEntry = {\n\tevent: ImproveEventName\n\timprove: {\n\t\ttest: string\n\t\tvariant: string\n\t\tvisitorId: string\n\t}\n\t/** GA4 numeric value / currency, when provided on the analytic. */\n\tvalue?: number\n\tcurrency?: string\n\t/**\n\t * Marks the entry as originating from Improve so platform-side dataLayer\n\t * importers can ignore it (loop prevention).\n\t */\n\t_improve: true\n\t/** Any extra event params (e.g. a GA4 `ecommerce` object) spread onto the entry. */\n\t[param: string]: unknown\n}\n\ndeclare global {\n\tinterface Window {\n\t\tdataLayer?: Record<string, unknown>[]\n\t}\n}\n\n/**\n * Mirror an analytic onto the GTM dataLayer (with experiment dimensions) so it\n * can drive Google Tag Manager / Google Ads conversions. No-op outside the\n * browser. Initializes window.dataLayer if GTM hasn't yet.\n */\nexport const pushDataLayer = (entry: ImproveDataLayerEntry) => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push(entry)\n}\n\n/**\n * Clear the dataLayer `ecommerce` object before pushing an ecommerce event, so\n * leftover keys (old `items`, a stale `coupon`…) from a previous event don't\n * bleed into the next one — the GTM/GA4 recommended reset. No-op outside the\n * browser.\n */\nexport const resetDataLayerEcommerce = () => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push({ ecommerce: null })\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","length","slug","value","Math","random","reduce","acc","split","match","find","VISITOR_ID_PREFIX","BASE_URL","ANALYTICS_PATH","delay","ms","Promise","resolve","setTimeout","REASON_MESSAGES","timeout","network","unauthorized","server","client","ImproveFetchError","Error","reason","name","status","retryAfterMs","cause","isRetryable","getReasonFromStatus","CHAR_SET","normalizeConfig","body","Array","isArray","raw","version","tests","flags","parseRetryAfterMs","header","now","Date","seconds","Number","isFinite","max","round","date","parse","isNaN","getBackoffDelayMs","attempt","baseDelay","maxDelay","min","getTimeoutError","controller","abort","timeoutFetch","url","AbortController","race","fetch","signal","BaseImproveSDK","organizationId","environment","state","config","fetchTimeout","baseUrl","configRetries","configRevalidate","disableWarnings","_fetchConfig","retries","revalidate","requestInit","next","lastError","res","aborted","ok","json","headers","get","loadConfig","generateVisitorId","getRandomString","characters","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","_warn","message","console","warn","validateTestValue","testName","testValue","testConfig","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl","getCookie","cookie","document","row","setCookie","setDate","getDate","toUTCString","SNAKE_CASE_EVENT_NAME","truncate","maxLength","slice","ImproveClientSDK","args","Set","fetchConfig","setupVisitor","userAgent","window","navigator","cookieVisitorId","validCookieVisitorId","parsedUserAgent","parseUserAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType","getFlagValue","flagSlug","flagConfig","flagValue","getTestValue","testSlug","defaultValue","allocation","cookieTestValue","validCookieTestValue","setAnalyticsUrls","postAnalytic","event","payload","size","startsWith","test","has","add","currency","params","hasValue","validParams","undefined","testSlugAnalytics","testId","id","visitorId","screen","innerWidth","visitor","entry","dataLayer","push","ecommerce","improve","variant","_improve","serializedBody","JSON","stringify","TextEncoder","encode","request","method","keepalive","then","ANALYTIC_RATE_LIMIT_COOLDOWN_MS","catch"],"mappings":"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"}
package/dist/server.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,"__esModule",{value:!0});var t=require("ua-parser-js");let e=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],i=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],s=["mac os","ios","android","windows","unix"],r=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],o=["wearable","mobile","tablet"],n=(t,e)=>!t||Object.entries(t).every(([t,i])=>e[t]===i),a=t=>{if(0===t.length)return null;if(1===t.length)return t[0].slug;let e=Math.random()*t.reduce((t,{split:e})=>t+e,0);return(t.find(({split:t})=>(e-=t)<=0)||t[0]).slug},l="visi",h="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",u=async(t=1e3,e)=>(await new Promise(e=>setTimeout(e,t)),e?.abort(),null);class f{#t;constructor({organizationId:t,environment:e,state:i,config:s,fetchTimeout:r,baseUrl:o}){this.environment="develop",this.#t=null,this.config=null,this._fetchConfig=async t=>{if(this.config)return;if(!this.#t)throw Error("No config fetch setup provided");let e=await ((t=3e3,e,i)=>{let s=new AbortController;return Promise.race([fetch(e,{...i,signal:s.signal}),u(t,s)])})(this.#t.timeout,this.#t.url,t);if(!e||!e.ok)throw Error("Configuration fetch timed-out");return this.config=await e.json(),this.config},this.loadConfig=t=>{this.config=t},this.generateVisitorId=()=>[l,(function(t=5){return t&&"number"==typeof t?Array(t).fill("").reduce(t=>t+=h.charAt(Math.floor(Math.random()*h.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this.validateTestValue=(t,e)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let i=this.config.tests[t];if(!i)throw Error(`No config found for ${t}`);return!!i.options.find(t=>t.slug===e)},this.validateVisitorId=t=>{let e=t.split("_");if(2!==e.length)return!1;let[i,s]=e;return i===l&&26===s.length},this.organizationId=t,this.environment=e,this.state=i,this._baseUrl=o||"https://improve.obelism.studio",s?this.config=s:this.#t={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:r||3e3}}}exports.ImproveServerSDK=class extends f{#e;#i;#s;constructor({token:l,maxVisitors:h,...u}){super(u),this.#e=new Map,this.fetchConfig=async t=>this._fetchConfig({...t,headers:{...t?.headers,token:this.#s}}),this.getFlagConfig=t=>this.config?.flags?.[t],this.getTestConfig=t=>this.config?.tests?.[t],this.#r=(n,a)=>{let l=this.#e.get(n);if(l)this.#e.delete(n),this.#e.set(n,l);else{if(this.#e.size>=this.#i){let t=this.#e.keys().next().value;t&&this.#e.delete(t)}l={},this.#e.set(n,l)}return l[a]=l[a]||(n=>{var a;if(!n||"string"!=typeof n)return null;let l=new t.UAParser(n).getResult(),h=(a=l.device.type)&&e.includes(a)?a:"desktop";return{pointer:o.includes(h)?"coarse":"fine",device:h,browser:((t="")=>{if(!t)return"other";let e=t.toLowerCase(),s=i.find(t=>e.includes(t));return s||(r.includes(e)?"social":"other")})(l.browser.name),os:((t="")=>{if(!t)return"unix";let e=t.toLowerCase(),i=s.find(t=>e.includes(t));return i||"unix"})(l.os.name)}})(a),l},this.getFlagValue=(t,e,i)=>{let s=this.getFlagConfig(t);if(!s||!this.config)return null;if(!e)return s.options[0].slug;let r=this.#r(e,i);if(r[i]?.[t])return r[i][t];if(!n(this.config.audience[s.audience],r[i]))return s.options[0].slug;let o=a(s.options);return o?(r[i][t]=o,o):null},this.getTestValue=(t,e,i)=>{let s=this.getTestConfig(t);if(!s||!this.config)return null;if(!e||!i)return s.defaultValue;let r=this.#r(e,i);if(r[i]?.[t])return r[i][t];if(!n(this.config.audience[s.audience],r[i]))return s.defaultValue;if(s.allocation<100&&100*Math.random()>s.allocation)return r[i][t]=s.defaultValue,r[i][t];let o=a(s.options);return o?(r[i][t]=o,o):null},this.#s=l,this.#i=h??1e4}#r};
1
+ Object.defineProperty(exports,"__esModule",{value:!0});var e=require("ua-parser-js");let t=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],i=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],r=["mac os","ios","android","windows","unix"],s=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],n=["wearable","mobile","tablet"],o=(e,t)=>!e||Object.entries(e).every(([e,i])=>t[e]===i),a=e=>{if(0===e.length)return null;if(1===e.length)return e[0].slug;let t=Math.random()*e.reduce((e,{split:t})=>e+t,0);return(e.find(({split:e})=>(t-=e)<=0)||e[0]).slug},u="visi",l=e=>new Promise(t=>setTimeout(t,e)),h={timeout:"Configuration request timed out",network:"Configuration request could not reach the server",unauthorized:"Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment","rate-limited":"Configuration request was rate limited (429): too many requests, or the organization usage limit was reached",server:"Configuration request failed with a server error",client:"Configuration request was rejected by the server","invalid-response":"Configuration response could not be parsed as JSON"};class f extends Error{constructor(e,t){super(h[e]),this.name="ImproveFetchError",this.reason=e,this.status=t?.status,this.retryAfterMs=t?.retryAfterMs,t&&"cause"in t&&(this.cause=t.cause)}get isRetryable(){return"rate-limited"===this.reason||"server"===this.reason||"timeout"===this.reason||"network"===this.reason}}let c=e=>429===e?"rate-limited":401===e||403===e?"unauthorized":e>=500?"server":"client",g="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",d=e=>{if("object"!=typeof e||null===e||Array.isArray(e))throw new f("invalid-response");return{name:"string"==typeof e.name?e.name:"",version:"number"==typeof e.version?e.version:0,tests:e.tests??{},flags:e.flags??{},audience:e.audience??{}}},m=(e,t=Date.now())=>{if(!e)return;let i=Number(e);if(Number.isFinite(i))return Math.max(0,Math.round(1e3*i));let r=Date.parse(e);if(!Number.isNaN(r))return Math.max(0,r-t)},v=(e,t,i)=>Math.round(Math.random()*Math.min(i,t*2**e)),w=async(e=1e3,t)=>(await l(e),t?.abort(),null),p=(e=3e3,t,i)=>{let r=new AbortController;return Promise.race([fetch(t,{...i,signal:r.signal}),w(e,r)])};class b{#e;#t;constructor({organizationId:e,environment:t,state:i,config:r,fetchTimeout:s,baseUrl:n,configRetries:o,configRevalidate:a,disableWarnings:h}){this.environment="develop",this.#e=null,this.#t=!1,this.config=null,this._fetchConfig=async e=>{if(this.config)return;if(!this.#e)throw Error("No config fetch setup provided");let{url:t,timeout:i,retries:r,revalidate:s}=this.#e,n={...e};"number"==typeof s&&(n.next={...e?.next,revalidate:s});let o=new f("network");for(let e=0;e<=r;e++){let r;e>0&&await l(o.retryAfterMs??v(e-1,300,3e3));try{r=await p(i,t,n)}catch(e){o=new f(e instanceof Error&&"AbortError"===e.name?"timeout":"network",{cause:e});continue}if(!r){o=new f("timeout");continue}if(r.ok){try{this.config=d(await r.json())}catch(e){o=e instanceof f?e:new f("invalid-response",{cause:e});continue}return this.config}if(!(o=new f(c(r.status),{status:r.status,retryAfterMs:m(r.headers.get("Retry-After"))})).isRetryable)break}throw o},this.loadConfig=e=>{this.config=e},this.generateVisitorId=()=>[u,(function(e=5){return e&&"number"==typeof e?Array(e).fill("").reduce(e=>e+=g.charAt(Math.floor(Math.random()*g.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this._warn=e=>{this.#t||"u">typeof console&&console.warn(`[Improve] ${e}`)},this.validateTestValue=(e,t)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let i=this.config.tests[e];if(!i)throw Error(`No config found for ${e}`);return!!i.options.find(e=>e.slug===t)},this.validateVisitorId=e=>{let t=e.split("_");if(2!==t.length)return!1;let[i,r]=t;return i===u&&26===r.length},this.organizationId=e,this.environment=t,this.state=i,this._baseUrl=n||"https://improve.obelism.studio",this.#t=h??!1,r?this.config=r:this.#e={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:s||3e3,retries:o??2,revalidate:a}}}exports.ImproveServerSDK=class extends b{#i;#r;#s;constructor({token:u,maxVisitors:l,...h}){super(h),this.#i=new Map,this.fetchConfig=async e=>this._fetchConfig({...e,headers:{...e?.headers,token:this.#s}}),this.getFlagConfig=e=>this.config?.flags?.[e],this.getTestConfig=e=>this.config?.tests?.[e],this.#n=(o,a)=>{let u=this.#i.get(o);if(u)this.#i.delete(o),this.#i.set(o,u);else{if(this.#i.size>=this.#r){let e=this.#i.keys().next().value;e&&this.#i.delete(e)}u={},this.#i.set(o,u)}return u[a]=u[a]||(o=>{var a;if(!o||"string"!=typeof o)return null;let u=new e.UAParser(o).getResult(),l=(a=u.device.type)&&t.includes(a)?a:"desktop";return{pointer:n.includes(l)?"coarse":"fine",device:l,browser:((e="")=>{if(!e)return"other";let t=e.toLowerCase(),r=i.find(e=>t.includes(e));return r||(s.includes(t)?"social":"other")})(u.browser.name),os:((e="")=>{if(!e)return"unix";let t=e.toLowerCase(),i=r.find(e=>t.includes(e));return i||"unix"})(u.os.name)}})(a),u},this.getFlagValue=(e,t,i)=>{let r=this.getFlagConfig(e);if(!r||!this.config)return null;if(!t)return r.options[0].slug;let s=this.#n(t,i);if(s[i]?.[e])return s[i][e];if(!o(this.config.audience[r.audience],s[i]))return r.options[0].slug;let n=a(r.options);return n?(s[i][e]=n,n):null},this.getTestValue=(e,t,i)=>{let r=this.getTestConfig(e);if(!r||!this.config)return null;if(!t||!i)return r.defaultValue;let s=this.#n(t,i);if(s[i]?.[e])return s[i][e];if(!o(this.config.audience[r.audience],s[i]))return r.defaultValue;if(r.allocation<100&&100*Math.random()>r.allocation)return s[i][e]=r.defaultValue,s[i][e];let n=a(r.options);return n?(s[i][e]=n,n):null},this.#s=u,this.#r=l??1e4}#n};
2
2
  //# sourceMappingURL=server.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.cjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/utils/getRandomString.ts","../src/utils/getTimeoutError.ts","../src/utils/delay.ts","../src/base.ts","../src/utils/timeoutFetch.ts","../src/config/urls.ts","../src/server.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tif (options.length === 0) return null\n\tif (options.length === 1) return options[0].slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || options[0]\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","import {\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { getRandomString } from './utils/getRandomString'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState\n\n\t#configFetch: ConfigFetch | null = null\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst res = await timeoutFetch(\n\t\t\tthis.#configFetch.timeout,\n\t\t\tthis.#configFetch.url,\n\t\t\tconfig,\n\t\t)\n\t\tif (!res || !res.ok) throw new Error('Configuration fetch timed-out')\n\n\t\tthis.config = await res.json()\n\t\treturn this.config\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\nexport const ANALYTICS_PATH = `/api/log`\n","import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { ImproveConfiguration, ImproveSetupArgs } from './types'\n\nconst DEFAULT_MAX_VISITORS = 10_000\n\ntype VisitorData = {\n\t[userAgent: string]: ParsedUserAgent & {\n\t\t[testSlug: string]: string\n\t}\n}\n\ntype ImproveServerSetupArgs =\n\t| (Omit<ImproveSetupArgs, 'config' | 'baseUrl'> & {\n\t\t\tconfig: ImproveConfiguration\n\t\t\tmaxVisitors?: number\n\t })\n\t| (Omit<ImproveSetupArgs, 'config'> & {\n\t\t\ttoken: string\n\t\t\tmaxVisitors?: number\n\t })\n\nexport class ImproveServerSDK extends BaseImproveSDK {\n\t#visitors: Map<string, VisitorData> = new Map()\n\t#maxVisitors: number\n\t#token: string\n\n\t// @ts-ignore It could be there\n\tconstructor({ token, maxVisitors, ...args }: ImproveServerSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#token = token\n\t\tthis.#maxVisitors = maxVisitors ?? DEFAULT_MAX_VISITORS\n\t}\n\n\tfetchConfig = async (config?: RequestInit) => {\n\t\treturn this._fetchConfig({\n\t\t\t...config,\n\t\t\theaders: {\n\t\t\t\t...config?.headers,\n\t\t\t\ttoken: this.#token,\n\t\t\t},\n\t\t})\n\t}\n\n\tgetFlagConfig = (flagSlug: string) => this.config?.flags?.[flagSlug]\n\n\tgetTestConfig = (testSlug: string) => this.config?.tests?.[testSlug]\n\n\t#getVisitor = (visitorId: string, userAgent: string) => {\n\t\tlet visitor = this.#visitors.get(visitorId)\n\t\tif (visitor) {\n\t\t\t// Move to end for LRU freshness\n\t\t\tthis.#visitors.delete(visitorId)\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t} else {\n\t\t\t// Evict oldest entries when at capacity\n\t\t\tif (this.#visitors.size >= this.#maxVisitors) {\n\t\t\t\tconst oldest = this.#visitors.keys().next().value\n\t\t\t\tif (oldest) this.#visitors.delete(oldest)\n\t\t\t}\n\t\t\tvisitor = {}\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t}\n\t\tvisitor[userAgent] = visitor[userAgent] || parseUserAgent(userAgent)\n\t\treturn visitor\n\t}\n\n\tgetFlagValue = (flagSlug: string, visitorId: string, userAgent: string) => {\n\t\tconst flagConfig = this.getFlagConfig(flagSlug)\n\n\t\tif (!flagConfig || !this.config) return null\n\t\tif (!visitorId) return flagConfig.options[0].slug\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\n\t\tif (visitor[userAgent]?.[flagSlug]) {\n\t\t\treturn visitor[userAgent][flagSlug]\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tvisitor[userAgent],\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tvisitor[userAgent][flagSlug] = flagValue\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string, visitorId: string, userAgent: string) => {\n\t\tconst testConfig = this.getTestConfig(testSlug)\n\n\t\tif (!testConfig || !this.config) return null\n\n\t\tif (!visitorId || !userAgent) return testConfig.defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\n\t\tif (visitor[userAgent]?.[testSlug]) {\n\t\t\treturn visitor[userAgent][testSlug]\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tvisitor[userAgent],\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tvisitor[userAgent][testSlug] = testConfig.defaultValue\n\t\t\treturn visitor[userAgent][testSlug]\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tvisitor[userAgent][testSlug] = testValue\n\t\treturn testValue\n\t}\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","length","slug","value","Math","random","reduce","acc","split","match","find","VISITOR_ID_PREFIX","CHAR_SET","getTimeoutError","timeout","controller","Promise","resolve","setTimeout","abort","BaseImproveSDK","organizationId","environment","state","config","fetchTimeout","baseUrl","_fetchConfig","Error","res","timeoutFetch","url","AbortController","race","fetch","signal","ok","json","loadConfig","generateVisitorId","getRandomString","characters","Array","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","validateTestValue","testName","testValue","testConfig","tests","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl","token","maxVisitors","args","Map","fetchConfig","headers","getFlagConfig","flagSlug","flags","getTestConfig","testSlug","visitorId","userAgent","visitor","get","delete","set","size","oldest","keys","next","parseUserAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","name","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType","getFlagValue","flagConfig","flagValue","getTestValue","defaultValue","allocation"],"mappings":"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"}
1
+ {"version":3,"file":"server.cjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/utils/delay.ts","../src/utils/errors.ts","../src/utils/getRandomString.ts","../src/utils/normalizeConfig.ts","../src/utils/retry.ts","../src/utils/getTimeoutError.ts","../src/utils/timeoutFetch.ts","../src/base.ts","../src/config/urls.ts","../src/server.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tif (options.length === 0) return null\n\tif (options.length === 1) return options[0].slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || options[0]\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n\n/** Default number of retries for a failed config fetch (network / 5xx / 429). */\nexport const CONFIG_RETRY_COUNT = 2\n\n/** Base delay for config-fetch backoff, in ms. */\nexport const CONFIG_RETRY_BASE_DELAY_MS = 300\n\n/** Upper bound for a single config-fetch backoff wait, in ms. */\nexport const CONFIG_RETRY_MAX_DELAY_MS = 3000\n\n/**\n * Max length of a single analytic string field. The backend rejects any\n * field longer than this (varchar(256)) with a 400, so the client caps\n * developer-controlled values (event, message) to avoid silently dropping\n * the event.\n */\nexport const MAX_ANALYTIC_FIELD_LENGTH = 256\n\n/**\n * Max length of the analytic `currency` field. The backend column is\n * `varchar(8)` (ISO 4217 is 3 chars), and a longer value throws at insert —\n * failing the *entire* event, not just the currency — so the client caps it\n * here rather than at the generic 256-char limit.\n */\nexport const MAX_CURRENCY_FIELD_LENGTH = 8\n\n/**\n * Max size of the analytic POST body. The backend rejects anything larger with\n * a 413 (`MAX_BODY_BYTES = 8 * 1024`), so an oversized `params` payload silently\n * loses the event. Kept in sync with the backend limit.\n */\nexport const MAX_ANALYTIC_BODY_BYTES = 8 * 1024\n\n/**\n * How long to stop sending analytics after the backend returns 429 (usage\n * limit / rate limit) without a `Retry-After`. Avoids hammering an org that is\n * already over its quota.\n */\nexport const ANALYTIC_RATE_LIMIT_COOLDOWN_MS = 60_000\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","export type ImproveFetchErrorReason =\n\t| 'timeout'\n\t| 'network'\n\t| 'unauthorized'\n\t| 'rate-limited'\n\t| 'server'\n\t| 'client'\n\t| 'invalid-response'\n\nconst REASON_MESSAGES: Record<ImproveFetchErrorReason, string> = {\n\ttimeout: 'Configuration request timed out',\n\tnetwork: 'Configuration request could not reach the server',\n\tunauthorized:\n\t\t'Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment',\n\t'rate-limited':\n\t\t'Configuration request was rate limited (429): too many requests, or the organization usage limit was reached',\n\tserver: 'Configuration request failed with a server error',\n\tclient: 'Configuration request was rejected by the server',\n\t'invalid-response': 'Configuration response could not be parsed as JSON',\n}\n\n/**\n * Error thrown by the config fetch layer. Carries the HTTP `status`, a coarse\n * `reason`, and (when the server sent one) the `Retry-After` delay in ms, so\n * callers can distinguish a misconfiguration (unauthorized) from a transient\n * failure (rate-limited/server/timeout) instead of guessing from a string.\n */\nexport class ImproveFetchError extends Error {\n\treason: ImproveFetchErrorReason\n\tstatus?: number\n\tretryAfterMs?: number\n\n\tconstructor(\n\t\treason: ImproveFetchErrorReason,\n\t\toptions?: { status?: number; retryAfterMs?: number; cause?: unknown },\n\t) {\n\t\tsuper(REASON_MESSAGES[reason])\n\t\tthis.name = 'ImproveFetchError'\n\t\tthis.reason = reason\n\t\tthis.status = options?.status\n\t\tthis.retryAfterMs = options?.retryAfterMs\n\t\tif (options && 'cause' in options) {\n\t\t\t;(this as { cause?: unknown }).cause = options.cause\n\t\t}\n\t}\n\n\t/** Whether retrying the request could plausibly succeed. */\n\tget isRetryable() {\n\t\treturn (\n\t\t\tthis.reason === 'rate-limited' ||\n\t\t\tthis.reason === 'server' ||\n\t\t\tthis.reason === 'timeout' ||\n\t\t\tthis.reason === 'network'\n\t\t)\n\t}\n}\n\n/** Map an HTTP status onto a coarse {@link ImproveFetchErrorReason}. */\nexport const getReasonFromStatus = (\n\tstatus: number,\n): ImproveFetchErrorReason => {\n\tif (status === 429) return 'rate-limited'\n\tif (status === 401 || status === 403) return 'unauthorized'\n\tif (status >= 500) return 'server'\n\treturn 'client'\n}\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { ImproveConfiguration } from '../types'\nimport { ImproveFetchError } from './errors'\n\n/**\n * @function normalizeConfig\n * @description Coerce a parsed config response into a well-formed\n * {@link ImproveConfiguration}. The backend returns `200 {}` for an unknown\n * org/environment (and on its own internal error). An empty object is truthy,\n * so it slips past `if (!this.config)` guards and then crashes downstream on\n * `config.tests[slug]` (`Cannot read properties of undefined`). Guaranteeing\n * `tests`/`flags`/`audience` are always objects lets the SDK fail open to\n * default values instead of throwing during render/middleware.\n *\n * Throws {@link ImproveFetchError} `invalid-response` when the body isn't an\n * object at all (e.g. an array, string, or null).\n */\nexport const normalizeConfig = (body: unknown): ImproveConfiguration => {\n\tif (typeof body !== 'object' || body === null || Array.isArray(body)) {\n\t\tthrow new ImproveFetchError('invalid-response')\n\t}\n\n\tconst raw = body as Partial<ImproveConfiguration>\n\n\treturn {\n\t\tname: typeof raw.name === 'string' ? raw.name : '',\n\t\tversion: typeof raw.version === 'number' ? raw.version : 0,\n\t\ttests: raw.tests ?? {},\n\t\tflags: raw.flags ?? {},\n\t\taudience: raw.audience ?? {},\n\t}\n}\n","/**\n * @function parseRetryAfterMs\n * @description Parse an HTTP `Retry-After` header into milliseconds. Supports\n * both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Returns `undefined` when the header is\n * absent or unparseable so the caller can fall back to computed backoff.\n *\n * @param {string | null} [header] - the raw `Retry-After` header value\n * @param {number} [now] - current epoch ms (injectable for testing)\n */\nexport const parseRetryAfterMs = (\n\theader: string | null | undefined,\n\tnow: number = Date.now(),\n): number | undefined => {\n\tif (!header) return undefined\n\n\tconst seconds = Number(header)\n\tif (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1000))\n\n\tconst date = Date.parse(header)\n\tif (Number.isNaN(date)) return undefined\n\n\treturn Math.max(0, date - now)\n}\n\n/**\n * @function getBackoffDelayMs\n * @description Exponential backoff with full jitter, capped at `maxDelay`.\n * Jitter spreads retries so concurrent clients don't stampede the origin in\n * lockstep after a shared failure.\n *\n * @param {number} attempt - zero-based retry attempt index\n * @param {number} baseDelay - base delay in ms\n * @param {number} maxDelay - upper bound in ms\n */\nexport const getBackoffDelayMs = (\n\tattempt: number,\n\tbaseDelay: number,\n\tmaxDelay: number,\n): number => {\n\tconst exponential = Math.min(maxDelay, baseDelay * 2 ** attempt)\n\treturn Math.round(Math.random() * exponential)\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","import {\n\tCONFIG_RETRY_BASE_DELAY_MS,\n\tCONFIG_RETRY_COUNT,\n\tCONFIG_RETRY_MAX_DELAY_MS,\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { delay } from './utils/delay'\nimport { getReasonFromStatus, ImproveFetchError } from './utils/errors'\nimport { getRandomString } from './utils/getRandomString'\nimport { normalizeConfig } from './utils/normalizeConfig'\nimport { getBackoffDelayMs, parseRetryAfterMs } from './utils/retry'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n\tretries: number\n\trevalidate?: number\n}\n\n// Next.js augments RequestInit with a `next` field for its fetch cache.\ntype RequestInitWithNext = RequestInit & {\n\tnext?: { revalidate?: number; tags?: string[] }\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState\n\n\t#configFetch: ConfigFetch | null = null\n\n\t#warningsDisabled: boolean = false\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t\tconfigRetries,\n\t\tconfigRevalidate,\n\t\tdisableWarnings,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\t\tthis.#warningsDisabled = disableWarnings ?? false\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t\tretries: configRetries ?? CONFIG_RETRY_COUNT,\n\t\t\t\trevalidate: configRevalidate,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst { url, timeout, retries, revalidate } = this.#configFetch\n\n\t\t// Opt into the Next.js fetch cache when a revalidate window is configured,\n\t\t// so the datafile is reused across requests instead of re-fetched.\n\t\tconst requestInit: RequestInitWithNext = { ...config }\n\t\tif (typeof revalidate === 'number') {\n\t\t\trequestInit.next = {\n\t\t\t\t...(config as RequestInitWithNext | undefined)?.next,\n\t\t\t\trevalidate,\n\t\t\t}\n\t\t}\n\n\t\t// Seed with a generic network error so an exhausted loop always has a\n\t\t// meaningful, typed error to throw.\n\t\tlet lastError = new ImproveFetchError('network')\n\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\t// Prefer a server-provided Retry-After; otherwise back off with jitter.\n\t\t\t\tawait delay(\n\t\t\t\t\tlastError.retryAfterMs ??\n\t\t\t\t\t\tgetBackoffDelayMs(\n\t\t\t\t\t\t\tattempt - 1,\n\t\t\t\t\t\t\tCONFIG_RETRY_BASE_DELAY_MS,\n\t\t\t\t\t\t\tCONFIG_RETRY_MAX_DELAY_MS,\n\t\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tlet res: Response | null\n\t\t\ttry {\n\t\t\t\tres = await timeoutFetch(timeout, url, requestInit)\n\t\t\t} catch (cause) {\n\t\t\t\t// timeoutFetch aborts on timeout (AbortError) and rejects on\n\t\t\t\t// network-level failures — both are transient and retryable.\n\t\t\t\tconst aborted = cause instanceof Error && cause.name === 'AbortError'\n\t\t\t\tlastError = new ImproveFetchError(aborted ? 'timeout' : 'network', {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// timeoutFetch resolves null when the timeout wins the race.\n\t\t\tif (!res) {\n\t\t\t\tlastError = new ImproveFetchError('timeout')\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (res.ok) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.config = normalizeConfig(await res.json())\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tlastError =\n\t\t\t\t\t\tcause instanceof ImproveFetchError\n\t\t\t\t\t\t\t? cause\n\t\t\t\t\t\t\t: new ImproveFetchError('invalid-response', { cause })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn this.config\n\t\t\t}\n\n\t\t\tlastError = new ImproveFetchError(getReasonFromStatus(res.status), {\n\t\t\t\tstatus: res.status,\n\t\t\t\tretryAfterMs: parseRetryAfterMs(res.headers.get('Retry-After')),\n\t\t\t})\n\n\t\t\t// Auth/validation rejections won't change on retry — fail fast.\n\t\t\tif (!lastError.isRetryable) break\n\t\t}\n\n\t\tthrow lastError\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\t/**\n\t * Emit a development warning, unless warnings were disabled via the\n\t * `disableWarnings` setup option. Centralized so every SDK warning honors the\n\t * same switch. No-op where `console` is unavailable.\n\t */\n\tprotected _warn = (message: string) => {\n\t\tif (this.#warningsDisabled) return\n\t\tif (typeof console === 'undefined') return\n\t\tconsole.warn(`[Improve] ${message}`)\n\t}\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\nexport const ANALYTICS_PATH = `/api/log`\n","import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { ImproveConfiguration, ImproveSetupArgs } from './types'\n\nconst DEFAULT_MAX_VISITORS = 10_000\n\ntype VisitorData = {\n\t[userAgent: string]: ParsedUserAgent & {\n\t\t[testSlug: string]: string\n\t}\n}\n\ntype ImproveServerSetupArgs =\n\t| (Omit<ImproveSetupArgs, 'config' | 'baseUrl'> & {\n\t\t\tconfig: ImproveConfiguration\n\t\t\tmaxVisitors?: number\n\t })\n\t| (Omit<ImproveSetupArgs, 'config'> & {\n\t\t\ttoken: string\n\t\t\tmaxVisitors?: number\n\t })\n\nexport class ImproveServerSDK extends BaseImproveSDK {\n\t#visitors: Map<string, VisitorData> = new Map()\n\t#maxVisitors: number\n\t#token: string\n\n\t// @ts-ignore It could be there\n\tconstructor({ token, maxVisitors, ...args }: ImproveServerSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#token = token\n\t\tthis.#maxVisitors = maxVisitors ?? DEFAULT_MAX_VISITORS\n\t}\n\n\tfetchConfig = async (config?: RequestInit) => {\n\t\treturn this._fetchConfig({\n\t\t\t...config,\n\t\t\theaders: {\n\t\t\t\t...config?.headers,\n\t\t\t\ttoken: this.#token,\n\t\t\t},\n\t\t})\n\t}\n\n\tgetFlagConfig = (flagSlug: string) => this.config?.flags?.[flagSlug]\n\n\tgetTestConfig = (testSlug: string) => this.config?.tests?.[testSlug]\n\n\t#getVisitor = (visitorId: string, userAgent: string) => {\n\t\tlet visitor = this.#visitors.get(visitorId)\n\t\tif (visitor) {\n\t\t\t// Move to end for LRU freshness\n\t\t\tthis.#visitors.delete(visitorId)\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t} else {\n\t\t\t// Evict oldest entries when at capacity\n\t\t\tif (this.#visitors.size >= this.#maxVisitors) {\n\t\t\t\tconst oldest = this.#visitors.keys().next().value\n\t\t\t\tif (oldest) this.#visitors.delete(oldest)\n\t\t\t}\n\t\t\tvisitor = {}\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t}\n\t\tvisitor[userAgent] = visitor[userAgent] || parseUserAgent(userAgent)\n\t\treturn visitor\n\t}\n\n\tgetFlagValue = (flagSlug: string, visitorId: string, userAgent: string) => {\n\t\tconst flagConfig = this.getFlagConfig(flagSlug)\n\n\t\tif (!flagConfig || !this.config) return null\n\t\tif (!visitorId) return flagConfig.options[0].slug\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\n\t\tif (visitor[userAgent]?.[flagSlug]) {\n\t\t\treturn visitor[userAgent][flagSlug]\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tvisitor[userAgent],\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tvisitor[userAgent][flagSlug] = flagValue\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string, visitorId: string, userAgent: string) => {\n\t\tconst testConfig = this.getTestConfig(testSlug)\n\n\t\tif (!testConfig || !this.config) return null\n\n\t\tif (!visitorId || !userAgent) return testConfig.defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\n\t\tif (visitor[userAgent]?.[testSlug]) {\n\t\t\treturn visitor[userAgent][testSlug]\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tvisitor[userAgent],\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tvisitor[userAgent][testSlug] = testConfig.defaultValue\n\t\t\treturn visitor[userAgent][testSlug]\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tvisitor[userAgent][testSlug] = testValue\n\t\treturn testValue\n\t}\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","length","slug","value","Math","random","reduce","acc","split","match","find","VISITOR_ID_PREFIX","delay","ms","Promise","resolve","setTimeout","REASON_MESSAGES","timeout","network","unauthorized","server","client","ImproveFetchError","Error","reason","name","status","retryAfterMs","cause","isRetryable","getReasonFromStatus","CHAR_SET","normalizeConfig","body","Array","isArray","raw","version","tests","flags","parseRetryAfterMs","header","now","Date","seconds","Number","isFinite","max","round","date","parse","isNaN","getBackoffDelayMs","attempt","baseDelay","maxDelay","min","getTimeoutError","controller","abort","timeoutFetch","url","AbortController","race","fetch","signal","BaseImproveSDK","organizationId","environment","state","config","fetchTimeout","baseUrl","configRetries","configRevalidate","disableWarnings","_fetchConfig","retries","revalidate","requestInit","next","lastError","res","aborted","ok","json","headers","get","loadConfig","generateVisitorId","getRandomString","characters","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","_warn","message","console","warn","validateTestValue","testName","testValue","testConfig","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl","token","maxVisitors","args","Map","fetchConfig","getFlagConfig","flagSlug","getTestConfig","testSlug","visitorId","userAgent","visitor","delete","set","size","oldest","keys","parseUserAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType","getFlagValue","flagConfig","flagValue","getTestValue","defaultValue","allocation"],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -8,11 +8,17 @@ declare class BaseImproveSDK {
8
8
  state: ImproveTestState;
9
9
  config: ImproveConfiguration | null;
10
10
  _baseUrl: undefined | string;
11
- constructor({ organizationId, environment, state, config, fetchTimeout, baseUrl, }: ImproveSetupArgs);
11
+ constructor({ organizationId, environment, state, config, fetchTimeout, baseUrl, configRetries, configRevalidate, disableWarnings, }: ImproveSetupArgs);
12
12
  _fetchConfig: (config?: RequestInit) => Promise<ImproveConfiguration>;
13
13
  loadConfig: (config: ImproveConfiguration) => void;
14
14
  generateVisitorId: () => string;
15
15
  getVisitorCookieName: () => string;
16
+ /**
17
+ * Emit a development warning, unless warnings were disabled via the
18
+ * `disableWarnings` setup option. Centralized so every SDK warning honors the
19
+ * same switch. No-op where `console` is unavailable.
20
+ */
21
+ protected _warn: (message: string) => void;
16
22
  validateTestValue: (testName: string, testValue: string) => boolean;
17
23
  validateVisitorId: (possibleVisitorId: string) => boolean;
18
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sources":["../src/base.ts","../src/server.ts"],"mappings":";;;AACA,cAAc,cAAc;AAC5B;AACA;AACA,iBAAiB,wBAAwB;AACzC,WAAW,gBAAgB;AAC3B,YAAY,oBAAoB;AAChC;AACA,wFAAwF,gBAAgB;AACxG,4BAA4B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACxE,yBAAyB,oBAAoB;AAC7C;AACA;AACA;AACA;AACA;;ACZA,KAAK,sBAAsB,IAAI,IAAI,CAAC,gBAAgB;AACpD,YAAY,oBAAoB;AAChC;AACA,MAAM,IAAI,CAAC,gBAAgB;AAC3B;AACA;AACA;AACA,cAAc,gBAAgB,SAAS,cAAc;AACrD;AACA,iDAAiD,sBAAsB;AACvE,2BAA2B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACvE,yCAAyC,OAAO,CAAC,WAAW;AAC5D,yCAAyC,OAAO,CAAC,WAAW;AAC5D;AACA;AACA;;;;","names":[]}
1
+ {"version":3,"file":"server.d.ts","sources":["../src/base.ts","../src/server.ts"],"mappings":";;;AACA,cAAc,cAAc;AAC5B;AACA;AACA,iBAAiB,wBAAwB;AACzC,WAAW,gBAAgB;AAC3B,YAAY,oBAAoB;AAChC;AACA,0IAA0I,gBAAgB;AAC1J,4BAA4B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACxE,yBAAyB,oBAAoB;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AClBA,KAAK,sBAAsB,IAAI,IAAI,CAAC,gBAAgB;AACpD,YAAY,oBAAoB;AAChC;AACA,MAAM,IAAI,CAAC,gBAAgB;AAC3B;AACA;AACA;AACA,cAAc,gBAAgB,SAAS,cAAc;AACrD;AACA,iDAAiD,sBAAsB;AACvE,2BAA2B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACvE,yCAAyC,OAAO,CAAC,WAAW;AAC5D,yCAAyC,OAAO,CAAC,WAAW;AAC5D;AACA;AACA;;;;","names":[]}
package/dist/server.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{UAParser as t}from"ua-parser-js";let i=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],e=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],s=["mac os","ios","android","windows","unix"],r=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],o=["wearable","mobile","tablet"],n=(t,i)=>!t||Object.entries(t).every(([t,e])=>i[t]===e),a=t=>{if(0===t.length)return null;if(1===t.length)return t[0].slug;let i=Math.random()*t.reduce((t,{split:i})=>t+i,0);return(t.find(({split:t})=>(i-=t)<=0)||t[0]).slug},l="visi",h="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",u=async(t=1e3,i)=>(await new Promise(i=>setTimeout(i,t)),i?.abort(),null);class f{#t;constructor({organizationId:t,environment:i,state:e,config:s,fetchTimeout:r,baseUrl:o}){this.environment="develop",this.#t=null,this.config=null,this._fetchConfig=async t=>{if(this.config)return;if(!this.#t)throw Error("No config fetch setup provided");let i=await ((t=3e3,i,e)=>{let s=new AbortController;return Promise.race([fetch(i,{...e,signal:s.signal}),u(t,s)])})(this.#t.timeout,this.#t.url,t);if(!i||!i.ok)throw Error("Configuration fetch timed-out");return this.config=await i.json(),this.config},this.loadConfig=t=>{this.config=t},this.generateVisitorId=()=>[l,(function(t=5){return t&&"number"==typeof t?Array(t).fill("").reduce(t=>t+=h.charAt(Math.floor(Math.random()*h.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this.validateTestValue=(t,i)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let e=this.config.tests[t];if(!e)throw Error(`No config found for ${t}`);return!!e.options.find(t=>t.slug===i)},this.validateVisitorId=t=>{let i=t.split("_");if(2!==i.length)return!1;let[e,s]=i;return e===l&&26===s.length},this.organizationId=t,this.environment=i,this.state=e,this._baseUrl=o||"https://improve.obelism.studio",s?this.config=s:this.#t={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:r||3e3}}}class c extends f{#i;#e;#s;constructor({token:l,maxVisitors:h,...u}){super(u),this.#i=new Map,this.fetchConfig=async t=>this._fetchConfig({...t,headers:{...t?.headers,token:this.#s}}),this.getFlagConfig=t=>this.config?.flags?.[t],this.getTestConfig=t=>this.config?.tests?.[t],this.#r=(n,a)=>{let l=this.#i.get(n);if(l)this.#i.delete(n),this.#i.set(n,l);else{if(this.#i.size>=this.#e){let t=this.#i.keys().next().value;t&&this.#i.delete(t)}l={},this.#i.set(n,l)}return l[a]=l[a]||(n=>{var a;if(!n||"string"!=typeof n)return null;let l=new t(n).getResult(),h=(a=l.device.type)&&i.includes(a)?a:"desktop";return{pointer:o.includes(h)?"coarse":"fine",device:h,browser:((t="")=>{if(!t)return"other";let i=t.toLowerCase(),s=e.find(t=>i.includes(t));return s||(r.includes(i)?"social":"other")})(l.browser.name),os:((t="")=>{if(!t)return"unix";let i=t.toLowerCase(),e=s.find(t=>i.includes(t));return e||"unix"})(l.os.name)}})(a),l},this.getFlagValue=(t,i,e)=>{let s=this.getFlagConfig(t);if(!s||!this.config)return null;if(!i)return s.options[0].slug;let r=this.#r(i,e);if(r[e]?.[t])return r[e][t];if(!n(this.config.audience[s.audience],r[e]))return s.options[0].slug;let o=a(s.options);return o?(r[e][t]=o,o):null},this.getTestValue=(t,i,e)=>{let s=this.getTestConfig(t);if(!s||!this.config)return null;if(!i||!e)return s.defaultValue;let r=this.#r(i,e);if(r[e]?.[t])return r[e][t];if(!n(this.config.audience[s.audience],r[e]))return s.defaultValue;if(s.allocation<100&&100*Math.random()>s.allocation)return r[e][t]=s.defaultValue,r[e][t];let o=a(s.options);return o?(r[e][t]=o,o):null},this.#s=l,this.#e=h??1e4}#r}export{c as ImproveServerSDK};
1
+ import{UAParser as e}from"ua-parser-js";let t=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],i=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],r=["mac os","ios","android","windows","unix"],s=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],n=["wearable","mobile","tablet"],o=(e,t)=>!e||Object.entries(e).every(([e,i])=>t[e]===i),a=e=>{if(0===e.length)return null;if(1===e.length)return e[0].slug;let t=Math.random()*e.reduce((e,{split:t})=>e+t,0);return(e.find(({split:e})=>(t-=e)<=0)||e[0]).slug},u="visi",l=e=>new Promise(t=>setTimeout(t,e)),h={timeout:"Configuration request timed out",network:"Configuration request could not reach the server",unauthorized:"Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment","rate-limited":"Configuration request was rate limited (429): too many requests, or the organization usage limit was reached",server:"Configuration request failed with a server error",client:"Configuration request was rejected by the server","invalid-response":"Configuration response could not be parsed as JSON"};class f extends Error{constructor(e,t){super(h[e]),this.name="ImproveFetchError",this.reason=e,this.status=t?.status,this.retryAfterMs=t?.retryAfterMs,t&&"cause"in t&&(this.cause=t.cause)}get isRetryable(){return"rate-limited"===this.reason||"server"===this.reason||"timeout"===this.reason||"network"===this.reason}}let c=e=>429===e?"rate-limited":401===e||403===e?"unauthorized":e>=500?"server":"client",g="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",d=e=>{if("object"!=typeof e||null===e||Array.isArray(e))throw new f("invalid-response");return{name:"string"==typeof e.name?e.name:"",version:"number"==typeof e.version?e.version:0,tests:e.tests??{},flags:e.flags??{},audience:e.audience??{}}},m=(e,t=Date.now())=>{if(!e)return;let i=Number(e);if(Number.isFinite(i))return Math.max(0,Math.round(1e3*i));let r=Date.parse(e);if(!Number.isNaN(r))return Math.max(0,r-t)},v=(e,t,i)=>Math.round(Math.random()*Math.min(i,t*2**e)),w=async(e=1e3,t)=>(await l(e),t?.abort(),null),p=(e=3e3,t,i)=>{let r=new AbortController;return Promise.race([fetch(t,{...i,signal:r.signal}),w(e,r)])};class b{#e;#t;constructor({organizationId:e,environment:t,state:i,config:r,fetchTimeout:s,baseUrl:n,configRetries:o,configRevalidate:a,disableWarnings:h}){this.environment="develop",this.#e=null,this.#t=!1,this.config=null,this._fetchConfig=async e=>{if(this.config)return;if(!this.#e)throw Error("No config fetch setup provided");let{url:t,timeout:i,retries:r,revalidate:s}=this.#e,n={...e};"number"==typeof s&&(n.next={...e?.next,revalidate:s});let o=new f("network");for(let e=0;e<=r;e++){let r;e>0&&await l(o.retryAfterMs??v(e-1,300,3e3));try{r=await p(i,t,n)}catch(e){o=new f(e instanceof Error&&"AbortError"===e.name?"timeout":"network",{cause:e});continue}if(!r){o=new f("timeout");continue}if(r.ok){try{this.config=d(await r.json())}catch(e){o=e instanceof f?e:new f("invalid-response",{cause:e});continue}return this.config}if(!(o=new f(c(r.status),{status:r.status,retryAfterMs:m(r.headers.get("Retry-After"))})).isRetryable)break}throw o},this.loadConfig=e=>{this.config=e},this.generateVisitorId=()=>[u,(function(e=5){return e&&"number"==typeof e?Array(e).fill("").reduce(e=>e+=g.charAt(Math.floor(Math.random()*g.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this._warn=e=>{this.#t||"u">typeof console&&console.warn(`[Improve] ${e}`)},this.validateTestValue=(e,t)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let i=this.config.tests[e];if(!i)throw Error(`No config found for ${e}`);return!!i.options.find(e=>e.slug===t)},this.validateVisitorId=e=>{let t=e.split("_");if(2!==t.length)return!1;let[i,r]=t;return i===u&&26===r.length},this.organizationId=e,this.environment=t,this.state=i,this._baseUrl=n||"https://improve.obelism.studio",this.#t=h??!1,r?this.config=r:this.#e={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:s||3e3,retries:o??2,revalidate:a}}}class y extends b{#i;#r;#s;constructor({token:u,maxVisitors:l,...h}){super(h),this.#i=new Map,this.fetchConfig=async e=>this._fetchConfig({...e,headers:{...e?.headers,token:this.#s}}),this.getFlagConfig=e=>this.config?.flags?.[e],this.getTestConfig=e=>this.config?.tests?.[e],this.#n=(o,a)=>{let u=this.#i.get(o);if(u)this.#i.delete(o),this.#i.set(o,u);else{if(this.#i.size>=this.#r){let e=this.#i.keys().next().value;e&&this.#i.delete(e)}u={},this.#i.set(o,u)}return u[a]=u[a]||(o=>{var a;if(!o||"string"!=typeof o)return null;let u=new e(o).getResult(),l=(a=u.device.type)&&t.includes(a)?a:"desktop";return{pointer:n.includes(l)?"coarse":"fine",device:l,browser:((e="")=>{if(!e)return"other";let t=e.toLowerCase(),r=i.find(e=>t.includes(e));return r||(s.includes(t)?"social":"other")})(u.browser.name),os:((e="")=>{if(!e)return"unix";let t=e.toLowerCase(),i=r.find(e=>t.includes(e));return i||"unix"})(u.os.name)}})(a),u},this.getFlagValue=(e,t,i)=>{let r=this.getFlagConfig(e);if(!r||!this.config)return null;if(!t)return r.options[0].slug;let s=this.#n(t,i);if(s[i]?.[e])return s[i][e];if(!o(this.config.audience[r.audience],s[i]))return r.options[0].slug;let n=a(r.options);return n?(s[i][e]=n,n):null},this.getTestValue=(e,t,i)=>{let r=this.getTestConfig(e);if(!r||!this.config)return null;if(!t||!i)return r.defaultValue;let s=this.#n(t,i);if(s[i]?.[e])return s[i][e];if(!o(this.config.audience[r.audience],s[i]))return r.defaultValue;if(r.allocation<100&&100*Math.random()>r.allocation)return s[i][e]=r.defaultValue,s[i][e];let n=a(r.options);return n?(s[i][e]=n,n):null},this.#s=u,this.#r=l??1e4}#n}export{y as ImproveServerSDK};
2
2
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.mjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/utils/getRandomString.ts","../src/utils/getTimeoutError.ts","../src/utils/delay.ts","../src/base.ts","../src/utils/timeoutFetch.ts","../src/config/urls.ts","../src/server.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tif (options.length === 0) return null\n\tif (options.length === 1) return options[0].slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || options[0]\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","import {\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { getRandomString } from './utils/getRandomString'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState\n\n\t#configFetch: ConfigFetch | null = null\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst res = await timeoutFetch(\n\t\t\tthis.#configFetch.timeout,\n\t\t\tthis.#configFetch.url,\n\t\t\tconfig,\n\t\t)\n\t\tif (!res || !res.ok) throw new Error('Configuration fetch timed-out')\n\n\t\tthis.config = await res.json()\n\t\treturn this.config\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\nexport const ANALYTICS_PATH = `/api/log`\n","import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { ImproveConfiguration, ImproveSetupArgs } from './types'\n\nconst DEFAULT_MAX_VISITORS = 10_000\n\ntype VisitorData = {\n\t[userAgent: string]: ParsedUserAgent & {\n\t\t[testSlug: string]: string\n\t}\n}\n\ntype ImproveServerSetupArgs =\n\t| (Omit<ImproveSetupArgs, 'config' | 'baseUrl'> & {\n\t\t\tconfig: ImproveConfiguration\n\t\t\tmaxVisitors?: number\n\t })\n\t| (Omit<ImproveSetupArgs, 'config'> & {\n\t\t\ttoken: string\n\t\t\tmaxVisitors?: number\n\t })\n\nexport class ImproveServerSDK extends BaseImproveSDK {\n\t#visitors: Map<string, VisitorData> = new Map()\n\t#maxVisitors: number\n\t#token: string\n\n\t// @ts-ignore It could be there\n\tconstructor({ token, maxVisitors, ...args }: ImproveServerSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#token = token\n\t\tthis.#maxVisitors = maxVisitors ?? DEFAULT_MAX_VISITORS\n\t}\n\n\tfetchConfig = async (config?: RequestInit) => {\n\t\treturn this._fetchConfig({\n\t\t\t...config,\n\t\t\theaders: {\n\t\t\t\t...config?.headers,\n\t\t\t\ttoken: this.#token,\n\t\t\t},\n\t\t})\n\t}\n\n\tgetFlagConfig = (flagSlug: string) => this.config?.flags?.[flagSlug]\n\n\tgetTestConfig = (testSlug: string) => this.config?.tests?.[testSlug]\n\n\t#getVisitor = (visitorId: string, userAgent: string) => {\n\t\tlet visitor = this.#visitors.get(visitorId)\n\t\tif (visitor) {\n\t\t\t// Move to end for LRU freshness\n\t\t\tthis.#visitors.delete(visitorId)\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t} else {\n\t\t\t// Evict oldest entries when at capacity\n\t\t\tif (this.#visitors.size >= this.#maxVisitors) {\n\t\t\t\tconst oldest = this.#visitors.keys().next().value\n\t\t\t\tif (oldest) this.#visitors.delete(oldest)\n\t\t\t}\n\t\t\tvisitor = {}\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t}\n\t\tvisitor[userAgent] = visitor[userAgent] || parseUserAgent(userAgent)\n\t\treturn visitor\n\t}\n\n\tgetFlagValue = (flagSlug: string, visitorId: string, userAgent: string) => {\n\t\tconst flagConfig = this.getFlagConfig(flagSlug)\n\n\t\tif (!flagConfig || !this.config) return null\n\t\tif (!visitorId) return flagConfig.options[0].slug\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\n\t\tif (visitor[userAgent]?.[flagSlug]) {\n\t\t\treturn visitor[userAgent][flagSlug]\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tvisitor[userAgent],\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tvisitor[userAgent][flagSlug] = flagValue\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string, visitorId: string, userAgent: string) => {\n\t\tconst testConfig = this.getTestConfig(testSlug)\n\n\t\tif (!testConfig || !this.config) return null\n\n\t\tif (!visitorId || !userAgent) return testConfig.defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\n\t\tif (visitor[userAgent]?.[testSlug]) {\n\t\t\treturn visitor[userAgent][testSlug]\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tvisitor[userAgent],\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tvisitor[userAgent][testSlug] = testConfig.defaultValue\n\t\t\treturn visitor[userAgent][testSlug]\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tvisitor[userAgent][testSlug] = testValue\n\t\treturn testValue\n\t}\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","length","slug","value","Math","random","reduce","acc","split","match","find","VISITOR_ID_PREFIX","CHAR_SET","getTimeoutError","timeout","controller","Promise","resolve","setTimeout","abort","BaseImproveSDK","organizationId","environment","state","config","fetchTimeout","baseUrl","_fetchConfig","Error","res","timeoutFetch","url","AbortController","race","fetch","signal","ok","json","loadConfig","generateVisitorId","getRandomString","characters","Array","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","validateTestValue","testName","testValue","testConfig","tests","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl","ImproveServerSDK","token","maxVisitors","args","Map","fetchConfig","headers","getFlagConfig","flagSlug","flags","getTestConfig","testSlug","visitorId","userAgent","visitor","get","delete","set","size","oldest","keys","next","parseUserAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","name","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType","getFlagValue","flagConfig","flagValue","getTestValue","defaultValue","allocation"],"mappings":"wCAIO,MAGE,CACP,WACA,SACA,SACA,UACA,UACA,WACA,UACA,GACQ,CACR,SACA,SACA,UACA,OACA,KACA,mBACA,SACA,QACA,GACG,CAAC,SAAU,MAAO,UAAW,UAAW,OAAO,CCC9CA,EAAyC,CAC9C,SACA,SACA,QACA,WACA,SACA,OACA,YACA,WACA,SACA,QACA,CAiBKC,EAA4D,CACjE,WACA,SACA,SACA,CCvDYC,EAA4B,CACxCC,EACAC,IAEA,CAAKD,GACEE,OAAOC,OAAO,CAACH,GAAUI,KAAK,CAAC,CAAC,CAACC,EAAUC,EAAW,GACrDL,CAAa,CAACI,EAA6B,GAAKC,GCJ5CC,EAAqB,AAACC,IAClC,GAAIA,AAAmB,IAAnBA,EAAQC,MAAM,CAAQ,OAAO,KACjC,GAAID,AAAmB,IAAnBA,EAAQC,MAAM,CAAQ,OAAOD,CAAO,CAAC,EAAE,CAACE,IAAI,CAIhD,IAAIC,EAAQC,KAAKC,MAAM,GADXL,EAAQM,MAAM,CAAC,CAACC,EAAK,CAAEC,MAAAA,CAAK,CAAE,GAAKD,EAAMC,EAAO,GAS5D,MAAOC,AALNT,CAAAA,EAAQU,IAAI,CAAC,CAAC,CAAEF,MAAAA,CAAK,CAAE,GAEfL,AADPA,CAAAA,GAASK,CAAAA,GACO,IACXR,CAAO,CAAC,EAAE,AAAF,EAEFE,IAAI,AAClB,ECnBaS,EAAoB,OCE3BC,EAAW,uCCKJC,EAAkB,MAC9BC,EAAkB,GAAI,CACtBC,KAEA,MCZA,IAAIC,QAAQ,AAACC,GAAYC,WAAWD,EDYxBH,IACZC,GAAYI,QACL,KEMD,OAAMC,EAKZ,CAAA,CAAY,AAAA,AAMZ,aAAY,CACXC,eAAAA,CAAc,CACdC,YAAAA,CAAW,CACXC,MAAAA,CAAK,CACLC,OAAAA,CAAM,CACNC,aAAAA,CAAY,CACZC,QAAAA,CAAO,CACW,CAAE,MAhBrBJ,WAAAA,CAAwC,UAGxC,IAAA,CAAA,CAAA,CAAY,CAAuB,UAEnCE,MAAAA,CAAsC,KAgCtCG,IAAAA,CAAAA,YAAAA,CAAe,MAAOH,IACrB,GAAI,IAAI,CAACA,MAAM,CAAE,OAEjB,GAAI,CAAC,IAAI,CAAC,CAAA,CAAY,CAAE,MAAM,AAAII,MAAM,kCAExC,IAAMC,EAAM,MAAMC,ACvDQ,CAAA,CAC3BhB,EAAkB,GAAI,CACtBiB,EACA/B,KAEA,IAAMe,EAAa,IAAIiB,gBACvB,OAAOhB,QAAQiB,IAAI,CAAC,CACnBC,MAAMH,EAAK,CACV,GAAG/B,CAAO,CACVmC,OAAQpB,EAAWoB,MAAAA,AACpB,GACAtB,EAAgBC,EAASC,GACzB,CACF,CAAA,ED2CG,IAAI,CAAC,CAAA,CAAY,CAACD,OAAO,CACzB,IAAI,CAAC,CAAA,CAAY,CAACiB,GAAG,CACrBP,GAED,GAAI,CAACK,GAAO,CAACA,EAAIO,EAAE,CAAE,MAAM,AAAIR,MAAM,iCAGrC,OADA,IAAI,CAACJ,MAAM,CAAG,MAAMK,EAAIQ,IAAI,GACrB,IAAI,CAACb,MAAM,AACnB,EAEAc,IAAAA,CAAAA,UAAAA,CAAa,AAACd,IACb,IAAI,CAACA,MAAM,CAAGA,CACf,OAEAe,iBAAAA,CAAoB,IACZ,CACN5B,EACA6B,AHzEI,CAAA,SAAyBC,EAAqB,CAAC,SACrD,AAAI,AAACA,GAAc,AAAsB,UAAtB,OAAOA,EACnBC,MAAMD,GACXE,IAAI,CAAC,IACLrC,MAAM,CAAC,AAACC,GACRA,GAAOK,EAASgC,MAAM,CAACxC,KAAKyC,KAAK,CAACzC,KAAKC,MAAM,GAAKO,EAASX,MAAM,GAE/D,IANsD,EAO3D,CAAA,EDdiC,II+EK6C,WAAW,GAC9C,CAACC,IAAI,CJ9E4B,KIiFnCC,IAAAA,CAAAA,oBAAAA,CAAuB,IJvFW,YIyFlCC,IAAAA,CAAAA,iBAAAA,CAAoB,CAACC,EAAkBC,KACtC,GAAI,CAAC,IAAI,CAAC3B,MAAM,CACf,MAAM,AAAII,MACT,qHAGF,IAAMwB,EAAa,IAAI,CAAC5B,MAAM,CAAC6B,KAAK,CAACH,EAAS,CAE9C,GAAI,CAACE,EAAY,MAAM,AAAIxB,MAAM,CAAC,oBAAoB,EAAEsB,EAAAA,CAAU,EAElE,MAAOI,CAAAA,CACNF,EAAWpD,OAAO,CAACU,IAAI,CAAC,AAAC6C,GAAWA,EAAOrD,IAAI,GAAKiD,EAEtD,EAEAK,IAAAA,CAAAA,iBAAAA,CAAoB,AAACC,IACpB,IAAMC,EAAiBD,EAAkBjD,KAAK,CJnGZ,KIoGlC,GAAIkD,AAA0B,IAA1BA,EAAezD,MAAM,CAAQ,MAAO,CAAA,EACxC,GAAM,CAAC0D,EAAKxD,EAAM,CAAGuD,EACrB,OAAOC,IAAQhD,GAAqBR,AJxGL,KIwGKA,EAAMF,MAAM,AACjD,EArEC,IAAI,CAACoB,cAAc,CAAGA,EACtB,IAAI,CAACC,WAAW,CAAGA,EACnB,IAAI,CAACC,KAAK,CAAGA,EACb,IAAI,CAACqC,QAAQ,CAAGlC,GE3CM,iCF6ClBF,EACH,IAAI,CAACA,MAAM,CAAGA,EAEd,IAAI,CAAC,CAAA,CAAY,CAAG,CACnBO,IAAK,CACJ,GAAG,IAAI,CAAC6B,QAAQ,SAAgB,CAChC,IAAI,CAACvC,cAAc,CACnB,IAAI,CAACC,WAAW,CAChB,IAAI,CAACC,KAAK,EAAI,SACd,CAACwB,IAAI,CAAC,KACPjC,QAASW,GAAgB,GAC1B,CAEF,CAoDD,CGtFO,MAAMoC,UAAyBzC,EACrC,CAAA,CAAS,AAAA,AACT,EAAA,CAAY,AAAA,AACZ,EAAA,CAAM,AAAA,AAGN,aAAY,CAAE0C,MAAAA,CAAK,CAAEC,YAAAA,CAAW,CAAE,GAAGC,EAA8B,CAAE,CACpE,KAAK,CAACA,QANP,CAAA,CAAS,CAA6B,IAAIC,IAAAA,IAAAA,CAW1CC,YAAc,MAAO1C,GACb,IAAI,CAACG,YAAY,CAAC,CACxB,GAAGH,CAAM,CACT2C,QAAS,CACR,GAAG3C,GAAQ2C,OAAO,CAClBL,MAAO,IAAI,CAAC,CAAA,CAAA,AACb,CACD,GACD,IAAA,CAEAM,aAAAA,CAAgB,AAACC,GAAqB,IAAI,CAAC7C,MAAM,EAAE8C,OAAAA,CAAQD,EAAS,CAAA,IAAA,CAEpEE,aAAAA,CAAgB,AAACC,GAAqB,IAAI,CAAChD,MAAM,EAAE6B,OAAAA,CAAQmB,EAAS,CAAA,IAAA,CAEpE,CAAA,CAAW,CAAG,CAACC,EAAmBC,KACjC,IAAIC,EAAU,IAAI,CAAC,CAAA,CAAS,CAACC,GAAG,CAACH,GACjC,GAAIE,EAEH,IAAI,CAAC,CAAA,CAAS,CAACE,MAAM,CAACJ,GACtB,IAAI,CAAC,CAAA,CAAS,CAACK,GAAG,CAACL,EAAWE,OACxB,CAEN,GAAI,IAAI,CAAC,CAAA,CAAS,CAACI,IAAI,EAAI,IAAI,CAAC,CAAA,CAAY,CAAE,CAC7C,IAAMC,EAAS,IAAI,CAAC,CAAA,CAAS,CAACC,IAAI,GAAGC,IAAI,GAAG/E,KAAK,AAC7C6E,CAAAA,GAAQ,IAAI,CAAC,CAAA,CAAS,CAACH,MAAM,CAACG,EACnC,CACAL,EAAU,CAAA,EACV,IAAI,CAAC,CAAA,CAAS,CAACG,GAAG,CAACL,EAAWE,EAC/B,CAEA,OADAA,CAAO,CAACD,EAAU,CAAGC,CAAO,CAACD,EAAU,EAAIS,AVaf,CAAA,AAACT,QA7DLU,EA8DzB,GAAI,CAACV,GAAa,AAAqB,UAArB,OAAOA,EAAwB,OAAO,KAGxD,IAAMW,EAAUC,AADD,IAAIC,EAASb,GACLc,SAAS,GAE1BC,EAlEN,AACCL,CAFwBA,EAmEOC,EAAQI,MAAM,CAACC,IAAI,GAhElDC,EAAuBC,QAAQ,CAACR,GAEzBA,EAED,UA8DP,MAAO,CACNS,QAzBMvG,EAAsBsG,QAAQ,CAyBbH,GAzBwB,SAAW,OA0B1DA,OAAQA,EACRK,QAASC,AAjDW,CAAA,CAACC,EAAsB,EAAE,IAC9C,GAAI,CAACA,EAAa,MAAO,QAEzB,IAAMC,EAAOD,EAAYE,WAAW,GAE9BC,EAAmBR,EAAwBjF,IAAI,CAAC,AAAC0F,GAC/CH,EAAKL,QAAQ,CAACQ,WAGtB,AAAID,IAEA9G,EAAgBuG,QAAQ,CAACK,GAAc,SACpC,QACR,CAAA,EAoCyBZ,EAAQS,OAAO,CAACG,IAAI,EAC3CI,GAAIC,AAzBW,CAAA,CAACC,EAAiB,EAAE,IACpC,GAAI,CAACA,EAAQ,MAAO,OAEpB,IAAMF,EAAKE,EAAOL,WAAW,GAEvBM,EAAcb,EAAmBjF,IAAI,CAAC,AAAC+F,GACrCJ,EAAGT,QAAQ,CAACa,WAGpB,AAAID,GACG,MACR,CAAA,EAcenB,EAAQgB,EAAE,CAACJ,IAAI,CAC7B,CACD,CAAA,EU3B4DvB,GACnDC,CACR,EAAA,IAAA,CAEA+B,YAAAA,CAAe,CAACrC,EAAkBI,EAAmBC,KACpD,IAAMiC,EAAa,IAAI,CAACvC,aAAa,CAACC,GAEtC,GAAI,CAACsC,GAAc,CAAC,IAAI,CAACnF,MAAM,CAAE,OAAO,KACxC,GAAI,CAACiD,EAAW,OAAOkC,EAAW3G,OAAO,CAAC,EAAE,CAACE,IAAI,CAEjD,IAAMyE,EAAU,IAAI,CAAC,CAAA,CAAW,CAACF,EAAWC,GAE5C,GAAIC,CAAO,CAACD,EAAU,EAAA,CAAGL,EAAS,CACjC,OAAOM,CAAO,CAACD,EAAU,CAACL,EAAS,CAQpC,GAAI,CAL2B9E,EAC9B,IAAI,CAACiC,MAAM,CAAChC,QAAQ,CAACmH,EAAWnH,QAAQ,CAAC,CACzCmF,CAAO,CAACD,EAAU,EAGU,OAAOiC,EAAW3G,OAAO,CAAC,EAAE,CAACE,IAAI,CAE9D,IAAM0G,EAAY7G,EAAmB4G,EAAW3G,OAAO,SAEvD,AAAK4G,GAELjC,CAAO,CAACD,EAAU,CAACL,EAAS,CAAGuC,EACxBA,GAHgB,IAIxB,EAAA,IAAA,CAEAC,YAAAA,CAAe,CAACrC,EAAkBC,EAAmBC,KACpD,IAAMtB,EAAa,IAAI,CAACmB,aAAa,CAACC,GAEtC,GAAI,CAACpB,GAAc,CAAC,IAAI,CAAC5B,MAAM,CAAE,OAAO,KAExC,GAAI,CAACiD,GAAa,CAACC,EAAW,OAAOtB,EAAW0D,YAAY,CAE5D,IAAMnC,EAAU,IAAI,CAAC,CAAA,CAAW,CAACF,EAAWC,GAE5C,GAAIC,CAAO,CAACD,EAAU,EAAA,CAAGF,EAAS,CACjC,OAAOG,CAAO,CAACD,EAAU,CAACF,EAAS,CAQpC,GAAI,CAL2BjF,EAC9B,IAAI,CAACiC,MAAM,CAAChC,QAAQ,CAAC4D,EAAW5D,QAAQ,CAAC,CACzCmF,CAAO,CAACD,EAAU,EAGU,OAAOtB,EAAW0D,YAAY,CAE3D,GACC1D,EAAW2D,UAAU,CAAG,KACxB3G,AAAgB,IAAhBA,KAAKC,MAAM,GAAW+C,EAAW2D,UAAU,CAG3C,OADApC,CAAO,CAACD,EAAU,CAACF,EAAS,CAAGpB,EAAW0D,YAAY,CAC/CnC,CAAO,CAACD,EAAU,CAACF,EAAS,CAGpC,IAAMrB,EAAYpD,EAAmBqD,EAAWpD,OAAO,SAEvD,AAAKmD,GAELwB,CAAO,CAACD,EAAU,CAACF,EAAS,CAAGrB,EACxBA,GAHgB,IAIxB,EAlGC,IAAI,CAAC,CAAA,CAAM,CAAGW,EACd,IAAI,CAAC,CAAA,CAAY,CAAGC,GA3BO,GA4B5B,CAgBA,CAAA,CAAW,AAAA,AAiFZ"}
1
+ {"version":3,"file":"server.mjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/utils/delay.ts","../src/utils/errors.ts","../src/utils/getRandomString.ts","../src/utils/normalizeConfig.ts","../src/utils/retry.ts","../src/utils/getTimeoutError.ts","../src/utils/timeoutFetch.ts","../src/base.ts","../src/config/urls.ts","../src/server.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tif (options.length === 0) return null\n\tif (options.length === 1) return options[0].slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || options[0]\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n\n/** Default number of retries for a failed config fetch (network / 5xx / 429). */\nexport const CONFIG_RETRY_COUNT = 2\n\n/** Base delay for config-fetch backoff, in ms. */\nexport const CONFIG_RETRY_BASE_DELAY_MS = 300\n\n/** Upper bound for a single config-fetch backoff wait, in ms. */\nexport const CONFIG_RETRY_MAX_DELAY_MS = 3000\n\n/**\n * Max length of a single analytic string field. The backend rejects any\n * field longer than this (varchar(256)) with a 400, so the client caps\n * developer-controlled values (event, message) to avoid silently dropping\n * the event.\n */\nexport const MAX_ANALYTIC_FIELD_LENGTH = 256\n\n/**\n * Max length of the analytic `currency` field. The backend column is\n * `varchar(8)` (ISO 4217 is 3 chars), and a longer value throws at insert —\n * failing the *entire* event, not just the currency — so the client caps it\n * here rather than at the generic 256-char limit.\n */\nexport const MAX_CURRENCY_FIELD_LENGTH = 8\n\n/**\n * Max size of the analytic POST body. The backend rejects anything larger with\n * a 413 (`MAX_BODY_BYTES = 8 * 1024`), so an oversized `params` payload silently\n * loses the event. Kept in sync with the backend limit.\n */\nexport const MAX_ANALYTIC_BODY_BYTES = 8 * 1024\n\n/**\n * How long to stop sending analytics after the backend returns 429 (usage\n * limit / rate limit) without a `Retry-After`. Avoids hammering an org that is\n * already over its quota.\n */\nexport const ANALYTIC_RATE_LIMIT_COOLDOWN_MS = 60_000\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","export type ImproveFetchErrorReason =\n\t| 'timeout'\n\t| 'network'\n\t| 'unauthorized'\n\t| 'rate-limited'\n\t| 'server'\n\t| 'client'\n\t| 'invalid-response'\n\nconst REASON_MESSAGES: Record<ImproveFetchErrorReason, string> = {\n\ttimeout: 'Configuration request timed out',\n\tnetwork: 'Configuration request could not reach the server',\n\tunauthorized:\n\t\t'Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment',\n\t'rate-limited':\n\t\t'Configuration request was rate limited (429): too many requests, or the organization usage limit was reached',\n\tserver: 'Configuration request failed with a server error',\n\tclient: 'Configuration request was rejected by the server',\n\t'invalid-response': 'Configuration response could not be parsed as JSON',\n}\n\n/**\n * Error thrown by the config fetch layer. Carries the HTTP `status`, a coarse\n * `reason`, and (when the server sent one) the `Retry-After` delay in ms, so\n * callers can distinguish a misconfiguration (unauthorized) from a transient\n * failure (rate-limited/server/timeout) instead of guessing from a string.\n */\nexport class ImproveFetchError extends Error {\n\treason: ImproveFetchErrorReason\n\tstatus?: number\n\tretryAfterMs?: number\n\n\tconstructor(\n\t\treason: ImproveFetchErrorReason,\n\t\toptions?: { status?: number; retryAfterMs?: number; cause?: unknown },\n\t) {\n\t\tsuper(REASON_MESSAGES[reason])\n\t\tthis.name = 'ImproveFetchError'\n\t\tthis.reason = reason\n\t\tthis.status = options?.status\n\t\tthis.retryAfterMs = options?.retryAfterMs\n\t\tif (options && 'cause' in options) {\n\t\t\t;(this as { cause?: unknown }).cause = options.cause\n\t\t}\n\t}\n\n\t/** Whether retrying the request could plausibly succeed. */\n\tget isRetryable() {\n\t\treturn (\n\t\t\tthis.reason === 'rate-limited' ||\n\t\t\tthis.reason === 'server' ||\n\t\t\tthis.reason === 'timeout' ||\n\t\t\tthis.reason === 'network'\n\t\t)\n\t}\n}\n\n/** Map an HTTP status onto a coarse {@link ImproveFetchErrorReason}. */\nexport const getReasonFromStatus = (\n\tstatus: number,\n): ImproveFetchErrorReason => {\n\tif (status === 429) return 'rate-limited'\n\tif (status === 401 || status === 403) return 'unauthorized'\n\tif (status >= 500) return 'server'\n\treturn 'client'\n}\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { ImproveConfiguration } from '../types'\nimport { ImproveFetchError } from './errors'\n\n/**\n * @function normalizeConfig\n * @description Coerce a parsed config response into a well-formed\n * {@link ImproveConfiguration}. The backend returns `200 {}` for an unknown\n * org/environment (and on its own internal error). An empty object is truthy,\n * so it slips past `if (!this.config)` guards and then crashes downstream on\n * `config.tests[slug]` (`Cannot read properties of undefined`). Guaranteeing\n * `tests`/`flags`/`audience` are always objects lets the SDK fail open to\n * default values instead of throwing during render/middleware.\n *\n * Throws {@link ImproveFetchError} `invalid-response` when the body isn't an\n * object at all (e.g. an array, string, or null).\n */\nexport const normalizeConfig = (body: unknown): ImproveConfiguration => {\n\tif (typeof body !== 'object' || body === null || Array.isArray(body)) {\n\t\tthrow new ImproveFetchError('invalid-response')\n\t}\n\n\tconst raw = body as Partial<ImproveConfiguration>\n\n\treturn {\n\t\tname: typeof raw.name === 'string' ? raw.name : '',\n\t\tversion: typeof raw.version === 'number' ? raw.version : 0,\n\t\ttests: raw.tests ?? {},\n\t\tflags: raw.flags ?? {},\n\t\taudience: raw.audience ?? {},\n\t}\n}\n","/**\n * @function parseRetryAfterMs\n * @description Parse an HTTP `Retry-After` header into milliseconds. Supports\n * both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Returns `undefined` when the header is\n * absent or unparseable so the caller can fall back to computed backoff.\n *\n * @param {string | null} [header] - the raw `Retry-After` header value\n * @param {number} [now] - current epoch ms (injectable for testing)\n */\nexport const parseRetryAfterMs = (\n\theader: string | null | undefined,\n\tnow: number = Date.now(),\n): number | undefined => {\n\tif (!header) return undefined\n\n\tconst seconds = Number(header)\n\tif (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1000))\n\n\tconst date = Date.parse(header)\n\tif (Number.isNaN(date)) return undefined\n\n\treturn Math.max(0, date - now)\n}\n\n/**\n * @function getBackoffDelayMs\n * @description Exponential backoff with full jitter, capped at `maxDelay`.\n * Jitter spreads retries so concurrent clients don't stampede the origin in\n * lockstep after a shared failure.\n *\n * @param {number} attempt - zero-based retry attempt index\n * @param {number} baseDelay - base delay in ms\n * @param {number} maxDelay - upper bound in ms\n */\nexport const getBackoffDelayMs = (\n\tattempt: number,\n\tbaseDelay: number,\n\tmaxDelay: number,\n): number => {\n\tconst exponential = Math.min(maxDelay, baseDelay * 2 ** attempt)\n\treturn Math.round(Math.random() * exponential)\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","import {\n\tCONFIG_RETRY_BASE_DELAY_MS,\n\tCONFIG_RETRY_COUNT,\n\tCONFIG_RETRY_MAX_DELAY_MS,\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { delay } from './utils/delay'\nimport { getReasonFromStatus, ImproveFetchError } from './utils/errors'\nimport { getRandomString } from './utils/getRandomString'\nimport { normalizeConfig } from './utils/normalizeConfig'\nimport { getBackoffDelayMs, parseRetryAfterMs } from './utils/retry'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n\tretries: number\n\trevalidate?: number\n}\n\n// Next.js augments RequestInit with a `next` field for its fetch cache.\ntype RequestInitWithNext = RequestInit & {\n\tnext?: { revalidate?: number; tags?: string[] }\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState\n\n\t#configFetch: ConfigFetch | null = null\n\n\t#warningsDisabled: boolean = false\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t\tconfigRetries,\n\t\tconfigRevalidate,\n\t\tdisableWarnings,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\t\tthis.#warningsDisabled = disableWarnings ?? false\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t\tretries: configRetries ?? CONFIG_RETRY_COUNT,\n\t\t\t\trevalidate: configRevalidate,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst { url, timeout, retries, revalidate } = this.#configFetch\n\n\t\t// Opt into the Next.js fetch cache when a revalidate window is configured,\n\t\t// so the datafile is reused across requests instead of re-fetched.\n\t\tconst requestInit: RequestInitWithNext = { ...config }\n\t\tif (typeof revalidate === 'number') {\n\t\t\trequestInit.next = {\n\t\t\t\t...(config as RequestInitWithNext | undefined)?.next,\n\t\t\t\trevalidate,\n\t\t\t}\n\t\t}\n\n\t\t// Seed with a generic network error so an exhausted loop always has a\n\t\t// meaningful, typed error to throw.\n\t\tlet lastError = new ImproveFetchError('network')\n\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\t// Prefer a server-provided Retry-After; otherwise back off with jitter.\n\t\t\t\tawait delay(\n\t\t\t\t\tlastError.retryAfterMs ??\n\t\t\t\t\t\tgetBackoffDelayMs(\n\t\t\t\t\t\t\tattempt - 1,\n\t\t\t\t\t\t\tCONFIG_RETRY_BASE_DELAY_MS,\n\t\t\t\t\t\t\tCONFIG_RETRY_MAX_DELAY_MS,\n\t\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tlet res: Response | null\n\t\t\ttry {\n\t\t\t\tres = await timeoutFetch(timeout, url, requestInit)\n\t\t\t} catch (cause) {\n\t\t\t\t// timeoutFetch aborts on timeout (AbortError) and rejects on\n\t\t\t\t// network-level failures — both are transient and retryable.\n\t\t\t\tconst aborted = cause instanceof Error && cause.name === 'AbortError'\n\t\t\t\tlastError = new ImproveFetchError(aborted ? 'timeout' : 'network', {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// timeoutFetch resolves null when the timeout wins the race.\n\t\t\tif (!res) {\n\t\t\t\tlastError = new ImproveFetchError('timeout')\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (res.ok) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.config = normalizeConfig(await res.json())\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tlastError =\n\t\t\t\t\t\tcause instanceof ImproveFetchError\n\t\t\t\t\t\t\t? cause\n\t\t\t\t\t\t\t: new ImproveFetchError('invalid-response', { cause })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn this.config\n\t\t\t}\n\n\t\t\tlastError = new ImproveFetchError(getReasonFromStatus(res.status), {\n\t\t\t\tstatus: res.status,\n\t\t\t\tretryAfterMs: parseRetryAfterMs(res.headers.get('Retry-After')),\n\t\t\t})\n\n\t\t\t// Auth/validation rejections won't change on retry — fail fast.\n\t\t\tif (!lastError.isRetryable) break\n\t\t}\n\n\t\tthrow lastError\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\t/**\n\t * Emit a development warning, unless warnings were disabled via the\n\t * `disableWarnings` setup option. Centralized so every SDK warning honors the\n\t * same switch. No-op where `console` is unavailable.\n\t */\n\tprotected _warn = (message: string) => {\n\t\tif (this.#warningsDisabled) return\n\t\tif (typeof console === 'undefined') return\n\t\tconsole.warn(`[Improve] ${message}`)\n\t}\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\nexport const ANALYTICS_PATH = `/api/log`\n","import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { ImproveConfiguration, ImproveSetupArgs } from './types'\n\nconst DEFAULT_MAX_VISITORS = 10_000\n\ntype VisitorData = {\n\t[userAgent: string]: ParsedUserAgent & {\n\t\t[testSlug: string]: string\n\t}\n}\n\ntype ImproveServerSetupArgs =\n\t| (Omit<ImproveSetupArgs, 'config' | 'baseUrl'> & {\n\t\t\tconfig: ImproveConfiguration\n\t\t\tmaxVisitors?: number\n\t })\n\t| (Omit<ImproveSetupArgs, 'config'> & {\n\t\t\ttoken: string\n\t\t\tmaxVisitors?: number\n\t })\n\nexport class ImproveServerSDK extends BaseImproveSDK {\n\t#visitors: Map<string, VisitorData> = new Map()\n\t#maxVisitors: number\n\t#token: string\n\n\t// @ts-ignore It could be there\n\tconstructor({ token, maxVisitors, ...args }: ImproveServerSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#token = token\n\t\tthis.#maxVisitors = maxVisitors ?? DEFAULT_MAX_VISITORS\n\t}\n\n\tfetchConfig = async (config?: RequestInit) => {\n\t\treturn this._fetchConfig({\n\t\t\t...config,\n\t\t\theaders: {\n\t\t\t\t...config?.headers,\n\t\t\t\ttoken: this.#token,\n\t\t\t},\n\t\t})\n\t}\n\n\tgetFlagConfig = (flagSlug: string) => this.config?.flags?.[flagSlug]\n\n\tgetTestConfig = (testSlug: string) => this.config?.tests?.[testSlug]\n\n\t#getVisitor = (visitorId: string, userAgent: string) => {\n\t\tlet visitor = this.#visitors.get(visitorId)\n\t\tif (visitor) {\n\t\t\t// Move to end for LRU freshness\n\t\t\tthis.#visitors.delete(visitorId)\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t} else {\n\t\t\t// Evict oldest entries when at capacity\n\t\t\tif (this.#visitors.size >= this.#maxVisitors) {\n\t\t\t\tconst oldest = this.#visitors.keys().next().value\n\t\t\t\tif (oldest) this.#visitors.delete(oldest)\n\t\t\t}\n\t\t\tvisitor = {}\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t}\n\t\tvisitor[userAgent] = visitor[userAgent] || parseUserAgent(userAgent)\n\t\treturn visitor\n\t}\n\n\tgetFlagValue = (flagSlug: string, visitorId: string, userAgent: string) => {\n\t\tconst flagConfig = this.getFlagConfig(flagSlug)\n\n\t\tif (!flagConfig || !this.config) return null\n\t\tif (!visitorId) return flagConfig.options[0].slug\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\n\t\tif (visitor[userAgent]?.[flagSlug]) {\n\t\t\treturn visitor[userAgent][flagSlug]\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tvisitor[userAgent],\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tvisitor[userAgent][flagSlug] = flagValue\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string, visitorId: string, userAgent: string) => {\n\t\tconst testConfig = this.getTestConfig(testSlug)\n\n\t\tif (!testConfig || !this.config) return null\n\n\t\tif (!visitorId || !userAgent) return testConfig.defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\n\t\tif (visitor[userAgent]?.[testSlug]) {\n\t\t\treturn visitor[userAgent][testSlug]\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tvisitor[userAgent],\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tvisitor[userAgent][testSlug] = testConfig.defaultValue\n\t\t\treturn visitor[userAgent][testSlug]\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tvisitor[userAgent][testSlug] = testValue\n\t\treturn testValue\n\t}\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","length","slug","value","Math","random","reduce","acc","split","match","find","VISITOR_ID_PREFIX","delay","ms","Promise","resolve","setTimeout","REASON_MESSAGES","timeout","network","unauthorized","server","client","ImproveFetchError","Error","reason","name","status","retryAfterMs","cause","isRetryable","getReasonFromStatus","CHAR_SET","normalizeConfig","body","Array","isArray","raw","version","tests","flags","parseRetryAfterMs","header","now","Date","seconds","Number","isFinite","max","round","date","parse","isNaN","getBackoffDelayMs","attempt","baseDelay","maxDelay","min","getTimeoutError","controller","abort","timeoutFetch","url","AbortController","race","fetch","signal","BaseImproveSDK","organizationId","environment","state","config","fetchTimeout","baseUrl","configRetries","configRevalidate","disableWarnings","_fetchConfig","retries","revalidate","requestInit","next","lastError","res","aborted","ok","json","headers","get","loadConfig","generateVisitorId","getRandomString","characters","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","_warn","message","console","warn","validateTestValue","testName","testValue","testConfig","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl","ImproveServerSDK","token","maxVisitors","args","Map","fetchConfig","getFlagConfig","flagSlug","getTestConfig","testSlug","visitorId","userAgent","visitor","delete","set","size","oldest","keys","parseUserAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType","getFlagValue","flagConfig","flagValue","getTestValue","defaultValue","allocation"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -19,12 +19,37 @@ type ImproveSetupArgs = {
19
19
  config?: ImproveConfiguration;
20
20
  baseUrl?: string;
21
21
  fetchTimeout?: number;
22
+ /**
23
+ * How many times to retry a failed config fetch before giving up. Only
24
+ * transient failures are retried (network errors, timeouts, HTTP 5xx, and
25
+ * 429 rate limits); auth/validation rejections (403/404/400) fail fast.
26
+ * Retries use exponential backoff with jitter and honor `Retry-After`.
27
+ * Defaults to 2. Set to 0 to disable retries (e.g. in latency-sensitive
28
+ * middleware).
29
+ */
30
+ configRetries?: number;
31
+ /**
32
+ * When set, forwarded to the config fetch as `next: { revalidate }`
33
+ * (seconds), so Next.js / edge runtimes cache the config datafile between
34
+ * requests instead of fetching it on every request. The backend serves the
35
+ * datafile with a short `s-maxage` + `stale-while-revalidate`, so a small
36
+ * value here (e.g. 30) lets a deployment reuse it and shields the origin
37
+ * under load. No effect outside fetch implementations that honor
38
+ * `next.revalidate`.
39
+ */
40
+ configRevalidate?: number;
22
41
  /**
23
42
  * Mirror analytics onto the GTM dataLayer (window.dataLayer) with experiment
24
43
  * dimensions, so they can drive Google Tag Manager / Google Ads conversions.
25
44
  * Enabled by default; set to `false` to opt out. No effect server-side.
26
45
  */
27
46
  dataLayer?: boolean;
47
+ /**
48
+ * Silence all Improve development warnings (e.g. the nudge when an event name
49
+ * isn't `snake_case`). Warnings are on by default to steer teams toward the
50
+ * naming convention; set to `true` in production or to opt out entirely.
51
+ */
52
+ disableWarnings?: boolean;
28
53
  };
29
54
  type ImproveFlagOption = {
30
55
  name: string;
@@ -47,10 +72,63 @@ type ImproveFlag = {
47
72
  type ImproveFlags = {
48
73
  [flagSlug in string]: ImproveFlag;
49
74
  };
75
+ /**
76
+ * Recommended analytic event names, aligned with the Google Analytics 4 /
77
+ * Google Tag Manager recommended events. Reusing these means your Improve
78
+ * events line up with any GA4/GTM tagging you already have — especially when
79
+ * mirroring analytics onto the GTM dataLayer (`dataLayer` setup option).
80
+ *
81
+ * @see https://support.google.com/analytics/answer/9267735
82
+ */
83
+ type ImproveRecommendedEventName = 'page_view' | 'view_item' | 'view_item_list' | 'select_item' | 'add_to_cart' | 'remove_from_cart' | 'view_cart' | 'begin_checkout' | 'add_payment_info' | 'add_shipping_info' | 'purchase' | 'refund' | 'sign_up' | 'login' | 'search' | 'select_content' | 'share' | 'generate_lead';
84
+ /**
85
+ * The name of an analytic event, e.g. the `event` passed to `postAnalytic`.
86
+ *
87
+ * Prefer **`snake_case`** names (`add_to_cart`, `sign_up`, `purchase`) to match
88
+ * GA4 / GTM conventions, and reuse the same name everywhere the action happens.
89
+ * Common GA4 recommended names are offered as autocomplete suggestions, but any
90
+ * string is accepted so you can still use your own custom event names.
91
+ */
92
+ type ImproveEventName = ImproveRecommendedEventName | (string & {});
50
93
  type ImproveEvents = {
51
- start: string;
52
- metrics: string[];
53
- conversion: string;
94
+ start: ImproveEventName;
95
+ metrics: ImproveEventName[];
96
+ conversion: ImproveEventName;
97
+ /**
98
+ * Ordered funnel steps for the multi-step conversion view, when the test
99
+ * defines one. The last step is the conversion; when absent, the funnel is
100
+ * `[start, ...metrics, conversion]`.
101
+ */
102
+ funnel?: ImproveEventName[];
103
+ };
104
+ /**
105
+ * Structured payload for an analytic event, aligned with Google Analytics 4
106
+ * event parameters.
107
+ *
108
+ * - `value` (with `currency`) is the numeric measure Improve aggregates into
109
+ * **revenue / average order value per variant** — pass an order total on your
110
+ * conversion event to compare not just how often a variant converts, but at
111
+ * what value.
112
+ * - `params` is passed through to the GTM dataLayer and stored as JSON, but is
113
+ * not aggregated by Improve's own results. Use it for a GA4 `ecommerce`
114
+ * object (`{ ecommerce: { items: [...] } }`) or any custom event parameters.
115
+ * - `message` is kept for the simple single-string case.
116
+ *
117
+ * `postAnalytic` also accepts a plain string as a shorthand for `{ message }`.
118
+ */
119
+ type ImproveAnalyticPayload = {
120
+ /** Numeric value of the event, e.g. an order total. GA4 `value`. */
121
+ value?: number;
122
+ /** ISO 4217 currency code for `value`, e.g. `'USD'`. GA4 `currency`. */
123
+ currency?: string;
124
+ /** A short freeform label stored alongside the event. */
125
+ message?: string;
126
+ /**
127
+ * Extra event parameters, spread onto the GTM dataLayer entry and stored as
128
+ * JSON. Provide `{ ecommerce: {...} }` for GA4 ecommerce tags; the SDK clears
129
+ * the previous `ecommerce` object first to avoid data bleed between events.
130
+ */
131
+ params?: Record<string, unknown>;
54
132
  };
55
133
  type ImproveResult = {
56
134
  result: {
@@ -91,5 +169,5 @@ type ImproveConfiguration = {
91
169
  audience: ImproveAudience;
92
170
  };
93
171
 
94
- export type { ImproveAudience, ImproveAudienceValue, ImproveConfiguration, ImproveEnvironmentOption, ImproveEvents, ImproveFlag, ImproveFlagOption, ImproveFlags, ImproveResult, ImproveSetupArgs, ImproveTest, ImproveTestOption, ImproveTestState, ImproveTests };
172
+ export type { ImproveAnalyticPayload, ImproveAudience, ImproveAudienceValue, ImproveConfiguration, ImproveEnvironmentOption, ImproveEventName, ImproveEvents, ImproveFlag, ImproveFlagOption, ImproveFlags, ImproveRecommendedEventName, ImproveResult, ImproveSetupArgs, ImproveTest, ImproveTestOption, ImproveTestState, ImproveTests };
95
173
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sources":["../src/config/audiences.ts","../src/types.ts"],"mappings":"AAAA;AACA;AACA;AACA;AACA,cAAc,eAAe;AAC7B;AACA;AACA;AACA;AACA;AACA,KAAK,gBAAgB,gBAAgB,eAAe;;ACTpD,KAAK,wBAAwB;AAC7B,KAAK,gBAAgB;AACrB,KAAK,gBAAgB;AACrB;AACA,iBAAiB,wBAAwB;AACzC,YAAY,gBAAgB;AAC5B,aAAa,oBAAoB;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB;AACA;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA,aAAa,iBAAiB;AAC9B;AACA,KAAK,YAAY;AACjB,0BAA0B,WAAW;AACrC;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA,aAAa,iBAAiB;AAC9B,YAAY,aAAa;AACzB;AACA,KAAK,YAAY;AACjB,0BAA0B,WAAW;AACrC;AACA,KAAK,oBAAoB;AACzB,YAAY,gBAAgB;AAC5B;AACA,KAAK,eAAe;AACpB,8BAA8B,oBAAoB;AAClD;AACA,KAAK,oBAAoB;AACzB;AACA;AACA,WAAW,YAAY;AACvB,WAAW,YAAY;AACvB,cAAc,eAAe;AAC7B;;;;","names":[]}
1
+ {"version":3,"file":"types.d.ts","sources":["../src/config/audiences.ts","../src/types.ts"],"mappings":"AAAA;AACA;AACA;AACA;AACA,cAAc,eAAe;AAC7B;AACA;AACA;AACA;AACA;AACA,KAAK,gBAAgB,gBAAgB,eAAe;;ACTpD,KAAK,wBAAwB;AAC7B,KAAK,gBAAgB;AACrB,KAAK,gBAAgB;AACrB;AACA,iBAAiB,wBAAwB;AACzC,YAAY,gBAAgB;AAC5B,aAAa,oBAAoB;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB;AACA;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA,aAAa,iBAAiB;AAC9B;AACA,KAAK,YAAY;AACjB,0BAA0B,WAAW;AACrC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,2BAA2B;AAChC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,gBAAgB,GAAG,2BAA2B;AACnD,KAAK,aAAa;AAClB,WAAW,gBAAgB;AAC3B,aAAa,gBAAgB;AAC7B,gBAAgB,gBAAgB;AAChC;AACA;AACA;AACA;AACA;AACA,aAAa,gBAAgB;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,sBAAsB;AAC3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA,aAAa,iBAAiB;AAC9B,YAAY,aAAa;AACzB;AACA,KAAK,YAAY;AACjB,0BAA0B,WAAW;AACrC;AACA,KAAK,oBAAoB;AACzB,YAAY,gBAAgB;AAC5B;AACA,KAAK,eAAe;AACpB,8BAA8B,oBAAoB;AAClD;AACA,KAAK,oBAAoB;AACzB;AACA;AACA,WAAW,YAAY;AACvB,WAAW,YAAY;AACvB,cAAc,eAAe;AAC7B;;;;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@obelism/improve-sdk",
3
3
  "description": "Obelism Improve SDK",
4
- "version": "1.0.0",
4
+ "version": "1.2.0",
5
5
  "keywords": [
6
6
  "ab-tests",
7
7
  "feature-flags",