@hifilabs/pixel 0.6.2 → 0.7.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/browser.js CHANGED
@@ -8,6 +8,7 @@ var BalancePixel = (() => {
8
8
 
9
9
  // src/browser.ts
10
10
  (function() {
11
+ const PIXEL_VERSION = "0.7.0";
11
12
  function parseUserAgent(ua) {
12
13
  let device_type = "desktop";
13
14
  if (/ipad|tablet|android(?!.*mobile)/i.test(ua))
@@ -85,7 +86,7 @@ var BalancePixel = (() => {
85
86
  }
86
87
  let trackingSource = "pixel";
87
88
  if (!artistId) {
88
- console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");
89
+ logError(" Error: data-artist-id attribute is required");
89
90
  return;
90
91
  }
91
92
  const SESSION_KEY = "session_id";
@@ -94,9 +95,11 @@ var BalancePixel = (() => {
94
95
  const FAN_ID_KEY = "fan_id_hash";
95
96
  const VISITOR_ID_KEY = "visitor_id";
96
97
  const CONSENT_STORAGE_KEY = "balance_consent";
98
+ const CONSENT_TTL = 365 * 24 * 60 * 60 * 1e3;
99
+ const CONSENT_REFRESH_THRESHOLD = 30 * 24 * 60 * 60 * 1e3;
97
100
  const SESSION_DURATION = 60 * 60 * 1e3;
98
101
  const STORAGE_PREFIX = "balance_";
99
- const API_ENDPOINT = customEndpoint ? customEndpoint : useEmulator ? `http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint` : `https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint`;
102
+ const API_ENDPOINT = customEndpoint ? customEndpoint : useEmulator ? `http://localhost:5001/artist-os-distro/us-central1/ingestEvents` : `https://us-central1-artist-os-distro.cloudfunctions.net/ingestEvents`;
100
103
  let sessionId = null;
101
104
  let visitorId = null;
102
105
  let fanIdHash = null;
@@ -113,9 +116,14 @@ var BalancePixel = (() => {
113
116
  const IDLE_TIMEOUT = 2 * 60 * 1e3;
114
117
  let lastActivityTime = Date.now();
115
118
  let isIdle = false;
119
+ const SILENT_MODE = !debug;
116
120
  const log = (...args) => {
117
121
  if (debug)
118
- console.log("[BALANCE Pixel]", ...args);
122
+ console.log("[BLN]", ...args);
123
+ };
124
+ const logError = (...args) => {
125
+ if (!SILENT_MODE)
126
+ console.error("[BLN]", ...args);
119
127
  };
120
128
  const CONSENT_STYLES = {
121
129
  base: `
@@ -227,6 +235,9 @@ var BalancePixel = (() => {
227
235
  }
228
236
  hasStoredConsent() {
229
237
  try {
238
+ if (window._balanceConsentNeedsRefresh) {
239
+ return false;
240
+ }
230
241
  return localStorage.getItem(CONSENT_STORAGE_KEY) !== null;
231
242
  } catch {
232
243
  return false;
@@ -269,6 +280,14 @@ var BalancePixel = (() => {
269
280
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
270
281
  });
271
282
  }
283
+ if (accepted && !window._balanceInitialPageviewFired) {
284
+ window._balanceInitialPageviewFired = true;
285
+ if (window.balance?.page) {
286
+ window.balance.page();
287
+ }
288
+ startHeartbeat();
289
+ log("Initial pageview fired after consent granted");
290
+ }
272
291
  this.remove();
273
292
  }
274
293
  remove() {
@@ -338,7 +357,7 @@ var BalancePixel = (() => {
338
357
  currentStorageTier = "local";
339
358
  log(`Storage tier upgraded, migrated ${keysToMigrate.length} items`);
340
359
  } catch (error) {
341
- console.error("[BALANCE Pixel] Storage migration failed:", error);
360
+ logError(" Storage migration failed:", error);
342
361
  }
343
362
  }
344
363
  function generateUUID() {
@@ -438,6 +457,19 @@ var BalancePixel = (() => {
438
457
  const stored = localStorage.getItem(CONSENT_STORAGE_KEY);
439
458
  if (stored) {
440
459
  const parsed = JSON.parse(stored);
460
+ if (parsed.expiresAt && Date.now() > parsed.expiresAt) {
461
+ log("Consent expired - clearing stored consent");
462
+ localStorage.removeItem(CONSENT_STORAGE_KEY);
463
+ consent = null;
464
+ return;
465
+ }
466
+ if (parsed.expiresAt) {
467
+ const timeUntilExpiry = parsed.expiresAt - Date.now();
468
+ if (timeUntilExpiry > 0 && timeUntilExpiry < CONSENT_REFRESH_THRESHOLD) {
469
+ log("Consent nearing expiration - will prompt for refresh");
470
+ window._balanceConsentNeedsRefresh = true;
471
+ }
472
+ }
441
473
  consent = parsed.preferences || null;
442
474
  log("Loaded consent:", consent);
443
475
  }
@@ -451,12 +483,15 @@ var BalancePixel = (() => {
451
483
  const storage = {
452
484
  preferences,
453
485
  method: "explicit",
454
- version: 1
486
+ version: 1,
487
+ expiresAt: Date.now() + CONSENT_TTL
488
+ // 12-month expiration
455
489
  };
456
490
  localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(storage));
457
- log("Consent saved:", preferences);
491
+ log("Consent saved with TTL:", preferences);
492
+ window._balanceConsentNeedsRefresh = false;
458
493
  } catch (e) {
459
- console.error("[BALANCE Pixel] Could not save consent:", e);
494
+ logError(" Could not save consent:", e);
460
495
  }
461
496
  if (preferences.analytics === true) {
462
497
  upgradeStorageTier();
@@ -547,7 +582,7 @@ var BalancePixel = (() => {
547
582
  }
548
583
  log("Events sent successfully");
549
584
  } catch (error) {
550
- console.error("[BALANCE Pixel] Failed to send events:", error);
585
+ logError(" Failed to send events:", error);
551
586
  if (eventQueue.length < 50) {
552
587
  eventQueue.push(...events);
553
588
  }
@@ -683,9 +718,10 @@ var BalancePixel = (() => {
683
718
  event_name: "identify",
684
719
  fan_id_hash: fanIdHash,
685
720
  metadata: {
686
- email,
687
- // Pass email for display_name fallback (extracted prefix only stored)
721
+ // GDPR FIX: Never send plaintext email - only hashed identifier
688
722
  email_sha256: fanIdHash,
723
+ email_display: maskedEmail,
724
+ // Safe for UI display (e.g., "j***@example.com")
689
725
  traits,
690
726
  consent_preferences: consent || void 0,
691
727
  storage_tier: currentStorageTier
@@ -693,7 +729,7 @@ var BalancePixel = (() => {
693
729
  });
694
730
  enqueueEvent(event);
695
731
  } catch (error) {
696
- console.error("[BALANCE Pixel] Failed to identify:", error);
732
+ logError(" Failed to identify:", error);
697
733
  }
698
734
  }
699
735
  function purchase(revenue, currency = "USD", properties = {}) {
@@ -725,14 +761,7 @@ var BalancePixel = (() => {
725
761
  if (consentUIEnabled) {
726
762
  log("Consent UI enabled, waiting for explicit user consent (Tier 0 mode)");
727
763
  } else {
728
- consent = {
729
- analytics: true,
730
- marketing: true,
731
- personalization: true,
732
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
733
- };
734
- log("Default consent enabled (all tracking):", consent);
735
- upgradeStorageTier();
764
+ log("No consent - operating in privacy-first mode (session storage only, limited tracking)");
736
765
  }
737
766
  }
738
767
  if (consent?.analytics && !visitorId) {
@@ -753,9 +782,19 @@ var BalancePixel = (() => {
753
782
  useEmulator,
754
783
  endpoint: API_ENDPOINT
755
784
  });
756
- window._balanceInitialPageviewFired = true;
757
- trackPageView();
758
- startHeartbeat();
785
+ if (consent?.analytics === true) {
786
+ window._balanceInitialPageviewFired = true;
787
+ trackPageView();
788
+ startHeartbeat();
789
+ log("Full tracking enabled (user consented)");
790
+ } else if (consentUIEnabled) {
791
+ log("Tracking dormant - waiting for explicit consent via ConsentManager");
792
+ } else {
793
+ window._balanceInitialPageviewFired = true;
794
+ trackPageView();
795
+ startHeartbeat();
796
+ log("Privacy-first tracking enabled (session-scoped, no persistent IDs)");
797
+ }
759
798
  ["mousemove", "keydown", "scroll", "touchstart"].forEach((eventType) => {
760
799
  document.addEventListener(eventType, resetIdleTimer, { passive: true });
761
800
  });
@@ -772,6 +811,12 @@ var BalancePixel = (() => {
772
811
  }
773
812
  });
774
813
  }
814
+ if (window.balance?._version && window.balance._version !== PIXEL_VERSION) {
815
+ console.warn(
816
+ `[BLN] Version conflict: ${window.balance._version} already loaded, skipping ${PIXEL_VERSION}`
817
+ );
818
+ return;
819
+ }
775
820
  const existingQueue = window.balance?.q || [];
776
821
  function processQueue() {
777
822
  if (existingQueue.length > 0) {
@@ -811,7 +856,9 @@ var BalancePixel = (() => {
811
856
  getAttribution: () => attribution,
812
857
  setConsent,
813
858
  getConsent,
814
- hasConsent
859
+ hasConsent,
860
+ _version: PIXEL_VERSION
861
+ // Expose version for collision detection
815
862
  };
816
863
  function initConsentManager() {
817
864
  if (consentUIEnabled && !consent) {
@@ -822,16 +869,17 @@ var BalancePixel = (() => {
822
869
  });
823
870
  }
824
871
  }
872
+ const scheduleIdleWork = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
825
873
  if (document.readyState === "loading") {
826
874
  document.addEventListener("DOMContentLoaded", () => {
827
875
  init();
828
876
  processQueue();
829
- initConsentManager();
877
+ scheduleIdleWork(() => initConsentManager());
830
878
  });
831
879
  } else {
832
880
  init();
833
881
  processQueue();
834
- initConsentManager();
882
+ scheduleIdleWork(() => initConsentManager());
835
883
  }
836
884
  log("Pixel script loaded");
837
885
  })();
@@ -1,4 +1,4 @@
1
- var BalancePixel=(()=>{var Me=Object.defineProperty;var Be=(f,a,g)=>a in f?Me(f,a,{enumerable:!0,configurable:!0,writable:!0,value:g}):f[a]=g;var T=(f,a,g)=>(Be(f,typeof a!="symbol"?a+"":a,g),g);(function(){function f(e){let t="desktop";/ipad|tablet|android(?!.*mobile)/i.test(e)?t="tablet":/mobile|iphone|android.*mobile|blackberry|iemobile/i.test(e)&&(t="mobile");let n="Unknown";/edg/i.test(e)?n="Edge":/opr|opera/i.test(e)?n="Opera":/firefox/i.test(e)?n="Firefox":/chrome/i.test(e)?n="Chrome":/safari/i.test(e)&&(n="Safari");let o="Unknown";return/iphone|ipad/i.test(e)?o="iOS":/android/i.test(e)?o="Android":/windows/i.test(e)?o="Windows":/mac os/i.test(e)?o="macOS":/linux/i.test(e)?o="Linux":/cros/i.test(e)&&(o="ChromeOS"),{device_type:t,browser:n,os:o}}let a=null;function g(){if(!a)try{a=f(navigator.userAgent)}catch{a={device_type:"desktop",browser:"Unknown",os:"Unknown"}}return a}let s=document.currentScript,A=s?.dataset.artistId,P=s?.dataset.projectId,D=P?fe(P):void 0;function fe(e){return!e||e.startsWith("release_")||e.startsWith("merch_")||e.startsWith("link_")||e.startsWith("custom_")?e:`custom_${e}`}let W=s?.dataset.emulator==="true",ge=s?.dataset.debug==="true",O=parseInt(s?.dataset.heartbeatInterval||"30000",10),q=s?.dataset.heartbeat!=="false",V=s?.dataset.source,Y=s?.dataset.endpoint,J=s?.dataset.consentUi==="true",pe=s?.dataset.consentStyle||"brutalist",me=s?.dataset.primaryColor,he=s?.dataset.bannerPosition||"bottom";function be(){if(V)return V;let e=typeof window.dataLayer<"u"&&Array.isArray(window.dataLayer),t=typeof window.gtag=="function";return e&&t?"gtm":"pixel"}let _="pixel";if(!A){console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");return}let G="session_id",L="session_timestamp",Q="attribution",X="fan_id_hash",N="visitor_id",U="balance_consent",ye=60*60*1e3,p="balance_",R=Y||(W?"http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint":"https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint"),k=null,c=null,l=null,i=null,m={},d=[],M=null,u="session",h=null,ve=0,B=0,b=0,w=!0,we=2*60*1e3,Z=Date.now(),x=!1,r=(...e)=>{ge&&console.log("[BALANCE Pixel]",...e)},H={base:`
1
+ var BalancePixel=(()=>{var ze=Object.defineProperty;var Ve=(l,d,c)=>d in l?ze(l,d,{enumerable:!0,configurable:!0,writable:!0,value:c}):l[d]=c;var A=(l,d,c)=>(Ve(l,typeof d!="symbol"?d+"":d,c),c);(function(){let l="0.7.0";function d(e){let t="desktop";/ipad|tablet|android(?!.*mobile)/i.test(e)?t="tablet":/mobile|iphone|android.*mobile|blackberry|iemobile/i.test(e)&&(t="mobile");let n="Unknown";/edg/i.test(e)?n="Edge":/opr|opera/i.test(e)?n="Opera":/firefox/i.test(e)?n="Firefox":/chrome/i.test(e)?n="Chrome":/safari/i.test(e)&&(n="Safari");let o="Unknown";return/iphone|ipad/i.test(e)?o="iOS":/android/i.test(e)?o="Android":/windows/i.test(e)?o="Windows":/mac os/i.test(e)?o="macOS":/linux/i.test(e)?o="Linux":/cros/i.test(e)&&(o="ChromeOS"),{device_type:t,browser:n,os:o}}let c=null;function be(){if(!c)try{c=d(navigator.userAgent)}catch{c={device_type:"desktop",browser:"Unknown",os:"Unknown"}}return c}let a=document.currentScript,O=a?.dataset.artistId,N=a?.dataset.projectId,L=N?he(N):void 0;function he(e){return!e||e.startsWith("release_")||e.startsWith("merch_")||e.startsWith("link_")||e.startsWith("custom_")?e:`custom_${e}`}let K=a?.dataset.emulator==="true",Y=a?.dataset.debug==="true",R=parseInt(a?.dataset.heartbeatInterval||"30000",10),J=a?.dataset.heartbeat!=="false",G=a?.dataset.source,Q=a?.dataset.endpoint,U=a?.dataset.consentUi==="true",ye=a?.dataset.consentStyle||"brutalist",ve=a?.dataset.primaryColor,we=a?.dataset.bannerPosition||"bottom";function Ie(){if(G)return G;let e=typeof window.dataLayer<"u"&&Array.isArray(window.dataLayer),t=typeof window.gtag=="function";return e&&t?"gtm":"pixel"}let k="pixel";if(!O){x(" Error: data-artist-id attribute is required");return}let X="session_id",M="session_timestamp",Z="attribution",ee="fan_id_hash",F="visitor_id",C="balance_consent",xe=365*24*60*60*1e3,Se=30*24*60*60*1e3,_e=60*60*1e3,p="balance_",H=Q||(K?"http://localhost:5001/artist-os-distro/us-central1/ingestEvents":"https://us-central1-artist-os-distro.cloudfunctions.net/ingestEvents"),E=null,s=null,u=null,i=null,m={},f=[],j=null,g="session",b=null,ke=0,B=0,h=0,w=!0,Ce=2*60*1e3,te=Date.now(),I=!1,Ee=!Y,r=(...e)=>{Y&&console.log("[BLN]",...e)},x=(...e)=>{Ee||console.error("[BLN]",...e)},$={base:`
2
2
  :host {
3
3
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4
4
  position: fixed;
@@ -83,12 +83,12 @@ var BalancePixel=(()=>{var Me=Object.defineProperty;var Be=(f,a,g)=>a in f?Me(f,
83
83
  }
84
84
  .btn.accept { background: #fff; color: #000; border-color: #fff; }
85
85
  .btn:hover { opacity: 0.8; }
86
- `};class xe{constructor(t){T(this,"container",null);T(this,"shadow",null);T(this,"config");if(this.config=t,this.hasStoredConsent()){r("ConsentManager: Consent already exists, not showing banner");return}this.container=document.createElement("div"),this.container.id="balance-consent-manager",this.shadow=this.container.attachShadow({mode:"closed"}),this.render(),document.body.appendChild(this.container),r("ConsentManager: Banner rendered")}hasStoredConsent(){try{return localStorage.getItem(U)!==null}catch{return!1}}render(){if(!this.shadow)return;let t=this.config.style||"brutalist",n=H.base,o=H[t]||H.brutalist,S=this.config.position==="top"?"top: 0;":"bottom: 0;",ue=this.config.primaryColor?`.btn.accept { background: ${this.config.primaryColor} !important; border-color: ${this.config.primaryColor} !important; color: #fff !important; }`:"";this.shadow.innerHTML=`
86
+ `};class Te{constructor(t){A(this,"container",null);A(this,"shadow",null);A(this,"config");if(this.config=t,this.hasStoredConsent()){r("ConsentManager: Consent already exists, not showing banner");return}this.container=document.createElement("div"),this.container.id="balance-consent-manager",this.shadow=this.container.attachShadow({mode:"closed"}),this.render(),document.body.appendChild(this.container),r("ConsentManager: Banner rendered")}hasStoredConsent(){try{return window._balanceConsentNeedsRefresh?!1:localStorage.getItem(C)!==null}catch{return!1}}render(){if(!this.shadow)return;let t=this.config.style||"brutalist",n=$.base,o=$[t]||$.brutalist,_=this.config.position==="top"?"top: 0;":"bottom: 0;",me=this.config.primaryColor?`.btn.accept { background: ${this.config.primaryColor} !important; border-color: ${this.config.primaryColor} !important; color: #fff !important; }`:"";this.shadow.innerHTML=`
87
87
  <style>
88
88
  ${n}
89
- :host { ${S} }
89
+ :host { ${_} }
90
90
  ${o}
91
- ${ue}
91
+ ${me}
92
92
  </style>
93
93
  <div class="banner" role="dialog" aria-label="Cookie consent" aria-modal="false">
94
94
  <p class="text">
@@ -99,4 +99,4 @@ var BalancePixel=(()=>{var Me=Object.defineProperty;var Be=(f,a,g)=>a in f?Me(f,
99
99
  <button id="accept" class="btn accept">Accept</button>
100
100
  </div>
101
101
  </div>
102
- `,this.shadow.getElementById("accept")?.addEventListener("click",()=>this.handleConsent(!0)),this.shadow.getElementById("decline")?.addEventListener("click",()=>this.handleConsent(!1))}handleConsent(t){window.balance?.setConsent&&window.balance.setConsent({analytics:t,marketing:t,personalization:t,timestamp:new Date().toISOString()}),this.remove()}remove(){this.container&&(this.container.remove(),this.container=null,this.shadow=null,r("ConsentManager: Banner removed"))}}function ee(){try{return u==="local"?localStorage:sessionStorage}catch{return null}}function C(e){let t=ee();if(!t)return null;try{let n=p+e,o=t.getItem(n);if(!o&&u==="session")try{o=localStorage.getItem(n)}catch{}return o}catch{return null}}function I(e,t){let n=ee();if(n)try{n.setItem(p+e,t)}catch{}}function j(){if(u!=="local"){r("Upgrading storage tier: session -> local");try{let e=[];for(let t=0;t<sessionStorage.length;t++){let n=sessionStorage.key(t);n?.startsWith(p)&&e.push(n)}for(let t of e){let n=sessionStorage.getItem(t);n&&localStorage.setItem(t,n)}for(let t of e)sessionStorage.removeItem(t);u="local",r(`Storage tier upgraded, migrated ${e.length} items`)}catch(e){console.error("[BALANCE Pixel] Storage migration failed:",e)}}}function F(){return crypto&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function Ie(){try{let e=C(G),t=C(L);if(e&&t&&Date.now()-parseInt(t,10)<ye)return I(L,Date.now().toString()),e;let n=F();return I(G,n),I(L,Date.now().toString()),n}catch{return F()}}function Se(){let e=new URLSearchParams(window.location.search),t={};return["source","medium","campaign","content","term"].forEach(n=>{let o=e.get(`utm_${n}`);o&&(t[`utm_${n}`]=o)}),t}function _e(){try{let e=C(Q);if(e){m=JSON.parse(e),r("Loaded attribution:",m);return}let t=Se();Object.keys(t).length>0&&(m=t,I(Q,JSON.stringify(t)),r("Captured attribution:",m))}catch{}}function ke(){try{l=C(X)}catch{}}function te(){if(!i?.analytics)return null;try{let e=localStorage.getItem(p+N);if(e)return e;let t=F();return localStorage.setItem(p+N,t),r("Created persistent visitor ID:",t.substring(0,8)+"..."),t}catch{return null}}function Ce(){if(!i?.analytics){c=null;return}try{c=localStorage.getItem(p+N),c&&r("Loaded visitor ID:",c.substring(0,8)+"...")}catch{}}function Ee(){try{let e=localStorage.getItem(U);e&&(i=JSON.parse(e).preferences||null,r("Loaded consent:",i))}catch{}}function ne(e){let t=i;i=e;try{let o={preferences:e,method:"explicit",version:1};localStorage.setItem(U,JSON.stringify(o)),r("Consent saved:",e)}catch(o){console.error("[BALANCE Pixel] Could not save consent:",o)}e.analytics===!0&&(j(),c||(c=te()));let n=y({event_name:"consent_updated",metadata:{consent_preferences:e,consent_method:"explicit",previous_consent:t||void 0}});v(n);try{window.dispatchEvent(new CustomEvent("balance:consent:updated",{detail:e})),r("DOM event balance:consent:updated dispatched")}catch{}}function Te(){return i}function Ae(e){return i?.[e]===!0}async function Pe(e){let t=e.toLowerCase().trim(),o=new TextEncoder().encode(t),S=await crypto.subtle.digest("SHA-256",o);return Array.from(new Uint8Array(S)).map(Re=>Re.toString(16).padStart(2,"0")).join("")}function y(e){let t=g(),n={artist_id:A,fan_session_id:k,visitor_id:c||void 0,fan_id_hash:l||void 0,timestamp:new Date().toISOString(),source_url:window.location.href,referrer_url:document.referrer||void 0,user_agent:navigator.userAgent,device_type:t.device_type,browser:t.browser,os:t.os,tracking_source:_,...e,...m};return D&&!e.projectId&&(n.projectId=D),n}function v(e){d.push(e),r("Event queued:",e.event_name,"(queue:",d.length,")"),d.length>=10&&E()}async function E(){if(d.length===0)return;let e=[...d];d=[],r("Flushing",e.length,"events to",R);try{let t=await fetch(R,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0});if(!t.ok)throw new Error(`HTTP ${t.status}`);r("Events sent successfully")}catch(t){console.error("[BALANCE Pixel] Failed to send events:",t),d.length<50&&d.push(...e)}}function De(){M&&clearInterval(M),M=window.setInterval(()=>{d.length>0&&E()},5e3)}function z(){b||(ve=Date.now()),b=Date.now(),w=!0,r("Active time tracking started/resumed")}function re(){b&&w&&(B+=Date.now()-b),w=!1,r("Active time tracking paused, accumulated:",B,"ms")}function Oe(){let e=B;return w&&b&&(e+=Date.now()-b),e}function Le(){Z=Date.now(),x&&(x=!1,z(),r("User returned from idle"))}function oe(){if(document.visibilityState==="hidden"){r("Skipping heartbeat - tab hidden");return}if(Date.now()-Z>we){x||(x=!0,re(),r("User idle - pausing heartbeat"));return}let e=Oe(),t=Math.round(e/1e3),n=y({event_name:"engagement_heartbeat",metadata:{time_on_page_seconds:t,time_on_page_ms:e,heartbeat_interval_ms:O,is_active:w&&!x}});v(n),r("Heartbeat sent:",t,"seconds active")}function Ne(){if(!q){r('Heartbeat disabled via data-heartbeat="false"');return}h&&clearInterval(h),z(),h=window.setInterval(()=>{oe()},O),r("Heartbeat started with interval:",O,"ms")}function Ue(){h&&(clearInterval(h),h=null),q&&(oe(),r("Heartbeat stopped, final time sent"))}function $(e={}){let t=y({event_name:"pageview",page_title:e.title||document.title,source_url:e.url||window.location.href});v(t)}function ie(e,t={}){let n=y({event_name:"custom",metadata:{event_type:e,...t}});v(n)}async function ae(e,t={}){try{if(i&&i.analytics===!1){r("Identify skipped - user declined analytics consent");return}l=await Pe(e),i?.analytics===!0&&j(),I(X,l);let n=e.split("@"),o=n[0].charAt(0)+"***@"+(n[1]||"");r("Fan identified:",{name:t.name||"(no name)",email:o,hash:l.substring(0,16)+"...",traits:t,storageTier:u});let S=y({event_name:"identify",fan_id_hash:l,metadata:{email:e,email_sha256:l,traits:t,consent_preferences:i||void 0,storage_tier:u}});v(S)}catch(n){console.error("[BALANCE Pixel] Failed to identify:",n)}}function se(e,t="USD",n={}){let o=y({event_name:"purchase",metadata:{revenue:e,currency:t,...n}});v(o)}function ce(){_=be(),r("Tracking source detected:",_),Ee(),i?.analytics===!0?(u="local",r("Storage tier: local (analytics consent granted)")):(u="session",r("Storage tier: session (privacy by default)")),k=Ie(),ke(),Ce(),i||(J?r("Consent UI enabled, waiting for explicit user consent (Tier 0 mode)"):(i={analytics:!0,marketing:!0,personalization:!0,timestamp:new Date().toISOString()},r("Default consent enabled (all tracking):",i),j())),i?.analytics&&!c&&(c=te()),_e(),De(),r("Initialized",{artistId:A,projectId:D||"(none - will track to all projects)",rawProjectId:P||"(none)",sessionId:k,visitorId:c?c.substring(0,8)+"...":null,fanIdHash:l,consent:i,storageTier:u,trackingSource:_,useEmulator:W,endpoint:R}),window._balanceInitialPageviewFired=!0,$(),Ne(),["mousemove","keydown","scroll","touchstart"].forEach(e=>{document.addEventListener(e,Le,{passive:!0})}),window.addEventListener("beforeunload",()=>{Ue(),E()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(re(),E()):z()})}let K=window.balance?.q||[];function le(){if(K.length>0){r("Processing",K.length,"queued commands");for(let e of K){let[t,...n]=e;switch(t){case"track":ie(n[0],n[1]);break;case"identify":ae(n[0],n[1]);break;case"page":$(n[0]);break;case"purchase":se(n[0],n[1],n[2]);break;case"setConsent":ne(n[0]);break;default:r("Unknown queued command:",t)}}}}window.balance={track:ie,identify:ae,page:$,purchase:se,getSessionId:()=>k,getVisitorId:()=>c,getFanIdHash:()=>l,getAttribution:()=>m,setConsent:ne,getConsent:Te,hasConsent:Ae};function de(){J&&!i&&new xe({style:pe,primaryColor:me,position:he})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{ce(),le(),de()}):(ce(),le(),de()),r("Pixel script loaded")})();})();
102
+ `,this.shadow.getElementById("accept")?.addEventListener("click",()=>this.handleConsent(!0)),this.shadow.getElementById("decline")?.addEventListener("click",()=>this.handleConsent(!1))}handleConsent(t){window.balance?.setConsent&&window.balance.setConsent({analytics:t,marketing:t,personalization:t,timestamp:new Date().toISOString()}),t&&!window._balanceInitialPageviewFired&&(window._balanceInitialPageviewFired=!0,window.balance?.page&&window.balance.page(),W(),r("Initial pageview fired after consent granted")),this.remove()}remove(){this.container&&(this.container.remove(),this.container=null,this.shadow=null,r("ConsentManager: Banner removed"))}}function ne(){try{return g==="local"?localStorage:sessionStorage}catch{return null}}function T(e){let t=ne();if(!t)return null;try{let n=p+e,o=t.getItem(n);if(!o&&g==="session")try{o=localStorage.getItem(n)}catch{}return o}catch{return null}}function S(e,t){let n=ne();if(n)try{n.setItem(p+e,t)}catch{}}function re(){if(g!=="local"){r("Upgrading storage tier: session -> local");try{let e=[];for(let t=0;t<sessionStorage.length;t++){let n=sessionStorage.key(t);n?.startsWith(p)&&e.push(n)}for(let t of e){let n=sessionStorage.getItem(t);n&&localStorage.setItem(t,n)}for(let t of e)sessionStorage.removeItem(t);g="local",r(`Storage tier upgraded, migrated ${e.length} items`)}catch(e){x(" Storage migration failed:",e)}}}function z(){return crypto&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function De(){try{let e=T(X),t=T(M);if(e&&t&&Date.now()-parseInt(t,10)<_e)return S(M,Date.now().toString()),e;let n=z();return S(X,n),S(M,Date.now().toString()),n}catch{return z()}}function Pe(){let e=new URLSearchParams(window.location.search),t={};return["source","medium","campaign","content","term"].forEach(n=>{let o=e.get(`utm_${n}`);o&&(t[`utm_${n}`]=o)}),t}function Ae(){try{let e=T(Z);if(e){m=JSON.parse(e),r("Loaded attribution:",m);return}let t=Pe();Object.keys(t).length>0&&(m=t,S(Z,JSON.stringify(t)),r("Captured attribution:",m))}catch{}}function Oe(){try{u=T(ee)}catch{}}function oe(){if(!i?.analytics)return null;try{let e=localStorage.getItem(p+F);if(e)return e;let t=z();return localStorage.setItem(p+F,t),r("Created persistent visitor ID:",t.substring(0,8)+"..."),t}catch{return null}}function Ne(){if(!i?.analytics){s=null;return}try{s=localStorage.getItem(p+F),s&&r("Loaded visitor ID:",s.substring(0,8)+"...")}catch{}}function Le(){try{let e=localStorage.getItem(C);if(e){let t=JSON.parse(e);if(t.expiresAt&&Date.now()>t.expiresAt){r("Consent expired - clearing stored consent"),localStorage.removeItem(C),i=null;return}if(t.expiresAt){let n=t.expiresAt-Date.now();n>0&&n<Se&&(r("Consent nearing expiration - will prompt for refresh"),window._balanceConsentNeedsRefresh=!0)}i=t.preferences||null,r("Loaded consent:",i)}}catch{}}function ie(e){let t=i;i=e;try{let o={preferences:e,method:"explicit",version:1,expiresAt:Date.now()+xe};localStorage.setItem(C,JSON.stringify(o)),r("Consent saved with TTL:",e),window._balanceConsentNeedsRefresh=!1}catch(o){x(" Could not save consent:",o)}e.analytics===!0&&(re(),s||(s=oe()));let n=y({event_name:"consent_updated",metadata:{consent_preferences:e,consent_method:"explicit",previous_consent:t||void 0}});v(n);try{window.dispatchEvent(new CustomEvent("balance:consent:updated",{detail:e})),r("DOM event balance:consent:updated dispatched")}catch{}}function Re(){return i}function Ue(e){return i?.[e]===!0}async function Me(e){let t=e.toLowerCase().trim(),o=new TextEncoder().encode(t),_=await crypto.subtle.digest("SHA-256",o);return Array.from(new Uint8Array(_)).map($e=>$e.toString(16).padStart(2,"0")).join("")}function y(e){let t=be(),n={artist_id:O,fan_session_id:E,visitor_id:s||void 0,fan_id_hash:u||void 0,timestamp:new Date().toISOString(),source_url:window.location.href,referrer_url:document.referrer||void 0,user_agent:navigator.userAgent,device_type:t.device_type,browser:t.browser,os:t.os,tracking_source:k,...e,...m};return L&&!e.projectId&&(n.projectId=L),n}function v(e){f.push(e),r("Event queued:",e.event_name,"(queue:",f.length,")"),f.length>=10&&D()}async function D(){if(f.length===0)return;let e=[...f];f=[],r("Flushing",e.length,"events to",H);try{let t=await fetch(H,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0});if(!t.ok)throw new Error(`HTTP ${t.status}`);r("Events sent successfully")}catch(t){x(" Failed to send events:",t),f.length<50&&f.push(...e)}}function Fe(){j&&clearInterval(j),j=window.setInterval(()=>{f.length>0&&D()},5e3)}function V(){h||(ke=Date.now()),h=Date.now(),w=!0,r("Active time tracking started/resumed")}function ae(){h&&w&&(B+=Date.now()-h),w=!1,r("Active time tracking paused, accumulated:",B,"ms")}function He(){let e=B;return w&&h&&(e+=Date.now()-h),e}function je(){te=Date.now(),I&&(I=!1,V(),r("User returned from idle"))}function se(){if(document.visibilityState==="hidden"){r("Skipping heartbeat - tab hidden");return}if(Date.now()-te>Ce){I||(I=!0,ae(),r("User idle - pausing heartbeat"));return}let e=He(),t=Math.round(e/1e3),n=y({event_name:"engagement_heartbeat",metadata:{time_on_page_seconds:t,time_on_page_ms:e,heartbeat_interval_ms:R,is_active:w&&!I}});v(n),r("Heartbeat sent:",t,"seconds active")}function W(){if(!J){r('Heartbeat disabled via data-heartbeat="false"');return}b&&clearInterval(b),V(),b=window.setInterval(()=>{se()},R),r("Heartbeat started with interval:",R,"ms")}function Be(){b&&(clearInterval(b),b=null),J&&(se(),r("Heartbeat stopped, final time sent"))}function P(e={}){let t=y({event_name:"pageview",page_title:e.title||document.title,source_url:e.url||window.location.href});v(t)}function ce(e,t={}){let n=y({event_name:"custom",metadata:{event_type:e,...t}});v(n)}async function le(e,t={}){try{if(i&&i.analytics===!1){r("Identify skipped - user declined analytics consent");return}u=await Me(e),i?.analytics===!0&&re(),S(ee,u);let n=e.split("@"),o=n[0].charAt(0)+"***@"+(n[1]||"");r("Fan identified:",{name:t.name||"(no name)",email:o,hash:u.substring(0,16)+"...",traits:t,storageTier:g});let _=y({event_name:"identify",fan_id_hash:u,metadata:{email_sha256:u,email_display:o,traits:t,consent_preferences:i||void 0,storage_tier:g}});v(_)}catch(n){x(" Failed to identify:",n)}}function de(e,t="USD",n={}){let o=y({event_name:"purchase",metadata:{revenue:e,currency:t,...n}});v(o)}function ue(){k=Ie(),r("Tracking source detected:",k),Le(),i?.analytics===!0?(g="local",r("Storage tier: local (analytics consent granted)")):(g="session",r("Storage tier: session (privacy by default)")),E=De(),Oe(),Ne(),i||r(U?"Consent UI enabled, waiting for explicit user consent (Tier 0 mode)":"No consent - operating in privacy-first mode (session storage only, limited tracking)"),i?.analytics&&!s&&(s=oe()),Ae(),Fe(),r("Initialized",{artistId:O,projectId:L||"(none - will track to all projects)",rawProjectId:N||"(none)",sessionId:E,visitorId:s?s.substring(0,8)+"...":null,fanIdHash:u,consent:i,storageTier:g,trackingSource:k,useEmulator:K,endpoint:H}),i?.analytics===!0?(window._balanceInitialPageviewFired=!0,P(),W(),r("Full tracking enabled (user consented)")):U?r("Tracking dormant - waiting for explicit consent via ConsentManager"):(window._balanceInitialPageviewFired=!0,P(),W(),r("Privacy-first tracking enabled (session-scoped, no persistent IDs)")),["mousemove","keydown","scroll","touchstart"].forEach(e=>{document.addEventListener(e,je,{passive:!0})}),window.addEventListener("beforeunload",()=>{Be(),D()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(ae(),D()):V()})}if(window.balance?._version&&window.balance._version!==l){console.warn(`[BLN] Version conflict: ${window.balance._version} already loaded, skipping ${l}`);return}let q=window.balance?.q||[];function fe(){if(q.length>0){r("Processing",q.length,"queued commands");for(let e of q){let[t,...n]=e;switch(t){case"track":ce(n[0],n[1]);break;case"identify":le(n[0],n[1]);break;case"page":P(n[0]);break;case"purchase":de(n[0],n[1],n[2]);break;case"setConsent":ie(n[0]);break;default:r("Unknown queued command:",t)}}}}window.balance={track:ce,identify:le,page:P,purchase:de,getSessionId:()=>E,getVisitorId:()=>s,getFanIdHash:()=>u,getAttribution:()=>m,setConsent:ie,getConsent:Re,hasConsent:Ue,_version:l};function ge(){U&&!i&&new Te({style:ye,primaryColor:ve,position:we})}let pe=window.requestIdleCallback||(e=>setTimeout(e,1));document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{ue(),fe(),pe(()=>ge())}):(ue(),fe(),pe(()=>ge())),r("Pixel script loaded")})();})();
package/dist/index.js CHANGED
@@ -2880,6 +2880,17 @@ var import_react23 = require("react");
2880
2880
 
2881
2881
  // src/storage/StorageManager.ts
2882
2882
  var DEFAULT_PREFIX = "balance_";
2883
+ var TTL_DURATIONS = {
2884
+ /** Consent should be refreshed annually per GDPR best practices */
2885
+ CONSENT: 365 * 24 * 60 * 60 * 1e3,
2886
+ // 12 months
2887
+ /** Session IDs expire with the session anyway */
2888
+ SESSION: 30 * 60 * 1e3,
2889
+ // 30 minutes
2890
+ /** Visitor IDs can persist longer for returning user detection */
2891
+ VISITOR: 90 * 24 * 60 * 60 * 1e3
2892
+ // 90 days
2893
+ };
2883
2894
  var StorageManager = class {
2884
2895
  constructor(config) {
2885
2896
  __publicField(this, "prefix");
@@ -3061,6 +3072,80 @@ var StorageManager = class {
3061
3072
  } catch {
3062
3073
  }
3063
3074
  }
3075
+ /**
3076
+ * Set item with TTL (Time To Live)
3077
+ * The item will automatically expire after the specified duration
3078
+ */
3079
+ setItemWithTTL(key, value, ttlMs) {
3080
+ const ttlWrapper = {
3081
+ value,
3082
+ expiresAt: Date.now() + ttlMs
3083
+ };
3084
+ this.setItem(key, JSON.stringify(ttlWrapper));
3085
+ }
3086
+ /**
3087
+ * Get item with TTL check
3088
+ * Returns null if the item has expired (and removes it)
3089
+ */
3090
+ getItemWithTTL(key) {
3091
+ const raw = this.getItem(key);
3092
+ if (!raw)
3093
+ return null;
3094
+ try {
3095
+ const parsed = JSON.parse(raw);
3096
+ if (typeof parsed === "object" && "expiresAt" in parsed && "value" in parsed) {
3097
+ if (Date.now() > parsed.expiresAt) {
3098
+ this.removeItem(key);
3099
+ return null;
3100
+ }
3101
+ return parsed.value;
3102
+ }
3103
+ return raw;
3104
+ } catch {
3105
+ return raw;
3106
+ }
3107
+ }
3108
+ /**
3109
+ * Set JSON item with TTL
3110
+ */
3111
+ setJSONWithTTL(key, value, ttlMs) {
3112
+ try {
3113
+ this.setItemWithTTL(key, JSON.stringify(value), ttlMs);
3114
+ } catch {
3115
+ }
3116
+ }
3117
+ /**
3118
+ * Get JSON item with TTL check
3119
+ */
3120
+ getJSONWithTTL(key) {
3121
+ const value = this.getItemWithTTL(key);
3122
+ if (!value)
3123
+ return null;
3124
+ try {
3125
+ return JSON.parse(value);
3126
+ } catch {
3127
+ return null;
3128
+ }
3129
+ }
3130
+ /**
3131
+ * Check if an item's TTL needs refresh (e.g., consent nearing expiration)
3132
+ * Returns true if the item will expire within the threshold
3133
+ */
3134
+ needsTTLRefresh(key, refreshThresholdMs = 30 * 24 * 60 * 60 * 1e3) {
3135
+ const raw = this.getItem(key);
3136
+ if (!raw)
3137
+ return false;
3138
+ try {
3139
+ const parsed = JSON.parse(raw);
3140
+ if (typeof parsed === "object" && "expiresAt" in parsed) {
3141
+ const timeUntilExpiry = parsed.expiresAt - Date.now();
3142
+ return timeUntilExpiry > 0 && timeUntilExpiry < refreshThresholdMs;
3143
+ }
3144
+ return false;
3145
+ } catch {
3146
+ return false;
3147
+ }
3148
+ }
3064
3149
  };
3065
3150
  var storageManagerInstance = null;
3066
3151
  function getStorageManager() {
package/dist/index.mjs CHANGED
@@ -2811,6 +2811,17 @@ import { useCallback as useCallback16, useEffect as useEffect9, useRef as useRef
2811
2811
 
2812
2812
  // src/storage/StorageManager.ts
2813
2813
  var DEFAULT_PREFIX = "balance_";
2814
+ var TTL_DURATIONS = {
2815
+ /** Consent should be refreshed annually per GDPR best practices */
2816
+ CONSENT: 365 * 24 * 60 * 60 * 1e3,
2817
+ // 12 months
2818
+ /** Session IDs expire with the session anyway */
2819
+ SESSION: 30 * 60 * 1e3,
2820
+ // 30 minutes
2821
+ /** Visitor IDs can persist longer for returning user detection */
2822
+ VISITOR: 90 * 24 * 60 * 60 * 1e3
2823
+ // 90 days
2824
+ };
2814
2825
  var StorageManager = class {
2815
2826
  constructor(config) {
2816
2827
  __publicField(this, "prefix");
@@ -2992,6 +3003,80 @@ var StorageManager = class {
2992
3003
  } catch {
2993
3004
  }
2994
3005
  }
3006
+ /**
3007
+ * Set item with TTL (Time To Live)
3008
+ * The item will automatically expire after the specified duration
3009
+ */
3010
+ setItemWithTTL(key, value, ttlMs) {
3011
+ const ttlWrapper = {
3012
+ value,
3013
+ expiresAt: Date.now() + ttlMs
3014
+ };
3015
+ this.setItem(key, JSON.stringify(ttlWrapper));
3016
+ }
3017
+ /**
3018
+ * Get item with TTL check
3019
+ * Returns null if the item has expired (and removes it)
3020
+ */
3021
+ getItemWithTTL(key) {
3022
+ const raw = this.getItem(key);
3023
+ if (!raw)
3024
+ return null;
3025
+ try {
3026
+ const parsed = JSON.parse(raw);
3027
+ if (typeof parsed === "object" && "expiresAt" in parsed && "value" in parsed) {
3028
+ if (Date.now() > parsed.expiresAt) {
3029
+ this.removeItem(key);
3030
+ return null;
3031
+ }
3032
+ return parsed.value;
3033
+ }
3034
+ return raw;
3035
+ } catch {
3036
+ return raw;
3037
+ }
3038
+ }
3039
+ /**
3040
+ * Set JSON item with TTL
3041
+ */
3042
+ setJSONWithTTL(key, value, ttlMs) {
3043
+ try {
3044
+ this.setItemWithTTL(key, JSON.stringify(value), ttlMs);
3045
+ } catch {
3046
+ }
3047
+ }
3048
+ /**
3049
+ * Get JSON item with TTL check
3050
+ */
3051
+ getJSONWithTTL(key) {
3052
+ const value = this.getItemWithTTL(key);
3053
+ if (!value)
3054
+ return null;
3055
+ try {
3056
+ return JSON.parse(value);
3057
+ } catch {
3058
+ return null;
3059
+ }
3060
+ }
3061
+ /**
3062
+ * Check if an item's TTL needs refresh (e.g., consent nearing expiration)
3063
+ * Returns true if the item will expire within the threshold
3064
+ */
3065
+ needsTTLRefresh(key, refreshThresholdMs = 30 * 24 * 60 * 60 * 1e3) {
3066
+ const raw = this.getItem(key);
3067
+ if (!raw)
3068
+ return false;
3069
+ try {
3070
+ const parsed = JSON.parse(raw);
3071
+ if (typeof parsed === "object" && "expiresAt" in parsed) {
3072
+ const timeUntilExpiry = parsed.expiresAt - Date.now();
3073
+ return timeUntilExpiry > 0 && timeUntilExpiry < refreshThresholdMs;
3074
+ }
3075
+ return false;
3076
+ } catch {
3077
+ return false;
3078
+ }
3079
+ }
2995
3080
  };
2996
3081
  var storageManagerInstance = null;
2997
3082
  function getStorageManager() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hifilabs/pixel",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "BALANCE Pixel - Lightweight browser tracking script for artist fan analytics",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",