@hifilabs/pixel 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,291 @@
1
+ var BalancePixel = (() => {
2
+ // src/index.ts
3
+ (function() {
4
+ const currentScript = document.currentScript;
5
+ const artistId = currentScript?.dataset.artistId;
6
+ const useEmulator = currentScript?.dataset.emulator === "true";
7
+ const debug = currentScript?.dataset.debug === "true";
8
+ if (!artistId) {
9
+ console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");
10
+ return;
11
+ }
12
+ const SESSION_KEY = "balance_session_id";
13
+ const SESSION_TIMESTAMP_KEY = "balance_session_timestamp";
14
+ const ATTRIBUTION_KEY = "balance_attribution";
15
+ const FAN_ID_KEY = "balance_fan_id_hash";
16
+ const CONSENT_STORAGE_KEY = "balance_consent";
17
+ const SESSION_DURATION = 30 * 60 * 1e3;
18
+ const API_ENDPOINT = useEmulator ? `http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint` : `https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint`;
19
+ let sessionId = null;
20
+ let fanIdHash = null;
21
+ let consent = null;
22
+ let attribution = {};
23
+ let eventQueue = [];
24
+ let flushTimer = null;
25
+ const log = (...args) => {
26
+ if (debug)
27
+ console.log("[BALANCE Pixel]", ...args);
28
+ };
29
+ function generateUUID() {
30
+ if (crypto && crypto.randomUUID) {
31
+ return crypto.randomUUID();
32
+ }
33
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
34
+ const r = Math.random() * 16 | 0;
35
+ const v = c === "x" ? r : r & 3 | 8;
36
+ return v.toString(16);
37
+ });
38
+ }
39
+ function getOrCreateSession() {
40
+ try {
41
+ const stored = localStorage.getItem(SESSION_KEY);
42
+ const timestamp = localStorage.getItem(SESSION_TIMESTAMP_KEY);
43
+ if (stored && timestamp) {
44
+ const age = Date.now() - parseInt(timestamp, 10);
45
+ if (age < SESSION_DURATION) {
46
+ localStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
47
+ return stored;
48
+ }
49
+ }
50
+ const newId = generateUUID();
51
+ localStorage.setItem(SESSION_KEY, newId);
52
+ localStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
53
+ return newId;
54
+ } catch (e) {
55
+ return generateUUID();
56
+ }
57
+ }
58
+ function extractUTM() {
59
+ const params = new URLSearchParams(window.location.search);
60
+ const utm = {};
61
+ ["source", "medium", "campaign", "content", "term"].forEach((param) => {
62
+ const value = params.get(`utm_${param}`);
63
+ if (value)
64
+ utm[`utm_${param}`] = value;
65
+ });
66
+ return utm;
67
+ }
68
+ function captureAttribution() {
69
+ try {
70
+ const stored = localStorage.getItem(ATTRIBUTION_KEY);
71
+ if (stored) {
72
+ attribution = JSON.parse(stored);
73
+ log("Loaded attribution:", attribution);
74
+ return;
75
+ }
76
+ const utm = extractUTM();
77
+ if (Object.keys(utm).length > 0) {
78
+ attribution = utm;
79
+ localStorage.setItem(ATTRIBUTION_KEY, JSON.stringify(utm));
80
+ log("Captured attribution:", attribution);
81
+ }
82
+ } catch (e) {
83
+ }
84
+ }
85
+ function loadFanId() {
86
+ try {
87
+ fanIdHash = localStorage.getItem(FAN_ID_KEY);
88
+ } catch (e) {
89
+ }
90
+ }
91
+ function loadConsent() {
92
+ try {
93
+ const stored = localStorage.getItem(CONSENT_STORAGE_KEY);
94
+ if (stored) {
95
+ const parsed = JSON.parse(stored);
96
+ consent = parsed.preferences || null;
97
+ log("Loaded consent:", consent);
98
+ }
99
+ } catch (e) {
100
+ }
101
+ }
102
+ function setConsent(preferences) {
103
+ const previousConsent = consent;
104
+ consent = preferences;
105
+ try {
106
+ const storage = {
107
+ preferences,
108
+ method: "explicit",
109
+ version: 1
110
+ };
111
+ localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(storage));
112
+ log("Consent saved:", preferences);
113
+ } catch (e) {
114
+ console.error("[BALANCE Pixel] Could not save consent:", e);
115
+ }
116
+ const event = buildEvent({
117
+ event_name: "consent_updated",
118
+ metadata: {
119
+ consent_preferences: preferences,
120
+ consent_method: "explicit",
121
+ previous_consent: previousConsent || void 0
122
+ }
123
+ });
124
+ enqueueEvent(event);
125
+ }
126
+ function getConsent() {
127
+ return consent;
128
+ }
129
+ function hasConsent(type) {
130
+ return consent?.[type] === true;
131
+ }
132
+ async function hashEmail(email) {
133
+ const normalized = email.toLowerCase().trim();
134
+ const encoder = new TextEncoder();
135
+ const data = encoder.encode(normalized);
136
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
137
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
138
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
139
+ }
140
+ function buildEvent(partial) {
141
+ return {
142
+ artist_id: artistId,
143
+ fan_session_id: sessionId,
144
+ fan_id_hash: fanIdHash || void 0,
145
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
146
+ source_url: window.location.href,
147
+ referrer_url: document.referrer || void 0,
148
+ user_agent: navigator.userAgent,
149
+ ...partial,
150
+ ...attribution
151
+ };
152
+ }
153
+ function enqueueEvent(event) {
154
+ const essentialEvents = ["identify", "consent_updated"];
155
+ const isEssential = essentialEvents.includes(event.event_name);
156
+ if (!isEssential && !hasConsent("analytics")) {
157
+ log(`Event '${event.event_name}' blocked - no analytics consent`);
158
+ return;
159
+ }
160
+ eventQueue.push(event);
161
+ log("Event queued:", event.event_name, "(queue:", eventQueue.length, ")");
162
+ if (eventQueue.length >= 10) {
163
+ flush();
164
+ }
165
+ }
166
+ async function flush() {
167
+ if (eventQueue.length === 0)
168
+ return;
169
+ const events = [...eventQueue];
170
+ eventQueue = [];
171
+ log("Flushing", events.length, "events to", API_ENDPOINT);
172
+ try {
173
+ const response = await fetch(API_ENDPOINT, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify({ events }),
177
+ keepalive: true
178
+ // Send even if page is closing
179
+ });
180
+ if (!response.ok) {
181
+ throw new Error(`HTTP ${response.status}`);
182
+ }
183
+ log("Events sent successfully");
184
+ } catch (error) {
185
+ console.error("[BALANCE Pixel] Failed to send events:", error);
186
+ if (eventQueue.length < 50) {
187
+ eventQueue.push(...events);
188
+ }
189
+ }
190
+ }
191
+ function startFlushTimer() {
192
+ if (flushTimer)
193
+ clearInterval(flushTimer);
194
+ flushTimer = window.setInterval(() => {
195
+ if (eventQueue.length > 0)
196
+ flush();
197
+ }, 5e3);
198
+ }
199
+ function trackPageView(options = {}) {
200
+ const event = buildEvent({
201
+ event_name: "pageview",
202
+ page_title: options.title || document.title,
203
+ source_url: options.url || window.location.href
204
+ });
205
+ enqueueEvent(event);
206
+ }
207
+ function track(eventName, properties = {}) {
208
+ const event = buildEvent({
209
+ event_name: "custom",
210
+ metadata: {
211
+ event_type: eventName,
212
+ ...properties
213
+ }
214
+ });
215
+ enqueueEvent(event);
216
+ }
217
+ async function identify(email, traits = {}) {
218
+ try {
219
+ fanIdHash = await hashEmail(email);
220
+ localStorage.setItem(FAN_ID_KEY, fanIdHash);
221
+ log("Fan identified:", fanIdHash);
222
+ const event = buildEvent({
223
+ event_name: "identify",
224
+ fan_id_hash: fanIdHash,
225
+ metadata: {
226
+ email_sha256: fanIdHash,
227
+ traits,
228
+ consent_preferences: consent || void 0
229
+ }
230
+ });
231
+ enqueueEvent(event);
232
+ } catch (error) {
233
+ console.error("[BALANCE Pixel] Failed to identify:", error);
234
+ }
235
+ }
236
+ function purchase(revenue, currency = "USD", properties = {}) {
237
+ const event = buildEvent({
238
+ event_name: "purchase",
239
+ metadata: {
240
+ revenue,
241
+ currency,
242
+ ...properties
243
+ }
244
+ });
245
+ enqueueEvent(event);
246
+ }
247
+ function init() {
248
+ sessionId = getOrCreateSession();
249
+ loadFanId();
250
+ loadConsent();
251
+ captureAttribution();
252
+ startFlushTimer();
253
+ log("Initialized", {
254
+ artistId,
255
+ sessionId,
256
+ fanIdHash,
257
+ consent,
258
+ useEmulator,
259
+ endpoint: API_ENDPOINT
260
+ });
261
+ trackPageView();
262
+ window.addEventListener("beforeunload", () => {
263
+ flush();
264
+ });
265
+ document.addEventListener("visibilitychange", () => {
266
+ if (document.visibilityState === "hidden") {
267
+ flush();
268
+ }
269
+ });
270
+ }
271
+ window.balance = {
272
+ track,
273
+ identify,
274
+ page: trackPageView,
275
+ purchase,
276
+ getSessionId: () => sessionId,
277
+ getFanIdHash: () => fanIdHash,
278
+ getAttribution: () => attribution,
279
+ setConsent,
280
+ getConsent,
281
+ hasConsent
282
+ };
283
+ if (document.readyState === "loading") {
284
+ document.addEventListener("DOMContentLoaded", init);
285
+ } else {
286
+ init();
287
+ }
288
+ log("Pixel script loaded");
289
+ })();
290
+ })();
291
+ //# sourceMappingURL=balance-pixel.js.map
@@ -0,0 +1 @@
1
+ var BalancePixel=(()=>{(function(){let f=document.currentScript,m=f?.dataset.artistId,_=f?.dataset.emulator==="true",C=f?.dataset.debug==="true";if(!m){console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");return}let v="balance_session_id",p="balance_session_timestamp",x="balance_attribution",S="balance_fan_id_hash",I="balance_consent",P=30*60*1e3,h=_?"http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint":"https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint",u=null,s=null,i=null,c={},a=[],y=null,r=(...e)=>{C&&console.log("[BALANCE Pixel]",...e)};function E(){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 N(){try{let e=localStorage.getItem(v),t=localStorage.getItem(p);if(e&&t&&Date.now()-parseInt(t,10)<P)return localStorage.setItem(p,Date.now().toString()),e;let n=E();return localStorage.setItem(v,n),localStorage.setItem(p,Date.now().toString()),n}catch{return E()}}function O(){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 T(){try{let e=localStorage.getItem(x);if(e){c=JSON.parse(e),r("Loaded attribution:",c);return}let t=O();Object.keys(t).length>0&&(c=t,localStorage.setItem(x,JSON.stringify(t)),r("Captured attribution:",c))}catch{}}function L(){try{s=localStorage.getItem(S)}catch{}}function R(){try{let e=localStorage.getItem(I);e&&(i=JSON.parse(e).preferences||null,r("Loaded consent:",i))}catch{}}function D(e){let t=i;i=e;try{let o={preferences:e,method:"explicit",version:1};localStorage.setItem(I,JSON.stringify(o)),r("Consent saved:",e)}catch(o){console.error("[BALANCE Pixel] Could not save consent:",o)}let n=l({event_name:"consent_updated",metadata:{consent_preferences:e,consent_method:"explicit",previous_consent:t||void 0}});d(n)}function U(){return i}function b(e){return i?.[e]===!0}async function k(e){let t=e.toLowerCase().trim(),o=new TextEncoder().encode(t),z=await crypto.subtle.digest("SHA-256",o);return Array.from(new Uint8Array(z)).map(J=>J.toString(16).padStart(2,"0")).join("")}function l(e){return{artist_id:m,fan_session_id:u,fan_id_hash:s||void 0,timestamp:new Date().toISOString(),source_url:window.location.href,referrer_url:document.referrer||void 0,user_agent:navigator.userAgent,...e,...c}}function d(e){if(!["identify","consent_updated"].includes(e.event_name)&&!b("analytics")){r(`Event '${e.event_name}' blocked - no analytics consent`);return}a.push(e),r("Event queued:",e.event_name,"(queue:",a.length,")"),a.length>=10&&g()}async function g(){if(a.length===0)return;let e=[...a];a=[],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){console.error("[BALANCE Pixel] Failed to send events:",t),a.length<50&&a.push(...e)}}function B(){y&&clearInterval(y),y=window.setInterval(()=>{a.length>0&&g()},5e3)}function w(e={}){let t=l({event_name:"pageview",page_title:e.title||document.title,source_url:e.url||window.location.href});d(t)}function F(e,t={}){let n=l({event_name:"custom",metadata:{event_type:e,...t}});d(n)}async function H(e,t={}){try{s=await k(e),localStorage.setItem(S,s),r("Fan identified:",s);let n=l({event_name:"identify",fan_id_hash:s,metadata:{email_sha256:s,traits:t,consent_preferences:i||void 0}});d(n)}catch(n){console.error("[BALANCE Pixel] Failed to identify:",n)}}function M(e,t="USD",n={}){let o=l({event_name:"purchase",metadata:{revenue:e,currency:t,...n}});d(o)}function A(){u=N(),L(),R(),T(),B(),r("Initialized",{artistId:m,sessionId:u,fanIdHash:s,consent:i,useEmulator:_,endpoint:h}),w(),window.addEventListener("beforeunload",()=>{g()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&g()})}window.balance={track:F,identify:H,page:w,purchase:M,getSessionId:()=>u,getFanIdHash:()=>s,getAttribution:()=>c,setConsent:D,getConsent:U,hasConsent:b},document.readyState==="loading"?document.addEventListener("DOMContentLoaded",A):A(),r("Pixel script loaded")})();})();
package/dist/index.js ADDED
@@ -0,0 +1,290 @@
1
+ var BalancePixel = (() => {
2
+ // src/index.ts
3
+ (function() {
4
+ const currentScript = document.currentScript;
5
+ const artistId = currentScript?.dataset.artistId;
6
+ const useEmulator = currentScript?.dataset.emulator === "true";
7
+ const debug = currentScript?.dataset.debug === "true";
8
+ if (!artistId) {
9
+ console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");
10
+ return;
11
+ }
12
+ const SESSION_KEY = "balance_session_id";
13
+ const SESSION_TIMESTAMP_KEY = "balance_session_timestamp";
14
+ const ATTRIBUTION_KEY = "balance_attribution";
15
+ const FAN_ID_KEY = "balance_fan_id_hash";
16
+ const CONSENT_STORAGE_KEY = "balance_consent";
17
+ const SESSION_DURATION = 30 * 60 * 1e3;
18
+ const API_ENDPOINT = useEmulator ? `http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint` : `https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint`;
19
+ let sessionId = null;
20
+ let fanIdHash = null;
21
+ let consent = null;
22
+ let attribution = {};
23
+ let eventQueue = [];
24
+ let flushTimer = null;
25
+ const log = (...args) => {
26
+ if (debug)
27
+ console.log("[BALANCE Pixel]", ...args);
28
+ };
29
+ function generateUUID() {
30
+ if (crypto && crypto.randomUUID) {
31
+ return crypto.randomUUID();
32
+ }
33
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
34
+ const r = Math.random() * 16 | 0;
35
+ const v = c === "x" ? r : r & 3 | 8;
36
+ return v.toString(16);
37
+ });
38
+ }
39
+ function getOrCreateSession() {
40
+ try {
41
+ const stored = localStorage.getItem(SESSION_KEY);
42
+ const timestamp = localStorage.getItem(SESSION_TIMESTAMP_KEY);
43
+ if (stored && timestamp) {
44
+ const age = Date.now() - parseInt(timestamp, 10);
45
+ if (age < SESSION_DURATION) {
46
+ localStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
47
+ return stored;
48
+ }
49
+ }
50
+ const newId = generateUUID();
51
+ localStorage.setItem(SESSION_KEY, newId);
52
+ localStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
53
+ return newId;
54
+ } catch (e) {
55
+ return generateUUID();
56
+ }
57
+ }
58
+ function extractUTM() {
59
+ const params = new URLSearchParams(window.location.search);
60
+ const utm = {};
61
+ ["source", "medium", "campaign", "content", "term"].forEach((param) => {
62
+ const value = params.get(`utm_${param}`);
63
+ if (value)
64
+ utm[`utm_${param}`] = value;
65
+ });
66
+ return utm;
67
+ }
68
+ function captureAttribution() {
69
+ try {
70
+ const stored = localStorage.getItem(ATTRIBUTION_KEY);
71
+ if (stored) {
72
+ attribution = JSON.parse(stored);
73
+ log("Loaded attribution:", attribution);
74
+ return;
75
+ }
76
+ const utm = extractUTM();
77
+ if (Object.keys(utm).length > 0) {
78
+ attribution = utm;
79
+ localStorage.setItem(ATTRIBUTION_KEY, JSON.stringify(utm));
80
+ log("Captured attribution:", attribution);
81
+ }
82
+ } catch (e) {
83
+ }
84
+ }
85
+ function loadFanId() {
86
+ try {
87
+ fanIdHash = localStorage.getItem(FAN_ID_KEY);
88
+ } catch (e) {
89
+ }
90
+ }
91
+ function loadConsent() {
92
+ try {
93
+ const stored = localStorage.getItem(CONSENT_STORAGE_KEY);
94
+ if (stored) {
95
+ const parsed = JSON.parse(stored);
96
+ consent = parsed.preferences || null;
97
+ log("Loaded consent:", consent);
98
+ }
99
+ } catch (e) {
100
+ }
101
+ }
102
+ function setConsent(preferences) {
103
+ const previousConsent = consent;
104
+ consent = preferences;
105
+ try {
106
+ const storage = {
107
+ preferences,
108
+ method: "explicit",
109
+ version: 1
110
+ };
111
+ localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(storage));
112
+ log("Consent saved:", preferences);
113
+ } catch (e) {
114
+ console.error("[BALANCE Pixel] Could not save consent:", e);
115
+ }
116
+ const event = buildEvent({
117
+ event_name: "consent_updated",
118
+ metadata: {
119
+ consent_preferences: preferences,
120
+ consent_method: "explicit",
121
+ previous_consent: previousConsent || void 0
122
+ }
123
+ });
124
+ enqueueEvent(event);
125
+ }
126
+ function getConsent() {
127
+ return consent;
128
+ }
129
+ function hasConsent(type) {
130
+ return consent?.[type] === true;
131
+ }
132
+ async function hashEmail(email) {
133
+ const normalized = email.toLowerCase().trim();
134
+ const encoder = new TextEncoder();
135
+ const data = encoder.encode(normalized);
136
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
137
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
138
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
139
+ }
140
+ function buildEvent(partial) {
141
+ return {
142
+ artist_id: artistId,
143
+ fan_session_id: sessionId,
144
+ fan_id_hash: fanIdHash || void 0,
145
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
146
+ source_url: window.location.href,
147
+ referrer_url: document.referrer || void 0,
148
+ user_agent: navigator.userAgent,
149
+ ...partial,
150
+ ...attribution
151
+ };
152
+ }
153
+ function enqueueEvent(event) {
154
+ const essentialEvents = ["identify", "consent_updated"];
155
+ const isEssential = essentialEvents.includes(event.event_name);
156
+ if (!isEssential && !hasConsent("analytics")) {
157
+ log(`Event '${event.event_name}' blocked - no analytics consent`);
158
+ return;
159
+ }
160
+ eventQueue.push(event);
161
+ log("Event queued:", event.event_name, "(queue:", eventQueue.length, ")");
162
+ if (eventQueue.length >= 10) {
163
+ flush();
164
+ }
165
+ }
166
+ async function flush() {
167
+ if (eventQueue.length === 0)
168
+ return;
169
+ const events = [...eventQueue];
170
+ eventQueue = [];
171
+ log("Flushing", events.length, "events to", API_ENDPOINT);
172
+ try {
173
+ const response = await fetch(API_ENDPOINT, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify({ events }),
177
+ keepalive: true
178
+ // Send even if page is closing
179
+ });
180
+ if (!response.ok) {
181
+ throw new Error(`HTTP ${response.status}`);
182
+ }
183
+ log("Events sent successfully");
184
+ } catch (error) {
185
+ console.error("[BALANCE Pixel] Failed to send events:", error);
186
+ if (eventQueue.length < 50) {
187
+ eventQueue.push(...events);
188
+ }
189
+ }
190
+ }
191
+ function startFlushTimer() {
192
+ if (flushTimer)
193
+ clearInterval(flushTimer);
194
+ flushTimer = window.setInterval(() => {
195
+ if (eventQueue.length > 0)
196
+ flush();
197
+ }, 5e3);
198
+ }
199
+ function trackPageView(options = {}) {
200
+ const event = buildEvent({
201
+ event_name: "pageview",
202
+ page_title: options.title || document.title,
203
+ source_url: options.url || window.location.href
204
+ });
205
+ enqueueEvent(event);
206
+ }
207
+ function track(eventName, properties = {}) {
208
+ const event = buildEvent({
209
+ event_name: "custom",
210
+ metadata: {
211
+ event_type: eventName,
212
+ ...properties
213
+ }
214
+ });
215
+ enqueueEvent(event);
216
+ }
217
+ async function identify(email, traits = {}) {
218
+ try {
219
+ fanIdHash = await hashEmail(email);
220
+ localStorage.setItem(FAN_ID_KEY, fanIdHash);
221
+ log("Fan identified:", fanIdHash);
222
+ const event = buildEvent({
223
+ event_name: "identify",
224
+ fan_id_hash: fanIdHash,
225
+ metadata: {
226
+ email_sha256: fanIdHash,
227
+ traits,
228
+ consent_preferences: consent || void 0
229
+ }
230
+ });
231
+ enqueueEvent(event);
232
+ } catch (error) {
233
+ console.error("[BALANCE Pixel] Failed to identify:", error);
234
+ }
235
+ }
236
+ function purchase(revenue, currency = "USD", properties = {}) {
237
+ const event = buildEvent({
238
+ event_name: "purchase",
239
+ metadata: {
240
+ revenue,
241
+ currency,
242
+ ...properties
243
+ }
244
+ });
245
+ enqueueEvent(event);
246
+ }
247
+ function init() {
248
+ sessionId = getOrCreateSession();
249
+ loadFanId();
250
+ loadConsent();
251
+ captureAttribution();
252
+ startFlushTimer();
253
+ log("Initialized", {
254
+ artistId,
255
+ sessionId,
256
+ fanIdHash,
257
+ consent,
258
+ useEmulator,
259
+ endpoint: API_ENDPOINT
260
+ });
261
+ trackPageView();
262
+ window.addEventListener("beforeunload", () => {
263
+ flush();
264
+ });
265
+ document.addEventListener("visibilitychange", () => {
266
+ if (document.visibilityState === "hidden") {
267
+ flush();
268
+ }
269
+ });
270
+ }
271
+ window.balance = {
272
+ track,
273
+ identify,
274
+ page: trackPageView,
275
+ purchase,
276
+ getSessionId: () => sessionId,
277
+ getFanIdHash: () => fanIdHash,
278
+ getAttribution: () => attribution,
279
+ setConsent,
280
+ getConsent,
281
+ hasConsent
282
+ };
283
+ if (document.readyState === "loading") {
284
+ document.addEventListener("DOMContentLoaded", init);
285
+ } else {
286
+ init();
287
+ }
288
+ log("Pixel script loaded");
289
+ })();
290
+ })();
@@ -0,0 +1 @@
1
+ var BalancePixel=(()=>{(function(){let f=document.currentScript,m=f?.dataset.artistId,_=f?.dataset.emulator==="true",C=f?.dataset.debug==="true";if(!m){console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");return}let v="balance_session_id",p="balance_session_timestamp",x="balance_attribution",S="balance_fan_id_hash",I="balance_consent",P=30*60*1e3,h=_?"http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint":"https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint",u=null,s=null,i=null,c={},a=[],y=null,r=(...e)=>{C&&console.log("[BALANCE Pixel]",...e)};function E(){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 N(){try{let e=localStorage.getItem(v),t=localStorage.getItem(p);if(e&&t&&Date.now()-parseInt(t,10)<P)return localStorage.setItem(p,Date.now().toString()),e;let n=E();return localStorage.setItem(v,n),localStorage.setItem(p,Date.now().toString()),n}catch{return E()}}function O(){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 T(){try{let e=localStorage.getItem(x);if(e){c=JSON.parse(e),r("Loaded attribution:",c);return}let t=O();Object.keys(t).length>0&&(c=t,localStorage.setItem(x,JSON.stringify(t)),r("Captured attribution:",c))}catch{}}function L(){try{s=localStorage.getItem(S)}catch{}}function R(){try{let e=localStorage.getItem(I);e&&(i=JSON.parse(e).preferences||null,r("Loaded consent:",i))}catch{}}function D(e){let t=i;i=e;try{let o={preferences:e,method:"explicit",version:1};localStorage.setItem(I,JSON.stringify(o)),r("Consent saved:",e)}catch(o){console.error("[BALANCE Pixel] Could not save consent:",o)}let n=l({event_name:"consent_updated",metadata:{consent_preferences:e,consent_method:"explicit",previous_consent:t||void 0}});d(n)}function U(){return i}function b(e){return i?.[e]===!0}async function k(e){let t=e.toLowerCase().trim(),o=new TextEncoder().encode(t),z=await crypto.subtle.digest("SHA-256",o);return Array.from(new Uint8Array(z)).map(J=>J.toString(16).padStart(2,"0")).join("")}function l(e){return{artist_id:m,fan_session_id:u,fan_id_hash:s||void 0,timestamp:new Date().toISOString(),source_url:window.location.href,referrer_url:document.referrer||void 0,user_agent:navigator.userAgent,...e,...c}}function d(e){if(!["identify","consent_updated"].includes(e.event_name)&&!b("analytics")){r(`Event '${e.event_name}' blocked - no analytics consent`);return}a.push(e),r("Event queued:",e.event_name,"(queue:",a.length,")"),a.length>=10&&g()}async function g(){if(a.length===0)return;let e=[...a];a=[],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){console.error("[BALANCE Pixel] Failed to send events:",t),a.length<50&&a.push(...e)}}function B(){y&&clearInterval(y),y=window.setInterval(()=>{a.length>0&&g()},5e3)}function w(e={}){let t=l({event_name:"pageview",page_title:e.title||document.title,source_url:e.url||window.location.href});d(t)}function F(e,t={}){let n=l({event_name:"custom",metadata:{event_type:e,...t}});d(n)}async function H(e,t={}){try{s=await k(e),localStorage.setItem(S,s),r("Fan identified:",s);let n=l({event_name:"identify",fan_id_hash:s,metadata:{email_sha256:s,traits:t,consent_preferences:i||void 0}});d(n)}catch(n){console.error("[BALANCE Pixel] Failed to identify:",n)}}function M(e,t="USD",n={}){let o=l({event_name:"purchase",metadata:{revenue:e,currency:t,...n}});d(o)}function A(){u=N(),L(),R(),T(),B(),r("Initialized",{artistId:m,sessionId:u,fanIdHash:s,consent:i,useEmulator:_,endpoint:h}),w(),window.addEventListener("beforeunload",()=>{g()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&g()})}window.balance={track:F,identify:H,page:w,purchase:M,getSessionId:()=>u,getFanIdHash:()=>s,getAttribution:()=>c,setConsent:D,getConsent:U,hasConsent:b},document.readyState==="loading"?document.addEventListener("DOMContentLoaded",A):A(),r("Pixel script loaded")})();})();
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@hifilabs/pixel",
3
+ "version": "0.0.2",
4
+ "description": "BALANCE Pixel - Lightweight browser tracking script for artist fan analytics",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "restricted"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "dev": "esbuild src/index.ts --bundle --outfile=dist/index.js --watch",
15
+ "build": "node build.js"
16
+ },
17
+ "keywords": [
18
+ "analytics",
19
+ "tracking",
20
+ "pixel",
21
+ "balance",
22
+ "artist",
23
+ "fan-analytics",
24
+ "event-tracking",
25
+ "consent-management"
26
+ ],
27
+ "author": "HiFi Labs",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/hifilabs/artist-os-distro"
32
+ },
33
+ "homepage": "https://github.com/hifilabs/artist-os-distro#readme",
34
+ "devDependencies": {
35
+ "esbuild": "^0.19.0",
36
+ "typescript": "^5.4.5"
37
+ }
38
+ }