@clickroom/sdk-js 0.2.1 → 0.3.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/clickroom.js CHANGED
@@ -1 +1 @@
1
- "use strict";(()=>{var ee=new Set(["a","button","input","select","textarea","label"]),te="ch-no-capture",ne="data-ch-no-capture";function _(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/;/g,"\\;").replace(/\|/g,"\\|").replace(/\r?\n/g," ").trim()}function re(e){let t=1,n=e.previousElementSibling;for(;n;)t+=1,n=n.previousElementSibling;return t}function oe(e){let t=1,n=e.tagName,r=e.previousElementSibling;for(;r;)r.tagName===n&&(t+=1),r=r.previousElementSibling;return t}function ie(e){var r,i;let n=((i=(r=e.innerText)!=null?r:e.textContent)!=null?i:"").replace(/\s+/g," ").trim();return n.length>255?n.slice(0,255):n}function ae(e){let t=e.tagName.toLowerCase(),n=[],r=e.getAttribute("id");r&&n.push(`attr__id="${_(r)}"`);let i=typeof e.className=="string"?e.className:"";if(i&&n.push(`attr__class="${_(i)}"`),t==="a"){let c=e.getAttribute("href");c&&n.push(`href="${_(c)}"`)}let l=ie(e);l&&n.push(`text="${_(l)}"`),n.push(`nth-child="${re(e)}"`),n.push(`nth-of-type="${oe(e)}"`);let s=e.attributes;for(let c=0;c<s.length;c+=1){let f=s.item(c);f!=null&&f.name.startsWith("data-")&&n.push(`attr__${f.name}="${_(f.value)}"`)}return`${t}:${n.join(";")}`}function N(e){let t=[],n=e,r=0;for(;n&&r<15;){let i=n.tagName.toLowerCase();if(i==="body"||i==="html")break;t.push(ae(n)),n=n.parentElement,r+=1}return t.join("|")}function se(e){var n;let t=e;for(;t;){if((n=t.classList)!=null&&n.contains(te)||t.hasAttribute(ne))return!0;t=t.parentElement}return!1}function le(e){let t=e.tagName.toLowerCase();return!!(ee.has(t)||e.hasAttribute("data-ch-capture")||e.getAttribute("role")||e.hasAttribute("onclick"))}function D(e){let t=e,n=0;for(;t&&n<15;){if(se(t))return null;if(le(t))return t;t=t.parentElement,n+=1}return null}function H(){return{$current_url:location.href,$host:location.host,$pathname:location.pathname}}function B(e){if(typeof document=="undefined"||typeof window=="undefined")return()=>{};let t=f=>{try{let u=f.target;if(!(u instanceof Element))return;let a=D(u);if(!a)return;let g=N(a);e.capture("$autocapture",{$event_type:f.type,...H()},g)}catch(u){}},n=["click","submit","change"];if(e.config.autocapture)for(let f of n)document.addEventListener(f,t,!0);let r="",i=()=>{try{if(!e.config.capture_pageview||location.href===r)return;r=location.href,e.capture("$pageview",{...H(),$referrer:document.referrer})}catch(f){}},l=history.pushState.bind(history),s=history.replaceState.bind(history),c=!1;return e.config.capture_pageview&&(history.pushState=(...f)=>{l(...f),i()},history.replaceState=(...f)=>{s(...f),i()},c=!0,window.addEventListener("popstate",i),window.addEventListener("hashchange",i),i()),()=>{if(e.config.autocapture)for(let f of n)document.removeEventListener(f,t,!0);c&&(history.pushState=l,history.replaceState=s,window.removeEventListener("popstate",i),window.removeEventListener("hashchange",i))}}var ce=/^(cpc|ppc|paid|display)$/i,ue=["google.","bing.com","duckduckgo.com","yahoo.","baidu.com","yandex.","ecosia.org","ask.com","aol.com"],fe=["facebook.com","fb.com","twitter.com","x.com","t.co","linkedin.com","instagram.com","tiktok.com","reddit.com","pinterest.","youtube.com","threads.net"],U=(e,t)=>t.some(n=>e.includes(n)),de=e=>{if(e)try{return new URL(e).hostname.toLowerCase()}catch(t){return}};function V(e,t,n){let r=de(e),i=t.$utm_medium,l=!!r,s=Object.keys(t).length>0;return!l&&!s?"direct":i&&ce.test(i)?"paid":r&&U(r,ue)?"organic search":r&&U(r,fe)?"social":i&&i.toLowerCase()==="email"?"email":r&&r!==n.toLowerCase()?"referral":"direct"}var ge=[{name:"Edge",re:/Edg(?:e|A|iOS)?\/(\d+)/},{name:"Opera",re:/(?:OPR|Opera)\/(\d+)/},{name:"Samsung Internet",re:/SamsungBrowser\/(\d+)/},{name:"Firefox",re:/(?:Firefox|FxiOS)\/(\d+)/},{name:"Chrome",re:/(?:Chrome|CriOS)\/(\d+)/},{name:"Safari",re:/Version\/(\d+).*Safari/}],pe=e=>{for(let t of ge){let n=e.match(t.re);if(n)return{name:t.name,version:n[1]}}return{}},me=e=>{if(/Windows/.test(e))return"Windows";if(/iPhone|iPad|iPod/.test(e))return"iOS";if(/Android/.test(e))return"Android";if(/CrOS/.test(e))return"Chrome OS";if(/Mac OS X|Macintosh/.test(e))return"macOS";if(/Linux/.test(e))return"Linux"},he=e=>/iPad/.test(e)?"Tablet":/Android/.test(e)?/Mobile/.test(e)?"Mobile":"Tablet":/iPhone|iPod/.test(e)||/Mobile/.test(e)?"Mobile":"Desktop";function X(e,t){let n={},r=e!=null?e:"";if(r){let i=pe(r);i.name&&(n.$browser=i.name),i.version&&(n.$browser_version=i.version);let l=me(r);l&&(n.$os=l),n.$device_type=he(r)}return t&&(n.$lang=t),n}var ze=Number(BigInt("0xfffffffffffffff"));var R="ckrm_flags",P=()=>{try{return typeof localStorage=="undefined"?null:localStorage}catch(e){return null}};function j(e){let t=n();function n(){var u;try{let a=(u=P())==null?void 0:u.getItem(R);if(!a)return{};let g=JSON.parse(a);if(g&&typeof g=="object")return g}catch(a){}return{}}let r=u=>{var a;try{(a=P())==null||a.setItem(R,JSON.stringify(u))}catch(g){}},i,l=()=>`${e.getApiHost().replace(/\/+$/,"")}/v1/flags`,s=()=>`${e.getApiHost().replace(/\/+$/,"")}/v1/flags/version`,c=async()=>{try{if(typeof fetch!="function")return t;let u=await fetch(l(),{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${e.getWriteKey()}`},body:JSON.stringify({distinct_id:e.getDistinctId(),person_properties:e.getPersonProperties()})});if(!u.ok)return t;let a=await u.json(),g=a==null?void 0:a.flags;return g&&typeof g=="object"&&(t=g,r(t)),t}catch(u){return t}};return{getCache:()=>t,isFeatureEnabled:u=>{var a,g;return(g=(a=t[u])==null?void 0:a.enabled)!=null?g:!1},getFeatureFlag:u=>{let a=t[u];return a!=null&&a.enabled?a.variant!=null?a.variant:!0:!1},getFeatureFlagPayload:u=>{var a,g;return(g=(a=t[u])==null?void 0:a.payload)!=null?g:null},fetchFlags:c,checkVersionAndMaybeFetch:async()=>{let u=!0,a;try{if(typeof fetch=="function"){let w=await fetch(s(),{method:"GET",headers:{authorization:`Bearer ${e.getWriteKey()}`}});if(w.ok){let y=await w.json();a=y==null?void 0:y.version,u=i===void 0||a!==i}}}catch(w){}if(!u)return{changed:!1,flags:t};let g=await c();return a!==void 0&&(i=a),{changed:!0,flags:g}},clear:()=>{var u;t={};try{(u=P())==null||u.removeItem(R)}catch(a){}}}}var x="ckrm_sid",ye=e=>{if(!e)return null;try{let t=JSON.parse(e);if(t&&typeof t=="object"&&typeof t.id=="string"&&typeof t.start=="number"&&typeof t.last=="number")return t}catch(t){}return null};function G({now:e,storage:t,genId:n}){let r=()=>{var s,c;try{return ye((c=(s=t())==null?void 0:s.getItem(x))!=null?c:null)}catch(f){return null}},i=s=>{var c;try{(c=t())==null||c.setItem(x,JSON.stringify(s))}catch(f){}},l=s=>{let c={id:n(),start:s,last:s};return i(c),c};return{getSessionId(){let s=e(),c=r();return!c||s-c.last>18e5||s-c.start>864e5?l(s).id:(c.last=s,i(c),c.id)},reset(){var s;try{(s=t())==null||s.removeItem(x)}catch(c){}}}}var A=()=>typeof window!="undefined";function K(e,t){let n=[],r=[],i=0,l=null,s=null,c=()=>{let{api_host:d}=e();return`${d.replace(/\/+$/,"")}/v1/capture?token=${encodeURIComponent(t())}`},f=d=>JSON.stringify({batch:d}),u=()=>{if(l!==null||r.length===0)return;let d=Math.min(1e3*2**i,3e4);try{l=setTimeout(()=>{l=null,v(!1)},d)}catch(m){l=null}},a=d=>{r=r.concat(d),r.length>1e3&&(r=r.slice(r.length-1e3)),i=Math.min(i+1,5),u()},g=(d,m)=>{try{if(typeof navigator=="undefined"||typeof navigator.sendBeacon!="function")return!1;let p=new Blob([m],{type:"text/plain"});return navigator.sendBeacon(d,p)}catch(p){return!1}},w=async(d,m)=>{let p;try{p=await d.json()}catch(h){}console.warn(`[clickroom] capture() batch rejected by server (HTTP ${d.status}); dropping ${m.length} event(s)`,p)},y=(d,m,p)=>{try{if(typeof fetch!="function"){a(p);return}fetch(d,{method:"POST",keepalive:!0,headers:{"content-type":"application/json"},body:m}).then(h=>{if(h.ok){i=0,r.length>0&&u();return}if(h.status>=500){a(p);return}w(h,p)}).catch(()=>{a(p)})}catch(h){a(p)}};function v(d=!1){if(!A()){n=[];return}let m=r.concat(n);if(n=[],r=[],m.length!==0)try{let p=c();for(let h=0;h<m.length;h+=100){let k=m.slice(h,h+100),I=f(k);if(!(d&&g(p,I))){if(d){y(p,I,k);continue}y(p,I,k)}}}catch(p){}}let O=()=>{try{typeof document!="undefined"&&document.hidden&&v(!0)}catch(d){}},S=()=>{v(!0)},Z=d=>{if(A())try{n.push(d),n.length>=20&&v(!1)}catch(m){}},Q=()=>{try{s!==null&&(clearInterval(s),s=null),l!==null&&(clearTimeout(l),l=null),typeof document!="undefined"&&document.removeEventListener("visibilitychange",O),typeof window!="undefined"&&(window.removeEventListener("pagehide",S),window.removeEventListener("beforeunload",S))}catch(d){}};if(A())try{s=setInterval(()=>v(!1),3e3),typeof document!="undefined"&&document.addEventListener("visibilitychange",O),window.addEventListener("pagehide",S),window.addEventListener("beforeunload",S)}catch(d){}return{enqueue:Z,flush:v,teardown:Q}}var ve=["source","medium","campaign","term","content"];function z(e){let t={};if(!e)return t;try{let n=new URLSearchParams(e);for(let r of ve){let i=n.get(`utm_${r}`);i&&(t[`$utm_${r}`]=i)}}catch(n){}return t}var we="clickroom-js",_e="0.2.1",C="ckrm_did",be="ckrm_",E={api_host:"https://capture.clickroom.co",autocapture:!0,capture_pageview:!0},Se=()=>typeof window!="undefined",Ee=5e3,o={initialized:!1,warnedNotInit:!1,writeKey:"",config:{...E},distinctId:"",personProperties:{},transport:null,session:null,sessionContext:null,teardownAutocapture:null,flagListeners:[],flagClient:null,flagsLoaded:!1,emittedFlagCalls:new Set,flagPollId:null},M=()=>{try{return typeof localStorage=="undefined"?null:localStorage}catch(e){return null}},Ce=()=>{try{return typeof sessionStorage=="undefined"?null:sessionStorage}catch(e){return null}},Fe=e=>{var t,n;try{return(n=(t=M())==null?void 0:t.getItem(e))!=null?n:null}catch(r){return null}},T=(e,t)=>{var n;try{(n=M())==null||n.setItem(e,t)}catch(r){}},Me=e=>{var t;try{(t=M())==null||t.removeItem(e)}catch(n){}},$=()=>{try{if(typeof crypto!="undefined"&&typeof crypto.randomUUID=="function")return crypto.randomUUID()}catch(e){}return`ckrm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,12)}`},ke=()=>{let e=Fe(C);if(e)return e;let t=$();return T(C,t),t},Ie=()=>{let e={$lib:we,$lib_version:_e};try{typeof location!="undefined"&&(e.$current_url=location.href,e.$host=location.host,e.$pathname=location.pathname),typeof document!="undefined"&&(e.$referrer=document.referrer),typeof window!="undefined"&&window.screen&&(e.$screen_height=window.screen.height,e.$screen_width=window.screen.width),typeof window!="undefined"&&(e.$viewport_height=window.innerHeight,e.$viewport_width=window.innerWidth),typeof navigator!="undefined"&&Object.assign(e,X(navigator.userAgent,navigator.language))}catch(t){}return e},W=()=>{try{let e=typeof location!="undefined"?location.search:"",t=typeof document!="undefined"?document.referrer:"",n=typeof location!="undefined"?location.hostname:"",r=z(e);return{...r,$channel_type:V(t,r,n)}}catch(e){return{}}},Re=e=>e?((!o.sessionContext||o.sessionContext.sessionId!==e)&&(o.sessionContext={sessionId:e,props:W()}),o.sessionContext.props):W(),Y=()=>{if(!o.warnedNotInit){o.warnedNotInit=!0;try{console.warn("[clickroom] capture() called before init(); event dropped")}catch(e){}}},b=(e,t,n)=>{var r;if(!o.initialized||!o.transport){Y();return}try{let i=(r=o.session)==null?void 0:r.getSessionId(),l=i?{$session_id:i}:{},s={event:e,distinct_id:o.distinctId,timestamp:new Date().toISOString(),properties:{...Ie(),...l,...Re(i),...t!=null?t:{}}};n&&(s.elements_chain=n),o.transport.enqueue(s)}catch(i){}},J=e=>{let t={};for(let[n,r]of Object.entries(e))r.enabled&&(t[n]=r.variant!=null?r.variant:!0);return t},Pe=()=>{if(!o.flagClient)return;let e=J(o.flagClient.getCache());for(let t of o.flagListeners)try{t(e)}catch(n){}},F=()=>{o.flagClient&&o.flagClient.checkVersionAndMaybeFetch().then(()=>{o.flagsLoaded=!0,Pe()}).catch(()=>{})},q=(e,t)=>{try{if(!o.flagsLoaded)return;let n=String(t),r=`${e}:${n}`;if(o.emittedFlagCalls.has(r))return;o.emittedFlagCalls.add(r),b("$feature_flag_called",{$feature_flag:e,$feature_flag_response:n})}catch(n){}};function xe(e,t){var n,r,i;if(!o.initialized&&Se()){if(o.writeKey=e,o.config={api_host:(n=t==null?void 0:t.api_host)!=null?n:E.api_host,autocapture:(r=t==null?void 0:t.autocapture)!=null?r:E.autocapture,capture_pageview:(i=t==null?void 0:t.capture_pageview)!=null?i:E.capture_pageview},o.distinctId=ke(),o.session=G({now:()=>Date.now(),storage:Ce,genId:$}),o.transport=K(()=>o.config,()=>o.writeKey),o.flagClient=j({getApiHost:()=>o.config.api_host,getWriteKey:()=>o.writeKey,getDistinctId:()=>o.distinctId,getPersonProperties:()=>o.personProperties}),o.initialized=!0,F(),t!=null&&t.feature_flag_poll_interval){let l=Math.max(t.feature_flag_poll_interval,Ee);o.flagPollId=setInterval(()=>F(),l)}if(o.config.autocapture){let l={config:o.config,capture:b};try{o.teardownAutocapture=B(l)}catch(s){}}else o.config.capture_pageview&&b("$pageview")}}function Ae(e,t){b(e,t)}function Te(e,t){if(!o.initialized){Y();return}let n=o.distinctId;o.distinctId=e,T(C,e),t&&(o.personProperties={...o.personProperties,...t}),b("$identify",{$set:t!=null?t:{},...n!==e?{$anon_distinct_id:n}:{}}),o.emittedFlagCalls.clear(),F()}function $e(){var e,t;try{let n=M();if(n){let r=[];for(let i=0;i<n.length;i+=1){let l=n.key(i);l!=null&&l.startsWith(be)&&r.push(l)}for(let i of r)Me(i)}}catch(n){}o.distinctId=$(),T(C,o.distinctId),(e=o.session)==null||e.reset(),o.sessionContext=null,o.personProperties={},o.flagsLoaded=!1,o.emittedFlagCalls.clear(),(t=o.flagClient)==null||t.clear()}function Le(e){var n,r;let t=(r=(n=o.flagClient)==null?void 0:n.isFeatureEnabled(e))!=null?r:!1;return q(e,t),t}function Oe(e){var n,r;let t=(r=(n=o.flagClient)==null?void 0:n.getFeatureFlag(e))!=null?r:!1;return q(e,t),t}function Ne(e){var t,n;return(n=(t=o.flagClient)==null?void 0:t.getFeatureFlagPayload(e))!=null?n:null}function De(e){if(o.flagListeners.push(e),o.flagsLoaded&&o.flagClient)try{e(J(o.flagClient.getCache()))}catch(t){}}function He(){F()}function Be(){o.flagPollId!==null&&(clearInterval(o.flagPollId),o.flagPollId=null)}var Ue={init:xe,capture:Ae,identify:Te,reset:$e,isFeatureEnabled:Le,getFeatureFlag:Oe,getFeatureFlagPayload:Ne,onFeatureFlags:De,refreshFeatureFlags:He,stopFeatureFlagPolling:Be};var L=Ue;if(typeof window!="undefined"){let e=L;e.__loaded__=!0,window.clickroom=e}var st=L;})();
1
+ "use strict";(()=>{var ne=new Set(["a","button","input","select","textarea","label"]),re="ch-no-capture",oe="data-ch-no-capture";function E(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/;/g,"\\;").replace(/\|/g,"\\|").replace(/\r?\n/g," ").trim()}function ie(e){let t=1,n=e.previousElementSibling;for(;n;)t+=1,n=n.previousElementSibling;return t}function ae(e){let t=1,n=e.tagName,r=e.previousElementSibling;for(;r;)r.tagName===n&&(t+=1),r=r.previousElementSibling;return t}function se(e){var r,i;let n=((i=(r=e.innerText)!=null?r:e.textContent)!=null?i:"").replace(/\s+/g," ").trim();return n.length>255?n.slice(0,255):n}function le(e){let t=e.tagName.toLowerCase(),n=[],r=e.getAttribute("id");r&&n.push(`attr__id="${E(r)}"`);let i=typeof e.className=="string"?e.className:"";if(i&&n.push(`attr__class="${E(i)}"`),t==="a"){let h=e.getAttribute("href");h&&n.push(`href="${E(h)}"`)}let s=se(e);s&&n.push(`text="${E(s)}"`),n.push(`nth-child="${ie(e)}"`),n.push(`nth-of-type="${ae(e)}"`);let m=e.attributes;for(let h=0;h<m.length;h+=1){let g=m.item(h);g!=null&&g.name.startsWith("data-")&&n.push(`attr__${g.name}="${E(g.value)}"`)}return`${t}:${n.join(";")}`}function U(e){let t=[],n=e,r=0;for(;n&&r<15;){let i=n.tagName.toLowerCase();if(i==="body"||i==="html")break;t.push(le(n)),n=n.parentElement,r+=1}return t.join("|")}function ce(e){var n;let t=e;for(;t;){if((n=t.classList)!=null&&n.contains(re)||t.hasAttribute(oe))return!0;t=t.parentElement}return!1}function ue(e){let t=e.tagName.toLowerCase();return!!(ne.has(t)||e.hasAttribute("data-ch-capture")||e.getAttribute("role")||e.hasAttribute("onclick"))}function W(e){let t=e,n=0;for(;t&&n<15;){if(ce(t))return null;if(ue(t))return t;t=t.parentElement,n+=1}return null}function V(){return{$current_url:location.href,$host:location.host,$pathname:location.pathname}}function X(e){if(typeof document=="undefined"||typeof window=="undefined")return()=>{};let t=g=>{try{let u=g.target;if(!(u instanceof Element))return;let c=W(u);if(!c)return;let f=U(c);e.capture("$autocapture",{$event_type:g.type,...V()},f)}catch(u){}},n=["click","submit","change"];if(e.config.autocapture)for(let g of n)document.addEventListener(g,t,!0);let r="",i=()=>{try{if(!e.config.capture_pageview||location.href===r)return;r=location.href,e.capture("$pageview",{...V(),$referrer:document.referrer})}catch(g){}},s=history.pushState.bind(history),m=history.replaceState.bind(history),h=!1;return e.config.capture_pageview&&(history.pushState=(...g)=>{s(...g),i()},history.replaceState=(...g)=>{m(...g),i()},h=!0,window.addEventListener("popstate",i),window.addEventListener("hashchange",i),i()),()=>{if(e.config.autocapture)for(let g of n)document.removeEventListener(g,t,!0);h&&(history.pushState=s,history.replaceState=m,window.removeEventListener("popstate",i),window.removeEventListener("hashchange",i))}}var de=/^(cpc|ppc|paid|display)$/i,fe=["google.","bing.com","duckduckgo.com","yahoo.","baidu.com","yandex.","ecosia.org","ask.com","aol.com"],ge=["facebook.com","fb.com","twitter.com","x.com","t.co","linkedin.com","instagram.com","tiktok.com","reddit.com","pinterest.","youtube.com","threads.net"],j=(e,t)=>t.some(n=>e.includes(n)),pe=e=>{if(e)try{return new URL(e).hostname.toLowerCase()}catch(t){return}};function G(e,t,n){let r=pe(e),i=t.$utm_medium,s=!!r,m=Object.keys(t).length>0;return!s&&!m?"direct":i&&de.test(i)?"paid":r&&j(r,fe)?"organic search":r&&j(r,ge)?"social":i&&i.toLowerCase()==="email"?"email":r&&r!==n.toLowerCase()?"referral":"direct"}var me=[{name:"Edge",re:/Edg(?:e|A|iOS)?\/(\d+)/},{name:"Opera",re:/(?:OPR|Opera)\/(\d+)/},{name:"Samsung Internet",re:/SamsungBrowser\/(\d+)/},{name:"Firefox",re:/(?:Firefox|FxiOS)\/(\d+)/},{name:"Chrome",re:/(?:Chrome|CriOS)\/(\d+)/},{name:"Safari",re:/Version\/(\d+).*Safari/}],he=e=>{for(let t of me){let n=e.match(t.re);if(n)return{name:t.name,version:n[1]}}return{}},ye=e=>{if(/Windows/.test(e))return"Windows";if(/iPhone|iPad|iPod/.test(e))return"iOS";if(/Android/.test(e))return"Android";if(/CrOS/.test(e))return"Chrome OS";if(/Mac OS X|Macintosh/.test(e))return"macOS";if(/Linux/.test(e))return"Linux"},ve=e=>/iPad/.test(e)?"Tablet":/Android/.test(e)?/Mobile/.test(e)?"Mobile":"Tablet":/iPhone|iPod/.test(e)||/Mobile/.test(e)?"Mobile":"Desktop";function K(e,t){let n={},r=e!=null?e:"";if(r){let i=he(r);i.name&&(n.$browser=i.name),i.version&&(n.$browser_version=i.version);let s=ye(r);s&&(n.$os=s),n.$device_type=ve(r)}return t&&(n.$lang=t),n}var ze=Number(BigInt("0xfffffffffffffff"));var A="ckrm_flags",$=()=>{try{return typeof localStorage=="undefined"?null:localStorage}catch(e){return null}};function Y(e){let t=n();function n(){var u;try{let c=(u=$())==null?void 0:u.getItem(A);if(!c)return{};let f=JSON.parse(c);if(f&&typeof f=="object")return f}catch(c){}return{}}let r=u=>{var c;try{(c=$())==null||c.setItem(A,JSON.stringify(u))}catch(f){}},i,s=()=>`${e.getApiHost().replace(/\/+$/,"")}/v1/flags`,m=()=>`${e.getApiHost().replace(/\/+$/,"")}/v1/flags/version`,h=async()=>{try{if(typeof fetch!="function")return t;let u=await fetch(s(),{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${e.getWriteKey()}`},body:JSON.stringify({distinct_id:e.getDistinctId(),person_properties:e.getPersonProperties()})});if(!u.ok)return t;let c=await u.json(),f=c==null?void 0:c.flags;return f&&typeof f=="object"&&(t=f,r(t)),t}catch(u){return t}};return{getCache:()=>t,isFeatureEnabled:u=>{var c,f;return(f=(c=t[u])==null?void 0:c.enabled)!=null?f:!1},getFeatureFlag:u=>{let c=t[u];return c!=null&&c.enabled?c.variant!=null?c.variant:!0:!1},getFeatureFlagPayload:u=>{var c,f;return(f=(c=t[u])==null?void 0:c.payload)!=null?f:null},fetchFlags:h,checkVersionAndMaybeFetch:async()=>{let u=!0,c;try{if(typeof fetch=="function"){let v=await fetch(m(),{method:"GET",headers:{authorization:`Bearer ${e.getWriteKey()}`}});if(v.ok){let w=await v.json();c=w==null?void 0:w.version,u=i===void 0||c!==i}}}catch(v){}if(!u)return{changed:!1,flags:t};let f=await h();return c!==void 0&&(i=c),{changed:!0,flags:f}},clear:()=>{var u;t={};try{(u=$())==null||u.removeItem(A)}catch(c){}}}}var T="ckrm_sid",L="ckrm_wid",O="ckrm_primary_window_exists",we=e=>{if(!e)return null;try{let t=JSON.parse(e);if(t&&typeof t=="object"&&typeof t.id=="string"&&typeof t.start=="number"&&typeof t.last=="number")return t}catch(t){}return null};function z({now:e,storage:t,sessionStorage:n,genId:r}){let i=null,s=null,m=!1,h=()=>{var l,a;try{return we((a=(l=t())==null?void 0:l.getItem(T))!=null?a:null)}catch(d){return null}},g=l=>{var a;try{(a=t())==null||a.setItem(T,JSON.stringify(l))}catch(d){}},u=()=>{var l;try{return(l=n==null?void 0:n())!=null?l:null}catch(a){return null}},c=l=>{var a,d;try{return(d=(a=u())==null?void 0:a.getItem(l))!=null?d:null}catch(p){return null}},f=(l,a)=>{var d;try{(d=u())==null||d.setItem(l,a)}catch(p){}},v=l=>{var a;try{(a=u())==null||a.removeItem(l)}catch(d){}},w=()=>{let l=h();return l?i?l.id===i.id?{id:i.id,start:Math.min(i.start,l.start),last:Math.max(i.last,l.last)}:l.last>=i.last?l:i:l:i},b=(l,a,d)=>!l||a-l.start>864e5?!0:d?!1:a-l.last>18e5,I=(l,a)=>{let d=w();return d&&!b(d,l,a)?d:{id:r(),start:l,last:l}},_=()=>{if(!m){m=!0;let l=c(L),a=c(O)!==null;l&&!a&&(s=l),f(O,"1")}return s||(s=r(),f(L,s)),s},M=()=>{s=null,v(L)};return{getSessionId(l=!1){var S;let a=e(),d=(S=i==null?void 0:i.id)!=null?S:null,p=I(a,l),y=l?p:{...p,last:a};return(!l||y.id!==d)&&g(y),i=y,d!==null&&y.id!==d&&M(),y.id},getWindowId(){return _()},reset(){var l;i=null;try{(l=t())==null||l.removeItem(T)}catch(a){}M()},clearPrimaryWindow(){v(O)}}}var N=()=>typeof window!="undefined";function J(e,t){let n=[],r=[],i=0,s=null,m=null,h=()=>{let{api_host:a}=e();return`${a.replace(/\/+$/,"")}/v1/capture?token=${encodeURIComponent(t())}`},g=a=>JSON.stringify({batch:a}),u=()=>{if(s!==null||r.length===0)return;let a=Math.min(1e3*2**i,3e4);try{s=setTimeout(()=>{s=null,b(!1)},a)}catch(d){s=null}},c=a=>{r=r.concat(a),r.length>1e3&&(r=r.slice(r.length-1e3)),i=Math.min(i+1,5),u()},f=(a,d)=>{try{if(typeof navigator=="undefined"||typeof navigator.sendBeacon!="function")return!1;let p=new Blob([d],{type:"text/plain"});return navigator.sendBeacon(a,p)}catch(p){return!1}},v=async(a,d)=>{let p;try{p=await a.json()}catch(y){}console.warn(`[clickroom] capture() batch rejected by server (HTTP ${a.status}); dropping ${d.length} event(s)`,p)},w=(a,d,p)=>{try{if(typeof fetch!="function"){c(p);return}fetch(a,{method:"POST",keepalive:!0,headers:{"content-type":"application/json"},body:d}).then(y=>{if(y.ok){i=0,r.length>0&&u();return}if(y.status>=500){c(p);return}v(y,p)}).catch(()=>{c(p)})}catch(y){c(p)}};function b(a=!1){if(!N()){n=[];return}let d=r.concat(n);if(n=[],r=[],d.length!==0)try{let p=h();for(let y=0;y<d.length;y+=100){let S=d.slice(y,y+100),x=g(S);if(!(a&&f(p,x))){if(a){w(p,x,S);continue}w(p,x,S)}}}catch(p){}}let I=()=>{try{typeof document!="undefined"&&document.hidden&&b(!0)}catch(a){}},_=()=>{b(!0)},M=a=>{if(N())try{n.push(a),n.length>=20&&b(!1)}catch(d){}},l=()=>{try{m!==null&&(clearInterval(m),m=null),s!==null&&(clearTimeout(s),s=null),typeof document!="undefined"&&document.removeEventListener("visibilitychange",I),typeof window!="undefined"&&(window.removeEventListener("pagehide",_),window.removeEventListener("beforeunload",_))}catch(a){}};if(N())try{m=setInterval(()=>b(!1),3e3),typeof document!="undefined"&&document.addEventListener("visibilitychange",I),window.addEventListener("pagehide",_),window.addEventListener("beforeunload",_)}catch(a){}return{enqueue:M,flush:b,teardown:l}}var be=["source","medium","campaign","term","content"];function q(e){let t={};if(!e)return t;try{let n=new URLSearchParams(e);for(let r of be){let i=n.get(`utm_${r}`);i&&(t[`$utm_${r}`]=i)}}catch(n){}return t}var _e="clickroom-js",Se="0.3.0",R="ckrm_did",Ee="ckrm_",k={api_host:"https://capture.clickroom.co",autocapture:!0,capture_pageview:!0},Ce=()=>typeof window!="undefined",Fe=5e3,o={initialized:!1,warnedNotInit:!1,writeKey:"",config:{...k},distinctId:"",personProperties:{},transport:null,session:null,sessionContext:null,teardownAutocapture:null,flagListeners:[],flagClient:null,flagsLoaded:!1,emittedFlagCalls:new Set,flagPollId:null},F=()=>{try{return typeof localStorage=="undefined"?null:localStorage}catch(e){return null}},Ie=()=>{try{return typeof sessionStorage=="undefined"?null:sessionStorage}catch(e){return null}},Me=e=>{var t,n;try{return(n=(t=F())==null?void 0:t.getItem(e))!=null?n:null}catch(r){return null}},D=(e,t)=>{var n;try{(n=F())==null||n.setItem(e,t)}catch(r){}},ke=e=>{var t;try{(t=F())==null||t.removeItem(e)}catch(n){}},H=()=>{try{if(typeof crypto!="undefined"&&typeof crypto.randomUUID=="function")return crypto.randomUUID()}catch(e){}return`ckrm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,12)}`},Re=()=>{let e=Me(R);if(e)return e;let t=H();return D(R,t),t},Pe=()=>{let e={$lib:_e,$lib_version:Se};try{typeof location!="undefined"&&(e.$current_url=location.href,e.$host=location.host,e.$pathname=location.pathname),typeof document!="undefined"&&(e.$referrer=document.referrer),typeof window!="undefined"&&window.screen&&(e.$screen_height=window.screen.height,e.$screen_width=window.screen.width),typeof window!="undefined"&&(e.$viewport_height=window.innerHeight,e.$viewport_width=window.innerWidth),typeof navigator!="undefined"&&Object.assign(e,K(navigator.userAgent,navigator.language))}catch(t){}return e},Z=()=>{try{let e=typeof location!="undefined"?location.search:"",t=typeof document!="undefined"?document.referrer:"",n=typeof location!="undefined"?location.hostname:"",r=q(e);return{...r,$channel_type:G(t,r,n)}}catch(e){return{}}},xe=e=>e?((!o.sessionContext||o.sessionContext.sessionId!==e)&&(o.sessionContext={sessionId:e,props:Z()}),o.sessionContext.props):Z(),Q=()=>{if(!o.warnedNotInit){o.warnedNotInit=!0;try{console.warn("[clickroom] capture() called before init(); event dropped")}catch(e){}}},C=(e,t,n)=>{var r,i;if(!o.initialized||!o.transport){Q();return}try{let s=(r=o.session)==null?void 0:r.getSessionId(),m=s?{$session_id:s,$window_id:(i=o.session)==null?void 0:i.getWindowId()}:{},h={event:e,distinct_id:o.distinctId,timestamp:new Date().toISOString(),properties:{...Pe(),...m,...xe(s),...t!=null?t:{}}};n&&(h.elements_chain=n),o.transport.enqueue(h)}catch(s){}},ee=e=>{let t={};for(let[n,r]of Object.entries(e))r.enabled&&(t[n]=r.variant!=null?r.variant:!0);return t},Ae=()=>{if(!o.flagClient)return;let e=ee(o.flagClient.getCache());for(let t of o.flagListeners)try{t(e)}catch(n){}},P=()=>{o.flagClient&&o.flagClient.checkVersionAndMaybeFetch().then(()=>{o.flagsLoaded=!0,Ae()}).catch(()=>{})},te=(e,t)=>{try{if(!o.flagsLoaded)return;let n=String(t),r=`${e}:${n}`;if(o.emittedFlagCalls.has(r))return;o.emittedFlagCalls.add(r),C("$feature_flag_called",{$feature_flag:e,$feature_flag_response:n})}catch(n){}};function $e(e,t){var n,r,i;if(!o.initialized&&Ce()){o.writeKey=e,o.config={api_host:(n=t==null?void 0:t.api_host)!=null?n:k.api_host,autocapture:(r=t==null?void 0:t.autocapture)!=null?r:k.autocapture,capture_pageview:(i=t==null?void 0:t.capture_pageview)!=null?i:k.capture_pageview},o.distinctId=Re(),o.session=z({now:()=>Date.now(),storage:F,sessionStorage:Ie,genId:H});try{window.addEventListener("beforeunload",()=>{var s;return(s=o.session)==null?void 0:s.clearPrimaryWindow()})}catch(s){}if(o.transport=J(()=>o.config,()=>o.writeKey),o.flagClient=Y({getApiHost:()=>o.config.api_host,getWriteKey:()=>o.writeKey,getDistinctId:()=>o.distinctId,getPersonProperties:()=>o.personProperties}),o.initialized=!0,P(),t!=null&&t.feature_flag_poll_interval){let s=Math.max(t.feature_flag_poll_interval,Fe);o.flagPollId=setInterval(()=>P(),s)}if(o.config.autocapture){let s={config:o.config,capture:C};try{o.teardownAutocapture=X(s)}catch(m){}}else o.config.capture_pageview&&C("$pageview")}}function Te(e,t){C(e,t)}function Le(e,t){if(!o.initialized){Q();return}let n=o.distinctId;o.distinctId=e,D(R,e),t&&(o.personProperties={...o.personProperties,...t}),C("$identify",{$set:t!=null?t:{},...n!==e?{$anon_distinct_id:n}:{}}),o.emittedFlagCalls.clear(),P()}function Oe(){var e,t;try{let n=F();if(n){let r=[];for(let i=0;i<n.length;i+=1){let s=n.key(i);s!=null&&s.startsWith(Ee)&&r.push(s)}for(let i of r)ke(i)}}catch(n){}o.distinctId=H(),D(R,o.distinctId),(e=o.session)==null||e.reset(),o.sessionContext=null,o.personProperties={},o.flagsLoaded=!1,o.emittedFlagCalls.clear(),(t=o.flagClient)==null||t.clear()}function Ne(e){var n,r;let t=(r=(n=o.flagClient)==null?void 0:n.isFeatureEnabled(e))!=null?r:!1;return te(e,t),t}function De(e){var n,r;let t=(r=(n=o.flagClient)==null?void 0:n.getFeatureFlag(e))!=null?r:!1;return te(e,t),t}function He(e){var t,n;return(n=(t=o.flagClient)==null?void 0:t.getFeatureFlagPayload(e))!=null?n:null}function Be(e){if(o.flagListeners.push(e),o.flagsLoaded&&o.flagClient)try{e(ee(o.flagClient.getCache()))}catch(t){}}function Ue(){P()}function We(){o.flagPollId!==null&&(clearInterval(o.flagPollId),o.flagPollId=null)}var Ve={init:$e,capture:Te,identify:Le,reset:Oe,isFeatureEnabled:Ne,getFeatureFlag:De,getFeatureFlagPayload:He,onFeatureFlags:Be,refreshFeatureFlags:Ue,stopFeatureFlagPolling:We};var B=Ve;if(typeof window!="undefined"){let e=B;e.__loaded__=!0,window.clickroom=e}var ct=B;})();
package/dist/eval.js CHANGED
@@ -94,6 +94,11 @@ function hashFlag(key, distinctId, salt) {
94
94
  const n = Number(BigInt(`0x${hex.slice(0, 15)}`));
95
95
  return n / HASH_DENOM;
96
96
  }
97
+ function inRollout(bucket, rolloutPercentage) {
98
+ if (rolloutPercentage <= 0) return false;
99
+ if (rolloutPercentage >= 100) return true;
100
+ return bucket < rolloutPercentage / 100;
101
+ }
97
102
  function propMatch(op, actual, value) {
98
103
  switch (op) {
99
104
  case "exact":
@@ -133,7 +138,8 @@ function evaluate(flag, distinctId, personProps = {}) {
133
138
  const filters = flag.filters ?? { groups: [] };
134
139
  for (const group of filters.groups ?? []) {
135
140
  if (!groupMatches(group.properties ?? [], personProps)) continue;
136
- if (hashFlag(flag.key, distinctId, "") <= group.rollout_percentage / 100) {
141
+ if (inRollout(hashFlag(flag.key, distinctId, ""), group.rollout_percentage)) {
142
+ if (group.serve === false) return { enabled: false, variant: null, payload: null };
137
143
  const variant = filters.multivariate ? pickVariant(flag, distinctId) : group.variant ?? null;
138
144
  const payload = variant != null ? filters.payloads?.[variant] ?? null : null;
139
145
  return { enabled: true, variant, payload };
package/dist/eval.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/sha1.ts", "../src/flags.ts"],
4
- "sourcesContent": ["// @clickroom/sdk-js \u2014 pure-JS synchronous SHA-1 (lowercase hex). Zero deps, browser + node.\n// Correctness anchor: sha1(\"abc\") === \"a9993e364706816aba3e25717850c26c9cd0d89d\".\n// Used only for the deterministic flag bucket hash (D-05) \u2014 NOT for security.\n\nconst rotl = (n: number, s: number): number => (n << s) | (n >>> (32 - s))\n\n// UTF-8 encode a string into a byte array (avoids TextEncoder for older targets).\nconst utf8Bytes = (str: string): number[] => {\n const bytes: number[] = []\n for (let i = 0; i < str.length; i += 1) {\n let code = str.charCodeAt(i)\n if (code < 0x80) {\n bytes.push(code)\n } else if (code < 0x800) {\n bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f))\n } else if (code >= 0xd800 && code <= 0xdbff && i + 1 < str.length) {\n // High surrogate \u2014 combine with the following low surrogate.\n const next = str.charCodeAt(i + 1)\n if (next >= 0xdc00 && next <= 0xdfff) {\n code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00)\n i += 1\n bytes.push(\n 0xf0 | (code >> 18),\n 0x80 | ((code >> 12) & 0x3f),\n 0x80 | ((code >> 6) & 0x3f),\n 0x80 | (code & 0x3f),\n )\n } else {\n bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f))\n }\n } else {\n bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f))\n }\n }\n return bytes\n}\n\n/** Compute the SHA-1 digest of a UTF-8 string, returned as 40 lowercase hex chars. */\nexport function sha1(input: string): string {\n const bytes = utf8Bytes(input)\n const bitLen = bytes.length * 8\n\n // Pad: append 0x80, then zeros, then the 64-bit big-endian length.\n bytes.push(0x80)\n while (bytes.length % 64 !== 56) bytes.push(0)\n // 64-bit length; bitLen fits well under 2^32 for our inputs, high word = 0.\n bytes.push(0, 0, 0, 0)\n bytes.push((bitLen >>> 24) & 0xff, (bitLen >>> 16) & 0xff, (bitLen >>> 8) & 0xff, bitLen & 0xff)\n\n let h0 = 0x67452301\n let h1 = 0xefcdab89\n let h2 = 0x98badcfe\n let h3 = 0x10325476\n let h4 = 0xc3d2e1f0\n\n const w = new Array<number>(80)\n\n for (let chunk = 0; chunk < bytes.length; chunk += 64) {\n for (let i = 0; i < 16; i += 1) {\n const j = chunk + i * 4\n w[i] = (bytes[j] << 24) | (bytes[j + 1] << 16) | (bytes[j + 2] << 8) | bytes[j + 3]\n }\n for (let i = 16; i < 80; i += 1) {\n w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1)\n }\n\n let a = h0\n let b = h1\n let c = h2\n let d = h3\n let e = h4\n\n for (let i = 0; i < 80; i += 1) {\n let f: number\n let k: number\n if (i < 20) {\n f = (b & c) | (~b & d)\n k = 0x5a827999\n } else if (i < 40) {\n f = b ^ c ^ d\n k = 0x6ed9eba1\n } else if (i < 60) {\n f = (b & c) | (b & d) | (c & d)\n k = 0x8f1bbcdc\n } else {\n f = b ^ c ^ d\n k = 0xca62c1d6\n }\n const temp = (rotl(a, 5) + f + e + k + w[i]) | 0\n e = d\n d = c\n c = rotl(b, 30)\n b = a\n a = temp\n }\n\n h0 = (h0 + a) | 0\n h1 = (h1 + b) | 0\n h2 = (h2 + c) | 0\n h3 = (h3 + d) | 0\n h4 = (h4 + e) | 0\n }\n\n const toHex = (n: number): string => (n >>> 0).toString(16).padStart(8, '0')\n return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4)\n}\n", "// @clickroom/sdk-js \u2014 canonical feature-flag evaluation + remote fetch cache (Phase 10.A).\n// Semantics are the SHARED SOURCE OF TRUTH (D-05, CANONICAL-FLAGS.md). Any deviation\n// breaks SDK<->server parity (AC-5/6/7). Keep byte-for-byte agreement with Rust + tRPC.\n\nimport { sha1 } from './sha1'\n\n// ---- filter data model (\u00A71) --------------------------------------------\n\nexport type FlagOperator = 'exact' | 'is_not' | 'icontains' | 'gt' | 'lt' | 'is_set'\n\nexport interface PropFilter {\n key: string\n operator: FlagOperator\n value: unknown\n}\n\nexport interface Group {\n properties: PropFilter[]\n rollout_percentage: number\n variant?: string | null\n}\n\nexport interface Variant {\n key: string\n rollout_percentage: number\n}\n\nexport interface Multivariate {\n variants: Variant[]\n}\n\nexport interface Filters {\n groups: Group[]\n multivariate?: Multivariate\n payloads?: Record<string, unknown>\n}\n\nexport interface Flag {\n key: string\n active: boolean\n filters: Filters\n}\n\nexport interface FlagResult {\n enabled: boolean\n variant: string | null\n payload: unknown\n}\n\n// ---- hash (\u00A72) ----------------------------------------------------------\n\n// 15 hex f's == 1152921504606846975. Built via BigInt(string) rather than a `\u2026n`\n// literal so the es2018 CDN bundle target accepts it (identical numeric value).\nconst HASH_DENOM = Number(BigInt('0xfffffffffffffff'))\n\n/** Deterministic bucket in [0,1): int(sha1(key+\".\"+distinctId+salt)[0:15]) / 0xFFFFFFFFFFFFFFF. */\nexport function hashFlag(key: string, distinctId: string, salt: string): number {\n const hex = sha1(`${key}.${distinctId}${salt}`)\n // BigInt (not parseInt) \u2014 15 hex chars is up to 60 bits, beyond 2^53 precision.\n const n = Number(BigInt(`0x${hex.slice(0, 15)}`))\n return n / HASH_DENOM\n}\n\n// ---- operators (\u00A74) -----------------------------------------------------\n\n/** actual = person_properties[key]; value = filter value. */\nexport function propMatch(op: FlagOperator, actual: unknown, value: unknown): boolean {\n switch (op) {\n case 'exact':\n return String(actual) === String(value)\n case 'is_not':\n return String(actual) !== String(value)\n case 'icontains':\n return actual != null && String(actual).toLowerCase().includes(String(value).toLowerCase())\n case 'gt':\n return actual != null && Number(actual) > Number(value)\n case 'lt':\n return actual != null && Number(actual) < Number(value)\n case 'is_set':\n return actual !== undefined && actual !== null\n default:\n return false\n }\n}\n\nconst groupMatches = (props: PropFilter[], pp: Record<string, unknown>): boolean => {\n for (const p of props) {\n if (!propMatch(p.operator, pp[p.key], p.value)) return false\n }\n return true\n}\n\nconst pickVariant = (flag: Flag, distinctId: string): string | null => {\n const variants = flag.filters.multivariate?.variants ?? []\n const f = hashFlag(flag.key, distinctId, 'variant')\n let cumulative = 0\n for (const v of variants) {\n cumulative += v.rollout_percentage / 100\n if (f < cumulative) return v.key // strict <\n }\n return variants.length > 0 ? variants[variants.length - 1].key : null\n}\n\n// ---- evaluate (\u00A73) ------------------------------------------------------\n\n/** First matching group wins; in-group-off stops (no fallthrough). */\nexport function evaluate(\n flag: Flag,\n distinctId: string,\n personProps: Record<string, unknown> = {},\n): FlagResult {\n if (!flag.active) return { enabled: false, variant: null, payload: null }\n\n const filters = flag.filters ?? { groups: [] }\n for (const group of filters.groups ?? []) {\n if (!groupMatches(group.properties ?? [], personProps)) continue\n\n if (hashFlag(flag.key, distinctId, '') <= group.rollout_percentage / 100) {\n const variant = filters.multivariate ? pickVariant(flag, distinctId) : (group.variant ?? null)\n const payload = variant != null ? (filters.payloads?.[variant] ?? null) : null\n return { enabled: true, variant, payload }\n }\n // In-group but rolled out off \u2014 stop, no fallthrough.\n return { enabled: false, variant: null, payload: null }\n }\n\n return { enabled: false, variant: null, payload: null }\n}\n\n// ---- remote fetch cache client -----------------------------------------\n\nconst FLAGS_STORAGE_KEY = 'ckrm_flags'\n\nexport type FlagMap = Record<string, FlagResult>\n\nexport interface FlagsResponse {\n flags: FlagMap\n}\n\nconst safeLocalStorage = (): Storage | null => {\n try {\n if (typeof localStorage === 'undefined') return null\n return localStorage\n } catch {\n return null\n }\n}\n\nexport interface FlagClientDeps {\n getApiHost: () => string\n getWriteKey: () => string\n getDistinctId: () => string\n getPersonProperties: () => Record<string, unknown>\n}\n\nexport interface FlagClient {\n /** In-memory cache of the last evaluated flags (or restored from localStorage). */\n getCache(): FlagMap\n isFeatureEnabled(key: string): boolean\n getFeatureFlag(key: string): boolean | string\n getFeatureFlagPayload(key: string): unknown\n /** Fetch + cache flags for the current distinct id/props. Never throws. */\n fetchFlags(): Promise<FlagMap>\n /**\n * Cheap version check (GET `{apiHost}/flags/version`) before paying for a\n * full `fetchFlags()`. Only calls `fetchFlags()` (and updates the stored\n * `lastVersion`) when the returned version differs from the last one seen\n * \u2014 or on the very first check. Network/parse failure on the version check\n * fails open (treated as \"changed\") and falls through to a full fetch,\n * matching this client's existing never-throw/best-effort tolerance.\n * Never throws.\n */\n checkVersionAndMaybeFetch(): Promise<{ changed: boolean; flags: FlagMap }>\n /** Drop the in-memory + persisted cache. */\n clear(): void\n}\n\nexport function createFlagClient(deps: FlagClientDeps): FlagClient {\n let cache: FlagMap = restoreCache()\n\n function restoreCache(): FlagMap {\n try {\n const raw = safeLocalStorage()?.getItem(FLAGS_STORAGE_KEY)\n if (!raw) return {}\n const parsed = JSON.parse(raw) as FlagMap\n if (parsed && typeof parsed === 'object') return parsed\n } catch {\n // ignore corrupt cache\n }\n return {}\n }\n\n const persist = (map: FlagMap): void => {\n try {\n safeLocalStorage()?.setItem(FLAGS_STORAGE_KEY, JSON.stringify(map))\n } catch {\n // ignore quota / disabled storage\n }\n }\n\n let lastVersion: number | undefined\n\n const buildUrl = (): string => {\n const base = deps.getApiHost().replace(/\\/+$/, '')\n return `${base}/v1/flags`\n }\n\n const buildVersionUrl = (): string => {\n const base = deps.getApiHost().replace(/\\/+$/, '')\n return `${base}/v1/flags/version`\n }\n\n const fetchFlags = async (): Promise<FlagMap> => {\n try {\n if (typeof fetch !== 'function') return cache\n const res = await fetch(buildUrl(), {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${deps.getWriteKey()}`,\n },\n body: JSON.stringify({\n distinct_id: deps.getDistinctId(),\n person_properties: deps.getPersonProperties(),\n }),\n })\n if (!res.ok) return cache // fall back to existing cache\n const data = (await res.json()) as FlagsResponse\n const flags = data?.flags\n if (flags && typeof flags === 'object') {\n cache = flags\n persist(cache)\n }\n return cache\n } catch {\n // Network / parse failure \u2014 keep whatever cache we have (incl. localStorage).\n return cache\n }\n }\n\n const checkVersionAndMaybeFetch = async (): Promise<{ changed: boolean; flags: FlagMap }> => {\n let changed = true\n let newVersion: number | undefined\n\n try {\n if (typeof fetch === 'function') {\n const res = await fetch(buildVersionUrl(), {\n method: 'GET',\n headers: {\n authorization: `Bearer ${deps.getWriteKey()}`,\n },\n })\n if (res.ok) {\n const data = (await res.json()) as { version?: number }\n newVersion = data?.version\n changed = lastVersion === undefined || newVersion !== lastVersion\n }\n // !res.ok \u2014 fail open, `changed` stays true (default).\n }\n } catch {\n // Network / parse failure \u2014 fail open, `changed` stays true.\n }\n\n if (!changed) return { changed: false, flags: cache }\n\n const flags = await fetchFlags()\n if (newVersion !== undefined) lastVersion = newVersion\n return { changed: true, flags }\n }\n\n return {\n getCache: () => cache,\n isFeatureEnabled: (key) => cache[key]?.enabled ?? false,\n getFeatureFlag: (key) => {\n const r = cache[key]\n if (!r?.enabled) return false\n return r.variant != null ? r.variant : true\n },\n getFeatureFlagPayload: (key) => cache[key]?.payload ?? null,\n fetchFlags,\n checkVersionAndMaybeFetch,\n clear: () => {\n cache = {}\n try {\n safeLocalStorage()?.removeItem(FLAGS_STORAGE_KEY)\n } catch {\n // ignore\n }\n },\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// @clickroom/sdk-js \u2014 pure-JS synchronous SHA-1 (lowercase hex). Zero deps, browser + node.\n// Correctness anchor: sha1(\"abc\") === \"a9993e364706816aba3e25717850c26c9cd0d89d\".\n// Used only for the deterministic flag bucket hash (D-05) \u2014 NOT for security.\n\nconst rotl = (n: number, s: number): number => (n << s) | (n >>> (32 - s))\n\n// UTF-8 encode a string into a byte array (avoids TextEncoder for older targets).\nconst utf8Bytes = (str: string): number[] => {\n const bytes: number[] = []\n for (let i = 0; i < str.length; i += 1) {\n let code = str.charCodeAt(i)\n if (code < 0x80) {\n bytes.push(code)\n } else if (code < 0x800) {\n bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f))\n } else if (code >= 0xd800 && code <= 0xdbff && i + 1 < str.length) {\n // High surrogate \u2014 combine with the following low surrogate.\n const next = str.charCodeAt(i + 1)\n if (next >= 0xdc00 && next <= 0xdfff) {\n code = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00)\n i += 1\n bytes.push(\n 0xf0 | (code >> 18),\n 0x80 | ((code >> 12) & 0x3f),\n 0x80 | ((code >> 6) & 0x3f),\n 0x80 | (code & 0x3f),\n )\n } else {\n bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f))\n }\n } else {\n bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f))\n }\n }\n return bytes\n}\n\n/** Compute the SHA-1 digest of a UTF-8 string, returned as 40 lowercase hex chars. */\nexport function sha1(input: string): string {\n const bytes = utf8Bytes(input)\n const bitLen = bytes.length * 8\n\n // Pad: append 0x80, then zeros, then the 64-bit big-endian length.\n bytes.push(0x80)\n while (bytes.length % 64 !== 56) bytes.push(0)\n // 64-bit length; bitLen fits well under 2^32 for our inputs, high word = 0.\n bytes.push(0, 0, 0, 0)\n bytes.push((bitLen >>> 24) & 0xff, (bitLen >>> 16) & 0xff, (bitLen >>> 8) & 0xff, bitLen & 0xff)\n\n let h0 = 0x67452301\n let h1 = 0xefcdab89\n let h2 = 0x98badcfe\n let h3 = 0x10325476\n let h4 = 0xc3d2e1f0\n\n const w = new Array<number>(80)\n\n for (let chunk = 0; chunk < bytes.length; chunk += 64) {\n for (let i = 0; i < 16; i += 1) {\n const j = chunk + i * 4\n w[i] = (bytes[j] << 24) | (bytes[j + 1] << 16) | (bytes[j + 2] << 8) | bytes[j + 3]\n }\n for (let i = 16; i < 80; i += 1) {\n w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1)\n }\n\n let a = h0\n let b = h1\n let c = h2\n let d = h3\n let e = h4\n\n for (let i = 0; i < 80; i += 1) {\n let f: number\n let k: number\n if (i < 20) {\n f = (b & c) | (~b & d)\n k = 0x5a827999\n } else if (i < 40) {\n f = b ^ c ^ d\n k = 0x6ed9eba1\n } else if (i < 60) {\n f = (b & c) | (b & d) | (c & d)\n k = 0x8f1bbcdc\n } else {\n f = b ^ c ^ d\n k = 0xca62c1d6\n }\n const temp = (rotl(a, 5) + f + e + k + w[i]) | 0\n e = d\n d = c\n c = rotl(b, 30)\n b = a\n a = temp\n }\n\n h0 = (h0 + a) | 0\n h1 = (h1 + b) | 0\n h2 = (h2 + c) | 0\n h3 = (h3 + d) | 0\n h4 = (h4 + e) | 0\n }\n\n const toHex = (n: number): string => (n >>> 0).toString(16).padStart(8, '0')\n return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4)\n}\n", "// @clickroom/sdk-js \u2014 canonical feature-flag evaluation + remote fetch cache (Phase 10.A).\n// Semantics are the SHARED SOURCE OF TRUTH (D-05, CANONICAL-FLAGS.md). Any deviation\n// breaks SDK<->server parity (AC-5/6/7). Keep byte-for-byte agreement with Rust + tRPC.\n\nimport { sha1 } from './sha1'\n\n// ---- filter data model (\u00A71) --------------------------------------------\n\nexport type FlagOperator = 'exact' | 'is_not' | 'icontains' | 'gt' | 'lt' | 'is_set'\n\nexport interface PropFilter {\n key: string\n operator: FlagOperator\n value: unknown\n}\n\nexport interface Group {\n properties: PropFilter[]\n rollout_percentage: number\n variant?: string | null\n /** Boolean served to a matched, in-rollout user. Absent means `true`. */\n serve?: boolean\n}\n\nexport interface Variant {\n key: string\n rollout_percentage: number\n}\n\nexport interface Multivariate {\n variants: Variant[]\n}\n\nexport interface Filters {\n groups: Group[]\n multivariate?: Multivariate\n payloads?: Record<string, unknown>\n}\n\nexport interface Flag {\n key: string\n active: boolean\n filters: Filters\n}\n\nexport interface FlagResult {\n enabled: boolean\n variant: string | null\n payload: unknown\n}\n\n// ---- hash (\u00A72) ----------------------------------------------------------\n\n// 15 hex f's == 1152921504606846975. Built via BigInt(string) rather than a `\u2026n`\n// literal so the es2018 CDN bundle target accepts it (identical numeric value).\nconst HASH_DENOM = Number(BigInt('0xfffffffffffffff'))\n\n/** Deterministic bucket in [0,1] \u2014 CLOSED, the numerator can reach the denominator:\n * int(sha1(key+\".\"+distinctId+salt)[0:15]) / 0xFFFFFFFFFFFFFFF. */\nexport function hashFlag(key: string, distinctId: string, salt: string): number {\n const hex = sha1(`${key}.${distinctId}${salt}`)\n // BigInt (not parseInt) \u2014 15 hex chars is up to 60 bits, beyond 2^53 precision.\n const n = Number(BigInt(`0x${hex.slice(0, 15)}`))\n return n / HASH_DENOM\n}\n\n/**\n * Rollout membership for a bucket produced by `hashFlag`.\n *\n * The endpoint guards are not redundant with the `<` compare: `hashFlag` divides by\n * 0xFFFFFFFFFFFFFFF, a value the numerator can actually reach, so buckets live in the\n * CLOSED range [0,1] \u2014 not [0,1). Without the `>= 100` guard, the max-hash user would be\n * excluded from a 100% rollout; without the `<= 0` guard, the zero-hash user would still\n * be let into a 0% rollout under a non-strict compare. In between, the compare is strict\n * so a boundary bucket belongs to the next slice (matching `pickVariant`).\n */\nexport function inRollout(bucket: number, rolloutPercentage: number): boolean {\n if (rolloutPercentage <= 0) return false\n if (rolloutPercentage >= 100) return true\n return bucket < rolloutPercentage / 100\n}\n\n// ---- operators (\u00A74) -----------------------------------------------------\n\n/** actual = person_properties[key]; value = filter value. */\nexport function propMatch(op: FlagOperator, actual: unknown, value: unknown): boolean {\n switch (op) {\n case 'exact':\n return String(actual) === String(value)\n case 'is_not':\n return String(actual) !== String(value)\n case 'icontains':\n return actual != null && String(actual).toLowerCase().includes(String(value).toLowerCase())\n case 'gt':\n return actual != null && Number(actual) > Number(value)\n case 'lt':\n return actual != null && Number(actual) < Number(value)\n case 'is_set':\n return actual !== undefined && actual !== null\n default:\n return false\n }\n}\n\nconst groupMatches = (props: PropFilter[], pp: Record<string, unknown>): boolean => {\n for (const p of props) {\n if (!propMatch(p.operator, pp[p.key], p.value)) return false\n }\n return true\n}\n\nconst pickVariant = (flag: Flag, distinctId: string): string | null => {\n const variants = flag.filters.multivariate?.variants ?? []\n const f = hashFlag(flag.key, distinctId, 'variant')\n let cumulative = 0\n for (const v of variants) {\n cumulative += v.rollout_percentage / 100\n if (f < cumulative) return v.key // strict <\n }\n return variants.length > 0 ? variants[variants.length - 1].key : null\n}\n\n// ---- evaluate (\u00A73) ------------------------------------------------------\n\n/** First matching group wins; in-group-off stops (no fallthrough). */\nexport function evaluate(\n flag: Flag,\n distinctId: string,\n personProps: Record<string, unknown> = {},\n): FlagResult {\n if (!flag.active) return { enabled: false, variant: null, payload: null }\n\n const filters = flag.filters ?? { groups: [] }\n for (const group of filters.groups ?? []) {\n if (!groupMatches(group.properties ?? [], personProps)) continue\n\n if (inRollout(hashFlag(flag.key, distinctId, ''), group.rollout_percentage)) {\n // Group pinned to serve OFF (boolean default groups) \u2014 stop, no fallthrough.\n if (group.serve === false) return { enabled: false, variant: null, payload: null }\n const variant = filters.multivariate ? pickVariant(flag, distinctId) : (group.variant ?? null)\n const payload = variant != null ? (filters.payloads?.[variant] ?? null) : null\n return { enabled: true, variant, payload }\n }\n // In-group but rolled out off \u2014 stop, no fallthrough.\n return { enabled: false, variant: null, payload: null }\n }\n\n return { enabled: false, variant: null, payload: null }\n}\n\n// ---- remote fetch cache client -----------------------------------------\n\nconst FLAGS_STORAGE_KEY = 'ckrm_flags'\n\nexport type FlagMap = Record<string, FlagResult>\n\nexport interface FlagsResponse {\n flags: FlagMap\n}\n\nconst safeLocalStorage = (): Storage | null => {\n try {\n if (typeof localStorage === 'undefined') return null\n return localStorage\n } catch {\n return null\n }\n}\n\nexport interface FlagClientDeps {\n getApiHost: () => string\n getWriteKey: () => string\n getDistinctId: () => string\n getPersonProperties: () => Record<string, unknown>\n}\n\nexport interface FlagClient {\n /** In-memory cache of the last evaluated flags (or restored from localStorage). */\n getCache(): FlagMap\n isFeatureEnabled(key: string): boolean\n getFeatureFlag(key: string): boolean | string\n getFeatureFlagPayload(key: string): unknown\n /** Fetch + cache flags for the current distinct id/props. Never throws. */\n fetchFlags(): Promise<FlagMap>\n /**\n * Cheap version check (GET `{apiHost}/flags/version`) before paying for a\n * full `fetchFlags()`. Only calls `fetchFlags()` (and updates the stored\n * `lastVersion`) when the returned version differs from the last one seen\n * \u2014 or on the very first check. Network/parse failure on the version check\n * fails open (treated as \"changed\") and falls through to a full fetch,\n * matching this client's existing never-throw/best-effort tolerance.\n * Never throws.\n */\n checkVersionAndMaybeFetch(): Promise<{ changed: boolean; flags: FlagMap }>\n /** Drop the in-memory + persisted cache. */\n clear(): void\n}\n\nexport function createFlagClient(deps: FlagClientDeps): FlagClient {\n let cache: FlagMap = restoreCache()\n\n function restoreCache(): FlagMap {\n try {\n const raw = safeLocalStorage()?.getItem(FLAGS_STORAGE_KEY)\n if (!raw) return {}\n const parsed = JSON.parse(raw) as FlagMap\n if (parsed && typeof parsed === 'object') return parsed\n } catch {\n // ignore corrupt cache\n }\n return {}\n }\n\n const persist = (map: FlagMap): void => {\n try {\n safeLocalStorage()?.setItem(FLAGS_STORAGE_KEY, JSON.stringify(map))\n } catch {\n // ignore quota / disabled storage\n }\n }\n\n let lastVersion: number | undefined\n\n const buildUrl = (): string => {\n const base = deps.getApiHost().replace(/\\/+$/, '')\n return `${base}/v1/flags`\n }\n\n const buildVersionUrl = (): string => {\n const base = deps.getApiHost().replace(/\\/+$/, '')\n return `${base}/v1/flags/version`\n }\n\n const fetchFlags = async (): Promise<FlagMap> => {\n try {\n if (typeof fetch !== 'function') return cache\n const res = await fetch(buildUrl(), {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${deps.getWriteKey()}`,\n },\n body: JSON.stringify({\n distinct_id: deps.getDistinctId(),\n person_properties: deps.getPersonProperties(),\n }),\n })\n if (!res.ok) return cache // fall back to existing cache\n const data = (await res.json()) as FlagsResponse\n const flags = data?.flags\n if (flags && typeof flags === 'object') {\n cache = flags\n persist(cache)\n }\n return cache\n } catch {\n // Network / parse failure \u2014 keep whatever cache we have (incl. localStorage).\n return cache\n }\n }\n\n const checkVersionAndMaybeFetch = async (): Promise<{ changed: boolean; flags: FlagMap }> => {\n let changed = true\n let newVersion: number | undefined\n\n try {\n if (typeof fetch === 'function') {\n const res = await fetch(buildVersionUrl(), {\n method: 'GET',\n headers: {\n authorization: `Bearer ${deps.getWriteKey()}`,\n },\n })\n if (res.ok) {\n const data = (await res.json()) as { version?: number }\n newVersion = data?.version\n changed = lastVersion === undefined || newVersion !== lastVersion\n }\n // !res.ok \u2014 fail open, `changed` stays true (default).\n }\n } catch {\n // Network / parse failure \u2014 fail open, `changed` stays true.\n }\n\n if (!changed) return { changed: false, flags: cache }\n\n const flags = await fetchFlags()\n if (newVersion !== undefined) lastVersion = newVersion\n return { changed: true, flags }\n }\n\n return {\n getCache: () => cache,\n isFeatureEnabled: (key) => cache[key]?.enabled ?? false,\n getFeatureFlag: (key) => {\n const r = cache[key]\n if (!r?.enabled) return false\n return r.variant != null ? r.variant : true\n },\n getFeatureFlagPayload: (key) => cache[key]?.payload ?? null,\n fetchFlags,\n checkVersionAndMaybeFetch,\n clear: () => {\n cache = {}\n try {\n safeLocalStorage()?.removeItem(FLAGS_STORAGE_KEY)\n } catch {\n // ignore\n }\n },\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/flags.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface Group {
8
8
  properties: PropFilter[];
9
9
  rollout_percentage: number;
10
10
  variant?: string | null;
11
+ /** Boolean served to a matched, in-rollout user. Absent means `true`. */
12
+ serve?: boolean;
11
13
  }
12
14
  export interface Variant {
13
15
  key: string;
@@ -31,8 +33,20 @@ export interface FlagResult {
31
33
  variant: string | null;
32
34
  payload: unknown;
33
35
  }
34
- /** Deterministic bucket in [0,1): int(sha1(key+"."+distinctId+salt)[0:15]) / 0xFFFFFFFFFFFFFFF. */
36
+ /** Deterministic bucket in [0,1] CLOSED, the numerator can reach the denominator:
37
+ * int(sha1(key+"."+distinctId+salt)[0:15]) / 0xFFFFFFFFFFFFFFF. */
35
38
  export declare function hashFlag(key: string, distinctId: string, salt: string): number;
39
+ /**
40
+ * Rollout membership for a bucket produced by `hashFlag`.
41
+ *
42
+ * The endpoint guards are not redundant with the `<` compare: `hashFlag` divides by
43
+ * 0xFFFFFFFFFFFFFFF, a value the numerator can actually reach, so buckets live in the
44
+ * CLOSED range [0,1] — not [0,1). Without the `>= 100` guard, the max-hash user would be
45
+ * excluded from a 100% rollout; without the `<= 0` guard, the zero-hash user would still
46
+ * be let into a 0% rollout under a non-strict compare. In between, the compare is strict
47
+ * so a boundary bucket belongs to the next slice (matching `pickVariant`).
48
+ */
49
+ export declare function inRollout(bucket: number, rolloutPercentage: number): boolean;
36
50
  /** actual = person_properties[key]; value = filter value. */
37
51
  export declare function propMatch(op: FlagOperator, actual: unknown, value: unknown): boolean;
38
52
  /** First matching group wins; in-group-off stops (no fallthrough). */
package/dist/index.js CHANGED
@@ -423,6 +423,8 @@ function createFlagClient(deps) {
423
423
  var IDLE_MS = 30 * 60 * 1e3;
424
424
  var MAX_MS = 24 * 60 * 60 * 1e3;
425
425
  var SID_KEY = "ckrm_sid";
426
+ var WID_KEY = "ckrm_wid";
427
+ var PRIMARY_WINDOW_KEY = "ckrm_primary_window_exists";
426
428
  var parse = (raw) => {
427
429
  if (!raw) return null;
428
430
  try {
@@ -434,8 +436,16 @@ var parse = (raw) => {
434
436
  }
435
437
  return null;
436
438
  };
437
- function createSessionManager({ now, storage, genId }) {
438
- const load = () => {
439
+ function createSessionManager({
440
+ now,
441
+ storage,
442
+ sessionStorage: sessionStorage2,
443
+ genId
444
+ }) {
445
+ let session = null;
446
+ let windowId = null;
447
+ let windowResolved = false;
448
+ const readStored = () => {
439
449
  try {
440
450
  return parse(storage()?.getItem(SID_KEY) ?? null);
441
451
  } catch {
@@ -448,28 +458,98 @@ function createSessionManager({ now, storage, genId }) {
448
458
  } catch {
449
459
  }
450
460
  };
451
- const start = (t) => {
452
- const s = { id: genId(), start: t, last: t };
453
- save(s);
454
- return s;
461
+ const tabStore = () => {
462
+ try {
463
+ return sessionStorage2?.() ?? null;
464
+ } catch {
465
+ return null;
466
+ }
467
+ };
468
+ const tabGet = (key) => {
469
+ try {
470
+ return tabStore()?.getItem(key) ?? null;
471
+ } catch {
472
+ return null;
473
+ }
474
+ };
475
+ const tabSet = (key, value) => {
476
+ try {
477
+ tabStore()?.setItem(key, value);
478
+ } catch {
479
+ }
480
+ };
481
+ const tabRemove = (key) => {
482
+ try {
483
+ tabStore()?.removeItem(key);
484
+ } catch {
485
+ }
486
+ };
487
+ const loadSession = () => {
488
+ const persisted = readStored();
489
+ if (!persisted) return session;
490
+ if (!session) return persisted;
491
+ if (persisted.id === session.id) {
492
+ return {
493
+ id: session.id,
494
+ start: Math.min(session.start, persisted.start),
495
+ last: Math.max(session.last, persisted.last)
496
+ };
497
+ }
498
+ return persisted.last >= session.last ? persisted : session;
499
+ };
500
+ const shouldRotate = (s, t, readOnly) => {
501
+ if (!s) return true;
502
+ if (t - s.start > MAX_MS) return true;
503
+ if (readOnly) return false;
504
+ return t - s.last > IDLE_MS;
505
+ };
506
+ const adoptOrRotate = (t, readOnly) => {
507
+ const merged = loadSession();
508
+ if (merged && !shouldRotate(merged, t, readOnly)) return merged;
509
+ return { id: genId(), start: t, last: t };
510
+ };
511
+ const resolveWindowId = () => {
512
+ if (!windowResolved) {
513
+ windowResolved = true;
514
+ const existing = tabGet(WID_KEY);
515
+ const cloned = tabGet(PRIMARY_WINDOW_KEY) !== null;
516
+ if (existing && !cloned) windowId = existing;
517
+ tabSet(PRIMARY_WINDOW_KEY, "1");
518
+ }
519
+ if (!windowId) {
520
+ windowId = genId();
521
+ tabSet(WID_KEY, windowId);
522
+ }
523
+ return windowId;
524
+ };
525
+ const rotateWindowId = () => {
526
+ windowId = null;
527
+ tabRemove(WID_KEY);
455
528
  };
456
529
  return {
457
- getSessionId() {
530
+ getSessionId(readOnly = false) {
458
531
  const t = now();
459
- const existing = load();
460
- const expired = !existing || t - existing.last > IDLE_MS || t - existing.start > MAX_MS;
461
- if (expired) {
462
- return start(t).id;
463
- }
464
- existing.last = t;
465
- save(existing);
466
- return existing.id;
532
+ const previousId = session?.id ?? null;
533
+ const merged = adoptOrRotate(t, readOnly);
534
+ const next = readOnly ? merged : { ...merged, last: t };
535
+ if (!readOnly || next.id !== previousId) save(next);
536
+ session = next;
537
+ if (previousId !== null && next.id !== previousId) rotateWindowId();
538
+ return next.id;
539
+ },
540
+ getWindowId() {
541
+ return resolveWindowId();
467
542
  },
468
543
  reset() {
544
+ session = null;
469
545
  try {
470
546
  storage()?.removeItem(SID_KEY);
471
547
  } catch {
472
548
  }
549
+ rotateWindowId();
550
+ },
551
+ clearPrimaryWindow() {
552
+ tabRemove(PRIMARY_WINDOW_KEY);
473
553
  }
474
554
  };
475
555
  }
@@ -659,7 +739,7 @@ function parseUtm(search) {
659
739
  // src/index.ts
660
740
  var PACKAGE_NAME = "@clickroom/sdk-js";
661
741
  var LIB_NAME = "clickroom-js";
662
- var LIB_VERSION = "0.2.1";
742
+ var LIB_VERSION = "0.3.0";
663
743
  var DID_KEY = "ckrm_did";
664
744
  var STORAGE_PREFIX = "ckrm_";
665
745
  var DEFAULT_CONFIG = {
@@ -802,7 +882,7 @@ var captureInternal = (event, properties, elementsChain) => {
802
882
  }
803
883
  try {
804
884
  const sessionId = state.session?.getSessionId();
805
- const sessionProps = sessionId ? { $session_id: sessionId } : {};
885
+ const sessionProps = sessionId ? { $session_id: sessionId, $window_id: state.session?.getWindowId() } : {};
806
886
  const payload = {
807
887
  event,
808
888
  distinct_id: state.distinctId,
@@ -873,9 +953,14 @@ function init(writeKey, opts) {
873
953
  state.distinctId = loadOrCreateDistinctId();
874
954
  state.session = createSessionManager({
875
955
  now: () => Date.now(),
876
- storage: safeSessionStorage,
956
+ storage: safeLocalStorage2,
957
+ sessionStorage: safeSessionStorage,
877
958
  genId: generateId
878
959
  });
960
+ try {
961
+ window.addEventListener("beforeunload", () => state.session?.clearPrimaryWindow());
962
+ } catch {
963
+ }
879
964
  state.transport = createTransport(
880
965
  () => state.config,
881
966
  () => state.writeKey
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/elements-chain.ts", "../src/autocapture.ts", "../src/channel.ts", "../src/device.ts", "../src/flags.ts", "../src/session.ts", "../src/transport.ts", "../src/utm.ts", "../src/index.ts"],
4
- "sourcesContent": ["// @clickroom/sdk-js \u2014 DOM `elements_chain` serialization (D-07).\n//\n// Serializes a click/submit/change target and its ancestors into a single\n// compact string that ClickHouse materializes into `elements_chain_text` /\n// `elements_chain_href` via `extract(elements_chain, 'text=\"([^\"]*)\"')` and the\n// equivalent `href=\"...\"`. The literal `text=\"` and `href=\"` tokens must appear\n// verbatim for those extractors to match, and values must not contain a raw `\"`\n// (the `[^\"]*` capture stops at the first quote) \u2014 so we backslash-escape.\n\n// Cap how far up the tree we walk, to bound the serialized size.\nconst MAX_DEPTH = 15\n// Truncate captured text to keep the chain compact on the wire.\nconst MAX_TEXT_LEN = 255\n\n// Elements that are inherently \"interesting\" click targets for autocapture.\nconst INTERESTING_TAGS = new Set(['a', 'button', 'input', 'select', 'textarea', 'label'])\n\n// Class / attribute opt-out markers: skip capture for these subtrees.\nconst OPT_OUT_CLASS = 'ch-no-capture'\nconst OPT_OUT_ATTR = 'data-ch-no-capture'\n\n// Escape a value so it survives inside `attr=\"<value>\"` and stays parseable:\n// backslash and double-quote are escaped; separators (`;` `|`) and newlines are\n// escaped so they never terminate a pair or an element boundary.\nfunction escapeValue(value: string): string {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/;/g, '\\\\;')\n .replace(/\\|/g, '\\\\|')\n .replace(/\\r?\\n/g, ' ')\n .trim()\n}\n\n// 1-indexed position among *all* siblings.\nfunction nthChild(el: Element): number {\n let n = 1\n let sib = el.previousElementSibling\n while (sib) {\n n += 1\n sib = sib.previousElementSibling\n }\n return n\n}\n\n// 1-indexed position among siblings sharing the same tag name.\nfunction nthOfType(el: Element): number {\n let n = 1\n const tag = el.tagName\n let sib = el.previousElementSibling\n while (sib) {\n if (sib.tagName === tag) {\n n += 1\n }\n sib = sib.previousElementSibling\n }\n return n\n}\n\n// Best-effort trimmed text for an element, truncated to MAX_TEXT_LEN.\nfunction elementText(el: Element): string {\n const raw = (el as HTMLElement).innerText ?? el.textContent ?? ''\n const trimmed = raw.replace(/\\s+/g, ' ').trim()\n return trimmed.length > MAX_TEXT_LEN ? trimmed.slice(0, MAX_TEXT_LEN) : trimmed\n}\n\n// Serialize a single element into `tag:attr=\"v\";attr2=\"v2\"`.\nfunction serializeElement(el: Element): string {\n const tag = el.tagName.toLowerCase()\n const pairs: string[] = []\n\n const id = el.getAttribute('id')\n if (id) {\n pairs.push(`attr__id=\"${escapeValue(id)}\"`)\n }\n\n const className = typeof el.className === 'string' ? el.className : ''\n if (className) {\n pairs.push(`attr__class=\"${escapeValue(className)}\"`)\n }\n\n if (tag === 'a') {\n const href = el.getAttribute('href')\n if (href) {\n pairs.push(`href=\"${escapeValue(href)}\"`)\n }\n }\n\n const text = elementText(el)\n if (text) {\n pairs.push(`text=\"${escapeValue(text)}\"`)\n }\n\n pairs.push(`nth-child=\"${nthChild(el)}\"`)\n pairs.push(`nth-of-type=\"${nthOfType(el)}\"`)\n\n // data-* attributes as attr__data-*.\n const attrs = el.attributes\n for (let i = 0; i < attrs.length; i += 1) {\n const attr = attrs.item(i)\n if (attr?.name.startsWith('data-')) {\n pairs.push(`attr__${attr.name}=\"${escapeValue(attr.value)}\"`)\n }\n }\n\n return `${tag}:${pairs.join(';')}`\n}\n\n/**\n * Serialize `target` and its ancestors (up to <body>, capped at MAX_DEPTH) into\n * an `elements_chain` string: each element as `tag:attr=\"v\";attr2=\"v2\"`, joined\n * by `|` with the target element first and its parents following.\n */\nexport function serializeElementsChain(target: Element): string {\n const parts: string[] = []\n let el: Element | null = target\n let depth = 0\n\n while (el && depth < MAX_DEPTH) {\n const tag = el.tagName.toLowerCase()\n if (tag === 'body' || tag === 'html') {\n break\n }\n parts.push(serializeElement(el))\n el = el.parentElement\n depth += 1\n }\n\n return parts.join('|')\n}\n\n// True if any ancestor (inclusive) opts out of capture.\nfunction isOptedOut(el: Element): boolean {\n let node: Element | null = el\n while (node) {\n if (node.classList?.contains(OPT_OUT_CLASS) || node.hasAttribute(OPT_OUT_ATTR)) {\n return true\n }\n node = node.parentElement\n }\n return false\n}\n\n// True if the element is an inherently interesting click target.\nfunction isInteresting(el: Element): boolean {\n const tag = el.tagName.toLowerCase()\n if (INTERESTING_TAGS.has(tag)) {\n return true\n }\n if (el.hasAttribute('data-ch-capture')) {\n return true\n }\n if (el.getAttribute('role')) {\n return true\n }\n // Inline handler attribute (`onclick=\"...\"`).\n if (el.hasAttribute('onclick')) {\n return true\n }\n return false\n}\n\n/**\n * Climb from the raw event target to the nearest \"interesting\" element to\n * attribute the event to (a, button, input, select, textarea, label, or any\n * element with a role / onclick / [data-ch-capture]). Returns null when nothing\n * meaningful is found or the subtree is opted out of capture.\n */\nexport function getEventTarget(el: Element): Element | null {\n let node: Element | null = el\n let depth = 0\n while (node && depth < MAX_DEPTH) {\n if (isOptedOut(node)) {\n return null\n }\n if (isInteresting(node)) {\n return node\n }\n node = node.parentElement\n depth += 1\n }\n return null\n}\n", "// @clickroom/sdk-js \u2014 DOM autocapture + pageview / SPA navigation tracking.\n//\n// Attaches capture-phase listeners for click/submit/change, serializes the\n// target's DOM path into `elements_chain`, and emits `$autocapture`. Also emits\n// `$pageview` on load and on SPA navigation (history pushState/replaceState,\n// popstate, hashchange). Every handler is wrapped so a listener error never\n// breaks the host page. `initAutocapture` returns a teardown that removes all\n// listeners and restores the patched history methods.\n\nimport { getEventTarget, serializeElementsChain } from './elements-chain'\nimport type { AutocaptureContext } from './types'\n\ntype HistoryMethod = History['pushState']\n\n// Common URL/location properties attached to every autocapture + pageview event.\nfunction locationProps(): Record<string, unknown> {\n return {\n $current_url: location.href,\n $host: location.host,\n $pathname: location.pathname,\n }\n}\n\n/**\n * Wire up autocapture + pageview tracking. Returns a teardown function that\n * removes every listener and restores `history.pushState`/`replaceState`.\n * A no-op teardown is returned in non-DOM (SSR) environments.\n */\nexport function initAutocapture(ctx: AutocaptureContext): () => void {\n if (typeof document === 'undefined' || typeof window === 'undefined') {\n return () => {}\n }\n\n // ---- Autocapture (click / submit / change) -----------------------------\n\n const handleInteraction = (event: Event): void => {\n try {\n const raw = event.target\n if (!(raw instanceof Element)) {\n return\n }\n const target = getEventTarget(raw)\n if (!target) {\n return\n }\n const chain = serializeElementsChain(target)\n ctx.capture(\n '$autocapture',\n {\n $event_type: event.type,\n ...locationProps(),\n },\n chain,\n )\n } catch {\n // Never let a listener error surface to the host page.\n }\n }\n\n const interactionEvents: string[] = ['click', 'submit', 'change']\n\n if (ctx.config.autocapture) {\n for (const type of interactionEvents) {\n document.addEventListener(type, handleInteraction, true)\n }\n }\n\n // ---- Pageviews (initial + SPA navigation) ------------------------------\n\n let lastUrl = ''\n\n const emitPageview = (): void => {\n try {\n if (!ctx.config.capture_pageview) {\n return\n }\n if (location.href === lastUrl) {\n return\n }\n lastUrl = location.href\n ctx.capture('$pageview', {\n ...locationProps(),\n $referrer: document.referrer,\n })\n } catch {\n // Swallow: pageview tracking must never break navigation.\n }\n }\n\n const origPushState: HistoryMethod = history.pushState.bind(history)\n const origReplaceState: HistoryMethod = history.replaceState.bind(history)\n let patched = false\n\n if (ctx.config.capture_pageview) {\n history.pushState = (...args: Parameters<HistoryMethod>): void => {\n origPushState(...args)\n emitPageview()\n }\n history.replaceState = (...args: Parameters<HistoryMethod>): void => {\n origReplaceState(...args)\n emitPageview()\n }\n patched = true\n\n window.addEventListener('popstate', emitPageview)\n window.addEventListener('hashchange', emitPageview)\n\n // Initial pageview for the current document.\n emitPageview()\n }\n\n // ---- Teardown ----------------------------------------------------------\n\n return () => {\n if (ctx.config.autocapture) {\n for (const type of interactionEvents) {\n document.removeEventListener(type, handleInteraction, true)\n }\n }\n if (patched) {\n history.pushState = origPushState\n history.replaceState = origReplaceState\n window.removeEventListener('popstate', emitPageview)\n window.removeEventListener('hashchange', emitPageview)\n }\n }\n}\n", "// @clickroom/sdk-js \u2014 traffic channel classification.\n// PURE, zero-dependency: this package bundles to a CDN script, so no imports.\n// Classifies `document.referrer` + the session's UTM params into a\n// PostHog-style `$channel_type`, using small static domain lists \u2014 no\n// external lookup/API call, synchronous, same style as device.ts's UA parser.\n\n// utm_medium values that indicate paid traffic.\nconst PAID_MEDIUM_RE = /^(cpc|ppc|paid|display)$/i\n\n// Referring-domain substrings for well-known search engines. Matched with a\n// simple `includes` so subdomains/ccTLDs (e.g. `www.google.co.uk`) still hit.\nconst SEARCH_ENGINE_DOMAINS = [\n 'google.',\n 'bing.com',\n 'duckduckgo.com',\n 'yahoo.',\n 'baidu.com',\n 'yandex.',\n 'ecosia.org',\n 'ask.com',\n 'aol.com',\n]\n\n// Referring-domain substrings for well-known social platforms.\nconst SOCIAL_DOMAINS = [\n 'facebook.com',\n 'fb.com',\n 'twitter.com',\n 'x.com',\n 't.co',\n 'linkedin.com',\n 'instagram.com',\n 'tiktok.com',\n 'reddit.com',\n 'pinterest.',\n 'youtube.com',\n 'threads.net',\n]\n\nconst matchesDomain = (domain: string, list: string[]): boolean =>\n list.some((needle) => domain.includes(needle))\n\nconst extractDomain = (url: string): string | undefined => {\n if (!url) return undefined\n try {\n return new URL(url).hostname.toLowerCase()\n } catch {\n return undefined\n }\n}\n\n/**\n * Classify a visit into a `$channel_type` bucket from its referrer + UTM\n * params. Rule order (first match wins): direct, paid, organic search,\n * social, email, referral, fallback direct.\n *\n * @param referrer `document.referrer`.\n * @param utm The `$utm_*` properties already parsed by `parseUtm`.\n * @param currentHostname The current page's hostname (`location.hostname`),\n * used to avoid classifying same-site referrers as `referral`.\n */\nexport function classifyChannel(\n referrer: string,\n utm: Record<string, string>,\n currentHostname: string,\n): string {\n const referrerDomain = extractDomain(referrer)\n const utmMedium = utm.$utm_medium\n const hasReferrer = Boolean(referrerDomain)\n const hasUtm = Object.keys(utm).length > 0\n\n if (!hasReferrer && !hasUtm) return 'direct'\n if (utmMedium && PAID_MEDIUM_RE.test(utmMedium)) return 'paid'\n if (referrerDomain && matchesDomain(referrerDomain, SEARCH_ENGINE_DOMAINS))\n return 'organic search'\n if (referrerDomain && matchesDomain(referrerDomain, SOCIAL_DOMAINS)) return 'social'\n if (utmMedium && utmMedium.toLowerCase() === 'email') return 'email'\n if (referrerDomain && referrerDomain !== currentHostname.toLowerCase()) return 'referral'\n return 'direct'\n}\n", "// @clickroom/sdk-js \u2014 lightweight user-agent parsing (device/OS/browser).\n// PURE, zero-dependency: this package bundles to a CDN script, so no imports.\n// Emits PostHog-style `$` property keys. Only returns keys it can determine.\n\n// ---- browser detection --------------------------------------------------\n// Order matters: Edg/OPR/SamsungBrowser must be checked before Chrome, and\n// Chrome before Safari (Chrome UAs also contain a \"Safari\" token).\n\ninterface BrowserMatch {\n name: string\n // Regex whose first capture group is the major version (when present).\n re: RegExp\n}\n\nconst BROWSERS: BrowserMatch[] = [\n { name: 'Edge', re: /Edg(?:e|A|iOS)?\\/(\\d+)/ },\n { name: 'Opera', re: /(?:OPR|Opera)\\/(\\d+)/ },\n { name: 'Samsung Internet', re: /SamsungBrowser\\/(\\d+)/ },\n { name: 'Firefox', re: /(?:Firefox|FxiOS)\\/(\\d+)/ },\n { name: 'Chrome', re: /(?:Chrome|CriOS)\\/(\\d+)/ },\n { name: 'Safari', re: /Version\\/(\\d+).*Safari/ },\n]\n\nconst detectBrowser = (ua: string): { name?: string; version?: string } => {\n for (const b of BROWSERS) {\n const m = ua.match(b.re)\n if (m) return { name: b.name, version: m[1] }\n }\n return {}\n}\n\n// ---- OS detection -------------------------------------------------------\n\nconst detectOS = (ua: string): string | undefined => {\n if (/Windows/.test(ua)) return 'Windows'\n // iOS must precede macOS (iPad/iPhone UAs may mention \"Mac OS X\").\n if (/iPhone|iPad|iPod/.test(ua)) return 'iOS'\n if (/Android/.test(ua)) return 'Android'\n if (/CrOS/.test(ua)) return 'Chrome OS'\n if (/Mac OS X|Macintosh/.test(ua)) return 'macOS'\n if (/Linux/.test(ua)) return 'Linux'\n return undefined\n}\n\n// ---- device type --------------------------------------------------------\n// iPad -> Tablet; a \"Mobile\" token -> Mobile; Android without \"Mobile\" -> Tablet.\n\nconst detectDeviceType = (ua: string): string => {\n if (/iPad/.test(ua)) return 'Tablet'\n if (/Android/.test(ua)) return /Mobile/.test(ua) ? 'Mobile' : 'Tablet'\n if (/iPhone|iPod/.test(ua)) return 'Mobile'\n if (/Mobile/.test(ua)) return 'Mobile'\n return 'Desktop'\n}\n\n/**\n * Parse a user-agent + language into PostHog-style device properties.\n * Returns only the keys it can determine (never undefined values).\n */\nexport function parseDevice(\n userAgent: string,\n language: string | undefined,\n): Record<string, string> {\n const props: Record<string, string> = {}\n\n const ua = userAgent ?? ''\n if (ua) {\n const browser = detectBrowser(ua)\n if (browser.name) props.$browser = browser.name\n if (browser.version) props.$browser_version = browser.version\n\n const os = detectOS(ua)\n if (os) props.$os = os\n\n props.$device_type = detectDeviceType(ua)\n }\n\n if (language) props.$lang = language\n\n return props\n}\n", "// @clickroom/sdk-js \u2014 canonical feature-flag evaluation + remote fetch cache (Phase 10.A).\n// Semantics are the SHARED SOURCE OF TRUTH (D-05, CANONICAL-FLAGS.md). Any deviation\n// breaks SDK<->server parity (AC-5/6/7). Keep byte-for-byte agreement with Rust + tRPC.\n\nimport { sha1 } from './sha1'\n\n// ---- filter data model (\u00A71) --------------------------------------------\n\nexport type FlagOperator = 'exact' | 'is_not' | 'icontains' | 'gt' | 'lt' | 'is_set'\n\nexport interface PropFilter {\n key: string\n operator: FlagOperator\n value: unknown\n}\n\nexport interface Group {\n properties: PropFilter[]\n rollout_percentage: number\n variant?: string | null\n}\n\nexport interface Variant {\n key: string\n rollout_percentage: number\n}\n\nexport interface Multivariate {\n variants: Variant[]\n}\n\nexport interface Filters {\n groups: Group[]\n multivariate?: Multivariate\n payloads?: Record<string, unknown>\n}\n\nexport interface Flag {\n key: string\n active: boolean\n filters: Filters\n}\n\nexport interface FlagResult {\n enabled: boolean\n variant: string | null\n payload: unknown\n}\n\n// ---- hash (\u00A72) ----------------------------------------------------------\n\n// 15 hex f's == 1152921504606846975. Built via BigInt(string) rather than a `\u2026n`\n// literal so the es2018 CDN bundle target accepts it (identical numeric value).\nconst HASH_DENOM = Number(BigInt('0xfffffffffffffff'))\n\n/** Deterministic bucket in [0,1): int(sha1(key+\".\"+distinctId+salt)[0:15]) / 0xFFFFFFFFFFFFFFF. */\nexport function hashFlag(key: string, distinctId: string, salt: string): number {\n const hex = sha1(`${key}.${distinctId}${salt}`)\n // BigInt (not parseInt) \u2014 15 hex chars is up to 60 bits, beyond 2^53 precision.\n const n = Number(BigInt(`0x${hex.slice(0, 15)}`))\n return n / HASH_DENOM\n}\n\n// ---- operators (\u00A74) -----------------------------------------------------\n\n/** actual = person_properties[key]; value = filter value. */\nexport function propMatch(op: FlagOperator, actual: unknown, value: unknown): boolean {\n switch (op) {\n case 'exact':\n return String(actual) === String(value)\n case 'is_not':\n return String(actual) !== String(value)\n case 'icontains':\n return actual != null && String(actual).toLowerCase().includes(String(value).toLowerCase())\n case 'gt':\n return actual != null && Number(actual) > Number(value)\n case 'lt':\n return actual != null && Number(actual) < Number(value)\n case 'is_set':\n return actual !== undefined && actual !== null\n default:\n return false\n }\n}\n\nconst groupMatches = (props: PropFilter[], pp: Record<string, unknown>): boolean => {\n for (const p of props) {\n if (!propMatch(p.operator, pp[p.key], p.value)) return false\n }\n return true\n}\n\nconst pickVariant = (flag: Flag, distinctId: string): string | null => {\n const variants = flag.filters.multivariate?.variants ?? []\n const f = hashFlag(flag.key, distinctId, 'variant')\n let cumulative = 0\n for (const v of variants) {\n cumulative += v.rollout_percentage / 100\n if (f < cumulative) return v.key // strict <\n }\n return variants.length > 0 ? variants[variants.length - 1].key : null\n}\n\n// ---- evaluate (\u00A73) ------------------------------------------------------\n\n/** First matching group wins; in-group-off stops (no fallthrough). */\nexport function evaluate(\n flag: Flag,\n distinctId: string,\n personProps: Record<string, unknown> = {},\n): FlagResult {\n if (!flag.active) return { enabled: false, variant: null, payload: null }\n\n const filters = flag.filters ?? { groups: [] }\n for (const group of filters.groups ?? []) {\n if (!groupMatches(group.properties ?? [], personProps)) continue\n\n if (hashFlag(flag.key, distinctId, '') <= group.rollout_percentage / 100) {\n const variant = filters.multivariate ? pickVariant(flag, distinctId) : (group.variant ?? null)\n const payload = variant != null ? (filters.payloads?.[variant] ?? null) : null\n return { enabled: true, variant, payload }\n }\n // In-group but rolled out off \u2014 stop, no fallthrough.\n return { enabled: false, variant: null, payload: null }\n }\n\n return { enabled: false, variant: null, payload: null }\n}\n\n// ---- remote fetch cache client -----------------------------------------\n\nconst FLAGS_STORAGE_KEY = 'ckrm_flags'\n\nexport type FlagMap = Record<string, FlagResult>\n\nexport interface FlagsResponse {\n flags: FlagMap\n}\n\nconst safeLocalStorage = (): Storage | null => {\n try {\n if (typeof localStorage === 'undefined') return null\n return localStorage\n } catch {\n return null\n }\n}\n\nexport interface FlagClientDeps {\n getApiHost: () => string\n getWriteKey: () => string\n getDistinctId: () => string\n getPersonProperties: () => Record<string, unknown>\n}\n\nexport interface FlagClient {\n /** In-memory cache of the last evaluated flags (or restored from localStorage). */\n getCache(): FlagMap\n isFeatureEnabled(key: string): boolean\n getFeatureFlag(key: string): boolean | string\n getFeatureFlagPayload(key: string): unknown\n /** Fetch + cache flags for the current distinct id/props. Never throws. */\n fetchFlags(): Promise<FlagMap>\n /**\n * Cheap version check (GET `{apiHost}/flags/version`) before paying for a\n * full `fetchFlags()`. Only calls `fetchFlags()` (and updates the stored\n * `lastVersion`) when the returned version differs from the last one seen\n * \u2014 or on the very first check. Network/parse failure on the version check\n * fails open (treated as \"changed\") and falls through to a full fetch,\n * matching this client's existing never-throw/best-effort tolerance.\n * Never throws.\n */\n checkVersionAndMaybeFetch(): Promise<{ changed: boolean; flags: FlagMap }>\n /** Drop the in-memory + persisted cache. */\n clear(): void\n}\n\nexport function createFlagClient(deps: FlagClientDeps): FlagClient {\n let cache: FlagMap = restoreCache()\n\n function restoreCache(): FlagMap {\n try {\n const raw = safeLocalStorage()?.getItem(FLAGS_STORAGE_KEY)\n if (!raw) return {}\n const parsed = JSON.parse(raw) as FlagMap\n if (parsed && typeof parsed === 'object') return parsed\n } catch {\n // ignore corrupt cache\n }\n return {}\n }\n\n const persist = (map: FlagMap): void => {\n try {\n safeLocalStorage()?.setItem(FLAGS_STORAGE_KEY, JSON.stringify(map))\n } catch {\n // ignore quota / disabled storage\n }\n }\n\n let lastVersion: number | undefined\n\n const buildUrl = (): string => {\n const base = deps.getApiHost().replace(/\\/+$/, '')\n return `${base}/v1/flags`\n }\n\n const buildVersionUrl = (): string => {\n const base = deps.getApiHost().replace(/\\/+$/, '')\n return `${base}/v1/flags/version`\n }\n\n const fetchFlags = async (): Promise<FlagMap> => {\n try {\n if (typeof fetch !== 'function') return cache\n const res = await fetch(buildUrl(), {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${deps.getWriteKey()}`,\n },\n body: JSON.stringify({\n distinct_id: deps.getDistinctId(),\n person_properties: deps.getPersonProperties(),\n }),\n })\n if (!res.ok) return cache // fall back to existing cache\n const data = (await res.json()) as FlagsResponse\n const flags = data?.flags\n if (flags && typeof flags === 'object') {\n cache = flags\n persist(cache)\n }\n return cache\n } catch {\n // Network / parse failure \u2014 keep whatever cache we have (incl. localStorage).\n return cache\n }\n }\n\n const checkVersionAndMaybeFetch = async (): Promise<{ changed: boolean; flags: FlagMap }> => {\n let changed = true\n let newVersion: number | undefined\n\n try {\n if (typeof fetch === 'function') {\n const res = await fetch(buildVersionUrl(), {\n method: 'GET',\n headers: {\n authorization: `Bearer ${deps.getWriteKey()}`,\n },\n })\n if (res.ok) {\n const data = (await res.json()) as { version?: number }\n newVersion = data?.version\n changed = lastVersion === undefined || newVersion !== lastVersion\n }\n // !res.ok \u2014 fail open, `changed` stays true (default).\n }\n } catch {\n // Network / parse failure \u2014 fail open, `changed` stays true.\n }\n\n if (!changed) return { changed: false, flags: cache }\n\n const flags = await fetchFlags()\n if (newVersion !== undefined) lastVersion = newVersion\n return { changed: true, flags }\n }\n\n return {\n getCache: () => cache,\n isFeatureEnabled: (key) => cache[key]?.enabled ?? false,\n getFeatureFlag: (key) => {\n const r = cache[key]\n if (!r?.enabled) return false\n return r.variant != null ? r.variant : true\n },\n getFeatureFlagPayload: (key) => cache[key]?.payload ?? null,\n fetchFlags,\n checkVersionAndMaybeFetch,\n clear: () => {\n cache = {}\n try {\n safeLocalStorage()?.removeItem(FLAGS_STORAGE_KEY)\n } catch {\n // ignore\n }\n },\n }\n}\n", "// @clickroom/sdk-js \u2014 session-id generation + idle rotation (phase 19.C).\n//\n// A session groups a user's events into a contiguous visit. The id is persisted\n// in sessionStorage (per-tab) and rotated when the tab goes idle past a timeout\n// or a session outlives the hard 24h cap. Every captured event carries the\n// current id as `$session_id`; ClickHouse materializes `session_id` from it so\n// the event-detail page can render a true session timeline + session count.\n\n/** Idle window: no captured activity for this long starts a fresh session. */\nconst IDLE_MS = 30 * 60 * 1000\n/** Hard cap: a single session never lives longer than this, even if active. */\nconst MAX_MS = 24 * 60 * 60 * 1000\n\nconst SID_KEY = 'ckrm_sid'\n\ninterface StoredSession {\n /** Session id. */\n id: string\n /** Epoch ms the session started (for the MAX_MS cap). */\n start: number\n /** Epoch ms of the last captured activity (for the IDLE_MS rotation). */\n last: number\n}\n\nexport interface SessionManager {\n /** Current session id, rotating first if idle/expired. Bumps last-activity. */\n getSessionId(): string\n /** Forget the current session \u2014 the next getSessionId() starts a fresh one. */\n reset(): void\n}\n\nexport interface SessionDeps {\n /** Epoch ms clock (injectable for tests). */\n now: () => number\n /** sessionStorage accessor (SSR-safe; returns null when unavailable). */\n storage: () => Storage | null\n /** New session id generator (shares the SDK's uuid helper). */\n genId: () => string\n}\n\nconst parse = (raw: string | null): StoredSession | null => {\n if (!raw) return null\n try {\n const v = JSON.parse(raw)\n if (\n v &&\n typeof v === 'object' &&\n typeof v.id === 'string' &&\n typeof v.start === 'number' &&\n typeof v.last === 'number'\n ) {\n return v as StoredSession\n }\n } catch {\n // fall through \u2014 treat as no session\n }\n return null\n}\n\nexport function createSessionManager({ now, storage, genId }: SessionDeps): SessionManager {\n const load = (): StoredSession | null => {\n try {\n return parse(storage()?.getItem(SID_KEY) ?? null)\n } catch {\n return null\n }\n }\n\n const save = (s: StoredSession): void => {\n try {\n storage()?.setItem(SID_KEY, JSON.stringify(s))\n } catch {\n // ignore \u2014 best-effort persistence\n }\n }\n\n const start = (t: number): StoredSession => {\n const s: StoredSession = { id: genId(), start: t, last: t }\n save(s)\n return s\n }\n\n return {\n getSessionId(): string {\n const t = now()\n const existing = load()\n const expired = !existing || t - existing.last > IDLE_MS || t - existing.start > MAX_MS\n if (expired) {\n return start(t).id\n }\n existing.last = t\n save(existing)\n return existing.id\n },\n reset(): void {\n try {\n storage()?.removeItem(SID_KEY)\n } catch {\n // ignore\n }\n },\n }\n}\n", "// @clickroom/sdk-js \u2014 batching transport with beacon + fetch fallback and bounded retry.\n\nimport type { ClickroomConfig, EventPayload } from './types'\n\nconst FLUSH_SIZE = 20\nconst BATCH_MAX = 100\nconst FLUSH_INTERVAL_MS = 3000\n\nconst RETRY_BASE_MS = 1000\nconst RETRY_CAP_MS = 30_000\nconst RETRY_MAX_EVENTS = 1000\n\nexport interface Transport {\n enqueue(event: EventPayload): void\n flush(useBeacon?: boolean): void\n teardown(): void\n}\n\nconst isBrowser = (): boolean => typeof window !== 'undefined'\n\nexport function createTransport(\n getConfig: () => ClickroomConfig,\n getWriteKey: () => string,\n): Transport {\n let buffer: EventPayload[] = []\n let retryBuffer: EventPayload[] = []\n let retryAttempt = 0\n let retryTimer: ReturnType<typeof setTimeout> | null = null\n let interval: ReturnType<typeof setInterval> | null = null\n\n const buildUrl = (): string => {\n const { api_host } = getConfig()\n const base = api_host.replace(/\\/+$/, '')\n return `${base}/v1/capture?token=${encodeURIComponent(getWriteKey())}`\n }\n\n const buildBody = (batch: EventPayload[]): string => JSON.stringify({ batch })\n\n const scheduleRetry = (): void => {\n if (retryTimer !== null || retryBuffer.length === 0) return\n const delay = Math.min(RETRY_BASE_MS * 2 ** retryAttempt, RETRY_CAP_MS)\n try {\n retryTimer = setTimeout(() => {\n retryTimer = null\n flush(false)\n }, delay)\n } catch {\n retryTimer = null\n }\n }\n\n const requeue = (batch: EventPayload[]): void => {\n // Re-queue oldest-first, dropping oldest when exceeding the cap.\n retryBuffer = retryBuffer.concat(batch)\n if (retryBuffer.length > RETRY_MAX_EVENTS) {\n retryBuffer = retryBuffer.slice(retryBuffer.length - RETRY_MAX_EVENTS)\n }\n retryAttempt = Math.min(retryAttempt + 1, 5)\n scheduleRetry()\n }\n\n const sendBeacon = (url: string, body: string): boolean => {\n try {\n if (typeof navigator === 'undefined' || typeof navigator.sendBeacon !== 'function') {\n return false\n }\n // `text/plain`, not `application/json`: sendBeacon() is *always*\n // credentialed (no opt-out) and a non-safelisted content-type makes it\n // a real cross-origin request subject to CORS credential checks \u2014\n // servers that permissively echo `Access-Control-Allow-Origin: *` (or\n // any wildcard) get hard-rejected by the browser for a credentialed\n // request. `text/plain` keeps this a CORS-simple request, matching\n // posthog-js's same workaround. capture-rs parses the body as raw\n // bytes regardless of declared content-type, so this is a no-op\n // server-side.\n const blob = new Blob([body], { type: 'text/plain' })\n return navigator.sendBeacon(url, blob)\n } catch {\n return false\n }\n }\n\n // 4xx responses mean the server will never accept this batch as-is (e.g.\n // schema-invalid events) \u2014 retrying would just resend a request that's\n // permanently rejected. Surface it (matching the existing console.warn\n // idiom used elsewhere in this SDK \u2014 see index.ts's capture-before-init\n // warning) and drop the batch instead of requeuing it.\n const reportRejection = async (res: Response, batch: EventPayload[]): Promise<void> => {\n let detail: unknown\n try {\n detail = await res.json()\n } catch {\n // Non-JSON or empty body \u2014 surface status only.\n }\n console.warn(\n `[clickroom] capture() batch rejected by server (HTTP ${res.status}); dropping ${batch.length} event(s)`,\n detail,\n )\n }\n\n const sendFetch = (url: string, body: string, batch: EventPayload[]): void => {\n try {\n if (typeof fetch !== 'function') {\n requeue(batch)\n return\n }\n fetch(url, {\n method: 'POST',\n keepalive: true,\n headers: { 'content-type': 'application/json' },\n body,\n })\n .then((res) => {\n if (res.ok) {\n // Delivered \u2014 reset backoff.\n retryAttempt = 0\n if (retryBuffer.length > 0) scheduleRetry()\n return\n }\n if (res.status >= 500) {\n // Server-side/transient failure \u2014 retry with existing backoff.\n requeue(batch)\n return\n }\n // 4xx \u2014 permanent failure, do not retry.\n void reportRejection(res, batch)\n })\n .catch(() => {\n // Network error \u2014 retry with existing backoff.\n requeue(batch)\n })\n } catch {\n requeue(batch)\n }\n }\n\n function flush(useBeacon = false): void {\n if (!isBrowser()) {\n buffer = []\n return\n }\n // Drain pending + retry buffers into batches.\n const pending = retryBuffer.concat(buffer)\n buffer = []\n retryBuffer = []\n if (pending.length === 0) return\n\n try {\n const url = buildUrl()\n for (let i = 0; i < pending.length; i += BATCH_MAX) {\n const batch = pending.slice(i, i + BATCH_MAX)\n const body = buildBody(batch)\n if (useBeacon && sendBeacon(url, body)) continue\n if (useBeacon) {\n // Beacon unavailable during unload \u2014 best-effort fetch fallback.\n sendFetch(url, body, batch)\n continue\n }\n sendFetch(url, body, batch)\n }\n } catch {\n // Never throw from flush.\n }\n }\n\n const onVisibilityChange = (): void => {\n try {\n if (typeof document !== 'undefined' && document.hidden) flush(true)\n } catch {\n // ignore\n }\n }\n\n const onPageHide = (): void => {\n flush(true)\n }\n\n const enqueue = (event: EventPayload): void => {\n if (!isBrowser()) return\n try {\n buffer.push(event)\n if (buffer.length >= FLUSH_SIZE) flush(false)\n } catch {\n // ignore\n }\n }\n\n const teardown = (): void => {\n try {\n if (interval !== null) {\n clearInterval(interval)\n interval = null\n }\n if (retryTimer !== null) {\n clearTimeout(retryTimer)\n retryTimer = null\n }\n if (typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', onVisibilityChange)\n }\n if (typeof window !== 'undefined') {\n window.removeEventListener('pagehide', onPageHide)\n window.removeEventListener('beforeunload', onPageHide)\n }\n } catch {\n // ignore\n }\n }\n\n // Wire up timers + lifecycle listeners.\n if (isBrowser()) {\n try {\n interval = setInterval(() => flush(false), FLUSH_INTERVAL_MS)\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', onVisibilityChange)\n }\n window.addEventListener('pagehide', onPageHide)\n window.addEventListener('beforeunload', onPageHide)\n } catch {\n // ignore\n }\n }\n\n return { enqueue, flush, teardown }\n}\n", "// @clickroom/sdk-js \u2014 UTM campaign parameter parsing.\n// PURE, zero-dependency: this package bundles to a CDN script, so no imports.\n// Parses `utm_*` query params off a URL's search string into PostHog-style\n// `$utm_*` properties. Only emits keys actually present in the query string \u2014\n// never an empty string or a key for an absent param.\n\nconst UTM_KEYS = ['source', 'medium', 'campaign', 'term', 'content'] as const\n\n/**\n * Parse `utm_source`/`utm_medium`/`utm_campaign`/`utm_term`/`utm_content` off\n * a `location.search`-style string into `$utm_*` properties.\n */\nexport function parseUtm(search: string): Record<string, string> {\n const props: Record<string, string> = {}\n if (!search) return props\n\n try {\n const params = new URLSearchParams(search)\n for (const key of UTM_KEYS) {\n const value = params.get(`utm_${key}`)\n if (value) props[`$utm_${key}`] = value\n }\n } catch {\n // Malformed query string \u2014 return whatever was parsed before the throw.\n }\n\n return props\n}\n", "// @clickroom/sdk-js \u2014 clickroom-js browser SDK core (autocapture + flags). Phase 06.A.\n\nimport { initAutocapture } from './autocapture'\nimport { classifyChannel } from './channel'\nimport { parseDevice } from './device'\nimport { createFlagClient, type FlagClient, type FlagMap } from './flags'\nimport { createSessionManager, type SessionManager } from './session'\nimport { createTransport, type Transport } from './transport'\nimport type { AutocaptureContext, ClickroomConfig, ClickroomOptions, EventPayload } from './types'\nimport { parseUtm } from './utm'\n\nexport const PACKAGE_NAME = '@clickroom/sdk-js'\n\nconst LIB_NAME = 'clickroom-js'\n// Stamped on every event as `$lib_version` \u2014 keep in lockstep with package.json.\nconst LIB_VERSION = '0.2.1'\n\nconst DID_KEY = 'ckrm_did'\nconst STORAGE_PREFIX = 'ckrm_'\n\nconst DEFAULT_CONFIG: ClickroomConfig = {\n // The ingest host events are actually POSTed to (infra/cdk/lib/config.ts `subdomains.capture`).\n // Was `sdk.clickroom.co`, which has never been provisioned \u2014 a caller who omitted `api_host`\n // silently dropped every event, since the transport swallows send failures by design.\n api_host: 'https://capture.clickroom.co',\n autocapture: true,\n capture_pageview: true,\n}\n\nconst isBrowser = (): boolean => typeof window !== 'undefined'\n\n// Floor for `feature_flag_poll_interval` \u2014 keeps a misconfigured caller from\n// hammering the flags endpoint (mirrors the intent of sdk-node's mandatory\n// `pollInterval`, just defensive instead of required).\nconst MIN_FLAG_POLL_INTERVAL_MS = 5_000\n\ntype FlagListener = (flags: Record<string, boolean | string>) => void\n\ninterface State {\n initialized: boolean\n warnedNotInit: boolean\n writeKey: string\n config: ClickroomConfig\n distinctId: string\n personProperties: Record<string, unknown>\n transport: Transport | null\n session: SessionManager | null\n // Referrer/channel/UTM context, computed once per session (on the first\n // captured event of that session id) and reused for every subsequent event\n // \u2014 mirrors how `$session_id` itself is scoped per session, not per event.\n sessionContext: { sessionId: string; props: Record<string, unknown> } | null\n teardownAutocapture: (() => void) | null\n flagListeners: FlagListener[]\n flagClient: FlagClient | null\n flagsLoaded: boolean\n emittedFlagCalls: Set<string>\n flagPollId: ReturnType<typeof setInterval> | null\n}\n\nconst state: State = {\n initialized: false,\n warnedNotInit: false,\n writeKey: '',\n config: { ...DEFAULT_CONFIG },\n distinctId: '',\n personProperties: {},\n transport: null,\n session: null,\n sessionContext: null,\n teardownAutocapture: null,\n flagListeners: [],\n flagClient: null,\n flagsLoaded: false,\n emittedFlagCalls: new Set(),\n flagPollId: null,\n}\n\n// ---- storage helpers (SSR-safe) ----------------------------------------\n\nconst safeLocalStorage = (): Storage | null => {\n try {\n if (typeof localStorage === 'undefined') return null\n return localStorage\n } catch {\n return null\n }\n}\n\nconst safeSessionStorage = (): Storage | null => {\n try {\n if (typeof sessionStorage === 'undefined') return null\n return sessionStorage\n } catch {\n return null\n }\n}\n\nconst storageGet = (key: string): string | null => {\n try {\n return safeLocalStorage()?.getItem(key) ?? null\n } catch {\n return null\n }\n}\n\nconst storageSet = (key: string, value: string): void => {\n try {\n safeLocalStorage()?.setItem(key, value)\n } catch {\n // ignore\n }\n}\n\nconst storageRemove = (key: string): void => {\n try {\n safeLocalStorage()?.removeItem(key)\n } catch {\n // ignore\n }\n}\n\nconst generateId = (): string => {\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID()\n }\n } catch {\n // fall through\n }\n // Fallback: not RFC-compliant but unique enough for an anonymous id.\n return `ckrm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`\n}\n\nconst loadOrCreateDistinctId = (): string => {\n const existing = storageGet(DID_KEY)\n if (existing) return existing\n const fresh = generateId()\n storageSet(DID_KEY, fresh)\n return fresh\n}\n\n// ---- property enrichment ------------------------------------------------\n\nconst baseProperties = (): Record<string, unknown> => {\n const props: Record<string, unknown> = {\n $lib: LIB_NAME,\n $lib_version: LIB_VERSION,\n }\n try {\n if (typeof location !== 'undefined') {\n props.$current_url = location.href\n props.$host = location.host\n props.$pathname = location.pathname\n }\n if (typeof document !== 'undefined') {\n props.$referrer = document.referrer\n }\n if (typeof window !== 'undefined' && window.screen) {\n props.$screen_height = window.screen.height\n props.$screen_width = window.screen.width\n }\n if (typeof window !== 'undefined') {\n props.$viewport_height = window.innerHeight\n props.$viewport_width = window.innerWidth\n }\n if (typeof navigator !== 'undefined') {\n Object.assign(props, parseDevice(navigator.userAgent, navigator.language))\n }\n } catch {\n // ignore \u2014 best-effort enrichment\n }\n return props\n}\n\n// Parses `document.referrer` + `location.search`'s utm_* params into\n// `$utm_*`/`$channel_type` properties. Computed once per session (see\n// `getSessionContextProps` below) rather than on every event.\nconst buildSessionContextProps = (): Record<string, unknown> => {\n try {\n const search = typeof location !== 'undefined' ? location.search : ''\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const hostname = typeof location !== 'undefined' ? location.hostname : ''\n const utm = parseUtm(search)\n return { ...utm, $channel_type: classifyChannel(referrer, utm, hostname) }\n } catch {\n return {}\n }\n}\n\n// Returns the current session's cached channel/UTM props, recomputing only\n// when the session id has rotated (new session -> re-parse the current URL).\nconst getSessionContextProps = (sessionId: string | undefined): Record<string, unknown> => {\n if (!sessionId) return buildSessionContextProps()\n if (!state.sessionContext || state.sessionContext.sessionId !== sessionId) {\n state.sessionContext = { sessionId, props: buildSessionContextProps() }\n }\n return state.sessionContext.props\n}\n\n// ---- core capture -------------------------------------------------------\n\nconst warnNotInit = (): void => {\n if (state.warnedNotInit) return\n state.warnedNotInit = true\n try {\n console.warn('[clickroom] capture() called before init(); event dropped')\n } catch {\n // ignore\n }\n}\n\n// Internal capture \u2014 accepts an optional elements_chain (used by autocapture).\nconst captureInternal = (\n event: string,\n properties?: Record<string, unknown>,\n elementsChain?: string,\n): void => {\n if (!state.initialized || !state.transport) {\n warnNotInit()\n return\n }\n try {\n const sessionId = state.session?.getSessionId()\n const sessionProps = sessionId ? { $session_id: sessionId } : {}\n const payload: EventPayload = {\n event,\n distinct_id: state.distinctId,\n timestamp: new Date().toISOString(),\n properties: {\n ...baseProperties(),\n ...sessionProps,\n ...getSessionContextProps(sessionId),\n ...(properties ?? {}),\n },\n }\n if (elementsChain) payload.elements_chain = elementsChain\n state.transport.enqueue(payload)\n } catch {\n // never throw\n }\n}\n\n// ---- feature flags ------------------------------------------------------\n\n// Flatten the cached flag results into the listener-facing map: `{ key: variant|enabled }`.\nconst flagFlatMap = (cache: FlagMap): Record<string, boolean | string> => {\n const flat: Record<string, boolean | string> = {}\n for (const [key, r] of Object.entries(cache)) {\n if (!r.enabled) continue\n flat[key] = r.variant != null ? r.variant : true\n }\n return flat\n}\n\nconst fireFlagListeners = (): void => {\n if (!state.flagClient) return\n const flat = flagFlatMap(state.flagClient.getCache())\n for (const cb of state.flagListeners) {\n try {\n cb(flat)\n } catch {\n // a listener must not break the others\n }\n }\n}\n\n// Version-gated flag refresh: cheap version check first, only paying for a\n// full fetch (+ exposure) when the version actually changed. Always notifies\n// listeners afterward \u2014 matches the pre-existing \"always notify after a\n// refresh call finishes\" contract, incl. fresh subscribers via onFeatureFlags.\n// Never throws.\nconst refreshFlags = (): void => {\n if (!state.flagClient) return\n state.flagClient\n .checkVersionAndMaybeFetch()\n .then(() => {\n state.flagsLoaded = true\n fireFlagListeners()\n })\n .catch(() => {\n // checkVersionAndMaybeFetch already swallows errors; belt-and-suspenders.\n })\n}\n\n// Emit a `$feature_flag_called` exposure event at most once per (key, response)\n// per identity. Never throws \u2014 feature-flag reads must stay SSR-safe.\nconst maybeEmitFlagCall = (key: string, response: boolean | string): void => {\n try {\n // No real value yet \u2014 don't emit a misleading `false` before flags load.\n if (!state.flagsLoaded) return\n const resp = String(response)\n const dedup = `${key}:${resp}`\n if (state.emittedFlagCalls.has(dedup)) return\n state.emittedFlagCalls.add(dedup)\n captureInternal('$feature_flag_called', {\n $feature_flag: key,\n $feature_flag_response: resp,\n })\n } catch {\n // never throw from a flag read\n }\n}\n\n// ---- public API ---------------------------------------------------------\n\nfunction init(writeKey: string, opts?: ClickroomOptions): void {\n if (state.initialized) return\n if (!isBrowser()) {\n // SSR: no-op init. Public methods remain safe no-ops.\n return\n }\n\n state.writeKey = writeKey\n state.config = {\n api_host: opts?.api_host ?? DEFAULT_CONFIG.api_host,\n autocapture: opts?.autocapture ?? DEFAULT_CONFIG.autocapture,\n capture_pageview: opts?.capture_pageview ?? DEFAULT_CONFIG.capture_pageview,\n }\n\n state.distinctId = loadOrCreateDistinctId()\n state.session = createSessionManager({\n now: () => Date.now(),\n storage: safeSessionStorage,\n genId: generateId,\n })\n state.transport = createTransport(\n () => state.config,\n () => state.writeKey,\n )\n state.flagClient = createFlagClient({\n getApiHost: () => state.config.api_host,\n getWriteKey: () => state.writeKey,\n getDistinctId: () => state.distinctId,\n getPersonProperties: () => state.personProperties,\n })\n state.initialized = true\n\n // Non-blocking: load flags for this distinct id, then notify listeners.\n refreshFlags()\n\n // Opt-in poll: re-fetch on an interval so long-lived pages (dashboards,\n // SPAs that rarely re-identify) see flag changes without a page reload.\n // Off by default \u2014 identical to pre-poll behavior when unset.\n if (opts?.feature_flag_poll_interval) {\n const intervalMs = Math.max(opts.feature_flag_poll_interval, MIN_FLAG_POLL_INTERVAL_MS)\n state.flagPollId = setInterval(() => refreshFlags(), intervalMs)\n }\n\n if (state.config.autocapture) {\n const ctx: AutocaptureContext = {\n config: state.config,\n capture: captureInternal,\n }\n try {\n state.teardownAutocapture = initAutocapture(ctx)\n } catch {\n // autocapture is best-effort; never break init\n }\n } else if (state.config.capture_pageview) {\n // Autocapture owns $pageview when enabled (incl. the first).\n // Only emit the initial pageview here when autocapture is off.\n captureInternal('$pageview')\n }\n}\n\nfunction capture(event: string, properties?: Record<string, unknown>): void {\n captureInternal(event, properties)\n}\n\nfunction identify(distinctId: string, personProps?: Record<string, unknown>): void {\n if (!state.initialized) {\n warnNotInit()\n return\n }\n const previousId = state.distinctId\n state.distinctId = distinctId\n storageSet(DID_KEY, distinctId)\n if (personProps) state.personProperties = { ...state.personProperties, ...personProps }\n captureInternal('$identify', {\n $set: personProps ?? {},\n ...(previousId !== distinctId ? { $anon_distinct_id: previousId } : {}),\n })\n // New identity re-exposes flags \u2014 drop the dedup set.\n state.emittedFlagCalls.clear()\n // Distinct id / person props changed \u2014 re-evaluate flags and re-notify.\n refreshFlags()\n}\n\nfunction reset(): void {\n try {\n // Clear all cached clickroom keys (flags, did, etc.).\n const ls = safeLocalStorage()\n if (ls) {\n const toRemove: string[] = []\n for (let i = 0; i < ls.length; i += 1) {\n const key = ls.key(i)\n if (key?.startsWith(STORAGE_PREFIX)) toRemove.push(key)\n }\n for (const key of toRemove) storageRemove(key)\n }\n } catch {\n // ignore\n }\n state.distinctId = generateId()\n storageSet(DID_KEY, state.distinctId)\n // New identity starts a fresh session.\n state.session?.reset()\n state.sessionContext = null\n // Drop cached flags + person props for the previous identity.\n state.personProperties = {}\n state.flagsLoaded = false\n state.emittedFlagCalls.clear()\n state.flagClient?.clear()\n}\n\n// ---- feature flags (public getters) -------------------------------------\n\nfunction isFeatureEnabled(key: string): boolean {\n const result = state.flagClient?.isFeatureEnabled(key) ?? false\n maybeEmitFlagCall(key, result)\n return result\n}\n\nfunction getFeatureFlag(key: string): boolean | string {\n const result = state.flagClient?.getFeatureFlag(key) ?? false\n maybeEmitFlagCall(key, result)\n return result\n}\n\nfunction getFeatureFlagPayload(key: string): unknown {\n return state.flagClient?.getFeatureFlagPayload(key) ?? null\n}\n\nfunction onFeatureFlags(cb: (flags: Record<string, boolean | string>) => void): void {\n state.flagListeners.push(cb)\n // If flags are already loaded, fire immediately with the current map.\n if (state.flagsLoaded && state.flagClient) {\n try {\n cb(flagFlatMap(state.flagClient.getCache()))\n } catch {\n // ignore listener errors\n }\n }\n}\n\n/** Manually re-fetch flag definitions and notify `onFeatureFlags` listeners.\n * Same trigger `identify()` already uses internally \u2014 exposed directly so a\n * consumer can refresh on its own signal (a manual \"reload flags\" button, a\n * websocket/SSE event from elsewhere, etc.) without faking an identify call. */\nfunction refreshFeatureFlags(): void {\n refreshFlags()\n}\n\n/** Stop the `feature_flag_poll_interval` loop started in `init()`, if any. */\nfunction stopFeatureFlagPolling(): void {\n if (state.flagPollId !== null) {\n clearInterval(state.flagPollId)\n state.flagPollId = null\n }\n}\n\n// ---- singleton + named exports ------------------------------------------\n\nconst clickroom = {\n init,\n capture,\n identify,\n reset,\n isFeatureEnabled,\n getFeatureFlag,\n getFeatureFlagPayload,\n onFeatureFlags,\n refreshFeatureFlags,\n stopFeatureFlagPolling,\n}\n\nexport type { AutocaptureContext, ClickroomConfig, ClickroomOptions, EventPayload }\nexport {\n capture,\n getFeatureFlag,\n getFeatureFlagPayload,\n identify,\n init,\n isFeatureEnabled,\n onFeatureFlags,\n refreshFeatureFlags,\n reset,\n stopFeatureFlagPolling,\n}\nexport default clickroom\n"],
5
- "mappings": "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",
6
- "names": ["isBrowser", "safeLocalStorage"]
4
+ "sourcesContent": ["// @clickroom/sdk-js \u2014 DOM `elements_chain` serialization (D-07).\n//\n// Serializes a click/submit/change target and its ancestors into a single\n// compact string that ClickHouse materializes into `elements_chain_text` /\n// `elements_chain_href` via `extract(elements_chain, 'text=\"([^\"]*)\"')` and the\n// equivalent `href=\"...\"`. The literal `text=\"` and `href=\"` tokens must appear\n// verbatim for those extractors to match, and values must not contain a raw `\"`\n// (the `[^\"]*` capture stops at the first quote) \u2014 so we backslash-escape.\n\n// Cap how far up the tree we walk, to bound the serialized size.\nconst MAX_DEPTH = 15\n// Truncate captured text to keep the chain compact on the wire.\nconst MAX_TEXT_LEN = 255\n\n// Elements that are inherently \"interesting\" click targets for autocapture.\nconst INTERESTING_TAGS = new Set(['a', 'button', 'input', 'select', 'textarea', 'label'])\n\n// Class / attribute opt-out markers: skip capture for these subtrees.\nconst OPT_OUT_CLASS = 'ch-no-capture'\nconst OPT_OUT_ATTR = 'data-ch-no-capture'\n\n// Escape a value so it survives inside `attr=\"<value>\"` and stays parseable:\n// backslash and double-quote are escaped; separators (`;` `|`) and newlines are\n// escaped so they never terminate a pair or an element boundary.\nfunction escapeValue(value: string): string {\n return value\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/;/g, '\\\\;')\n .replace(/\\|/g, '\\\\|')\n .replace(/\\r?\\n/g, ' ')\n .trim()\n}\n\n// 1-indexed position among *all* siblings.\nfunction nthChild(el: Element): number {\n let n = 1\n let sib = el.previousElementSibling\n while (sib) {\n n += 1\n sib = sib.previousElementSibling\n }\n return n\n}\n\n// 1-indexed position among siblings sharing the same tag name.\nfunction nthOfType(el: Element): number {\n let n = 1\n const tag = el.tagName\n let sib = el.previousElementSibling\n while (sib) {\n if (sib.tagName === tag) {\n n += 1\n }\n sib = sib.previousElementSibling\n }\n return n\n}\n\n// Best-effort trimmed text for an element, truncated to MAX_TEXT_LEN.\nfunction elementText(el: Element): string {\n const raw = (el as HTMLElement).innerText ?? el.textContent ?? ''\n const trimmed = raw.replace(/\\s+/g, ' ').trim()\n return trimmed.length > MAX_TEXT_LEN ? trimmed.slice(0, MAX_TEXT_LEN) : trimmed\n}\n\n// Serialize a single element into `tag:attr=\"v\";attr2=\"v2\"`.\nfunction serializeElement(el: Element): string {\n const tag = el.tagName.toLowerCase()\n const pairs: string[] = []\n\n const id = el.getAttribute('id')\n if (id) {\n pairs.push(`attr__id=\"${escapeValue(id)}\"`)\n }\n\n const className = typeof el.className === 'string' ? el.className : ''\n if (className) {\n pairs.push(`attr__class=\"${escapeValue(className)}\"`)\n }\n\n if (tag === 'a') {\n const href = el.getAttribute('href')\n if (href) {\n pairs.push(`href=\"${escapeValue(href)}\"`)\n }\n }\n\n const text = elementText(el)\n if (text) {\n pairs.push(`text=\"${escapeValue(text)}\"`)\n }\n\n pairs.push(`nth-child=\"${nthChild(el)}\"`)\n pairs.push(`nth-of-type=\"${nthOfType(el)}\"`)\n\n // data-* attributes as attr__data-*.\n const attrs = el.attributes\n for (let i = 0; i < attrs.length; i += 1) {\n const attr = attrs.item(i)\n if (attr?.name.startsWith('data-')) {\n pairs.push(`attr__${attr.name}=\"${escapeValue(attr.value)}\"`)\n }\n }\n\n return `${tag}:${pairs.join(';')}`\n}\n\n/**\n * Serialize `target` and its ancestors (up to <body>, capped at MAX_DEPTH) into\n * an `elements_chain` string: each element as `tag:attr=\"v\";attr2=\"v2\"`, joined\n * by `|` with the target element first and its parents following.\n */\nexport function serializeElementsChain(target: Element): string {\n const parts: string[] = []\n let el: Element | null = target\n let depth = 0\n\n while (el && depth < MAX_DEPTH) {\n const tag = el.tagName.toLowerCase()\n if (tag === 'body' || tag === 'html') {\n break\n }\n parts.push(serializeElement(el))\n el = el.parentElement\n depth += 1\n }\n\n return parts.join('|')\n}\n\n// True if any ancestor (inclusive) opts out of capture.\nfunction isOptedOut(el: Element): boolean {\n let node: Element | null = el\n while (node) {\n if (node.classList?.contains(OPT_OUT_CLASS) || node.hasAttribute(OPT_OUT_ATTR)) {\n return true\n }\n node = node.parentElement\n }\n return false\n}\n\n// True if the element is an inherently interesting click target.\nfunction isInteresting(el: Element): boolean {\n const tag = el.tagName.toLowerCase()\n if (INTERESTING_TAGS.has(tag)) {\n return true\n }\n if (el.hasAttribute('data-ch-capture')) {\n return true\n }\n if (el.getAttribute('role')) {\n return true\n }\n // Inline handler attribute (`onclick=\"...\"`).\n if (el.hasAttribute('onclick')) {\n return true\n }\n return false\n}\n\n/**\n * Climb from the raw event target to the nearest \"interesting\" element to\n * attribute the event to (a, button, input, select, textarea, label, or any\n * element with a role / onclick / [data-ch-capture]). Returns null when nothing\n * meaningful is found or the subtree is opted out of capture.\n */\nexport function getEventTarget(el: Element): Element | null {\n let node: Element | null = el\n let depth = 0\n while (node && depth < MAX_DEPTH) {\n if (isOptedOut(node)) {\n return null\n }\n if (isInteresting(node)) {\n return node\n }\n node = node.parentElement\n depth += 1\n }\n return null\n}\n", "// @clickroom/sdk-js \u2014 DOM autocapture + pageview / SPA navigation tracking.\n//\n// Attaches capture-phase listeners for click/submit/change, serializes the\n// target's DOM path into `elements_chain`, and emits `$autocapture`. Also emits\n// `$pageview` on load and on SPA navigation (history pushState/replaceState,\n// popstate, hashchange). Every handler is wrapped so a listener error never\n// breaks the host page. `initAutocapture` returns a teardown that removes all\n// listeners and restores the patched history methods.\n\nimport { getEventTarget, serializeElementsChain } from './elements-chain'\nimport type { AutocaptureContext } from './types'\n\ntype HistoryMethod = History['pushState']\n\n// Common URL/location properties attached to every autocapture + pageview event.\nfunction locationProps(): Record<string, unknown> {\n return {\n $current_url: location.href,\n $host: location.host,\n $pathname: location.pathname,\n }\n}\n\n/**\n * Wire up autocapture + pageview tracking. Returns a teardown function that\n * removes every listener and restores `history.pushState`/`replaceState`.\n * A no-op teardown is returned in non-DOM (SSR) environments.\n */\nexport function initAutocapture(ctx: AutocaptureContext): () => void {\n if (typeof document === 'undefined' || typeof window === 'undefined') {\n return () => {}\n }\n\n // ---- Autocapture (click / submit / change) -----------------------------\n\n const handleInteraction = (event: Event): void => {\n try {\n const raw = event.target\n if (!(raw instanceof Element)) {\n return\n }\n const target = getEventTarget(raw)\n if (!target) {\n return\n }\n const chain = serializeElementsChain(target)\n ctx.capture(\n '$autocapture',\n {\n $event_type: event.type,\n ...locationProps(),\n },\n chain,\n )\n } catch {\n // Never let a listener error surface to the host page.\n }\n }\n\n const interactionEvents: string[] = ['click', 'submit', 'change']\n\n if (ctx.config.autocapture) {\n for (const type of interactionEvents) {\n document.addEventListener(type, handleInteraction, true)\n }\n }\n\n // ---- Pageviews (initial + SPA navigation) ------------------------------\n\n let lastUrl = ''\n\n const emitPageview = (): void => {\n try {\n if (!ctx.config.capture_pageview) {\n return\n }\n if (location.href === lastUrl) {\n return\n }\n lastUrl = location.href\n ctx.capture('$pageview', {\n ...locationProps(),\n $referrer: document.referrer,\n })\n } catch {\n // Swallow: pageview tracking must never break navigation.\n }\n }\n\n const origPushState: HistoryMethod = history.pushState.bind(history)\n const origReplaceState: HistoryMethod = history.replaceState.bind(history)\n let patched = false\n\n if (ctx.config.capture_pageview) {\n history.pushState = (...args: Parameters<HistoryMethod>): void => {\n origPushState(...args)\n emitPageview()\n }\n history.replaceState = (...args: Parameters<HistoryMethod>): void => {\n origReplaceState(...args)\n emitPageview()\n }\n patched = true\n\n window.addEventListener('popstate', emitPageview)\n window.addEventListener('hashchange', emitPageview)\n\n // Initial pageview for the current document.\n emitPageview()\n }\n\n // ---- Teardown ----------------------------------------------------------\n\n return () => {\n if (ctx.config.autocapture) {\n for (const type of interactionEvents) {\n document.removeEventListener(type, handleInteraction, true)\n }\n }\n if (patched) {\n history.pushState = origPushState\n history.replaceState = origReplaceState\n window.removeEventListener('popstate', emitPageview)\n window.removeEventListener('hashchange', emitPageview)\n }\n }\n}\n", "// @clickroom/sdk-js \u2014 traffic channel classification.\n// PURE, zero-dependency: this package bundles to a CDN script, so no imports.\n// Classifies `document.referrer` + the session's UTM params into a\n// PostHog-style `$channel_type`, using small static domain lists \u2014 no\n// external lookup/API call, synchronous, same style as device.ts's UA parser.\n\n// utm_medium values that indicate paid traffic.\nconst PAID_MEDIUM_RE = /^(cpc|ppc|paid|display)$/i\n\n// Referring-domain substrings for well-known search engines. Matched with a\n// simple `includes` so subdomains/ccTLDs (e.g. `www.google.co.uk`) still hit.\nconst SEARCH_ENGINE_DOMAINS = [\n 'google.',\n 'bing.com',\n 'duckduckgo.com',\n 'yahoo.',\n 'baidu.com',\n 'yandex.',\n 'ecosia.org',\n 'ask.com',\n 'aol.com',\n]\n\n// Referring-domain substrings for well-known social platforms.\nconst SOCIAL_DOMAINS = [\n 'facebook.com',\n 'fb.com',\n 'twitter.com',\n 'x.com',\n 't.co',\n 'linkedin.com',\n 'instagram.com',\n 'tiktok.com',\n 'reddit.com',\n 'pinterest.',\n 'youtube.com',\n 'threads.net',\n]\n\nconst matchesDomain = (domain: string, list: string[]): boolean =>\n list.some((needle) => domain.includes(needle))\n\nconst extractDomain = (url: string): string | undefined => {\n if (!url) return undefined\n try {\n return new URL(url).hostname.toLowerCase()\n } catch {\n return undefined\n }\n}\n\n/**\n * Classify a visit into a `$channel_type` bucket from its referrer + UTM\n * params. Rule order (first match wins): direct, paid, organic search,\n * social, email, referral, fallback direct.\n *\n * @param referrer `document.referrer`.\n * @param utm The `$utm_*` properties already parsed by `parseUtm`.\n * @param currentHostname The current page's hostname (`location.hostname`),\n * used to avoid classifying same-site referrers as `referral`.\n */\nexport function classifyChannel(\n referrer: string,\n utm: Record<string, string>,\n currentHostname: string,\n): string {\n const referrerDomain = extractDomain(referrer)\n const utmMedium = utm.$utm_medium\n const hasReferrer = Boolean(referrerDomain)\n const hasUtm = Object.keys(utm).length > 0\n\n if (!hasReferrer && !hasUtm) return 'direct'\n if (utmMedium && PAID_MEDIUM_RE.test(utmMedium)) return 'paid'\n if (referrerDomain && matchesDomain(referrerDomain, SEARCH_ENGINE_DOMAINS))\n return 'organic search'\n if (referrerDomain && matchesDomain(referrerDomain, SOCIAL_DOMAINS)) return 'social'\n if (utmMedium && utmMedium.toLowerCase() === 'email') return 'email'\n if (referrerDomain && referrerDomain !== currentHostname.toLowerCase()) return 'referral'\n return 'direct'\n}\n", "// @clickroom/sdk-js \u2014 lightweight user-agent parsing (device/OS/browser).\n// PURE, zero-dependency: this package bundles to a CDN script, so no imports.\n// Emits PostHog-style `$` property keys. Only returns keys it can determine.\n\n// ---- browser detection --------------------------------------------------\n// Order matters: Edg/OPR/SamsungBrowser must be checked before Chrome, and\n// Chrome before Safari (Chrome UAs also contain a \"Safari\" token).\n\ninterface BrowserMatch {\n name: string\n // Regex whose first capture group is the major version (when present).\n re: RegExp\n}\n\nconst BROWSERS: BrowserMatch[] = [\n { name: 'Edge', re: /Edg(?:e|A|iOS)?\\/(\\d+)/ },\n { name: 'Opera', re: /(?:OPR|Opera)\\/(\\d+)/ },\n { name: 'Samsung Internet', re: /SamsungBrowser\\/(\\d+)/ },\n { name: 'Firefox', re: /(?:Firefox|FxiOS)\\/(\\d+)/ },\n { name: 'Chrome', re: /(?:Chrome|CriOS)\\/(\\d+)/ },\n { name: 'Safari', re: /Version\\/(\\d+).*Safari/ },\n]\n\nconst detectBrowser = (ua: string): { name?: string; version?: string } => {\n for (const b of BROWSERS) {\n const m = ua.match(b.re)\n if (m) return { name: b.name, version: m[1] }\n }\n return {}\n}\n\n// ---- OS detection -------------------------------------------------------\n\nconst detectOS = (ua: string): string | undefined => {\n if (/Windows/.test(ua)) return 'Windows'\n // iOS must precede macOS (iPad/iPhone UAs may mention \"Mac OS X\").\n if (/iPhone|iPad|iPod/.test(ua)) return 'iOS'\n if (/Android/.test(ua)) return 'Android'\n if (/CrOS/.test(ua)) return 'Chrome OS'\n if (/Mac OS X|Macintosh/.test(ua)) return 'macOS'\n if (/Linux/.test(ua)) return 'Linux'\n return undefined\n}\n\n// ---- device type --------------------------------------------------------\n// iPad -> Tablet; a \"Mobile\" token -> Mobile; Android without \"Mobile\" -> Tablet.\n\nconst detectDeviceType = (ua: string): string => {\n if (/iPad/.test(ua)) return 'Tablet'\n if (/Android/.test(ua)) return /Mobile/.test(ua) ? 'Mobile' : 'Tablet'\n if (/iPhone|iPod/.test(ua)) return 'Mobile'\n if (/Mobile/.test(ua)) return 'Mobile'\n return 'Desktop'\n}\n\n/**\n * Parse a user-agent + language into PostHog-style device properties.\n * Returns only the keys it can determine (never undefined values).\n */\nexport function parseDevice(\n userAgent: string,\n language: string | undefined,\n): Record<string, string> {\n const props: Record<string, string> = {}\n\n const ua = userAgent ?? ''\n if (ua) {\n const browser = detectBrowser(ua)\n if (browser.name) props.$browser = browser.name\n if (browser.version) props.$browser_version = browser.version\n\n const os = detectOS(ua)\n if (os) props.$os = os\n\n props.$device_type = detectDeviceType(ua)\n }\n\n if (language) props.$lang = language\n\n return props\n}\n", "// @clickroom/sdk-js \u2014 canonical feature-flag evaluation + remote fetch cache (Phase 10.A).\n// Semantics are the SHARED SOURCE OF TRUTH (D-05, CANONICAL-FLAGS.md). Any deviation\n// breaks SDK<->server parity (AC-5/6/7). Keep byte-for-byte agreement with Rust + tRPC.\n\nimport { sha1 } from './sha1'\n\n// ---- filter data model (\u00A71) --------------------------------------------\n\nexport type FlagOperator = 'exact' | 'is_not' | 'icontains' | 'gt' | 'lt' | 'is_set'\n\nexport interface PropFilter {\n key: string\n operator: FlagOperator\n value: unknown\n}\n\nexport interface Group {\n properties: PropFilter[]\n rollout_percentage: number\n variant?: string | null\n /** Boolean served to a matched, in-rollout user. Absent means `true`. */\n serve?: boolean\n}\n\nexport interface Variant {\n key: string\n rollout_percentage: number\n}\n\nexport interface Multivariate {\n variants: Variant[]\n}\n\nexport interface Filters {\n groups: Group[]\n multivariate?: Multivariate\n payloads?: Record<string, unknown>\n}\n\nexport interface Flag {\n key: string\n active: boolean\n filters: Filters\n}\n\nexport interface FlagResult {\n enabled: boolean\n variant: string | null\n payload: unknown\n}\n\n// ---- hash (\u00A72) ----------------------------------------------------------\n\n// 15 hex f's == 1152921504606846975. Built via BigInt(string) rather than a `\u2026n`\n// literal so the es2018 CDN bundle target accepts it (identical numeric value).\nconst HASH_DENOM = Number(BigInt('0xfffffffffffffff'))\n\n/** Deterministic bucket in [0,1] \u2014 CLOSED, the numerator can reach the denominator:\n * int(sha1(key+\".\"+distinctId+salt)[0:15]) / 0xFFFFFFFFFFFFFFF. */\nexport function hashFlag(key: string, distinctId: string, salt: string): number {\n const hex = sha1(`${key}.${distinctId}${salt}`)\n // BigInt (not parseInt) \u2014 15 hex chars is up to 60 bits, beyond 2^53 precision.\n const n = Number(BigInt(`0x${hex.slice(0, 15)}`))\n return n / HASH_DENOM\n}\n\n/**\n * Rollout membership for a bucket produced by `hashFlag`.\n *\n * The endpoint guards are not redundant with the `<` compare: `hashFlag` divides by\n * 0xFFFFFFFFFFFFFFF, a value the numerator can actually reach, so buckets live in the\n * CLOSED range [0,1] \u2014 not [0,1). Without the `>= 100` guard, the max-hash user would be\n * excluded from a 100% rollout; without the `<= 0` guard, the zero-hash user would still\n * be let into a 0% rollout under a non-strict compare. In between, the compare is strict\n * so a boundary bucket belongs to the next slice (matching `pickVariant`).\n */\nexport function inRollout(bucket: number, rolloutPercentage: number): boolean {\n if (rolloutPercentage <= 0) return false\n if (rolloutPercentage >= 100) return true\n return bucket < rolloutPercentage / 100\n}\n\n// ---- operators (\u00A74) -----------------------------------------------------\n\n/** actual = person_properties[key]; value = filter value. */\nexport function propMatch(op: FlagOperator, actual: unknown, value: unknown): boolean {\n switch (op) {\n case 'exact':\n return String(actual) === String(value)\n case 'is_not':\n return String(actual) !== String(value)\n case 'icontains':\n return actual != null && String(actual).toLowerCase().includes(String(value).toLowerCase())\n case 'gt':\n return actual != null && Number(actual) > Number(value)\n case 'lt':\n return actual != null && Number(actual) < Number(value)\n case 'is_set':\n return actual !== undefined && actual !== null\n default:\n return false\n }\n}\n\nconst groupMatches = (props: PropFilter[], pp: Record<string, unknown>): boolean => {\n for (const p of props) {\n if (!propMatch(p.operator, pp[p.key], p.value)) return false\n }\n return true\n}\n\nconst pickVariant = (flag: Flag, distinctId: string): string | null => {\n const variants = flag.filters.multivariate?.variants ?? []\n const f = hashFlag(flag.key, distinctId, 'variant')\n let cumulative = 0\n for (const v of variants) {\n cumulative += v.rollout_percentage / 100\n if (f < cumulative) return v.key // strict <\n }\n return variants.length > 0 ? variants[variants.length - 1].key : null\n}\n\n// ---- evaluate (\u00A73) ------------------------------------------------------\n\n/** First matching group wins; in-group-off stops (no fallthrough). */\nexport function evaluate(\n flag: Flag,\n distinctId: string,\n personProps: Record<string, unknown> = {},\n): FlagResult {\n if (!flag.active) return { enabled: false, variant: null, payload: null }\n\n const filters = flag.filters ?? { groups: [] }\n for (const group of filters.groups ?? []) {\n if (!groupMatches(group.properties ?? [], personProps)) continue\n\n if (inRollout(hashFlag(flag.key, distinctId, ''), group.rollout_percentage)) {\n // Group pinned to serve OFF (boolean default groups) \u2014 stop, no fallthrough.\n if (group.serve === false) return { enabled: false, variant: null, payload: null }\n const variant = filters.multivariate ? pickVariant(flag, distinctId) : (group.variant ?? null)\n const payload = variant != null ? (filters.payloads?.[variant] ?? null) : null\n return { enabled: true, variant, payload }\n }\n // In-group but rolled out off \u2014 stop, no fallthrough.\n return { enabled: false, variant: null, payload: null }\n }\n\n return { enabled: false, variant: null, payload: null }\n}\n\n// ---- remote fetch cache client -----------------------------------------\n\nconst FLAGS_STORAGE_KEY = 'ckrm_flags'\n\nexport type FlagMap = Record<string, FlagResult>\n\nexport interface FlagsResponse {\n flags: FlagMap\n}\n\nconst safeLocalStorage = (): Storage | null => {\n try {\n if (typeof localStorage === 'undefined') return null\n return localStorage\n } catch {\n return null\n }\n}\n\nexport interface FlagClientDeps {\n getApiHost: () => string\n getWriteKey: () => string\n getDistinctId: () => string\n getPersonProperties: () => Record<string, unknown>\n}\n\nexport interface FlagClient {\n /** In-memory cache of the last evaluated flags (or restored from localStorage). */\n getCache(): FlagMap\n isFeatureEnabled(key: string): boolean\n getFeatureFlag(key: string): boolean | string\n getFeatureFlagPayload(key: string): unknown\n /** Fetch + cache flags for the current distinct id/props. Never throws. */\n fetchFlags(): Promise<FlagMap>\n /**\n * Cheap version check (GET `{apiHost}/flags/version`) before paying for a\n * full `fetchFlags()`. Only calls `fetchFlags()` (and updates the stored\n * `lastVersion`) when the returned version differs from the last one seen\n * \u2014 or on the very first check. Network/parse failure on the version check\n * fails open (treated as \"changed\") and falls through to a full fetch,\n * matching this client's existing never-throw/best-effort tolerance.\n * Never throws.\n */\n checkVersionAndMaybeFetch(): Promise<{ changed: boolean; flags: FlagMap }>\n /** Drop the in-memory + persisted cache. */\n clear(): void\n}\n\nexport function createFlagClient(deps: FlagClientDeps): FlagClient {\n let cache: FlagMap = restoreCache()\n\n function restoreCache(): FlagMap {\n try {\n const raw = safeLocalStorage()?.getItem(FLAGS_STORAGE_KEY)\n if (!raw) return {}\n const parsed = JSON.parse(raw) as FlagMap\n if (parsed && typeof parsed === 'object') return parsed\n } catch {\n // ignore corrupt cache\n }\n return {}\n }\n\n const persist = (map: FlagMap): void => {\n try {\n safeLocalStorage()?.setItem(FLAGS_STORAGE_KEY, JSON.stringify(map))\n } catch {\n // ignore quota / disabled storage\n }\n }\n\n let lastVersion: number | undefined\n\n const buildUrl = (): string => {\n const base = deps.getApiHost().replace(/\\/+$/, '')\n return `${base}/v1/flags`\n }\n\n const buildVersionUrl = (): string => {\n const base = deps.getApiHost().replace(/\\/+$/, '')\n return `${base}/v1/flags/version`\n }\n\n const fetchFlags = async (): Promise<FlagMap> => {\n try {\n if (typeof fetch !== 'function') return cache\n const res = await fetch(buildUrl(), {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${deps.getWriteKey()}`,\n },\n body: JSON.stringify({\n distinct_id: deps.getDistinctId(),\n person_properties: deps.getPersonProperties(),\n }),\n })\n if (!res.ok) return cache // fall back to existing cache\n const data = (await res.json()) as FlagsResponse\n const flags = data?.flags\n if (flags && typeof flags === 'object') {\n cache = flags\n persist(cache)\n }\n return cache\n } catch {\n // Network / parse failure \u2014 keep whatever cache we have (incl. localStorage).\n return cache\n }\n }\n\n const checkVersionAndMaybeFetch = async (): Promise<{ changed: boolean; flags: FlagMap }> => {\n let changed = true\n let newVersion: number | undefined\n\n try {\n if (typeof fetch === 'function') {\n const res = await fetch(buildVersionUrl(), {\n method: 'GET',\n headers: {\n authorization: `Bearer ${deps.getWriteKey()}`,\n },\n })\n if (res.ok) {\n const data = (await res.json()) as { version?: number }\n newVersion = data?.version\n changed = lastVersion === undefined || newVersion !== lastVersion\n }\n // !res.ok \u2014 fail open, `changed` stays true (default).\n }\n } catch {\n // Network / parse failure \u2014 fail open, `changed` stays true.\n }\n\n if (!changed) return { changed: false, flags: cache }\n\n const flags = await fetchFlags()\n if (newVersion !== undefined) lastVersion = newVersion\n return { changed: true, flags }\n }\n\n return {\n getCache: () => cache,\n isFeatureEnabled: (key) => cache[key]?.enabled ?? false,\n getFeatureFlag: (key) => {\n const r = cache[key]\n if (!r?.enabled) return false\n return r.variant != null ? r.variant : true\n },\n getFeatureFlagPayload: (key) => cache[key]?.payload ?? null,\n fetchFlags,\n checkVersionAndMaybeFetch,\n clear: () => {\n cache = {}\n try {\n safeLocalStorage()?.removeItem(FLAGS_STORAGE_KEY)\n } catch {\n // ignore\n }\n },\n }\n}\n", "// @clickroom/sdk-js \u2014 session id + window id (phase 49; supersedes 19.C).\n//\n// Two ids, two scopes \u2014 mirrors posthog-js (`packages/browser/src/sessionid.ts`):\n//\n// session id (`ckrm_sid`, localStorage) \u2014 one *visit*, shared by every tab on\n// the origin, rotated when every tab has gone idle past IDLE_MS or the session\n// outlives the MAX_MS hard cap.\n// window id (`ckrm_wid`, sessionStorage) \u2014 one *tab* within that visit.\n// sessionStorage is per-tab and survives reloads, so it is the natural home for\n// it. It is also *copied* into a duplicated tab, hence the `primary_window_exists`\n// clone check below.\n//\n// The in-memory tuple is the source of truth; storage is a write-through cache that\n// must never throw out. With storage unavailable (private browsing, disabled storage,\n// embedded webview) this degrades to one stable id per page load \u2014 not a fresh id per\n// event, which is what the sessionStorage-only implementation used to do.\n\n/** Idle window: once every tab has been silent this long, the next event starts a fresh session. */\nconst IDLE_MS = 30 * 60 * 1000\n/** Hard cap: a single session never lives longer than this, even if continuously active. */\nconst MAX_MS = 24 * 60 * 60 * 1000\n\n/** localStorage \u2014 origin-wide, shared across tabs. */\nconst SID_KEY = 'ckrm_sid'\n/** sessionStorage \u2014 per tab. */\nconst WID_KEY = 'ckrm_wid'\n/** sessionStorage \u2014 set while a tab is live, cleared on beforeunload. Absent after a reload,\n * present in a duplicated tab (sessionStorage is cloned) \u2014 that asymmetry is the clone check. */\nconst PRIMARY_WINDOW_KEY = 'ckrm_primary_window_exists'\n\ninterface StoredSession {\n /** Session id. */\n id: string\n /** Epoch ms the session started (for the MAX_MS cap). */\n start: number\n /** Epoch ms of the last captured activity (for the IDLE_MS rotation). */\n last: number\n}\n\nexport interface SessionManager {\n /** Current session id, rotating first if idle/expired.\n * @param readOnly - a passive read: does not bump last-activity and does not idle-rotate\n * (the MAX_MS cap still applies). Use for reads that are not user activity. */\n getSessionId(readOnly?: boolean): string\n /** Current window (tab) id. Stable for the life of the tab, fresh in a duplicated tab. */\n getWindowId(): string\n /** Forget the current session *and* window \u2014 the next read starts fresh ones. */\n reset(): void\n /** Clear the primary-window flag. Callers wire this to `beforeunload` so that a reload\n * (flag absent) keeps its window id while a tab clone (flag present) gets a new one. */\n clearPrimaryWindow(): void\n}\n\nexport interface SessionDeps {\n /** Epoch ms clock (injectable for tests). */\n now: () => number\n /** localStorage accessor \u2014 holds the session id (SSR-safe; returns null when unavailable). */\n storage: () => Storage | null\n /** sessionStorage accessor \u2014 holds the window id. Omit/return null to fall back to memory. */\n sessionStorage?: () => Storage | null\n /** New id generator (shares the SDK's uuid helper). */\n genId: () => string\n}\n\nconst parse = (raw: string | null): StoredSession | null => {\n if (!raw) return null\n try {\n const v = JSON.parse(raw)\n if (\n v &&\n typeof v === 'object' &&\n typeof v.id === 'string' &&\n typeof v.start === 'number' &&\n typeof v.last === 'number'\n ) {\n return v as StoredSession\n }\n } catch {\n // fall through \u2014 treat as no session\n }\n return null\n}\n\nexport function createSessionManager({\n now,\n storage,\n sessionStorage,\n genId,\n}: SessionDeps): SessionManager {\n // ---- source of truth ---------------------------------------------------\n let session: StoredSession | null = null\n let windowId: string | null = null\n // The clone check runs once per manager, on the first window-id read.\n let windowResolved = false\n\n // ---- storage plumbing (best-effort; never throws) -----------------------\n\n const readStored = (): StoredSession | null => {\n try {\n return parse(storage()?.getItem(SID_KEY) ?? null)\n } catch {\n return null\n }\n }\n\n const save = (s: StoredSession): void => {\n try {\n storage()?.setItem(SID_KEY, JSON.stringify(s))\n } catch {\n // ignore \u2014 memory stays authoritative\n }\n }\n\n const tabStore = (): Storage | null => {\n try {\n return sessionStorage?.() ?? null\n } catch {\n return null\n }\n }\n\n const tabGet = (key: string): string | null => {\n try {\n return tabStore()?.getItem(key) ?? null\n } catch {\n return null\n }\n }\n\n const tabSet = (key: string, value: string): void => {\n try {\n tabStore()?.setItem(key, value)\n } catch {\n // ignore\n }\n }\n\n const tabRemove = (key: string): void => {\n try {\n tabStore()?.removeItem(key)\n } catch {\n // ignore\n }\n }\n\n // ---- session helpers ---------------------------------------------------\n\n /** Reconcile memory with what any sibling tab last wrote. Storage is shared\n * (localStorage), so the freshest `last` wins \u2014 that is the cross-tab idle\n * hardening: an idle tab sees an active sibling's activity, and a tab that\n * missed a rotation adopts the sibling's new id instead of minting a third. */\n const loadSession = (): StoredSession | null => {\n const persisted = readStored()\n if (!persisted) return session\n if (!session) return persisted\n if (persisted.id === session.id) {\n return {\n id: session.id,\n start: Math.min(session.start, persisted.start),\n last: Math.max(session.last, persisted.last),\n }\n }\n // Ids diverged \u2014 a sibling rotated. Whichever saw activity most recently wins.\n return persisted.last >= session.last ? persisted : session\n }\n\n const shouldRotate = (s: StoredSession | null, t: number, readOnly: boolean): boolean => {\n if (!s) return true\n if (t - s.start > MAX_MS) return true\n // A passive read must not rotate the session out from under an idle-but-live tab.\n if (readOnly) return false\n return t - s.last > IDLE_MS\n }\n\n /** The current session \u2014 the reconciled one when it's still alive, a brand-new one otherwise. */\n const adoptOrRotate = (t: number, readOnly: boolean): StoredSession => {\n const merged = loadSession()\n if (merged && !shouldRotate(merged, t, readOnly)) return merged\n return { id: genId(), start: t, last: t }\n }\n\n // ---- window helpers ----------------------------------------------------\n\n /** sessionStorage is copied into a duplicated tab / `window.open`, so a stored window id\n * alone doesn't prove ownership. The primary-window flag is cleared on `beforeunload`:\n * id + no flag \u2192 reload of this tab, keep the id.\n * id + flag \u2192 the source tab is still live, this is a clone \u2192 mint a fresh id. */\n const resolveWindowId = (): string => {\n if (!windowResolved) {\n windowResolved = true\n const existing = tabGet(WID_KEY)\n const cloned = tabGet(PRIMARY_WINDOW_KEY) !== null\n if (existing && !cloned) windowId = existing\n tabSet(PRIMARY_WINDOW_KEY, '1')\n }\n if (!windowId) {\n windowId = genId()\n tabSet(WID_KEY, windowId)\n }\n return windowId\n }\n\n const rotateWindowId = (): void => {\n windowId = null\n tabRemove(WID_KEY)\n }\n\n // ---- public ------------------------------------------------------------\n\n return {\n getSessionId(readOnly = false): string {\n const t = now()\n const previousId = session?.id ?? null\n const merged = adoptOrRotate(t, readOnly)\n const next = readOnly ? merged : { ...merged, last: t }\n // Persist a rotation even on a read-only call \u2014 otherwise the fresh id is\n // regenerated on the next read. Only the activity bump is read-only-gated.\n if (!readOnly || next.id !== previousId) save(next)\n session = next\n // A new visit gets a new window. Skipped on the very first read of a page load\n // (previousId === null) so a reload keeps the window id it restored from sessionStorage.\n if (previousId !== null && next.id !== previousId) rotateWindowId()\n return next.id\n },\n\n getWindowId(): string {\n return resolveWindowId()\n },\n\n reset(): void {\n session = null\n try {\n storage()?.removeItem(SID_KEY)\n } catch {\n // ignore\n }\n rotateWindowId()\n },\n\n clearPrimaryWindow(): void {\n tabRemove(PRIMARY_WINDOW_KEY)\n },\n }\n}\n", "// @clickroom/sdk-js \u2014 batching transport with beacon + fetch fallback and bounded retry.\n\nimport type { ClickroomConfig, EventPayload } from './types'\n\nconst FLUSH_SIZE = 20\nconst BATCH_MAX = 100\nconst FLUSH_INTERVAL_MS = 3000\n\nconst RETRY_BASE_MS = 1000\nconst RETRY_CAP_MS = 30_000\nconst RETRY_MAX_EVENTS = 1000\n\nexport interface Transport {\n enqueue(event: EventPayload): void\n flush(useBeacon?: boolean): void\n teardown(): void\n}\n\nconst isBrowser = (): boolean => typeof window !== 'undefined'\n\nexport function createTransport(\n getConfig: () => ClickroomConfig,\n getWriteKey: () => string,\n): Transport {\n let buffer: EventPayload[] = []\n let retryBuffer: EventPayload[] = []\n let retryAttempt = 0\n let retryTimer: ReturnType<typeof setTimeout> | null = null\n let interval: ReturnType<typeof setInterval> | null = null\n\n const buildUrl = (): string => {\n const { api_host } = getConfig()\n const base = api_host.replace(/\\/+$/, '')\n return `${base}/v1/capture?token=${encodeURIComponent(getWriteKey())}`\n }\n\n const buildBody = (batch: EventPayload[]): string => JSON.stringify({ batch })\n\n const scheduleRetry = (): void => {\n if (retryTimer !== null || retryBuffer.length === 0) return\n const delay = Math.min(RETRY_BASE_MS * 2 ** retryAttempt, RETRY_CAP_MS)\n try {\n retryTimer = setTimeout(() => {\n retryTimer = null\n flush(false)\n }, delay)\n } catch {\n retryTimer = null\n }\n }\n\n const requeue = (batch: EventPayload[]): void => {\n // Re-queue oldest-first, dropping oldest when exceeding the cap.\n retryBuffer = retryBuffer.concat(batch)\n if (retryBuffer.length > RETRY_MAX_EVENTS) {\n retryBuffer = retryBuffer.slice(retryBuffer.length - RETRY_MAX_EVENTS)\n }\n retryAttempt = Math.min(retryAttempt + 1, 5)\n scheduleRetry()\n }\n\n const sendBeacon = (url: string, body: string): boolean => {\n try {\n if (typeof navigator === 'undefined' || typeof navigator.sendBeacon !== 'function') {\n return false\n }\n // `text/plain`, not `application/json`: sendBeacon() is *always*\n // credentialed (no opt-out) and a non-safelisted content-type makes it\n // a real cross-origin request subject to CORS credential checks \u2014\n // servers that permissively echo `Access-Control-Allow-Origin: *` (or\n // any wildcard) get hard-rejected by the browser for a credentialed\n // request. `text/plain` keeps this a CORS-simple request, matching\n // posthog-js's same workaround. capture-rs parses the body as raw\n // bytes regardless of declared content-type, so this is a no-op\n // server-side.\n const blob = new Blob([body], { type: 'text/plain' })\n return navigator.sendBeacon(url, blob)\n } catch {\n return false\n }\n }\n\n // 4xx responses mean the server will never accept this batch as-is (e.g.\n // schema-invalid events) \u2014 retrying would just resend a request that's\n // permanently rejected. Surface it (matching the existing console.warn\n // idiom used elsewhere in this SDK \u2014 see index.ts's capture-before-init\n // warning) and drop the batch instead of requeuing it.\n const reportRejection = async (res: Response, batch: EventPayload[]): Promise<void> => {\n let detail: unknown\n try {\n detail = await res.json()\n } catch {\n // Non-JSON or empty body \u2014 surface status only.\n }\n console.warn(\n `[clickroom] capture() batch rejected by server (HTTP ${res.status}); dropping ${batch.length} event(s)`,\n detail,\n )\n }\n\n const sendFetch = (url: string, body: string, batch: EventPayload[]): void => {\n try {\n if (typeof fetch !== 'function') {\n requeue(batch)\n return\n }\n fetch(url, {\n method: 'POST',\n keepalive: true,\n headers: { 'content-type': 'application/json' },\n body,\n })\n .then((res) => {\n if (res.ok) {\n // Delivered \u2014 reset backoff.\n retryAttempt = 0\n if (retryBuffer.length > 0) scheduleRetry()\n return\n }\n if (res.status >= 500) {\n // Server-side/transient failure \u2014 retry with existing backoff.\n requeue(batch)\n return\n }\n // 4xx \u2014 permanent failure, do not retry.\n void reportRejection(res, batch)\n })\n .catch(() => {\n // Network error \u2014 retry with existing backoff.\n requeue(batch)\n })\n } catch {\n requeue(batch)\n }\n }\n\n function flush(useBeacon = false): void {\n if (!isBrowser()) {\n buffer = []\n return\n }\n // Drain pending + retry buffers into batches.\n const pending = retryBuffer.concat(buffer)\n buffer = []\n retryBuffer = []\n if (pending.length === 0) return\n\n try {\n const url = buildUrl()\n for (let i = 0; i < pending.length; i += BATCH_MAX) {\n const batch = pending.slice(i, i + BATCH_MAX)\n const body = buildBody(batch)\n if (useBeacon && sendBeacon(url, body)) continue\n if (useBeacon) {\n // Beacon unavailable during unload \u2014 best-effort fetch fallback.\n sendFetch(url, body, batch)\n continue\n }\n sendFetch(url, body, batch)\n }\n } catch {\n // Never throw from flush.\n }\n }\n\n const onVisibilityChange = (): void => {\n try {\n if (typeof document !== 'undefined' && document.hidden) flush(true)\n } catch {\n // ignore\n }\n }\n\n const onPageHide = (): void => {\n flush(true)\n }\n\n const enqueue = (event: EventPayload): void => {\n if (!isBrowser()) return\n try {\n buffer.push(event)\n if (buffer.length >= FLUSH_SIZE) flush(false)\n } catch {\n // ignore\n }\n }\n\n const teardown = (): void => {\n try {\n if (interval !== null) {\n clearInterval(interval)\n interval = null\n }\n if (retryTimer !== null) {\n clearTimeout(retryTimer)\n retryTimer = null\n }\n if (typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', onVisibilityChange)\n }\n if (typeof window !== 'undefined') {\n window.removeEventListener('pagehide', onPageHide)\n window.removeEventListener('beforeunload', onPageHide)\n }\n } catch {\n // ignore\n }\n }\n\n // Wire up timers + lifecycle listeners.\n if (isBrowser()) {\n try {\n interval = setInterval(() => flush(false), FLUSH_INTERVAL_MS)\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', onVisibilityChange)\n }\n window.addEventListener('pagehide', onPageHide)\n window.addEventListener('beforeunload', onPageHide)\n } catch {\n // ignore\n }\n }\n\n return { enqueue, flush, teardown }\n}\n", "// @clickroom/sdk-js \u2014 UTM campaign parameter parsing.\n// PURE, zero-dependency: this package bundles to a CDN script, so no imports.\n// Parses `utm_*` query params off a URL's search string into PostHog-style\n// `$utm_*` properties. Only emits keys actually present in the query string \u2014\n// never an empty string or a key for an absent param.\n\nconst UTM_KEYS = ['source', 'medium', 'campaign', 'term', 'content'] as const\n\n/**\n * Parse `utm_source`/`utm_medium`/`utm_campaign`/`utm_term`/`utm_content` off\n * a `location.search`-style string into `$utm_*` properties.\n */\nexport function parseUtm(search: string): Record<string, string> {\n const props: Record<string, string> = {}\n if (!search) return props\n\n try {\n const params = new URLSearchParams(search)\n for (const key of UTM_KEYS) {\n const value = params.get(`utm_${key}`)\n if (value) props[`$utm_${key}`] = value\n }\n } catch {\n // Malformed query string \u2014 return whatever was parsed before the throw.\n }\n\n return props\n}\n", "// @clickroom/sdk-js \u2014 clickroom-js browser SDK core (autocapture + flags). Phase 06.A.\n\nimport { initAutocapture } from './autocapture'\nimport { classifyChannel } from './channel'\nimport { parseDevice } from './device'\nimport { createFlagClient, type FlagClient, type FlagMap } from './flags'\nimport { createSessionManager, type SessionManager } from './session'\nimport { createTransport, type Transport } from './transport'\nimport type { AutocaptureContext, ClickroomConfig, ClickroomOptions, EventPayload } from './types'\nimport { parseUtm } from './utm'\n\nexport const PACKAGE_NAME = '@clickroom/sdk-js'\n\nconst LIB_NAME = 'clickroom-js'\n// Stamped on every event as `$lib_version` \u2014 keep in lockstep with package.json.\nconst LIB_VERSION = '0.3.0'\n\nconst DID_KEY = 'ckrm_did'\nconst STORAGE_PREFIX = 'ckrm_'\n\nconst DEFAULT_CONFIG: ClickroomConfig = {\n // The ingest host events are actually POSTed to (infra/cdk/lib/config.ts `subdomains.capture`).\n // Was `sdk.clickroom.co`, which has never been provisioned \u2014 a caller who omitted `api_host`\n // silently dropped every event, since the transport swallows send failures by design.\n api_host: 'https://capture.clickroom.co',\n autocapture: true,\n capture_pageview: true,\n}\n\nconst isBrowser = (): boolean => typeof window !== 'undefined'\n\n// Floor for `feature_flag_poll_interval` \u2014 keeps a misconfigured caller from\n// hammering the flags endpoint (mirrors the intent of sdk-node's mandatory\n// `pollInterval`, just defensive instead of required).\nconst MIN_FLAG_POLL_INTERVAL_MS = 5_000\n\ntype FlagListener = (flags: Record<string, boolean | string>) => void\n\ninterface State {\n initialized: boolean\n warnedNotInit: boolean\n writeKey: string\n config: ClickroomConfig\n distinctId: string\n personProperties: Record<string, unknown>\n transport: Transport | null\n session: SessionManager | null\n // Referrer/channel/UTM context, computed once per session (on the first\n // captured event of that session id) and reused for every subsequent event\n // \u2014 mirrors how `$session_id` itself is scoped per session, not per event.\n sessionContext: { sessionId: string; props: Record<string, unknown> } | null\n teardownAutocapture: (() => void) | null\n flagListeners: FlagListener[]\n flagClient: FlagClient | null\n flagsLoaded: boolean\n emittedFlagCalls: Set<string>\n flagPollId: ReturnType<typeof setInterval> | null\n}\n\nconst state: State = {\n initialized: false,\n warnedNotInit: false,\n writeKey: '',\n config: { ...DEFAULT_CONFIG },\n distinctId: '',\n personProperties: {},\n transport: null,\n session: null,\n sessionContext: null,\n teardownAutocapture: null,\n flagListeners: [],\n flagClient: null,\n flagsLoaded: false,\n emittedFlagCalls: new Set(),\n flagPollId: null,\n}\n\n// ---- storage helpers (SSR-safe) ----------------------------------------\n\nconst safeLocalStorage = (): Storage | null => {\n try {\n if (typeof localStorage === 'undefined') return null\n return localStorage\n } catch {\n return null\n }\n}\n\nconst safeSessionStorage = (): Storage | null => {\n try {\n if (typeof sessionStorage === 'undefined') return null\n return sessionStorage\n } catch {\n return null\n }\n}\n\nconst storageGet = (key: string): string | null => {\n try {\n return safeLocalStorage()?.getItem(key) ?? null\n } catch {\n return null\n }\n}\n\nconst storageSet = (key: string, value: string): void => {\n try {\n safeLocalStorage()?.setItem(key, value)\n } catch {\n // ignore\n }\n}\n\nconst storageRemove = (key: string): void => {\n try {\n safeLocalStorage()?.removeItem(key)\n } catch {\n // ignore\n }\n}\n\nconst generateId = (): string => {\n try {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID()\n }\n } catch {\n // fall through\n }\n // Fallback: not RFC-compliant but unique enough for an anonymous id.\n return `ckrm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`\n}\n\nconst loadOrCreateDistinctId = (): string => {\n const existing = storageGet(DID_KEY)\n if (existing) return existing\n const fresh = generateId()\n storageSet(DID_KEY, fresh)\n return fresh\n}\n\n// ---- property enrichment ------------------------------------------------\n\nconst baseProperties = (): Record<string, unknown> => {\n const props: Record<string, unknown> = {\n $lib: LIB_NAME,\n $lib_version: LIB_VERSION,\n }\n try {\n if (typeof location !== 'undefined') {\n props.$current_url = location.href\n props.$host = location.host\n props.$pathname = location.pathname\n }\n if (typeof document !== 'undefined') {\n props.$referrer = document.referrer\n }\n if (typeof window !== 'undefined' && window.screen) {\n props.$screen_height = window.screen.height\n props.$screen_width = window.screen.width\n }\n if (typeof window !== 'undefined') {\n props.$viewport_height = window.innerHeight\n props.$viewport_width = window.innerWidth\n }\n if (typeof navigator !== 'undefined') {\n Object.assign(props, parseDevice(navigator.userAgent, navigator.language))\n }\n } catch {\n // ignore \u2014 best-effort enrichment\n }\n return props\n}\n\n// Parses `document.referrer` + `location.search`'s utm_* params into\n// `$utm_*`/`$channel_type` properties. Computed once per session (see\n// `getSessionContextProps` below) rather than on every event.\nconst buildSessionContextProps = (): Record<string, unknown> => {\n try {\n const search = typeof location !== 'undefined' ? location.search : ''\n const referrer = typeof document !== 'undefined' ? document.referrer : ''\n const hostname = typeof location !== 'undefined' ? location.hostname : ''\n const utm = parseUtm(search)\n return { ...utm, $channel_type: classifyChannel(referrer, utm, hostname) }\n } catch {\n return {}\n }\n}\n\n// Returns the current session's cached channel/UTM props, recomputing only\n// when the session id has rotated (new session -> re-parse the current URL).\nconst getSessionContextProps = (sessionId: string | undefined): Record<string, unknown> => {\n if (!sessionId) return buildSessionContextProps()\n if (!state.sessionContext || state.sessionContext.sessionId !== sessionId) {\n state.sessionContext = { sessionId, props: buildSessionContextProps() }\n }\n return state.sessionContext.props\n}\n\n// ---- core capture -------------------------------------------------------\n\nconst warnNotInit = (): void => {\n if (state.warnedNotInit) return\n state.warnedNotInit = true\n try {\n console.warn('[clickroom] capture() called before init(); event dropped')\n } catch {\n // ignore\n }\n}\n\n// Internal capture \u2014 accepts an optional elements_chain (used by autocapture).\nconst captureInternal = (\n event: string,\n properties?: Record<string, unknown>,\n elementsChain?: string,\n): void => {\n if (!state.initialized || !state.transport) {\n warnNotInit()\n return\n }\n try {\n const sessionId = state.session?.getSessionId()\n const sessionProps = sessionId\n ? { $session_id: sessionId, $window_id: state.session?.getWindowId() }\n : {}\n const payload: EventPayload = {\n event,\n distinct_id: state.distinctId,\n timestamp: new Date().toISOString(),\n properties: {\n ...baseProperties(),\n ...sessionProps,\n ...getSessionContextProps(sessionId),\n ...(properties ?? {}),\n },\n }\n if (elementsChain) payload.elements_chain = elementsChain\n state.transport.enqueue(payload)\n } catch {\n // never throw\n }\n}\n\n// ---- feature flags ------------------------------------------------------\n\n// Flatten the cached flag results into the listener-facing map: `{ key: variant|enabled }`.\nconst flagFlatMap = (cache: FlagMap): Record<string, boolean | string> => {\n const flat: Record<string, boolean | string> = {}\n for (const [key, r] of Object.entries(cache)) {\n if (!r.enabled) continue\n flat[key] = r.variant != null ? r.variant : true\n }\n return flat\n}\n\nconst fireFlagListeners = (): void => {\n if (!state.flagClient) return\n const flat = flagFlatMap(state.flagClient.getCache())\n for (const cb of state.flagListeners) {\n try {\n cb(flat)\n } catch {\n // a listener must not break the others\n }\n }\n}\n\n// Version-gated flag refresh: cheap version check first, only paying for a\n// full fetch (+ exposure) when the version actually changed. Always notifies\n// listeners afterward \u2014 matches the pre-existing \"always notify after a\n// refresh call finishes\" contract, incl. fresh subscribers via onFeatureFlags.\n// Never throws.\nconst refreshFlags = (): void => {\n if (!state.flagClient) return\n state.flagClient\n .checkVersionAndMaybeFetch()\n .then(() => {\n state.flagsLoaded = true\n fireFlagListeners()\n })\n .catch(() => {\n // checkVersionAndMaybeFetch already swallows errors; belt-and-suspenders.\n })\n}\n\n// Emit a `$feature_flag_called` exposure event at most once per (key, response)\n// per identity. Never throws \u2014 feature-flag reads must stay SSR-safe.\nconst maybeEmitFlagCall = (key: string, response: boolean | string): void => {\n try {\n // No real value yet \u2014 don't emit a misleading `false` before flags load.\n if (!state.flagsLoaded) return\n const resp = String(response)\n const dedup = `${key}:${resp}`\n if (state.emittedFlagCalls.has(dedup)) return\n state.emittedFlagCalls.add(dedup)\n captureInternal('$feature_flag_called', {\n $feature_flag: key,\n $feature_flag_response: resp,\n })\n } catch {\n // never throw from a flag read\n }\n}\n\n// ---- public API ---------------------------------------------------------\n\nfunction init(writeKey: string, opts?: ClickroomOptions): void {\n if (state.initialized) return\n if (!isBrowser()) {\n // SSR: no-op init. Public methods remain safe no-ops.\n return\n }\n\n state.writeKey = writeKey\n state.config = {\n api_host: opts?.api_host ?? DEFAULT_CONFIG.api_host,\n autocapture: opts?.autocapture ?? DEFAULT_CONFIG.autocapture,\n capture_pageview: opts?.capture_pageview ?? DEFAULT_CONFIG.capture_pageview,\n }\n\n state.distinctId = loadOrCreateDistinctId()\n // Session id lives in localStorage (origin-wide \u2014 one visit across every tab);\n // the window id lives in sessionStorage (per tab). See phase 49.\n state.session = createSessionManager({\n now: () => Date.now(),\n storage: safeLocalStorage,\n sessionStorage: safeSessionStorage,\n genId: generateId,\n })\n // Clearing the primary-window flag on unload is what lets a reload keep its window\n // id while a duplicated tab (flag still set in the cloned sessionStorage) mints a new one.\n try {\n window.addEventListener('beforeunload', () => state.session?.clearPrimaryWindow())\n } catch {\n // ignore \u2014 window-id clone detection is best-effort\n }\n state.transport = createTransport(\n () => state.config,\n () => state.writeKey,\n )\n state.flagClient = createFlagClient({\n getApiHost: () => state.config.api_host,\n getWriteKey: () => state.writeKey,\n getDistinctId: () => state.distinctId,\n getPersonProperties: () => state.personProperties,\n })\n state.initialized = true\n\n // Non-blocking: load flags for this distinct id, then notify listeners.\n refreshFlags()\n\n // Opt-in poll: re-fetch on an interval so long-lived pages (dashboards,\n // SPAs that rarely re-identify) see flag changes without a page reload.\n // Off by default \u2014 identical to pre-poll behavior when unset.\n if (opts?.feature_flag_poll_interval) {\n const intervalMs = Math.max(opts.feature_flag_poll_interval, MIN_FLAG_POLL_INTERVAL_MS)\n state.flagPollId = setInterval(() => refreshFlags(), intervalMs)\n }\n\n if (state.config.autocapture) {\n const ctx: AutocaptureContext = {\n config: state.config,\n capture: captureInternal,\n }\n try {\n state.teardownAutocapture = initAutocapture(ctx)\n } catch {\n // autocapture is best-effort; never break init\n }\n } else if (state.config.capture_pageview) {\n // Autocapture owns $pageview when enabled (incl. the first).\n // Only emit the initial pageview here when autocapture is off.\n captureInternal('$pageview')\n }\n}\n\nfunction capture(event: string, properties?: Record<string, unknown>): void {\n captureInternal(event, properties)\n}\n\nfunction identify(distinctId: string, personProps?: Record<string, unknown>): void {\n if (!state.initialized) {\n warnNotInit()\n return\n }\n const previousId = state.distinctId\n state.distinctId = distinctId\n storageSet(DID_KEY, distinctId)\n if (personProps) state.personProperties = { ...state.personProperties, ...personProps }\n captureInternal('$identify', {\n $set: personProps ?? {},\n ...(previousId !== distinctId ? { $anon_distinct_id: previousId } : {}),\n })\n // New identity re-exposes flags \u2014 drop the dedup set.\n state.emittedFlagCalls.clear()\n // Distinct id / person props changed \u2014 re-evaluate flags and re-notify.\n refreshFlags()\n}\n\nfunction reset(): void {\n try {\n // Clear all cached clickroom keys (flags, did, etc.).\n const ls = safeLocalStorage()\n if (ls) {\n const toRemove: string[] = []\n for (let i = 0; i < ls.length; i += 1) {\n const key = ls.key(i)\n if (key?.startsWith(STORAGE_PREFIX)) toRemove.push(key)\n }\n for (const key of toRemove) storageRemove(key)\n }\n } catch {\n // ignore\n }\n state.distinctId = generateId()\n storageSet(DID_KEY, state.distinctId)\n // New identity starts a fresh session.\n state.session?.reset()\n state.sessionContext = null\n // Drop cached flags + person props for the previous identity.\n state.personProperties = {}\n state.flagsLoaded = false\n state.emittedFlagCalls.clear()\n state.flagClient?.clear()\n}\n\n// ---- feature flags (public getters) -------------------------------------\n\nfunction isFeatureEnabled(key: string): boolean {\n const result = state.flagClient?.isFeatureEnabled(key) ?? false\n maybeEmitFlagCall(key, result)\n return result\n}\n\nfunction getFeatureFlag(key: string): boolean | string {\n const result = state.flagClient?.getFeatureFlag(key) ?? false\n maybeEmitFlagCall(key, result)\n return result\n}\n\nfunction getFeatureFlagPayload(key: string): unknown {\n return state.flagClient?.getFeatureFlagPayload(key) ?? null\n}\n\nfunction onFeatureFlags(cb: (flags: Record<string, boolean | string>) => void): void {\n state.flagListeners.push(cb)\n // If flags are already loaded, fire immediately with the current map.\n if (state.flagsLoaded && state.flagClient) {\n try {\n cb(flagFlatMap(state.flagClient.getCache()))\n } catch {\n // ignore listener errors\n }\n }\n}\n\n/** Manually re-fetch flag definitions and notify `onFeatureFlags` listeners.\n * Same trigger `identify()` already uses internally \u2014 exposed directly so a\n * consumer can refresh on its own signal (a manual \"reload flags\" button, a\n * websocket/SSE event from elsewhere, etc.) without faking an identify call. */\nfunction refreshFeatureFlags(): void {\n refreshFlags()\n}\n\n/** Stop the `feature_flag_poll_interval` loop started in `init()`, if any. */\nfunction stopFeatureFlagPolling(): void {\n if (state.flagPollId !== null) {\n clearInterval(state.flagPollId)\n state.flagPollId = null\n }\n}\n\n// ---- singleton + named exports ------------------------------------------\n\nconst clickroom = {\n init,\n capture,\n identify,\n reset,\n isFeatureEnabled,\n getFeatureFlag,\n getFeatureFlagPayload,\n onFeatureFlags,\n refreshFeatureFlags,\n stopFeatureFlagPolling,\n}\n\nexport type { AutocaptureContext, ClickroomConfig, ClickroomOptions, EventPayload }\nexport {\n capture,\n getFeatureFlag,\n getFeatureFlagPayload,\n identify,\n init,\n isFeatureEnabled,\n onFeatureFlags,\n refreshFeatureFlags,\n reset,\n stopFeatureFlagPolling,\n}\nexport default clickroom\n"],
5
+ "mappings": ";AAUA,IAAM,YAAY;AAElB,IAAM,eAAe;AAGrB,IAAM,mBAAmB,oBAAI,IAAI,CAAC,KAAK,UAAU,SAAS,UAAU,YAAY,OAAO,CAAC;AAGxF,IAAM,gBAAgB;AACtB,IAAM,eAAe;AAKrB,SAAS,YAAY,OAAuB;AAC1C,SAAO,MACJ,QAAQ,OAAO,MAAM,EACrB,QAAQ,MAAM,KAAK,EACnB,QAAQ,MAAM,KAAK,EACnB,QAAQ,OAAO,KAAK,EACpB,QAAQ,UAAU,GAAG,EACrB,KAAK;AACV;AAGA,SAAS,SAAS,IAAqB;AACrC,MAAI,IAAI;AACR,MAAI,MAAM,GAAG;AACb,SAAO,KAAK;AACV,SAAK;AACL,UAAM,IAAI;AAAA,EACZ;AACA,SAAO;AACT;AAGA,SAAS,UAAU,IAAqB;AACtC,MAAI,IAAI;AACR,QAAM,MAAM,GAAG;AACf,MAAI,MAAM,GAAG;AACb,SAAO,KAAK;AACV,QAAI,IAAI,YAAY,KAAK;AACvB,WAAK;AAAA,IACP;AACA,UAAM,IAAI;AAAA,EACZ;AACA,SAAO;AACT;AAGA,SAAS,YAAY,IAAqB;AACxC,QAAM,MAAO,GAAmB,aAAa,GAAG,eAAe;AAC/D,QAAM,UAAU,IAAI,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAC9C,SAAO,QAAQ,SAAS,eAAe,QAAQ,MAAM,GAAG,YAAY,IAAI;AAC1E;AAGA,SAAS,iBAAiB,IAAqB;AAC7C,QAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,QAAM,QAAkB,CAAC;AAEzB,QAAM,KAAK,GAAG,aAAa,IAAI;AAC/B,MAAI,IAAI;AACN,UAAM,KAAK,aAAa,YAAY,EAAE,CAAC,GAAG;AAAA,EAC5C;AAEA,QAAM,YAAY,OAAO,GAAG,cAAc,WAAW,GAAG,YAAY;AACpE,MAAI,WAAW;AACb,UAAM,KAAK,gBAAgB,YAAY,SAAS,CAAC,GAAG;AAAA,EACtD;AAEA,MAAI,QAAQ,KAAK;AACf,UAAM,OAAO,GAAG,aAAa,MAAM;AACnC,QAAI,MAAM;AACR,YAAM,KAAK,SAAS,YAAY,IAAI,CAAC,GAAG;AAAA,IAC1C;AAAA,EACF;AAEA,QAAM,OAAO,YAAY,EAAE;AAC3B,MAAI,MAAM;AACR,UAAM,KAAK,SAAS,YAAY,IAAI,CAAC,GAAG;AAAA,EAC1C;AAEA,QAAM,KAAK,cAAc,SAAS,EAAE,CAAC,GAAG;AACxC,QAAM,KAAK,gBAAgB,UAAU,EAAE,CAAC,GAAG;AAG3C,QAAM,QAAQ,GAAG;AACjB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AACxC,UAAM,OAAO,MAAM,KAAK,CAAC;AACzB,QAAI,MAAM,KAAK,WAAW,OAAO,GAAG;AAClC,YAAM,KAAK,SAAS,KAAK,IAAI,KAAK,YAAY,KAAK,KAAK,CAAC,GAAG;AAAA,IAC9D;AAAA,EACF;AAEA,SAAO,GAAG,GAAG,IAAI,MAAM,KAAK,GAAG,CAAC;AAClC;AAOO,SAAS,uBAAuB,QAAyB;AAC9D,QAAM,QAAkB,CAAC;AACzB,MAAI,KAAqB;AACzB,MAAI,QAAQ;AAEZ,SAAO,MAAM,QAAQ,WAAW;AAC9B,UAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,QAAI,QAAQ,UAAU,QAAQ,QAAQ;AACpC;AAAA,IACF;AACA,UAAM,KAAK,iBAAiB,EAAE,CAAC;AAC/B,SAAK,GAAG;AACR,aAAS;AAAA,EACX;AAEA,SAAO,MAAM,KAAK,GAAG;AACvB;AAGA,SAAS,WAAW,IAAsB;AACxC,MAAI,OAAuB;AAC3B,SAAO,MAAM;AACX,QAAI,KAAK,WAAW,SAAS,aAAa,KAAK,KAAK,aAAa,YAAY,GAAG;AAC9E,aAAO;AAAA,IACT;AACA,WAAO,KAAK;AAAA,EACd;AACA,SAAO;AACT;AAGA,SAAS,cAAc,IAAsB;AAC3C,QAAM,MAAM,GAAG,QAAQ,YAAY;AACnC,MAAI,iBAAiB,IAAI,GAAG,GAAG;AAC7B,WAAO;AAAA,EACT;AACA,MAAI,GAAG,aAAa,iBAAiB,GAAG;AACtC,WAAO;AAAA,EACT;AACA,MAAI,GAAG,aAAa,MAAM,GAAG;AAC3B,WAAO;AAAA,EACT;AAEA,MAAI,GAAG,aAAa,SAAS,GAAG;AAC9B,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAQO,SAAS,eAAe,IAA6B;AAC1D,MAAI,OAAuB;AAC3B,MAAI,QAAQ;AACZ,SAAO,QAAQ,QAAQ,WAAW;AAChC,QAAI,WAAW,IAAI,GAAG;AACpB,aAAO;AAAA,IACT;AACA,QAAI,cAAc,IAAI,GAAG;AACvB,aAAO;AAAA,IACT;AACA,WAAO,KAAK;AACZ,aAAS;AAAA,EACX;AACA,SAAO;AACT;;;ACvKA,SAAS,gBAAyC;AAChD,SAAO;AAAA,IACL,cAAc,SAAS;AAAA,IACvB,OAAO,SAAS;AAAA,IAChB,WAAW,SAAS;AAAA,EACtB;AACF;AAOO,SAAS,gBAAgB,KAAqC;AACnE,MAAI,OAAO,aAAa,eAAe,OAAO,WAAW,aAAa;AACpE,WAAO,MAAM;AAAA,IAAC;AAAA,EAChB;AAIA,QAAM,oBAAoB,CAAC,UAAuB;AAChD,QAAI;AACF,YAAM,MAAM,MAAM;AAClB,UAAI,EAAE,eAAe,UAAU;AAC7B;AAAA,MACF;AACA,YAAM,SAAS,eAAe,GAAG;AACjC,UAAI,CAAC,QAAQ;AACX;AAAA,MACF;AACA,YAAM,QAAQ,uBAAuB,MAAM;AAC3C,UAAI;AAAA,QACF;AAAA,QACA;AAAA,UACE,aAAa,MAAM;AAAA,UACnB,GAAG,cAAc;AAAA,QACnB;AAAA,QACA;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,oBAA8B,CAAC,SAAS,UAAU,QAAQ;AAEhE,MAAI,IAAI,OAAO,aAAa;AAC1B,eAAW,QAAQ,mBAAmB;AACpC,eAAS,iBAAiB,MAAM,mBAAmB,IAAI;AAAA,IACzD;AAAA,EACF;AAIA,MAAI,UAAU;AAEd,QAAM,eAAe,MAAY;AAC/B,QAAI;AACF,UAAI,CAAC,IAAI,OAAO,kBAAkB;AAChC;AAAA,MACF;AACA,UAAI,SAAS,SAAS,SAAS;AAC7B;AAAA,MACF;AACA,gBAAU,SAAS;AACnB,UAAI,QAAQ,aAAa;AAAA,QACvB,GAAG,cAAc;AAAA,QACjB,WAAW,SAAS;AAAA,MACtB,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,gBAA+B,QAAQ,UAAU,KAAK,OAAO;AACnE,QAAM,mBAAkC,QAAQ,aAAa,KAAK,OAAO;AACzE,MAAI,UAAU;AAEd,MAAI,IAAI,OAAO,kBAAkB;AAC/B,YAAQ,YAAY,IAAI,SAA0C;AAChE,oBAAc,GAAG,IAAI;AACrB,mBAAa;AAAA,IACf;AACA,YAAQ,eAAe,IAAI,SAA0C;AACnE,uBAAiB,GAAG,IAAI;AACxB,mBAAa;AAAA,IACf;AACA,cAAU;AAEV,WAAO,iBAAiB,YAAY,YAAY;AAChD,WAAO,iBAAiB,cAAc,YAAY;AAGlD,iBAAa;AAAA,EACf;AAIA,SAAO,MAAM;AACX,QAAI,IAAI,OAAO,aAAa;AAC1B,iBAAW,QAAQ,mBAAmB;AACpC,iBAAS,oBAAoB,MAAM,mBAAmB,IAAI;AAAA,MAC5D;AAAA,IACF;AACA,QAAI,SAAS;AACX,cAAQ,YAAY;AACpB,cAAQ,eAAe;AACvB,aAAO,oBAAoB,YAAY,YAAY;AACnD,aAAO,oBAAoB,cAAc,YAAY;AAAA,IACvD;AAAA,EACF;AACF;;;ACvHA,IAAM,iBAAiB;AAIvB,IAAM,wBAAwB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,iBAAiB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,gBAAgB,CAAC,QAAgB,SACrC,KAAK,KAAK,CAAC,WAAW,OAAO,SAAS,MAAM,CAAC;AAE/C,IAAM,gBAAgB,CAAC,QAAoC;AACzD,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE,SAAS,YAAY;AAAA,EAC3C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAYO,SAAS,gBACd,UACA,KACA,iBACQ;AACR,QAAM,iBAAiB,cAAc,QAAQ;AAC7C,QAAM,YAAY,IAAI;AACtB,QAAM,cAAc,QAAQ,cAAc;AAC1C,QAAM,SAAS,OAAO,KAAK,GAAG,EAAE,SAAS;AAEzC,MAAI,CAAC,eAAe,CAAC,OAAQ,QAAO;AACpC,MAAI,aAAa,eAAe,KAAK,SAAS,EAAG,QAAO;AACxD,MAAI,kBAAkB,cAAc,gBAAgB,qBAAqB;AACvE,WAAO;AACT,MAAI,kBAAkB,cAAc,gBAAgB,cAAc,EAAG,QAAO;AAC5E,MAAI,aAAa,UAAU,YAAY,MAAM,QAAS,QAAO;AAC7D,MAAI,kBAAkB,mBAAmB,gBAAgB,YAAY,EAAG,QAAO;AAC/E,SAAO;AACT;;;ACjEA,IAAM,WAA2B;AAAA,EAC/B,EAAE,MAAM,QAAQ,IAAI,yBAAyB;AAAA,EAC7C,EAAE,MAAM,SAAS,IAAI,uBAAuB;AAAA,EAC5C,EAAE,MAAM,oBAAoB,IAAI,wBAAwB;AAAA,EACxD,EAAE,MAAM,WAAW,IAAI,2BAA2B;AAAA,EAClD,EAAE,MAAM,UAAU,IAAI,0BAA0B;AAAA,EAChD,EAAE,MAAM,UAAU,IAAI,yBAAyB;AACjD;AAEA,IAAM,gBAAgB,CAAC,OAAoD;AACzE,aAAW,KAAK,UAAU;AACxB,UAAM,IAAI,GAAG,MAAM,EAAE,EAAE;AACvB,QAAI,EAAG,QAAO,EAAE,MAAM,EAAE,MAAM,SAAS,EAAE,CAAC,EAAE;AAAA,EAC9C;AACA,SAAO,CAAC;AACV;AAIA,IAAM,WAAW,CAAC,OAAmC;AACnD,MAAI,UAAU,KAAK,EAAE,EAAG,QAAO;AAE/B,MAAI,mBAAmB,KAAK,EAAE,EAAG,QAAO;AACxC,MAAI,UAAU,KAAK,EAAE,EAAG,QAAO;AAC/B,MAAI,OAAO,KAAK,EAAE,EAAG,QAAO;AAC5B,MAAI,qBAAqB,KAAK,EAAE,EAAG,QAAO;AAC1C,MAAI,QAAQ,KAAK,EAAE,EAAG,QAAO;AAC7B,SAAO;AACT;AAKA,IAAM,mBAAmB,CAAC,OAAuB;AAC/C,MAAI,OAAO,KAAK,EAAE,EAAG,QAAO;AAC5B,MAAI,UAAU,KAAK,EAAE,EAAG,QAAO,SAAS,KAAK,EAAE,IAAI,WAAW;AAC9D,MAAI,cAAc,KAAK,EAAE,EAAG,QAAO;AACnC,MAAI,SAAS,KAAK,EAAE,EAAG,QAAO;AAC9B,SAAO;AACT;AAMO,SAAS,YACd,WACA,UACwB;AACxB,QAAM,QAAgC,CAAC;AAEvC,QAAM,KAAK,aAAa;AACxB,MAAI,IAAI;AACN,UAAM,UAAU,cAAc,EAAE;AAChC,QAAI,QAAQ,KAAM,OAAM,WAAW,QAAQ;AAC3C,QAAI,QAAQ,QAAS,OAAM,mBAAmB,QAAQ;AAEtD,UAAM,KAAK,SAAS,EAAE;AACtB,QAAI,GAAI,OAAM,MAAM;AAEpB,UAAM,eAAe,iBAAiB,EAAE;AAAA,EAC1C;AAEA,MAAI,SAAU,OAAM,QAAQ;AAE5B,SAAO;AACT;;;ACzBA,IAAM,aAAa,OAAO,OAAO,mBAAmB,CAAC;AAiGrD,IAAM,oBAAoB;AAQ1B,IAAM,mBAAmB,MAAsB;AAC7C,MAAI;AACF,QAAI,OAAO,iBAAiB,YAAa,QAAO;AAChD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AA+BO,SAAS,iBAAiB,MAAkC;AACjE,MAAI,QAAiB,aAAa;AAElC,WAAS,eAAwB;AAC/B,QAAI;AACF,YAAM,MAAM,iBAAiB,GAAG,QAAQ,iBAAiB;AACzD,UAAI,CAAC,IAAK,QAAO,CAAC;AAClB,YAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,UAAI,UAAU,OAAO,WAAW,SAAU,QAAO;AAAA,IACnD,QAAQ;AAAA,IAER;AACA,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,UAAU,CAAC,QAAuB;AACtC,QAAI;AACF,uBAAiB,GAAG,QAAQ,mBAAmB,KAAK,UAAU,GAAG,CAAC;AAAA,IACpE,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI;AAEJ,QAAM,WAAW,MAAc;AAC7B,UAAM,OAAO,KAAK,WAAW,EAAE,QAAQ,QAAQ,EAAE;AACjD,WAAO,GAAG,IAAI;AAAA,EAChB;AAEA,QAAM,kBAAkB,MAAc;AACpC,UAAM,OAAO,KAAK,WAAW,EAAE,QAAQ,QAAQ,EAAE;AACjD,WAAO,GAAG,IAAI;AAAA,EAChB;AAEA,QAAM,aAAa,YAA8B;AAC/C,QAAI;AACF,UAAI,OAAO,UAAU,WAAY,QAAO;AACxC,YAAM,MAAM,MAAM,MAAM,SAAS,GAAG;AAAA,QAClC,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,eAAe,UAAU,KAAK,YAAY,CAAC;AAAA,QAC7C;AAAA,QACA,MAAM,KAAK,UAAU;AAAA,UACnB,aAAa,KAAK,cAAc;AAAA,UAChC,mBAAmB,KAAK,oBAAoB;AAAA,QAC9C,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,GAAI,QAAO;AACpB,YAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,YAAM,QAAQ,MAAM;AACpB,UAAI,SAAS,OAAO,UAAU,UAAU;AACtC,gBAAQ;AACR,gBAAQ,KAAK;AAAA,MACf;AACA,aAAO;AAAA,IACT,QAAQ;AAEN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,4BAA4B,YAA2D;AAC3F,QAAI,UAAU;AACd,QAAI;AAEJ,QAAI;AACF,UAAI,OAAO,UAAU,YAAY;AAC/B,cAAM,MAAM,MAAM,MAAM,gBAAgB,GAAG;AAAA,UACzC,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,eAAe,UAAU,KAAK,YAAY,CAAC;AAAA,UAC7C;AAAA,QACF,CAAC;AACD,YAAI,IAAI,IAAI;AACV,gBAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,uBAAa,MAAM;AACnB,oBAAU,gBAAgB,UAAa,eAAe;AAAA,QACxD;AAAA,MAEF;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,QAAI,CAAC,QAAS,QAAO,EAAE,SAAS,OAAO,OAAO,MAAM;AAEpD,UAAM,QAAQ,MAAM,WAAW;AAC/B,QAAI,eAAe,OAAW,eAAc;AAC5C,WAAO,EAAE,SAAS,MAAM,MAAM;AAAA,EAChC;AAEA,SAAO;AAAA,IACL,UAAU,MAAM;AAAA,IAChB,kBAAkB,CAAC,QAAQ,MAAM,GAAG,GAAG,WAAW;AAAA,IAClD,gBAAgB,CAAC,QAAQ;AACvB,YAAM,IAAI,MAAM,GAAG;AACnB,UAAI,CAAC,GAAG,QAAS,QAAO;AACxB,aAAO,EAAE,WAAW,OAAO,EAAE,UAAU;AAAA,IACzC;AAAA,IACA,uBAAuB,CAAC,QAAQ,MAAM,GAAG,GAAG,WAAW;AAAA,IACvD;AAAA,IACA;AAAA,IACA,OAAO,MAAM;AACX,cAAQ,CAAC;AACT,UAAI;AACF,yBAAiB,GAAG,WAAW,iBAAiB;AAAA,MAClD,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;;;ACrSA,IAAM,UAAU,KAAK,KAAK;AAE1B,IAAM,SAAS,KAAK,KAAK,KAAK;AAG9B,IAAM,UAAU;AAEhB,IAAM,UAAU;AAGhB,IAAM,qBAAqB;AAoC3B,IAAM,QAAQ,CAAC,QAA6C;AAC1D,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,UAAM,IAAI,KAAK,MAAM,GAAG;AACxB,QACE,KACA,OAAO,MAAM,YACb,OAAO,EAAE,OAAO,YAChB,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,SAAS,UAClB;AACA,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAEO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA,gBAAAA;AAAA,EACA;AACF,GAAgC;AAE9B,MAAI,UAAgC;AACpC,MAAI,WAA0B;AAE9B,MAAI,iBAAiB;AAIrB,QAAM,aAAa,MAA4B;AAC7C,QAAI;AACF,aAAO,MAAM,QAAQ,GAAG,QAAQ,OAAO,KAAK,IAAI;AAAA,IAClD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,OAAO,CAAC,MAA2B;AACvC,QAAI;AACF,cAAQ,GAAG,QAAQ,SAAS,KAAK,UAAU,CAAC,CAAC;AAAA,IAC/C,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,WAAW,MAAsB;AACrC,QAAI;AACF,aAAOA,kBAAiB,KAAK;AAAA,IAC/B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,QAA+B;AAC7C,QAAI;AACF,aAAO,SAAS,GAAG,QAAQ,GAAG,KAAK;AAAA,IACrC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,SAAS,CAAC,KAAa,UAAwB;AACnD,QAAI;AACF,eAAS,GAAG,QAAQ,KAAK,KAAK;AAAA,IAChC,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,QAAsB;AACvC,QAAI;AACF,eAAS,GAAG,WAAW,GAAG;AAAA,IAC5B,QAAQ;AAAA,IAER;AAAA,EACF;AAQA,QAAM,cAAc,MAA4B;AAC9C,UAAM,YAAY,WAAW;AAC7B,QAAI,CAAC,UAAW,QAAO;AACvB,QAAI,CAAC,QAAS,QAAO;AACrB,QAAI,UAAU,OAAO,QAAQ,IAAI;AAC/B,aAAO;AAAA,QACL,IAAI,QAAQ;AAAA,QACZ,OAAO,KAAK,IAAI,QAAQ,OAAO,UAAU,KAAK;AAAA,QAC9C,MAAM,KAAK,IAAI,QAAQ,MAAM,UAAU,IAAI;AAAA,MAC7C;AAAA,IACF;AAEA,WAAO,UAAU,QAAQ,QAAQ,OAAO,YAAY;AAAA,EACtD;AAEA,QAAM,eAAe,CAAC,GAAyB,GAAW,aAA+B;AACvF,QAAI,CAAC,EAAG,QAAO;AACf,QAAI,IAAI,EAAE,QAAQ,OAAQ,QAAO;AAEjC,QAAI,SAAU,QAAO;AACrB,WAAO,IAAI,EAAE,OAAO;AAAA,EACtB;AAGA,QAAM,gBAAgB,CAAC,GAAW,aAAqC;AACrE,UAAM,SAAS,YAAY;AAC3B,QAAI,UAAU,CAAC,aAAa,QAAQ,GAAG,QAAQ,EAAG,QAAO;AACzD,WAAO,EAAE,IAAI,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE;AAAA,EAC1C;AAQA,QAAM,kBAAkB,MAAc;AACpC,QAAI,CAAC,gBAAgB;AACnB,uBAAiB;AACjB,YAAM,WAAW,OAAO,OAAO;AAC/B,YAAM,SAAS,OAAO,kBAAkB,MAAM;AAC9C,UAAI,YAAY,CAAC,OAAQ,YAAW;AACpC,aAAO,oBAAoB,GAAG;AAAA,IAChC;AACA,QAAI,CAAC,UAAU;AACb,iBAAW,MAAM;AACjB,aAAO,SAAS,QAAQ;AAAA,IAC1B;AACA,WAAO;AAAA,EACT;AAEA,QAAM,iBAAiB,MAAY;AACjC,eAAW;AACX,cAAU,OAAO;AAAA,EACnB;AAIA,SAAO;AAAA,IACL,aAAa,WAAW,OAAe;AACrC,YAAM,IAAI,IAAI;AACd,YAAM,aAAa,SAAS,MAAM;AAClC,YAAM,SAAS,cAAc,GAAG,QAAQ;AACxC,YAAM,OAAO,WAAW,SAAS,EAAE,GAAG,QAAQ,MAAM,EAAE;AAGtD,UAAI,CAAC,YAAY,KAAK,OAAO,WAAY,MAAK,IAAI;AAClD,gBAAU;AAGV,UAAI,eAAe,QAAQ,KAAK,OAAO,WAAY,gBAAe;AAClE,aAAO,KAAK;AAAA,IACd;AAAA,IAEA,cAAsB;AACpB,aAAO,gBAAgB;AAAA,IACzB;AAAA,IAEA,QAAc;AACZ,gBAAU;AACV,UAAI;AACF,gBAAQ,GAAG,WAAW,OAAO;AAAA,MAC/B,QAAQ;AAAA,MAER;AACA,qBAAe;AAAA,IACjB;AAAA,IAEA,qBAA2B;AACzB,gBAAU,kBAAkB;AAAA,IAC9B;AAAA,EACF;AACF;;;AC/OA,IAAM,aAAa;AACnB,IAAM,YAAY;AAClB,IAAM,oBAAoB;AAE1B,IAAM,gBAAgB;AACtB,IAAM,eAAe;AACrB,IAAM,mBAAmB;AAQzB,IAAM,YAAY,MAAe,OAAO,WAAW;AAE5C,SAAS,gBACd,WACA,aACW;AACX,MAAI,SAAyB,CAAC;AAC9B,MAAI,cAA8B,CAAC;AACnC,MAAI,eAAe;AACnB,MAAI,aAAmD;AACvD,MAAI,WAAkD;AAEtD,QAAM,WAAW,MAAc;AAC7B,UAAM,EAAE,SAAS,IAAI,UAAU;AAC/B,UAAM,OAAO,SAAS,QAAQ,QAAQ,EAAE;AACxC,WAAO,GAAG,IAAI,qBAAqB,mBAAmB,YAAY,CAAC,CAAC;AAAA,EACtE;AAEA,QAAM,YAAY,CAAC,UAAkC,KAAK,UAAU,EAAE,MAAM,CAAC;AAE7E,QAAM,gBAAgB,MAAY;AAChC,QAAI,eAAe,QAAQ,YAAY,WAAW,EAAG;AACrD,UAAM,QAAQ,KAAK,IAAI,gBAAgB,KAAK,cAAc,YAAY;AACtE,QAAI;AACF,mBAAa,WAAW,MAAM;AAC5B,qBAAa;AACb,cAAM,KAAK;AAAA,MACb,GAAG,KAAK;AAAA,IACV,QAAQ;AACN,mBAAa;AAAA,IACf;AAAA,EACF;AAEA,QAAM,UAAU,CAAC,UAAgC;AAE/C,kBAAc,YAAY,OAAO,KAAK;AACtC,QAAI,YAAY,SAAS,kBAAkB;AACzC,oBAAc,YAAY,MAAM,YAAY,SAAS,gBAAgB;AAAA,IACvE;AACA,mBAAe,KAAK,IAAI,eAAe,GAAG,CAAC;AAC3C,kBAAc;AAAA,EAChB;AAEA,QAAM,aAAa,CAAC,KAAa,SAA0B;AACzD,QAAI;AACF,UAAI,OAAO,cAAc,eAAe,OAAO,UAAU,eAAe,YAAY;AAClF,eAAO;AAAA,MACT;AAUA,YAAM,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,aAAa,CAAC;AACpD,aAAO,UAAU,WAAW,KAAK,IAAI;AAAA,IACvC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAOA,QAAM,kBAAkB,OAAO,KAAe,UAAyC;AACrF,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,IAAI,KAAK;AAAA,IAC1B,QAAQ;AAAA,IAER;AACA,YAAQ;AAAA,MACN,wDAAwD,IAAI,MAAM,eAAe,MAAM,MAAM;AAAA,MAC7F;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,CAAC,KAAa,MAAc,UAAgC;AAC5E,QAAI;AACF,UAAI,OAAO,UAAU,YAAY;AAC/B,gBAAQ,KAAK;AACb;AAAA,MACF;AACA,YAAM,KAAK;AAAA,QACT,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C;AAAA,MACF,CAAC,EACE,KAAK,CAAC,QAAQ;AACb,YAAI,IAAI,IAAI;AAEV,yBAAe;AACf,cAAI,YAAY,SAAS,EAAG,eAAc;AAC1C;AAAA,QACF;AACA,YAAI,IAAI,UAAU,KAAK;AAErB,kBAAQ,KAAK;AACb;AAAA,QACF;AAEA,aAAK,gBAAgB,KAAK,KAAK;AAAA,MACjC,CAAC,EACA,MAAM,MAAM;AAEX,gBAAQ,KAAK;AAAA,MACf,CAAC;AAAA,IACL,QAAQ;AACN,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,WAAS,MAAM,YAAY,OAAa;AACtC,QAAI,CAAC,UAAU,GAAG;AAChB,eAAS,CAAC;AACV;AAAA,IACF;AAEA,UAAM,UAAU,YAAY,OAAO,MAAM;AACzC,aAAS,CAAC;AACV,kBAAc,CAAC;AACf,QAAI,QAAQ,WAAW,EAAG;AAE1B,QAAI;AACF,YAAM,MAAM,SAAS;AACrB,eAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,WAAW;AAClD,cAAM,QAAQ,QAAQ,MAAM,GAAG,IAAI,SAAS;AAC5C,cAAM,OAAO,UAAU,KAAK;AAC5B,YAAI,aAAa,WAAW,KAAK,IAAI,EAAG;AACxC,YAAI,WAAW;AAEb,oBAAU,KAAK,MAAM,KAAK;AAC1B;AAAA,QACF;AACA,kBAAU,KAAK,MAAM,KAAK;AAAA,MAC5B;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,qBAAqB,MAAY;AACrC,QAAI;AACF,UAAI,OAAO,aAAa,eAAe,SAAS,OAAQ,OAAM,IAAI;AAAA,IACpE,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,aAAa,MAAY;AAC7B,UAAM,IAAI;AAAA,EACZ;AAEA,QAAM,UAAU,CAAC,UAA8B;AAC7C,QAAI,CAAC,UAAU,EAAG;AAClB,QAAI;AACF,aAAO,KAAK,KAAK;AACjB,UAAI,OAAO,UAAU,WAAY,OAAM,KAAK;AAAA,IAC9C,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,WAAW,MAAY;AAC3B,QAAI;AACF,UAAI,aAAa,MAAM;AACrB,sBAAc,QAAQ;AACtB,mBAAW;AAAA,MACb;AACA,UAAI,eAAe,MAAM;AACvB,qBAAa,UAAU;AACvB,qBAAa;AAAA,MACf;AACA,UAAI,OAAO,aAAa,aAAa;AACnC,iBAAS,oBAAoB,oBAAoB,kBAAkB;AAAA,MACrE;AACA,UAAI,OAAO,WAAW,aAAa;AACjC,eAAO,oBAAoB,YAAY,UAAU;AACjD,eAAO,oBAAoB,gBAAgB,UAAU;AAAA,MACvD;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAGA,MAAI,UAAU,GAAG;AACf,QAAI;AACF,iBAAW,YAAY,MAAM,MAAM,KAAK,GAAG,iBAAiB;AAC5D,UAAI,OAAO,aAAa,aAAa;AACnC,iBAAS,iBAAiB,oBAAoB,kBAAkB;AAAA,MAClE;AACA,aAAO,iBAAiB,YAAY,UAAU;AAC9C,aAAO,iBAAiB,gBAAgB,UAAU;AAAA,IACpD,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,OAAO,SAAS;AACpC;;;AC1NA,IAAM,WAAW,CAAC,UAAU,UAAU,YAAY,QAAQ,SAAS;AAM5D,SAAS,SAAS,QAAwC;AAC/D,QAAM,QAAgC,CAAC;AACvC,MAAI,CAAC,OAAQ,QAAO;AAEpB,MAAI;AACF,UAAM,SAAS,IAAI,gBAAgB,MAAM;AACzC,eAAW,OAAO,UAAU;AAC1B,YAAM,QAAQ,OAAO,IAAI,OAAO,GAAG,EAAE;AACrC,UAAI,MAAO,OAAM,QAAQ,GAAG,EAAE,IAAI;AAAA,IACpC;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;;;AChBO,IAAM,eAAe;AAE5B,IAAM,WAAW;AAEjB,IAAM,cAAc;AAEpB,IAAM,UAAU;AAChB,IAAM,iBAAiB;AAEvB,IAAM,iBAAkC;AAAA;AAAA;AAAA;AAAA,EAItC,UAAU;AAAA,EACV,aAAa;AAAA,EACb,kBAAkB;AACpB;AAEA,IAAMC,aAAY,MAAe,OAAO,WAAW;AAKnD,IAAM,4BAA4B;AAyBlC,IAAM,QAAe;AAAA,EACnB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,UAAU;AAAA,EACV,QAAQ,EAAE,GAAG,eAAe;AAAA,EAC5B,YAAY;AAAA,EACZ,kBAAkB,CAAC;AAAA,EACnB,WAAW;AAAA,EACX,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,qBAAqB;AAAA,EACrB,eAAe,CAAC;AAAA,EAChB,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,kBAAkB,oBAAI,IAAI;AAAA,EAC1B,YAAY;AACd;AAIA,IAAMC,oBAAmB,MAAsB;AAC7C,MAAI;AACF,QAAI,OAAO,iBAAiB,YAAa,QAAO;AAChD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,qBAAqB,MAAsB;AAC/C,MAAI;AACF,QAAI,OAAO,mBAAmB,YAAa,QAAO;AAClD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,aAAa,CAAC,QAA+B;AACjD,MAAI;AACF,WAAOA,kBAAiB,GAAG,QAAQ,GAAG,KAAK;AAAA,EAC7C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAM,aAAa,CAAC,KAAa,UAAwB;AACvD,MAAI;AACF,IAAAA,kBAAiB,GAAG,QAAQ,KAAK,KAAK;AAAA,EACxC,QAAQ;AAAA,EAER;AACF;AAEA,IAAM,gBAAgB,CAAC,QAAsB;AAC3C,MAAI;AACF,IAAAA,kBAAiB,GAAG,WAAW,GAAG;AAAA,EACpC,QAAQ;AAAA,EAER;AACF;AAEA,IAAM,aAAa,MAAc;AAC/B,MAAI;AACF,QAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,aAAO,OAAO,WAAW;AAAA,IAC3B;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO,QAAQ,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACnF;AAEA,IAAM,yBAAyB,MAAc;AAC3C,QAAM,WAAW,WAAW,OAAO;AACnC,MAAI,SAAU,QAAO;AACrB,QAAM,QAAQ,WAAW;AACzB,aAAW,SAAS,KAAK;AACzB,SAAO;AACT;AAIA,IAAM,iBAAiB,MAA+B;AACpD,QAAM,QAAiC;AAAA,IACrC,MAAM;AAAA,IACN,cAAc;AAAA,EAChB;AACA,MAAI;AACF,QAAI,OAAO,aAAa,aAAa;AACnC,YAAM,eAAe,SAAS;AAC9B,YAAM,QAAQ,SAAS;AACvB,YAAM,YAAY,SAAS;AAAA,IAC7B;AACA,QAAI,OAAO,aAAa,aAAa;AACnC,YAAM,YAAY,SAAS;AAAA,IAC7B;AACA,QAAI,OAAO,WAAW,eAAe,OAAO,QAAQ;AAClD,YAAM,iBAAiB,OAAO,OAAO;AACrC,YAAM,gBAAgB,OAAO,OAAO;AAAA,IACtC;AACA,QAAI,OAAO,WAAW,aAAa;AACjC,YAAM,mBAAmB,OAAO;AAChC,YAAM,kBAAkB,OAAO;AAAA,IACjC;AACA,QAAI,OAAO,cAAc,aAAa;AACpC,aAAO,OAAO,OAAO,YAAY,UAAU,WAAW,UAAU,QAAQ,CAAC;AAAA,IAC3E;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAKA,IAAM,2BAA2B,MAA+B;AAC9D,MAAI;AACF,UAAM,SAAS,OAAO,aAAa,cAAc,SAAS,SAAS;AACnE,UAAM,WAAW,OAAO,aAAa,cAAc,SAAS,WAAW;AACvE,UAAM,WAAW,OAAO,aAAa,cAAc,SAAS,WAAW;AACvE,UAAM,MAAM,SAAS,MAAM;AAC3B,WAAO,EAAE,GAAG,KAAK,eAAe,gBAAgB,UAAU,KAAK,QAAQ,EAAE;AAAA,EAC3E,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAIA,IAAM,yBAAyB,CAAC,cAA2D;AACzF,MAAI,CAAC,UAAW,QAAO,yBAAyB;AAChD,MAAI,CAAC,MAAM,kBAAkB,MAAM,eAAe,cAAc,WAAW;AACzE,UAAM,iBAAiB,EAAE,WAAW,OAAO,yBAAyB,EAAE;AAAA,EACxE;AACA,SAAO,MAAM,eAAe;AAC9B;AAIA,IAAM,cAAc,MAAY;AAC9B,MAAI,MAAM,cAAe;AACzB,QAAM,gBAAgB;AACtB,MAAI;AACF,YAAQ,KAAK,2DAA2D;AAAA,EAC1E,QAAQ;AAAA,EAER;AACF;AAGA,IAAM,kBAAkB,CACtB,OACA,YACA,kBACS;AACT,MAAI,CAAC,MAAM,eAAe,CAAC,MAAM,WAAW;AAC1C,gBAAY;AACZ;AAAA,EACF;AACA,MAAI;AACF,UAAM,YAAY,MAAM,SAAS,aAAa;AAC9C,UAAM,eAAe,YACjB,EAAE,aAAa,WAAW,YAAY,MAAM,SAAS,YAAY,EAAE,IACnE,CAAC;AACL,UAAM,UAAwB;AAAA,MAC5B;AAAA,MACA,aAAa,MAAM;AAAA,MACnB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,YAAY;AAAA,QACV,GAAG,eAAe;AAAA,QAClB,GAAG;AAAA,QACH,GAAG,uBAAuB,SAAS;AAAA,QACnC,GAAI,cAAc,CAAC;AAAA,MACrB;AAAA,IACF;AACA,QAAI,cAAe,SAAQ,iBAAiB;AAC5C,UAAM,UAAU,QAAQ,OAAO;AAAA,EACjC,QAAQ;AAAA,EAER;AACF;AAKA,IAAM,cAAc,CAAC,UAAqD;AACxE,QAAM,OAAyC,CAAC;AAChD,aAAW,CAAC,KAAK,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC5C,QAAI,CAAC,EAAE,QAAS;AAChB,SAAK,GAAG,IAAI,EAAE,WAAW,OAAO,EAAE,UAAU;AAAA,EAC9C;AACA,SAAO;AACT;AAEA,IAAM,oBAAoB,MAAY;AACpC,MAAI,CAAC,MAAM,WAAY;AACvB,QAAM,OAAO,YAAY,MAAM,WAAW,SAAS,CAAC;AACpD,aAAW,MAAM,MAAM,eAAe;AACpC,QAAI;AACF,SAAG,IAAI;AAAA,IACT,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAOA,IAAM,eAAe,MAAY;AAC/B,MAAI,CAAC,MAAM,WAAY;AACvB,QAAM,WACH,0BAA0B,EAC1B,KAAK,MAAM;AACV,UAAM,cAAc;AACpB,sBAAkB;AAAA,EACpB,CAAC,EACA,MAAM,MAAM;AAAA,EAEb,CAAC;AACL;AAIA,IAAM,oBAAoB,CAAC,KAAa,aAAqC;AAC3E,MAAI;AAEF,QAAI,CAAC,MAAM,YAAa;AACxB,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,QAAQ,GAAG,GAAG,IAAI,IAAI;AAC5B,QAAI,MAAM,iBAAiB,IAAI,KAAK,EAAG;AACvC,UAAM,iBAAiB,IAAI,KAAK;AAChC,oBAAgB,wBAAwB;AAAA,MACtC,eAAe;AAAA,MACf,wBAAwB;AAAA,IAC1B,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;AAIA,SAAS,KAAK,UAAkB,MAA+B;AAC7D,MAAI,MAAM,YAAa;AACvB,MAAI,CAACD,WAAU,GAAG;AAEhB;AAAA,EACF;AAEA,QAAM,WAAW;AACjB,QAAM,SAAS;AAAA,IACb,UAAU,MAAM,YAAY,eAAe;AAAA,IAC3C,aAAa,MAAM,eAAe,eAAe;AAAA,IACjD,kBAAkB,MAAM,oBAAoB,eAAe;AAAA,EAC7D;AAEA,QAAM,aAAa,uBAAuB;AAG1C,QAAM,UAAU,qBAAqB;AAAA,IACnC,KAAK,MAAM,KAAK,IAAI;AAAA,IACpB,SAASC;AAAA,IACT,gBAAgB;AAAA,IAChB,OAAO;AAAA,EACT,CAAC;AAGD,MAAI;AACF,WAAO,iBAAiB,gBAAgB,MAAM,MAAM,SAAS,mBAAmB,CAAC;AAAA,EACnF,QAAQ;AAAA,EAER;AACA,QAAM,YAAY;AAAA,IAChB,MAAM,MAAM;AAAA,IACZ,MAAM,MAAM;AAAA,EACd;AACA,QAAM,aAAa,iBAAiB;AAAA,IAClC,YAAY,MAAM,MAAM,OAAO;AAAA,IAC/B,aAAa,MAAM,MAAM;AAAA,IACzB,eAAe,MAAM,MAAM;AAAA,IAC3B,qBAAqB,MAAM,MAAM;AAAA,EACnC,CAAC;AACD,QAAM,cAAc;AAGpB,eAAa;AAKb,MAAI,MAAM,4BAA4B;AACpC,UAAM,aAAa,KAAK,IAAI,KAAK,4BAA4B,yBAAyB;AACtF,UAAM,aAAa,YAAY,MAAM,aAAa,GAAG,UAAU;AAAA,EACjE;AAEA,MAAI,MAAM,OAAO,aAAa;AAC5B,UAAM,MAA0B;AAAA,MAC9B,QAAQ,MAAM;AAAA,MACd,SAAS;AAAA,IACX;AACA,QAAI;AACF,YAAM,sBAAsB,gBAAgB,GAAG;AAAA,IACjD,QAAQ;AAAA,IAER;AAAA,EACF,WAAW,MAAM,OAAO,kBAAkB;AAGxC,oBAAgB,WAAW;AAAA,EAC7B;AACF;AAEA,SAAS,QAAQ,OAAe,YAA4C;AAC1E,kBAAgB,OAAO,UAAU;AACnC;AAEA,SAAS,SAAS,YAAoB,aAA6C;AACjF,MAAI,CAAC,MAAM,aAAa;AACtB,gBAAY;AACZ;AAAA,EACF;AACA,QAAM,aAAa,MAAM;AACzB,QAAM,aAAa;AACnB,aAAW,SAAS,UAAU;AAC9B,MAAI,YAAa,OAAM,mBAAmB,EAAE,GAAG,MAAM,kBAAkB,GAAG,YAAY;AACtF,kBAAgB,aAAa;AAAA,IAC3B,MAAM,eAAe,CAAC;AAAA,IACtB,GAAI,eAAe,aAAa,EAAE,mBAAmB,WAAW,IAAI,CAAC;AAAA,EACvE,CAAC;AAED,QAAM,iBAAiB,MAAM;AAE7B,eAAa;AACf;AAEA,SAAS,QAAc;AACrB,MAAI;AAEF,UAAM,KAAKA,kBAAiB;AAC5B,QAAI,IAAI;AACN,YAAM,WAAqB,CAAC;AAC5B,eAAS,IAAI,GAAG,IAAI,GAAG,QAAQ,KAAK,GAAG;AACrC,cAAM,MAAM,GAAG,IAAI,CAAC;AACpB,YAAI,KAAK,WAAW,cAAc,EAAG,UAAS,KAAK,GAAG;AAAA,MACxD;AACA,iBAAW,OAAO,SAAU,eAAc,GAAG;AAAA,IAC/C;AAAA,EACF,QAAQ;AAAA,EAER;AACA,QAAM,aAAa,WAAW;AAC9B,aAAW,SAAS,MAAM,UAAU;AAEpC,QAAM,SAAS,MAAM;AACrB,QAAM,iBAAiB;AAEvB,QAAM,mBAAmB,CAAC;AAC1B,QAAM,cAAc;AACpB,QAAM,iBAAiB,MAAM;AAC7B,QAAM,YAAY,MAAM;AAC1B;AAIA,SAAS,iBAAiB,KAAsB;AAC9C,QAAM,SAAS,MAAM,YAAY,iBAAiB,GAAG,KAAK;AAC1D,oBAAkB,KAAK,MAAM;AAC7B,SAAO;AACT;AAEA,SAAS,eAAe,KAA+B;AACrD,QAAM,SAAS,MAAM,YAAY,eAAe,GAAG,KAAK;AACxD,oBAAkB,KAAK,MAAM;AAC7B,SAAO;AACT;AAEA,SAAS,sBAAsB,KAAsB;AACnD,SAAO,MAAM,YAAY,sBAAsB,GAAG,KAAK;AACzD;AAEA,SAAS,eAAe,IAA6D;AACnF,QAAM,cAAc,KAAK,EAAE;AAE3B,MAAI,MAAM,eAAe,MAAM,YAAY;AACzC,QAAI;AACF,SAAG,YAAY,MAAM,WAAW,SAAS,CAAC,CAAC;AAAA,IAC7C,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAMA,SAAS,sBAA4B;AACnC,eAAa;AACf;AAGA,SAAS,yBAA+B;AACtC,MAAI,MAAM,eAAe,MAAM;AAC7B,kBAAc,MAAM,UAAU;AAC9B,UAAM,aAAa;AAAA,EACrB;AACF;AAIA,IAAM,YAAY;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAeA,IAAO,gBAAQ;",
6
+ "names": ["sessionStorage", "isBrowser", "safeLocalStorage"]
7
7
  }
package/dist/session.d.ts CHANGED
@@ -1,15 +1,24 @@
1
1
  export interface SessionManager {
2
- /** Current session id, rotating first if idle/expired. Bumps last-activity. */
3
- getSessionId(): string;
4
- /** Forget the current session the next getSessionId() starts a fresh one. */
2
+ /** Current session id, rotating first if idle/expired.
3
+ * @param readOnly - a passive read: does not bump last-activity and does not idle-rotate
4
+ * (the MAX_MS cap still applies). Use for reads that are not user activity. */
5
+ getSessionId(readOnly?: boolean): string;
6
+ /** Current window (tab) id. Stable for the life of the tab, fresh in a duplicated tab. */
7
+ getWindowId(): string;
8
+ /** Forget the current session *and* window — the next read starts fresh ones. */
5
9
  reset(): void;
10
+ /** Clear the primary-window flag. Callers wire this to `beforeunload` so that a reload
11
+ * (flag absent) keeps its window id while a tab clone (flag present) gets a new one. */
12
+ clearPrimaryWindow(): void;
6
13
  }
7
14
  export interface SessionDeps {
8
15
  /** Epoch ms clock (injectable for tests). */
9
16
  now: () => number;
10
- /** sessionStorage accessor (SSR-safe; returns null when unavailable). */
17
+ /** localStorage accessor — holds the session id (SSR-safe; returns null when unavailable). */
11
18
  storage: () => Storage | null;
12
- /** New session id generator (shares the SDK's uuid helper). */
19
+ /** sessionStorage accessor — holds the window id. Omit/return null to fall back to memory. */
20
+ sessionStorage?: () => Storage | null;
21
+ /** New id generator (shares the SDK's uuid helper). */
13
22
  genId: () => string;
14
23
  }
15
- export declare function createSessionManager({ now, storage, genId }: SessionDeps): SessionManager;
24
+ export declare function createSessionManager({ now, storage, sessionStorage, genId, }: SessionDeps): SessionManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clickroom/sdk-js",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Browser SDK for Clickroom — product analytics, feature flags and experiments.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/room-co/clickroom/tree/main/packages/sdk-js#readme",