@hifilabs/pixel 0.0.8 → 0.0.10

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.
@@ -18,3 +18,43 @@ export declare const setConsent: (preferences: {
18
18
  }) => void;
19
19
  export declare const getConsent: () => any;
20
20
  export declare const hasConsent: (type: 'analytics' | 'marketing' | 'personalization') => boolean;
21
+
22
+ // useBalanceIdentify hook - consent-aware identify wrapper
23
+ export declare function useBalanceIdentify(): {
24
+ /**
25
+ * Identify a user with BALANCE pixel.
26
+ * Handles consent checks, queuing, and storage tier automatically.
27
+ * @param email - User's email address
28
+ * @param traits - Optional additional user traits
29
+ * @returns true if identify was called immediately, false if queued or blocked
30
+ */
31
+ identify: (email: string, traits?: Record<string, unknown>) => boolean;
32
+ /**
33
+ * Check if analytics consent is currently granted
34
+ */
35
+ hasAnalyticsConsent: () => boolean;
36
+ };
37
+
38
+ // Storage types
39
+ export type StorageTier = 'session' | 'local';
40
+
41
+ export interface StorageConfig {
42
+ prefix: string;
43
+ tier: StorageTier;
44
+ }
45
+
46
+ // StorageManager class for tiered storage
47
+ export declare class StorageManager {
48
+ constructor(config?: Partial<StorageConfig>);
49
+ getTier(): StorageTier;
50
+ upgradeTier(newTier: StorageTier): void;
51
+ downgradeTier(newTier: StorageTier): void;
52
+ getItem(key: string): string | null;
53
+ setItem(key: string, value: string): void;
54
+ removeItem(key: string): void;
55
+ getJSON<T>(key: string): T | null;
56
+ setJSON<T>(key: string, value: T): void;
57
+ }
58
+
59
+ export declare function getStorageManager(): StorageManager;
60
+ export declare function initStorageWithConsent(hasAnalyticsConsent: boolean): StorageManager;
package/dist/index.esm.js CHANGED
@@ -1,16 +1,38 @@
1
1
  'use client';
2
+ var __defProp = Object.defineProperty;
3
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
+ var __publicField = (obj, key, value) => {
5
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
+ return value;
7
+ };
8
+
2
9
  // src/react/BalanceAnalytics.tsx
3
- import React, { useEffect, Suspense } from "react";
10
+ import React, { useEffect, useRef, Suspense } from "react";
4
11
  import { usePathname, useSearchParams } from "next/navigation";
5
12
  function BalanceAnalyticsInner() {
6
13
  const pathname = usePathname();
7
14
  const searchParams = useSearchParams();
15
+ const isFirstRender = useRef(true);
16
+ const lastTrackedPath = useRef(null);
8
17
  useEffect(() => {
9
- if (typeof window !== "undefined" && window.balance) {
10
- const url = window.location.href;
11
- const title = document.title;
12
- window.balance.page({ url, title });
18
+ if (typeof window === "undefined" || !window.balance)
19
+ return;
20
+ const currentPath = pathname + (searchParams?.toString() || "");
21
+ if (lastTrackedPath.current === currentPath) {
22
+ return;
13
23
  }
24
+ if (isFirstRender.current) {
25
+ isFirstRender.current = false;
26
+ if (window._balanceInitialPageviewFired) {
27
+ lastTrackedPath.current = currentPath;
28
+ console.log("[BalanceAnalytics] Skipping initial pageview (script already fired)");
29
+ return;
30
+ }
31
+ }
32
+ const url = window.location.href;
33
+ const title = document.title;
34
+ lastTrackedPath.current = currentPath;
35
+ window.balance.page({ url, title });
14
36
  }, [pathname, searchParams]);
15
37
  return null;
16
38
  }
@@ -18,6 +40,336 @@ function BalanceAnalytics() {
18
40
  return /* @__PURE__ */ React.createElement(Suspense, { fallback: null }, /* @__PURE__ */ React.createElement(BalanceAnalyticsInner, null));
19
41
  }
20
42
 
43
+ // src/react/useBalanceIdentify.ts
44
+ import { useCallback, useEffect as useEffect2, useRef as useRef2 } from "react";
45
+
46
+ // src/storage/StorageManager.ts
47
+ var DEFAULT_PREFIX = "balance_";
48
+ var StorageManager = class {
49
+ constructor(config) {
50
+ __publicField(this, "prefix");
51
+ __publicField(this, "currentTier");
52
+ this.prefix = config?.prefix ?? DEFAULT_PREFIX;
53
+ this.currentTier = config?.tier ?? "session";
54
+ }
55
+ /**
56
+ * Get the appropriate storage based on current tier
57
+ */
58
+ getStorage() {
59
+ if (typeof window === "undefined")
60
+ return null;
61
+ try {
62
+ return this.currentTier === "local" ? localStorage : sessionStorage;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+ /**
68
+ * Get current storage tier
69
+ */
70
+ getTier() {
71
+ return this.currentTier;
72
+ }
73
+ /**
74
+ * Upgrade storage tier (e.g., when user gives consent)
75
+ * Automatically migrates data from sessionStorage to localStorage
76
+ */
77
+ upgradeTier(newTier) {
78
+ if (typeof window === "undefined")
79
+ return;
80
+ if (this.currentTier === newTier)
81
+ return;
82
+ const oldTier = this.currentTier;
83
+ if (oldTier === "session" && newTier === "local") {
84
+ this.migrateToLocal();
85
+ }
86
+ this.currentTier = newTier;
87
+ console.log(`[StorageManager] Upgraded tier: ${oldTier} -> ${newTier}`);
88
+ }
89
+ /**
90
+ * Downgrade storage tier (e.g., when user revokes consent)
91
+ * Clears localStorage data and falls back to sessionStorage
92
+ */
93
+ downgradeTier(newTier) {
94
+ if (typeof window === "undefined")
95
+ return;
96
+ if (this.currentTier === newTier)
97
+ return;
98
+ const oldTier = this.currentTier;
99
+ if (oldTier === "local" && newTier === "session") {
100
+ this.clearLocalStorage();
101
+ }
102
+ this.currentTier = newTier;
103
+ console.log(`[StorageManager] Downgraded tier: ${oldTier} -> ${newTier}`);
104
+ }
105
+ /**
106
+ * Migrate all prefixed data from sessionStorage to localStorage
107
+ */
108
+ migrateToLocal() {
109
+ if (typeof window === "undefined")
110
+ return;
111
+ try {
112
+ const keysToMigrate = [];
113
+ for (let i = 0; i < sessionStorage.length; i++) {
114
+ const key = sessionStorage.key(i);
115
+ if (key?.startsWith(this.prefix)) {
116
+ keysToMigrate.push(key);
117
+ }
118
+ }
119
+ for (const key of keysToMigrate) {
120
+ const value = sessionStorage.getItem(key);
121
+ if (value) {
122
+ localStorage.setItem(key, value);
123
+ console.log(`[StorageManager] Migrated: ${key}`);
124
+ }
125
+ }
126
+ for (const key of keysToMigrate) {
127
+ sessionStorage.removeItem(key);
128
+ }
129
+ console.log(`[StorageManager] Migration complete: ${keysToMigrate.length} items`);
130
+ } catch (error) {
131
+ console.error("[StorageManager] Migration failed:", error);
132
+ }
133
+ }
134
+ /**
135
+ * Clear all prefixed data from localStorage
136
+ */
137
+ clearLocalStorage() {
138
+ if (typeof window === "undefined")
139
+ return;
140
+ try {
141
+ const keysToRemove = [];
142
+ for (let i = 0; i < localStorage.length; i++) {
143
+ const key = localStorage.key(i);
144
+ if (key?.startsWith(this.prefix)) {
145
+ keysToRemove.push(key);
146
+ }
147
+ }
148
+ for (const key of keysToRemove) {
149
+ localStorage.removeItem(key);
150
+ }
151
+ console.log(`[StorageManager] Cleared ${keysToRemove.length} items from localStorage`);
152
+ } catch (error) {
153
+ console.error("[StorageManager] Clear failed:", error);
154
+ }
155
+ }
156
+ /**
157
+ * Get item from current storage tier
158
+ */
159
+ getItem(key) {
160
+ const storage = this.getStorage();
161
+ if (!storage)
162
+ return null;
163
+ try {
164
+ const fullKey = this.prefix + key;
165
+ let value = storage.getItem(fullKey);
166
+ if (!value && this.currentTier === "session") {
167
+ try {
168
+ value = localStorage.getItem(fullKey);
169
+ } catch {
170
+ }
171
+ }
172
+ return value;
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+ /**
178
+ * Set item in current storage tier
179
+ */
180
+ setItem(key, value) {
181
+ const storage = this.getStorage();
182
+ if (!storage)
183
+ return;
184
+ try {
185
+ const fullKey = this.prefix + key;
186
+ storage.setItem(fullKey, value);
187
+ } catch (error) {
188
+ console.error("[StorageManager] setItem failed:", error);
189
+ }
190
+ }
191
+ /**
192
+ * Remove item from all storage tiers
193
+ */
194
+ removeItem(key) {
195
+ if (typeof window === "undefined")
196
+ return;
197
+ const fullKey = this.prefix + key;
198
+ try {
199
+ sessionStorage.removeItem(fullKey);
200
+ } catch {
201
+ }
202
+ try {
203
+ localStorage.removeItem(fullKey);
204
+ } catch {
205
+ }
206
+ }
207
+ /**
208
+ * Get JSON-parsed item
209
+ */
210
+ getJSON(key) {
211
+ const value = this.getItem(key);
212
+ if (!value)
213
+ return null;
214
+ try {
215
+ return JSON.parse(value);
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+ /**
221
+ * Set JSON-stringified item
222
+ */
223
+ setJSON(key, value) {
224
+ try {
225
+ this.setItem(key, JSON.stringify(value));
226
+ } catch {
227
+ }
228
+ }
229
+ };
230
+ var storageManagerInstance = null;
231
+ function getStorageManager() {
232
+ if (!storageManagerInstance) {
233
+ storageManagerInstance = new StorageManager();
234
+ }
235
+ return storageManagerInstance;
236
+ }
237
+ function initStorageWithConsent(hasAnalyticsConsent) {
238
+ const manager = getStorageManager();
239
+ if (hasAnalyticsConsent) {
240
+ manager.upgradeTier("local");
241
+ } else {
242
+ manager.downgradeTier("session");
243
+ }
244
+ return manager;
245
+ }
246
+
247
+ // src/react/useBalanceIdentify.ts
248
+ var PENDING_IDENTIFY_KEY = "pending_identify";
249
+ var MAX_PENDING_AGE_MS = 30 * 60 * 1e3;
250
+ function checkAnalyticsConsent() {
251
+ if (typeof window === "undefined")
252
+ return false;
253
+ try {
254
+ if (window.balance?.hasConsent) {
255
+ return window.balance.hasConsent("analytics");
256
+ }
257
+ const stored = localStorage.getItem("balance_consent");
258
+ if (stored) {
259
+ const consent = JSON.parse(stored);
260
+ return consent?.preferences?.analytics !== false;
261
+ }
262
+ return true;
263
+ } catch {
264
+ return true;
265
+ }
266
+ }
267
+ function useBalanceIdentify() {
268
+ const pollIntervalRef = useRef2(null);
269
+ const hasProcessedPending = useRef2(false);
270
+ const storageManager = typeof window !== "undefined" ? getStorageManager() : null;
271
+ const getPendingIdentify = useCallback(() => {
272
+ if (!storageManager)
273
+ return null;
274
+ const pending = storageManager.getJSON(PENDING_IDENTIFY_KEY);
275
+ if (!pending)
276
+ return null;
277
+ if (Date.now() - pending.timestamp > MAX_PENDING_AGE_MS) {
278
+ storageManager.removeItem(PENDING_IDENTIFY_KEY);
279
+ return null;
280
+ }
281
+ return pending;
282
+ }, [storageManager]);
283
+ const setPendingIdentify = useCallback((email, traits) => {
284
+ if (!storageManager)
285
+ return;
286
+ storageManager.setJSON(PENDING_IDENTIFY_KEY, {
287
+ email,
288
+ traits,
289
+ timestamp: Date.now()
290
+ });
291
+ }, [storageManager]);
292
+ const clearPendingIdentify = useCallback(() => {
293
+ if (!storageManager)
294
+ return;
295
+ storageManager.removeItem(PENDING_IDENTIFY_KEY);
296
+ }, [storageManager]);
297
+ const identify2 = useCallback((email, traits) => {
298
+ const hasConsent2 = checkAnalyticsConsent();
299
+ if (!hasConsent2) {
300
+ console.log("[useBalanceIdentify] Skipping identify - user declined analytics consent");
301
+ return false;
302
+ }
303
+ if (storageManager) {
304
+ initStorageWithConsent(hasConsent2);
305
+ }
306
+ if (typeof window !== "undefined" && window.balance?.identify) {
307
+ window.balance.identify(email, traits);
308
+ console.log("[useBalanceIdentify] User identified:", email.split("@")[0] + "***");
309
+ clearPendingIdentify();
310
+ return true;
311
+ }
312
+ console.log("[useBalanceIdentify] Pixel not ready, queueing identify");
313
+ setPendingIdentify(email, traits);
314
+ return false;
315
+ }, [storageManager, clearPendingIdentify, setPendingIdentify]);
316
+ const processPendingIdentify = useCallback(() => {
317
+ if (hasProcessedPending.current)
318
+ return;
319
+ const pending = getPendingIdentify();
320
+ if (!pending)
321
+ return;
322
+ if (typeof window !== "undefined" && window.balance?.identify) {
323
+ const hasConsent2 = checkAnalyticsConsent();
324
+ if (!hasConsent2) {
325
+ console.log("[useBalanceIdentify] Clearing pending identify - user declined analytics consent");
326
+ clearPendingIdentify();
327
+ hasProcessedPending.current = true;
328
+ return;
329
+ }
330
+ if (storageManager) {
331
+ initStorageWithConsent(hasConsent2);
332
+ }
333
+ console.log("[useBalanceIdentify] Processing pending identify");
334
+ window.balance.identify(pending.email, pending.traits);
335
+ clearPendingIdentify();
336
+ hasProcessedPending.current = true;
337
+ }
338
+ }, [getPendingIdentify, clearPendingIdentify, storageManager]);
339
+ useEffect2(() => {
340
+ processPendingIdentify();
341
+ const pending = getPendingIdentify();
342
+ if (pending && !hasProcessedPending.current) {
343
+ pollIntervalRef.current = setInterval(() => {
344
+ processPendingIdentify();
345
+ if (hasProcessedPending.current || !getPendingIdentify()) {
346
+ if (pollIntervalRef.current) {
347
+ clearInterval(pollIntervalRef.current);
348
+ pollIntervalRef.current = null;
349
+ }
350
+ }
351
+ }, 500);
352
+ }
353
+ return () => {
354
+ if (pollIntervalRef.current) {
355
+ clearInterval(pollIntervalRef.current);
356
+ pollIntervalRef.current = null;
357
+ }
358
+ };
359
+ }, [processPendingIdentify, getPendingIdentify]);
360
+ return {
361
+ /**
362
+ * Identify a user with BALANCE pixel.
363
+ * Handles consent checks, queuing, and storage tier automatically.
364
+ */
365
+ identify: identify2,
366
+ /**
367
+ * Check if analytics consent is currently granted
368
+ */
369
+ hasAnalyticsConsent: checkAnalyticsConsent
370
+ };
371
+ }
372
+
21
373
  // src/index.esm.ts
22
374
  var track = (eventName, properties) => {
23
375
  if (typeof window === "undefined")
@@ -92,14 +444,18 @@ var hasConsent = (type) => {
92
444
  };
93
445
  export {
94
446
  BalanceAnalytics,
447
+ StorageManager,
95
448
  getAttribution,
96
449
  getConsent,
97
450
  getFanIdHash,
98
451
  getSessionId,
452
+ getStorageManager,
99
453
  hasConsent,
100
454
  identify,
455
+ initStorageWithConsent,
101
456
  page,
102
457
  purchase,
103
458
  setConsent,
104
- track
459
+ track,
460
+ useBalanceIdentify
105
461
  };
package/dist/index.js CHANGED
@@ -56,12 +56,27 @@ var BalancePixel = (() => {
56
56
  function BalanceAnalyticsInner() {
57
57
  const pathname = (0, import_navigation.usePathname)();
58
58
  const searchParams = (0, import_navigation.useSearchParams)();
59
+ const isFirstRender = (0, import_react.useRef)(true);
60
+ const lastTrackedPath = (0, import_react.useRef)(null);
59
61
  (0, import_react.useEffect)(() => {
60
- if (typeof window !== "undefined" && window.balance) {
61
- const url = window.location.href;
62
- const title = document.title;
63
- window.balance.page({ url, title });
62
+ if (typeof window === "undefined" || !window.balance)
63
+ return;
64
+ const currentPath = pathname + (searchParams?.toString() || "");
65
+ if (lastTrackedPath.current === currentPath) {
66
+ return;
67
+ }
68
+ if (isFirstRender.current) {
69
+ isFirstRender.current = false;
70
+ if (window._balanceInitialPageviewFired) {
71
+ lastTrackedPath.current = currentPath;
72
+ console.log("[BalanceAnalytics] Skipping initial pageview (script already fired)");
73
+ return;
74
+ }
64
75
  }
76
+ const url = window.location.href;
77
+ const title = document.title;
78
+ lastTrackedPath.current = currentPath;
79
+ window.balance.page({ url, title });
65
80
  }, [pathname, searchParams]);
66
81
  return null;
67
82
  }
@@ -71,21 +86,67 @@ var BalancePixel = (() => {
71
86
 
72
87
  // src/index.ts
73
88
  (function() {
89
+ function parseUserAgent(ua) {
90
+ let device_type = "desktop";
91
+ if (/ipad|tablet|android(?!.*mobile)/i.test(ua))
92
+ device_type = "tablet";
93
+ else if (/mobile|iphone|android.*mobile|blackberry|iemobile/i.test(ua))
94
+ device_type = "mobile";
95
+ let browser = "Unknown";
96
+ if (/edg/i.test(ua))
97
+ browser = "Edge";
98
+ else if (/opr|opera/i.test(ua))
99
+ browser = "Opera";
100
+ else if (/firefox/i.test(ua))
101
+ browser = "Firefox";
102
+ else if (/chrome/i.test(ua))
103
+ browser = "Chrome";
104
+ else if (/safari/i.test(ua))
105
+ browser = "Safari";
106
+ let os = "Unknown";
107
+ if (/iphone|ipad/i.test(ua))
108
+ os = "iOS";
109
+ else if (/android/i.test(ua))
110
+ os = "Android";
111
+ else if (/windows/i.test(ua))
112
+ os = "Windows";
113
+ else if (/mac os/i.test(ua))
114
+ os = "macOS";
115
+ else if (/linux/i.test(ua))
116
+ os = "Linux";
117
+ else if (/cros/i.test(ua))
118
+ os = "ChromeOS";
119
+ return { device_type, browser, os };
120
+ }
121
+ let cachedDeviceInfo = null;
122
+ function getDeviceInfo() {
123
+ if (!cachedDeviceInfo) {
124
+ try {
125
+ cachedDeviceInfo = parseUserAgent(navigator.userAgent);
126
+ } catch {
127
+ cachedDeviceInfo = { device_type: "desktop", browser: "Unknown", os: "Unknown" };
128
+ }
129
+ }
130
+ return cachedDeviceInfo;
131
+ }
74
132
  const currentScript = document.currentScript;
75
133
  const artistId = currentScript?.dataset.artistId;
76
134
  const projectId = currentScript?.dataset.projectId;
77
135
  const useEmulator = currentScript?.dataset.emulator === "true";
78
136
  const debug = currentScript?.dataset.debug === "true";
137
+ const heartbeatInterval = parseInt(currentScript?.dataset.heartbeatInterval || "30000", 10);
138
+ const heartbeatEnabled = currentScript?.dataset.heartbeat !== "false";
79
139
  if (!artistId) {
80
140
  console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");
81
141
  return;
82
142
  }
83
- const SESSION_KEY = "balance_session_id";
84
- const SESSION_TIMESTAMP_KEY = "balance_session_timestamp";
85
- const ATTRIBUTION_KEY = "balance_attribution";
86
- const FAN_ID_KEY = "balance_fan_id_hash";
143
+ const SESSION_KEY = "session_id";
144
+ const SESSION_TIMESTAMP_KEY = "session_timestamp";
145
+ const ATTRIBUTION_KEY = "attribution";
146
+ const FAN_ID_KEY = "fan_id_hash";
87
147
  const CONSENT_STORAGE_KEY = "balance_consent";
88
148
  const SESSION_DURATION = 60 * 60 * 1e3;
149
+ const STORAGE_PREFIX = "balance_";
89
150
  const API_ENDPOINT = useEmulator ? `http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint` : `https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint`;
90
151
  let sessionId = null;
91
152
  let fanIdHash = null;
@@ -93,10 +154,80 @@ var BalancePixel = (() => {
93
154
  let attribution = {};
94
155
  let eventQueue = [];
95
156
  let flushTimer = null;
157
+ let currentStorageTier = "session";
158
+ let heartbeatTimer = null;
159
+ let pageStartTime = 0;
160
+ let activeTime = 0;
161
+ let lastActiveTimestamp = 0;
162
+ let isPageVisible = true;
163
+ const IDLE_TIMEOUT = 2 * 60 * 1e3;
164
+ let lastActivityTime = Date.now();
165
+ let isIdle = false;
96
166
  const log = (...args) => {
97
167
  if (debug)
98
168
  console.log("[BALANCE Pixel]", ...args);
99
169
  };
170
+ function getStorage() {
171
+ try {
172
+ return currentStorageTier === "local" ? localStorage : sessionStorage;
173
+ } catch {
174
+ return null;
175
+ }
176
+ }
177
+ function storageGet(key) {
178
+ const storage = getStorage();
179
+ if (!storage)
180
+ return null;
181
+ try {
182
+ const fullKey = STORAGE_PREFIX + key;
183
+ let value = storage.getItem(fullKey);
184
+ if (!value && currentStorageTier === "session") {
185
+ try {
186
+ value = localStorage.getItem(fullKey);
187
+ } catch {
188
+ }
189
+ }
190
+ return value;
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ function storageSet(key, value) {
196
+ const storage = getStorage();
197
+ if (!storage)
198
+ return;
199
+ try {
200
+ storage.setItem(STORAGE_PREFIX + key, value);
201
+ } catch {
202
+ }
203
+ }
204
+ function upgradeStorageTier() {
205
+ if (currentStorageTier === "local")
206
+ return;
207
+ log("Upgrading storage tier: session -> local");
208
+ try {
209
+ const keysToMigrate = [];
210
+ for (let i = 0; i < sessionStorage.length; i++) {
211
+ const key = sessionStorage.key(i);
212
+ if (key?.startsWith(STORAGE_PREFIX)) {
213
+ keysToMigrate.push(key);
214
+ }
215
+ }
216
+ for (const key of keysToMigrate) {
217
+ const value = sessionStorage.getItem(key);
218
+ if (value) {
219
+ localStorage.setItem(key, value);
220
+ }
221
+ }
222
+ for (const key of keysToMigrate) {
223
+ sessionStorage.removeItem(key);
224
+ }
225
+ currentStorageTier = "local";
226
+ log(`Storage tier upgraded, migrated ${keysToMigrate.length} items`);
227
+ } catch (error) {
228
+ console.error("[BALANCE Pixel] Storage migration failed:", error);
229
+ }
230
+ }
100
231
  function generateUUID() {
101
232
  if (crypto && crypto.randomUUID) {
102
233
  return crypto.randomUUID();
@@ -109,18 +240,18 @@ var BalancePixel = (() => {
109
240
  }
110
241
  function getOrCreateSession() {
111
242
  try {
112
- const stored = localStorage.getItem(SESSION_KEY);
113
- const timestamp = localStorage.getItem(SESSION_TIMESTAMP_KEY);
243
+ const stored = storageGet(SESSION_KEY);
244
+ const timestamp = storageGet(SESSION_TIMESTAMP_KEY);
114
245
  if (stored && timestamp) {
115
246
  const age = Date.now() - parseInt(timestamp, 10);
116
247
  if (age < SESSION_DURATION) {
117
- localStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
248
+ storageSet(SESSION_TIMESTAMP_KEY, Date.now().toString());
118
249
  return stored;
119
250
  }
120
251
  }
121
252
  const newId = generateUUID();
122
- localStorage.setItem(SESSION_KEY, newId);
123
- localStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
253
+ storageSet(SESSION_KEY, newId);
254
+ storageSet(SESSION_TIMESTAMP_KEY, Date.now().toString());
124
255
  return newId;
125
256
  } catch (e) {
126
257
  return generateUUID();
@@ -138,7 +269,7 @@ var BalancePixel = (() => {
138
269
  }
139
270
  function captureAttribution() {
140
271
  try {
141
- const stored = localStorage.getItem(ATTRIBUTION_KEY);
272
+ const stored = storageGet(ATTRIBUTION_KEY);
142
273
  if (stored) {
143
274
  attribution = JSON.parse(stored);
144
275
  log("Loaded attribution:", attribution);
@@ -147,7 +278,7 @@ var BalancePixel = (() => {
147
278
  const utm = extractUTM();
148
279
  if (Object.keys(utm).length > 0) {
149
280
  attribution = utm;
150
- localStorage.setItem(ATTRIBUTION_KEY, JSON.stringify(utm));
281
+ storageSet(ATTRIBUTION_KEY, JSON.stringify(utm));
151
282
  log("Captured attribution:", attribution);
152
283
  }
153
284
  } catch (e) {
@@ -155,7 +286,7 @@ var BalancePixel = (() => {
155
286
  }
156
287
  function loadFanId() {
157
288
  try {
158
- fanIdHash = localStorage.getItem(FAN_ID_KEY);
289
+ fanIdHash = storageGet(FAN_ID_KEY);
159
290
  } catch (e) {
160
291
  }
161
292
  }
@@ -184,6 +315,9 @@ var BalancePixel = (() => {
184
315
  } catch (e) {
185
316
  console.error("[BALANCE Pixel] Could not save consent:", e);
186
317
  }
318
+ if (preferences.analytics === true) {
319
+ upgradeStorageTier();
320
+ }
187
321
  const event = buildEvent({
188
322
  event_name: "consent_updated",
189
323
  metadata: {
@@ -209,6 +343,7 @@ var BalancePixel = (() => {
209
343
  return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
210
344
  }
211
345
  function buildEvent(partial) {
346
+ const deviceInfo = getDeviceInfo();
212
347
  const base = {
213
348
  artist_id: artistId,
214
349
  fan_session_id: sessionId,
@@ -217,6 +352,10 @@ var BalancePixel = (() => {
217
352
  source_url: window.location.href,
218
353
  referrer_url: document.referrer || void 0,
219
354
  user_agent: navigator.userAgent,
355
+ // Device info (parsed client-side)
356
+ device_type: deviceInfo.device_type,
357
+ browser: deviceInfo.browser,
358
+ os: deviceInfo.os,
220
359
  ...partial,
221
360
  ...attribution
222
361
  };
@@ -265,6 +404,86 @@ var BalancePixel = (() => {
265
404
  flush();
266
405
  }, 5e3);
267
406
  }
407
+ function startActiveTimeTracking() {
408
+ if (!lastActiveTimestamp) {
409
+ pageStartTime = Date.now();
410
+ }
411
+ lastActiveTimestamp = Date.now();
412
+ isPageVisible = true;
413
+ log("Active time tracking started/resumed");
414
+ }
415
+ function pauseActiveTimeTracking() {
416
+ if (lastActiveTimestamp && isPageVisible) {
417
+ activeTime += Date.now() - lastActiveTimestamp;
418
+ }
419
+ isPageVisible = false;
420
+ log("Active time tracking paused, accumulated:", activeTime, "ms");
421
+ }
422
+ function getCumulativeActiveTime() {
423
+ let total = activeTime;
424
+ if (isPageVisible && lastActiveTimestamp) {
425
+ total += Date.now() - lastActiveTimestamp;
426
+ }
427
+ return total;
428
+ }
429
+ function resetIdleTimer() {
430
+ lastActivityTime = Date.now();
431
+ if (isIdle) {
432
+ isIdle = false;
433
+ startActiveTimeTracking();
434
+ log("User returned from idle");
435
+ }
436
+ }
437
+ function sendHeartbeat() {
438
+ if (document.visibilityState === "hidden") {
439
+ log("Skipping heartbeat - tab hidden");
440
+ return;
441
+ }
442
+ if (Date.now() - lastActivityTime > IDLE_TIMEOUT) {
443
+ if (!isIdle) {
444
+ isIdle = true;
445
+ pauseActiveTimeTracking();
446
+ log("User idle - pausing heartbeat");
447
+ }
448
+ return;
449
+ }
450
+ const timeOnPage = getCumulativeActiveTime();
451
+ const timeOnPageSeconds = Math.round(timeOnPage / 1e3);
452
+ const event = buildEvent({
453
+ event_name: "engagement_heartbeat",
454
+ metadata: {
455
+ time_on_page_seconds: timeOnPageSeconds,
456
+ time_on_page_ms: timeOnPage,
457
+ heartbeat_interval_ms: heartbeatInterval,
458
+ is_active: isPageVisible && !isIdle
459
+ }
460
+ });
461
+ enqueueEvent(event);
462
+ log("Heartbeat sent:", timeOnPageSeconds, "seconds active");
463
+ }
464
+ function startHeartbeat() {
465
+ if (!heartbeatEnabled) {
466
+ log('Heartbeat disabled via data-heartbeat="false"');
467
+ return;
468
+ }
469
+ if (heartbeatTimer)
470
+ clearInterval(heartbeatTimer);
471
+ startActiveTimeTracking();
472
+ heartbeatTimer = window.setInterval(() => {
473
+ sendHeartbeat();
474
+ }, heartbeatInterval);
475
+ log("Heartbeat started with interval:", heartbeatInterval, "ms");
476
+ }
477
+ function stopHeartbeat() {
478
+ if (heartbeatTimer) {
479
+ clearInterval(heartbeatTimer);
480
+ heartbeatTimer = null;
481
+ }
482
+ if (heartbeatEnabled) {
483
+ sendHeartbeat();
484
+ log("Heartbeat stopped, final time sent");
485
+ }
486
+ }
268
487
  function trackPageView(options = {}) {
269
488
  const event = buildEvent({
270
489
  event_name: "pageview",
@@ -285,15 +504,23 @@ var BalancePixel = (() => {
285
504
  }
286
505
  async function identify2(email, traits = {}) {
287
506
  try {
507
+ if (consent && consent.analytics === false) {
508
+ log("Identify skipped - user declined analytics consent");
509
+ return;
510
+ }
288
511
  fanIdHash = await hashEmail(email);
289
- localStorage.setItem(FAN_ID_KEY, fanIdHash);
512
+ if (consent?.analytics === true) {
513
+ upgradeStorageTier();
514
+ }
515
+ storageSet(FAN_ID_KEY, fanIdHash);
290
516
  const emailParts = email.split("@");
291
517
  const maskedEmail = emailParts[0].charAt(0) + "***@" + (emailParts[1] || "");
292
518
  log("Fan identified:", {
293
519
  name: traits.name || "(no name)",
294
520
  email: maskedEmail,
295
521
  hash: fanIdHash.substring(0, 16) + "...",
296
- traits
522
+ traits,
523
+ storageTier: currentStorageTier
297
524
  });
298
525
  const event = buildEvent({
299
526
  event_name: "identify",
@@ -301,7 +528,8 @@ var BalancePixel = (() => {
301
528
  metadata: {
302
529
  email_sha256: fanIdHash,
303
530
  traits,
304
- consent_preferences: consent || void 0
531
+ consent_preferences: consent || void 0,
532
+ storage_tier: currentStorageTier
305
533
  }
306
534
  });
307
535
  enqueueEvent(event);
@@ -321,9 +549,16 @@ var BalancePixel = (() => {
321
549
  enqueueEvent(event);
322
550
  }
323
551
  function init() {
552
+ loadConsent();
553
+ if (consent?.analytics === true) {
554
+ currentStorageTier = "local";
555
+ log("Storage tier: local (analytics consent granted)");
556
+ } else {
557
+ currentStorageTier = "session";
558
+ log("Storage tier: session (privacy by default)");
559
+ }
324
560
  sessionId = getOrCreateSession();
325
561
  loadFanId();
326
- loadConsent();
327
562
  if (!consent) {
328
563
  consent = {
329
564
  analytics: true,
@@ -332,6 +567,7 @@ var BalancePixel = (() => {
332
567
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
333
568
  };
334
569
  log("Default consent enabled (all tracking):", consent);
570
+ upgradeStorageTier();
335
571
  }
336
572
  captureAttribution();
337
573
  startFlushTimer();
@@ -341,16 +577,26 @@ var BalancePixel = (() => {
341
577
  sessionId,
342
578
  fanIdHash,
343
579
  consent,
580
+ storageTier: currentStorageTier,
344
581
  useEmulator,
345
582
  endpoint: API_ENDPOINT
346
583
  });
584
+ window._balanceInitialPageviewFired = true;
347
585
  trackPageView();
586
+ startHeartbeat();
587
+ ["mousemove", "keydown", "scroll", "touchstart"].forEach((eventType) => {
588
+ document.addEventListener(eventType, resetIdleTimer, { passive: true });
589
+ });
348
590
  window.addEventListener("beforeunload", () => {
591
+ stopHeartbeat();
349
592
  flush();
350
593
  });
351
594
  document.addEventListener("visibilitychange", () => {
352
595
  if (document.visibilityState === "hidden") {
596
+ pauseActiveTimeTracking();
353
597
  flush();
598
+ } else {
599
+ startActiveTimeTracking();
354
600
  }
355
601
  });
356
602
  }
package/dist/index.min.js CHANGED
@@ -1 +1 @@
1
- var BalancePixel=(()=>{var V=Object.create;var x=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var X=Object.getOwnPropertyNames;var Z=Object.getPrototypeOf,ee=Object.prototype.hasOwnProperty;var T=(n=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(n,{get:(r,o)=>(typeof require<"u"?require:r)[o]}):n)(function(n){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+n+'" is not supported')});var ne=(n,r)=>{for(var o in r)x(n,o,{get:r[o],enumerable:!0})},B=(n,r,o,u)=>{if(r&&typeof r=="object"||typeof r=="function")for(let f of X(r))!ee.call(n,f)&&f!==o&&x(n,f,{get:()=>r[f],enumerable:!(u=W(r,f))||u.enumerable});return n};var te=(n,r,o)=>(o=n!=null?V(Z(n)):{},B(r||!n||!n.__esModule?x(o,"default",{value:n,enumerable:!0}):o,n)),re=n=>B(x({},"__esModule",{value:!0}),n);var me={};ne(me,{BalanceAnalytics:()=>R,getAttribution:()=>ue,getConsent:()=>ge,getFanIdHash:()=>de,getSessionId:()=>le,hasConsent:()=>pe,identify:()=>ae,page:()=>se,purchase:()=>ce,setConsent:()=>fe,track:()=>ie});var g=te(T("react")),b=T("next/navigation");function oe(){let n=(0,b.usePathname)(),r=(0,b.useSearchParams)();return(0,g.useEffect)(()=>{if(typeof window<"u"&&window.balance){let o=window.location.href,u=document.title;window.balance.page({url:o,title:u})}},[n,r]),null}function R(){return g.default.createElement(g.Suspense,{fallback:null},g.default.createElement(oe,null))}(function(){let n=document.currentScript,r=n?.dataset.artistId,o=n?.dataset.projectId,u=n?.dataset.emulator==="true",f=n?.dataset.debug==="true";if(!r){console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");return}let P="balance_session_id",S="balance_session_timestamp",E="balance_attribution",C="balance_fan_id_hash",A="balance_consent",D=60*60*1e3,I=u?"http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint":"https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint",h=null,l=null,s=null,p={},d=[],_=null,c=(...e)=>{f&&console.log("[BALANCE Pixel]",...e)};function N(){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 L(){try{let e=localStorage.getItem(P),t=localStorage.getItem(S);if(e&&t&&Date.now()-parseInt(t,10)<D)return localStorage.setItem(S,Date.now().toString()),e;let i=N();return localStorage.setItem(P,i),localStorage.setItem(S,Date.now().toString()),i}catch{return N()}}function z(){let e=new URLSearchParams(window.location.search),t={};return["source","medium","campaign","content","term"].forEach(i=>{let a=e.get(`utm_${i}`);a&&(t[`utm_${i}`]=a)}),t}function U(){try{let e=localStorage.getItem(E);if(e){p=JSON.parse(e),c("Loaded attribution:",p);return}let t=z();Object.keys(t).length>0&&(p=t,localStorage.setItem(E,JSON.stringify(t)),c("Captured attribution:",p))}catch{}}function F(){try{l=localStorage.getItem(C)}catch{}}function j(){try{let e=localStorage.getItem(A);e&&(s=JSON.parse(e).preferences||null,c("Loaded consent:",s))}catch{}}function H(e){let t=s;s=e;try{let a={preferences:e,method:"explicit",version:1};localStorage.setItem(A,JSON.stringify(a)),c("Consent saved:",e)}catch(a){console.error("[BALANCE Pixel] Could not save consent:",a)}let i=m({event_name:"consent_updated",metadata:{consent_preferences:e,consent_method:"explicit",previous_consent:t||void 0}});w(i)}function M(){return s}function J(e){return s?.[e]===!0}async function K(e){let t=e.toLowerCase().trim(),a=new TextEncoder().encode(t),v=await crypto.subtle.digest("SHA-256",a);return Array.from(new Uint8Array(v)).map(Q=>Q.toString(16).padStart(2,"0")).join("")}function m(e){let t={artist_id:r,fan_session_id:h,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,...e,...p};return o&&!e.projectId&&(t.projectId=o),t}function w(e){d.push(e),c("Event queued:",e.event_name,"(queue:",d.length,")"),d.length>=10&&y()}async function y(){if(d.length===0)return;let e=[...d];d=[],c("Flushing",e.length,"events to",I);try{let t=await fetch(I,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0});if(!t.ok)throw new Error(`HTTP ${t.status}`);c("Events sent successfully")}catch(t){console.error("[BALANCE Pixel] Failed to send events:",t),d.length<50&&d.push(...e)}}function Y(){_&&clearInterval(_),_=window.setInterval(()=>{d.length>0&&y()},5e3)}function k(e={}){let t=m({event_name:"pageview",page_title:e.title||document.title,source_url:e.url||window.location.href});w(t)}function q(e,t={}){let i=m({event_name:"custom",metadata:{event_type:e,...t}});w(i)}async function $(e,t={}){try{l=await K(e),localStorage.setItem(C,l);let i=e.split("@"),a=i[0].charAt(0)+"***@"+(i[1]||"");c("Fan identified:",{name:t.name||"(no name)",email:a,hash:l.substring(0,16)+"...",traits:t});let v=m({event_name:"identify",fan_id_hash:l,metadata:{email_sha256:l,traits:t,consent_preferences:s||void 0}});w(v)}catch(i){console.error("[BALANCE Pixel] Failed to identify:",i)}}function G(e,t="USD",i={}){let a=m({event_name:"purchase",metadata:{revenue:e,currency:t,...i}});w(a)}function O(){h=L(),F(),j(),s||(s={analytics:!0,marketing:!0,personalization:!0,timestamp:new Date().toISOString()},c("Default consent enabled (all tracking):",s)),U(),Y(),c("Initialized",{artistId:r,projectId:o||"(none - will track to all projects)",sessionId:h,fanIdHash:l,consent:s,useEmulator:u,endpoint:I}),k(),window.addEventListener("beforeunload",()=>{y()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&y()})}window.balance={track:q,identify:$,page:k,purchase:G,getSessionId:()=>h,getFanIdHash:()=>l,getAttribution:()=>p,setConsent:H,getConsent:M,hasConsent:J},document.readyState==="loading"?document.addEventListener("DOMContentLoaded",O):O(),c("Pixel script loaded")})();var ie=(n,r)=>{typeof window>"u"||(window.balance?window.balance.track(n,r):console.warn("[Balance Pixel] track() called before pixel initialized:",n))},ae=(n,r)=>typeof window>"u"?Promise.resolve():window.balance?window.balance.identify(n,r):(console.warn("[Balance Pixel] identify() called before pixel initialized"),Promise.resolve()),se=n=>{typeof window>"u"||(window.balance?window.balance.page(n):console.warn("[Balance Pixel] page() called before pixel initialized"))},ce=(n,r,o)=>{typeof window>"u"||(window.balance?window.balance.purchase(n,r,o):console.warn("[Balance Pixel] purchase() called before pixel initialized"))},le=()=>typeof window>"u"?null:window.balance?.getSessionId()??null,de=()=>typeof window>"u"?null:window.balance?.getFanIdHash()??null,ue=()=>typeof window>"u"?{}:window.balance?.getAttribution()??{},fe=n=>{typeof window>"u"||(window.balance?window.balance.setConsent(n):console.warn("[Balance Pixel] setConsent() called before pixel initialized"))},ge=()=>typeof window>"u"?null:window.balance?.getConsent()??null,pe=n=>typeof window>"u"?!1:window.balance?.hasConsent(n)??!1;return re(me);})();
1
+ var BalancePixel=(()=>{var Se=Object.create;var k=Object.defineProperty;var Ie=Object.getOwnPropertyDescriptor;var _e=Object.getOwnPropertyNames;var Ae=Object.getPrototypeOf,Pe=Object.prototype.hasOwnProperty;var Q=(i=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(i,{get:(r,s)=>(typeof require<"u"?require:r)[s]}):i)(function(i){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+i+'" is not supported')});var Ee=(i,r)=>{for(var s in r)k(i,s,{get:r[s],enumerable:!0})},X=(i,r,s,c)=>{if(r&&typeof r=="object"||typeof r=="function")for(let d of _e(r))!Pe.call(i,d)&&d!==s&&k(i,d,{get:()=>r[d],enumerable:!(c=Ie(r,d))||c.enumerable});return i};var ke=(i,r,s)=>(s=i!=null?Se(Ae(i)):{},X(r||!i||!i.__esModule?k(s,"default",{value:i,enumerable:!0}):s,i)),Ce=i=>X(k({},"__esModule",{value:!0}),i);var je={};Ee(je,{BalanceAnalytics:()=>Z,getAttribution:()=>Le,getConsent:()=>He,getFanIdHash:()=>Be,getSessionId:()=>Ue,hasConsent:()=>ze,identify:()=>Oe,page:()=>Ne,purchase:()=>Re,setConsent:()=>Fe,track:()=>De});var u=ke(Q("react")),C=Q("next/navigation");function Te(){let i=(0,C.usePathname)(),r=(0,C.useSearchParams)(),s=(0,u.useRef)(!0),c=(0,u.useRef)(null);return(0,u.useEffect)(()=>{if(typeof window>"u"||!window.balance)return;let d=i+(r?.toString()||"");if(c.current===d)return;if(s.current&&(s.current=!1,window._balanceInitialPageviewFired)){c.current=d,console.log("[BalanceAnalytics] Skipping initial pageview (script already fired)");return}let v=window.location.href,_=document.title;c.current=d,window.balance.page({url:v,title:_})},[i,r]),null}function Z(){return u.default.createElement(u.Suspense,{fallback:null},u.default.createElement(Te,null))}(function(){function i(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 r=null;function s(){if(!r)try{r=i(navigator.userAgent)}catch{r={device_type:"desktop",browser:"Unknown",os:"Unknown"}}return r}let c=document.currentScript,d=c?.dataset.artistId,v=c?.dataset.projectId,_=c?.dataset.emulator==="true",ee=c?.dataset.debug==="true",T=parseInt(c?.dataset.heartbeatInterval||"30000",10),H=c?.dataset.heartbeat!=="false";if(!d){console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");return}let z="session_id",D="session_timestamp",j="attribution",M="fan_id_hash",K="balance_consent",te=60*60*1e3,O="balance_",N=_?"http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint":"https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint",A=null,f=null,l=null,m={},g=[],R=null,p="session",w=null,ne=0,U=0,b=0,x=!0,ie=2*60*1e3,J=Date.now(),S=!1,a=(...e)=>{ee&&console.log("[BALANCE Pixel]",...e)};function Y(){try{return p==="local"?localStorage:sessionStorage}catch{return null}}function P(e){let t=Y();if(!t)return null;try{let n=O+e,o=t.getItem(n);if(!o&&p==="session")try{o=localStorage.getItem(n)}catch{}return o}catch{return null}}function I(e,t){let n=Y();if(n)try{n.setItem(O+e,t)}catch{}}function B(){if(p!=="local"){a("Upgrading storage tier: session -> local");try{let e=[];for(let t=0;t<sessionStorage.length;t++){let n=sessionStorage.key(t);n?.startsWith(O)&&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);p="local",a(`Storage tier upgraded, migrated ${e.length} items`)}catch(e){console.error("[BALANCE Pixel] Storage migration failed:",e)}}}function q(){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 re(){try{let e=P(z),t=P(D);if(e&&t&&Date.now()-parseInt(t,10)<te)return I(D,Date.now().toString()),e;let n=q();return I(z,n),I(D,Date.now().toString()),n}catch{return q()}}function ae(){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 oe(){try{let e=P(j);if(e){m=JSON.parse(e),a("Loaded attribution:",m);return}let t=ae();Object.keys(t).length>0&&(m=t,I(j,JSON.stringify(t)),a("Captured attribution:",m))}catch{}}function se(){try{f=P(M)}catch{}}function le(){try{let e=localStorage.getItem(K);e&&(l=JSON.parse(e).preferences||null,a("Loaded consent:",l))}catch{}}function ce(e){let t=l;l=e;try{let o={preferences:e,method:"explicit",version:1};localStorage.setItem(K,JSON.stringify(o)),a("Consent saved:",e)}catch(o){console.error("[BALANCE Pixel] Could not save consent:",o)}e.analytics===!0&&B();let n=h({event_name:"consent_updated",metadata:{consent_preferences:e,consent_method:"explicit",previous_consent:t||void 0}});y(n)}function de(){return l}function ue(e){return l?.[e]===!0}async function fe(e){let t=e.toLowerCase().trim(),o=new TextEncoder().encode(t),F=await crypto.subtle.digest("SHA-256",o);return Array.from(new Uint8Array(F)).map(xe=>xe.toString(16).padStart(2,"0")).join("")}function h(e){let t=s(),n={artist_id:d,fan_session_id:A,fan_id_hash:f||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,...e,...m};return v&&!e.projectId&&(n.projectId=v),n}function y(e){g.push(e),a("Event queued:",e.event_name,"(queue:",g.length,")"),g.length>=10&&E()}async function E(){if(g.length===0)return;let e=[...g];g=[],a("Flushing",e.length,"events to",N);try{let t=await fetch(N,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0});if(!t.ok)throw new Error(`HTTP ${t.status}`);a("Events sent successfully")}catch(t){console.error("[BALANCE Pixel] Failed to send events:",t),g.length<50&&g.push(...e)}}function ge(){R&&clearInterval(R),R=window.setInterval(()=>{g.length>0&&E()},5e3)}function L(){b||(ne=Date.now()),b=Date.now(),x=!0,a("Active time tracking started/resumed")}function $(){b&&x&&(U+=Date.now()-b),x=!1,a("Active time tracking paused, accumulated:",U,"ms")}function pe(){let e=U;return x&&b&&(e+=Date.now()-b),e}function me(){J=Date.now(),S&&(S=!1,L(),a("User returned from idle"))}function G(){if(document.visibilityState==="hidden"){a("Skipping heartbeat - tab hidden");return}if(Date.now()-J>ie){S||(S=!0,$(),a("User idle - pausing heartbeat"));return}let e=pe(),t=Math.round(e/1e3),n=h({event_name:"engagement_heartbeat",metadata:{time_on_page_seconds:t,time_on_page_ms:e,heartbeat_interval_ms:T,is_active:x&&!S}});y(n),a("Heartbeat sent:",t,"seconds active")}function we(){if(!H){a('Heartbeat disabled via data-heartbeat="false"');return}w&&clearInterval(w),L(),w=window.setInterval(()=>{G()},T),a("Heartbeat started with interval:",T,"ms")}function be(){w&&(clearInterval(w),w=null),H&&(G(),a("Heartbeat stopped, final time sent"))}function W(e={}){let t=h({event_name:"pageview",page_title:e.title||document.title,source_url:e.url||window.location.href});y(t)}function he(e,t={}){let n=h({event_name:"custom",metadata:{event_type:e,...t}});y(n)}async function ye(e,t={}){try{if(l&&l.analytics===!1){a("Identify skipped - user declined analytics consent");return}f=await fe(e),l?.analytics===!0&&B(),I(M,f);let n=e.split("@"),o=n[0].charAt(0)+"***@"+(n[1]||"");a("Fan identified:",{name:t.name||"(no name)",email:o,hash:f.substring(0,16)+"...",traits:t,storageTier:p});let F=h({event_name:"identify",fan_id_hash:f,metadata:{email_sha256:f,traits:t,consent_preferences:l||void 0,storage_tier:p}});y(F)}catch(n){console.error("[BALANCE Pixel] Failed to identify:",n)}}function ve(e,t="USD",n={}){let o=h({event_name:"purchase",metadata:{revenue:e,currency:t,...n}});y(o)}function V(){le(),l?.analytics===!0?(p="local",a("Storage tier: local (analytics consent granted)")):(p="session",a("Storage tier: session (privacy by default)")),A=re(),se(),l||(l={analytics:!0,marketing:!0,personalization:!0,timestamp:new Date().toISOString()},a("Default consent enabled (all tracking):",l),B()),oe(),ge(),a("Initialized",{artistId:d,projectId:v||"(none - will track to all projects)",sessionId:A,fanIdHash:f,consent:l,storageTier:p,useEmulator:_,endpoint:N}),window._balanceInitialPageviewFired=!0,W(),we(),["mousemove","keydown","scroll","touchstart"].forEach(e=>{document.addEventListener(e,me,{passive:!0})}),window.addEventListener("beforeunload",()=>{be(),E()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?($(),E()):L()})}window.balance={track:he,identify:ye,page:W,purchase:ve,getSessionId:()=>A,getFanIdHash:()=>f,getAttribution:()=>m,setConsent:ce,getConsent:de,hasConsent:ue},document.readyState==="loading"?document.addEventListener("DOMContentLoaded",V):V(),a("Pixel script loaded")})();var De=(i,r)=>{typeof window>"u"||(window.balance?window.balance.track(i,r):console.warn("[Balance Pixel] track() called before pixel initialized:",i))},Oe=(i,r)=>typeof window>"u"?Promise.resolve():window.balance?window.balance.identify(i,r):(console.warn("[Balance Pixel] identify() called before pixel initialized"),Promise.resolve()),Ne=i=>{typeof window>"u"||(window.balance?window.balance.page(i):console.warn("[Balance Pixel] page() called before pixel initialized"))},Re=(i,r,s)=>{typeof window>"u"||(window.balance?window.balance.purchase(i,r,s):console.warn("[Balance Pixel] purchase() called before pixel initialized"))},Ue=()=>typeof window>"u"?null:window.balance?.getSessionId()??null,Be=()=>typeof window>"u"?null:window.balance?.getFanIdHash()??null,Le=()=>typeof window>"u"?{}:window.balance?.getAttribution()??{},Fe=i=>{typeof window>"u"||(window.balance?window.balance.setConsent(i):console.warn("[Balance Pixel] setConsent() called before pixel initialized"))},He=()=>typeof window>"u"?null:window.balance?.getConsent()??null,ze=i=>typeof window>"u"?!1:window.balance?.hasConsent(i)??!1;return Ce(je);})();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@hifilabs/pixel",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "BALANCE Pixel - Lightweight browser tracking script for artist fan analytics",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.esm.js",
7
7
  "types": "./dist/index.esm.d.ts",
8
8
  "publishConfig": {
9
- "access": "public"
9
+ "access": "restricted"
10
10
  },
11
11
  "files": [
12
12
  "dist"