@hifilabs/pixel 0.0.7 → 0.0.9

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.
@@ -1 +1,60 @@
1
1
  export declare function BalanceAnalytics(): null;
2
+
3
+ // SSR-safe wrapper function type declarations
4
+ export declare const track: (eventName: string, properties?: Record<string, any>) => void;
5
+ export declare const identify: (email: string, traits?: Record<string, any>) => Promise<void>;
6
+ export declare const page: (options?: { title?: string; url?: string }) => void;
7
+ export declare const purchase: (revenue: number, currency?: string, properties?: Record<string, any>) => void;
8
+ export declare const getSessionId: () => string | null;
9
+ export declare const getFanIdHash: () => string | null;
10
+ export declare const getAttribution: () => Record<string, string>;
11
+ export declare const setConsent: (preferences: {
12
+ analytics: boolean;
13
+ marketing: boolean;
14
+ personalization: boolean;
15
+ timestamp: string;
16
+ ip_address?: string;
17
+ user_agent?: string;
18
+ }) => void;
19
+ export declare const getConsent: () => any;
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,22 +1,461 @@
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;
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
+ }
13
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
  }
17
39
  function BalanceAnalytics() {
18
40
  return /* @__PURE__ */ React.createElement(Suspense, { fallback: null }, /* @__PURE__ */ React.createElement(BalanceAnalyticsInner, null));
19
41
  }
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
+
373
+ // src/index.esm.ts
374
+ var track = (eventName, properties) => {
375
+ if (typeof window === "undefined")
376
+ return;
377
+ if (window.balance) {
378
+ window.balance.track(eventName, properties);
379
+ } else {
380
+ console.warn("[Balance Pixel] track() called before pixel initialized:", eventName);
381
+ }
382
+ };
383
+ var identify = (email, traits) => {
384
+ if (typeof window === "undefined")
385
+ return Promise.resolve();
386
+ if (window.balance) {
387
+ return window.balance.identify(email, traits);
388
+ } else {
389
+ console.warn("[Balance Pixel] identify() called before pixel initialized");
390
+ return Promise.resolve();
391
+ }
392
+ };
393
+ var page = (options) => {
394
+ if (typeof window === "undefined")
395
+ return;
396
+ if (window.balance) {
397
+ window.balance.page(options);
398
+ } else {
399
+ console.warn("[Balance Pixel] page() called before pixel initialized");
400
+ }
401
+ };
402
+ var purchase = (revenue, currency, properties) => {
403
+ if (typeof window === "undefined")
404
+ return;
405
+ if (window.balance) {
406
+ window.balance.purchase(revenue, currency, properties);
407
+ } else {
408
+ console.warn("[Balance Pixel] purchase() called before pixel initialized");
409
+ }
410
+ };
411
+ var getSessionId = () => {
412
+ if (typeof window === "undefined")
413
+ return null;
414
+ return window.balance?.getSessionId() ?? null;
415
+ };
416
+ var getFanIdHash = () => {
417
+ if (typeof window === "undefined")
418
+ return null;
419
+ return window.balance?.getFanIdHash() ?? null;
420
+ };
421
+ var getAttribution = () => {
422
+ if (typeof window === "undefined")
423
+ return {};
424
+ return window.balance?.getAttribution() ?? {};
425
+ };
426
+ var setConsent = (preferences) => {
427
+ if (typeof window === "undefined")
428
+ return;
429
+ if (window.balance) {
430
+ window.balance.setConsent(preferences);
431
+ } else {
432
+ console.warn("[Balance Pixel] setConsent() called before pixel initialized");
433
+ }
434
+ };
435
+ var getConsent = () => {
436
+ if (typeof window === "undefined")
437
+ return null;
438
+ return window.balance?.getConsent() ?? null;
439
+ };
440
+ var hasConsent = (type) => {
441
+ if (typeof window === "undefined")
442
+ return false;
443
+ return window.balance?.hasConsent(type) ?? false;
444
+ };
20
445
  export {
21
- BalanceAnalytics
446
+ BalanceAnalytics,
447
+ StorageManager,
448
+ getAttribution,
449
+ getConsent,
450
+ getFanIdHash,
451
+ getSessionId,
452
+ getStorageManager,
453
+ hasConsent,
454
+ identify,
455
+ initStorageWithConsent,
456
+ page,
457
+ purchase,
458
+ setConsent,
459
+ track,
460
+ useBalanceIdentify
22
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
  }
@@ -80,12 +95,13 @@ var BalancePixel = (() => {
80
95
  console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");
81
96
  return;
82
97
  }
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";
98
+ const SESSION_KEY = "session_id";
99
+ const SESSION_TIMESTAMP_KEY = "session_timestamp";
100
+ const ATTRIBUTION_KEY = "attribution";
101
+ const FAN_ID_KEY = "fan_id_hash";
87
102
  const CONSENT_STORAGE_KEY = "balance_consent";
88
103
  const SESSION_DURATION = 60 * 60 * 1e3;
104
+ const STORAGE_PREFIX = "balance_";
89
105
  const API_ENDPOINT = useEmulator ? `http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint` : `https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint`;
90
106
  let sessionId = null;
91
107
  let fanIdHash = null;
@@ -93,10 +109,72 @@ var BalancePixel = (() => {
93
109
  let attribution = {};
94
110
  let eventQueue = [];
95
111
  let flushTimer = null;
112
+ let currentStorageTier = "session";
96
113
  const log = (...args) => {
97
114
  if (debug)
98
115
  console.log("[BALANCE Pixel]", ...args);
99
116
  };
117
+ function getStorage() {
118
+ try {
119
+ return currentStorageTier === "local" ? localStorage : sessionStorage;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+ function storageGet(key) {
125
+ const storage = getStorage();
126
+ if (!storage)
127
+ return null;
128
+ try {
129
+ const fullKey = STORAGE_PREFIX + key;
130
+ let value = storage.getItem(fullKey);
131
+ if (!value && currentStorageTier === "session") {
132
+ try {
133
+ value = localStorage.getItem(fullKey);
134
+ } catch {
135
+ }
136
+ }
137
+ return value;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ function storageSet(key, value) {
143
+ const storage = getStorage();
144
+ if (!storage)
145
+ return;
146
+ try {
147
+ storage.setItem(STORAGE_PREFIX + key, value);
148
+ } catch {
149
+ }
150
+ }
151
+ function upgradeStorageTier() {
152
+ if (currentStorageTier === "local")
153
+ return;
154
+ log("Upgrading storage tier: session -> local");
155
+ try {
156
+ const keysToMigrate = [];
157
+ for (let i = 0; i < sessionStorage.length; i++) {
158
+ const key = sessionStorage.key(i);
159
+ if (key?.startsWith(STORAGE_PREFIX)) {
160
+ keysToMigrate.push(key);
161
+ }
162
+ }
163
+ for (const key of keysToMigrate) {
164
+ const value = sessionStorage.getItem(key);
165
+ if (value) {
166
+ localStorage.setItem(key, value);
167
+ }
168
+ }
169
+ for (const key of keysToMigrate) {
170
+ sessionStorage.removeItem(key);
171
+ }
172
+ currentStorageTier = "local";
173
+ log(`Storage tier upgraded, migrated ${keysToMigrate.length} items`);
174
+ } catch (error) {
175
+ console.error("[BALANCE Pixel] Storage migration failed:", error);
176
+ }
177
+ }
100
178
  function generateUUID() {
101
179
  if (crypto && crypto.randomUUID) {
102
180
  return crypto.randomUUID();
@@ -109,18 +187,18 @@ var BalancePixel = (() => {
109
187
  }
110
188
  function getOrCreateSession() {
111
189
  try {
112
- const stored = localStorage.getItem(SESSION_KEY);
113
- const timestamp = localStorage.getItem(SESSION_TIMESTAMP_KEY);
190
+ const stored = storageGet(SESSION_KEY);
191
+ const timestamp = storageGet(SESSION_TIMESTAMP_KEY);
114
192
  if (stored && timestamp) {
115
193
  const age = Date.now() - parseInt(timestamp, 10);
116
194
  if (age < SESSION_DURATION) {
117
- localStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
195
+ storageSet(SESSION_TIMESTAMP_KEY, Date.now().toString());
118
196
  return stored;
119
197
  }
120
198
  }
121
199
  const newId = generateUUID();
122
- localStorage.setItem(SESSION_KEY, newId);
123
- localStorage.setItem(SESSION_TIMESTAMP_KEY, Date.now().toString());
200
+ storageSet(SESSION_KEY, newId);
201
+ storageSet(SESSION_TIMESTAMP_KEY, Date.now().toString());
124
202
  return newId;
125
203
  } catch (e) {
126
204
  return generateUUID();
@@ -138,7 +216,7 @@ var BalancePixel = (() => {
138
216
  }
139
217
  function captureAttribution() {
140
218
  try {
141
- const stored = localStorage.getItem(ATTRIBUTION_KEY);
219
+ const stored = storageGet(ATTRIBUTION_KEY);
142
220
  if (stored) {
143
221
  attribution = JSON.parse(stored);
144
222
  log("Loaded attribution:", attribution);
@@ -147,7 +225,7 @@ var BalancePixel = (() => {
147
225
  const utm = extractUTM();
148
226
  if (Object.keys(utm).length > 0) {
149
227
  attribution = utm;
150
- localStorage.setItem(ATTRIBUTION_KEY, JSON.stringify(utm));
228
+ storageSet(ATTRIBUTION_KEY, JSON.stringify(utm));
151
229
  log("Captured attribution:", attribution);
152
230
  }
153
231
  } catch (e) {
@@ -155,7 +233,7 @@ var BalancePixel = (() => {
155
233
  }
156
234
  function loadFanId() {
157
235
  try {
158
- fanIdHash = localStorage.getItem(FAN_ID_KEY);
236
+ fanIdHash = storageGet(FAN_ID_KEY);
159
237
  } catch (e) {
160
238
  }
161
239
  }
@@ -184,6 +262,9 @@ var BalancePixel = (() => {
184
262
  } catch (e) {
185
263
  console.error("[BALANCE Pixel] Could not save consent:", e);
186
264
  }
265
+ if (preferences.analytics === true) {
266
+ upgradeStorageTier();
267
+ }
187
268
  const event = buildEvent({
188
269
  event_name: "consent_updated",
189
270
  metadata: {
@@ -285,15 +366,23 @@ var BalancePixel = (() => {
285
366
  }
286
367
  async function identify2(email, traits = {}) {
287
368
  try {
369
+ if (consent && consent.analytics === false) {
370
+ log("Identify skipped - user declined analytics consent");
371
+ return;
372
+ }
288
373
  fanIdHash = await hashEmail(email);
289
- localStorage.setItem(FAN_ID_KEY, fanIdHash);
374
+ if (consent?.analytics === true) {
375
+ upgradeStorageTier();
376
+ }
377
+ storageSet(FAN_ID_KEY, fanIdHash);
290
378
  const emailParts = email.split("@");
291
379
  const maskedEmail = emailParts[0].charAt(0) + "***@" + (emailParts[1] || "");
292
380
  log("Fan identified:", {
293
381
  name: traits.name || "(no name)",
294
382
  email: maskedEmail,
295
383
  hash: fanIdHash.substring(0, 16) + "...",
296
- traits
384
+ traits,
385
+ storageTier: currentStorageTier
297
386
  });
298
387
  const event = buildEvent({
299
388
  event_name: "identify",
@@ -301,7 +390,8 @@ var BalancePixel = (() => {
301
390
  metadata: {
302
391
  email_sha256: fanIdHash,
303
392
  traits,
304
- consent_preferences: consent || void 0
393
+ consent_preferences: consent || void 0,
394
+ storage_tier: currentStorageTier
305
395
  }
306
396
  });
307
397
  enqueueEvent(event);
@@ -321,9 +411,16 @@ var BalancePixel = (() => {
321
411
  enqueueEvent(event);
322
412
  }
323
413
  function init() {
414
+ loadConsent();
415
+ if (consent?.analytics === true) {
416
+ currentStorageTier = "local";
417
+ log("Storage tier: local (analytics consent granted)");
418
+ } else {
419
+ currentStorageTier = "session";
420
+ log("Storage tier: session (privacy by default)");
421
+ }
324
422
  sessionId = getOrCreateSession();
325
423
  loadFanId();
326
- loadConsent();
327
424
  if (!consent) {
328
425
  consent = {
329
426
  analytics: true,
@@ -332,6 +429,7 @@ var BalancePixel = (() => {
332
429
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
333
430
  };
334
431
  log("Default consent enabled (all tracking):", consent);
432
+ upgradeStorageTier();
335
433
  }
336
434
  captureAttribution();
337
435
  startFlushTimer();
@@ -341,9 +439,11 @@ var BalancePixel = (() => {
341
439
  sessionId,
342
440
  fanIdHash,
343
441
  consent,
442
+ storageTier: currentStorageTier,
344
443
  useEmulator,
345
444
  endpoint: API_ENDPOINT
346
445
  });
446
+ window._balanceInitialPageviewFired = true;
347
447
  trackPageView();
348
448
  window.addEventListener("beforeunload", () => {
349
449
  flush();
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 te=Object.create;var _=Object.defineProperty;var re=Object.getOwnPropertyDescriptor;var ie=Object.getOwnPropertyNames;var oe=Object.getPrototypeOf,ae=Object.prototype.hasOwnProperty;var z=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(i,o)=>(typeof require<"u"?require:i)[o]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var se=(t,i)=>{for(var o in i)_(t,o,{get:i[o],enumerable:!0})},U=(t,i,o,d)=>{if(i&&typeof i=="object"||typeof i=="function")for(let l of ie(i))!ae.call(t,l)&&l!==o&&_(t,l,{get:()=>i[l],enumerable:!(d=re(i,l))||d.enumerable});return t};var ce=(t,i,o)=>(o=t!=null?te(oe(t)):{},U(i||!t||!t.__esModule?_(o,"default",{value:t,enumerable:!0}):o,t)),le=t=>U(_({},"__esModule",{value:!0}),t);var Se={};se(Se,{BalanceAnalytics:()=>j,getAttribution:()=>ye,getConsent:()=>xe,getFanIdHash:()=>me,getSessionId:()=>we,hasConsent:()=>be,identify:()=>ge,page:()=>fe,purchase:()=>pe,setConsent:()=>he,track:()=>de});var u=ce(z("react")),P=z("next/navigation");function ue(){let t=(0,P.usePathname)(),i=(0,P.useSearchParams)(),o=(0,u.useRef)(!0),d=(0,u.useRef)(null);return(0,u.useEffect)(()=>{if(typeof window>"u"||!window.balance)return;let l=t+(i?.toString()||"");if(d.current===l)return;if(o.current&&(o.current=!1,window._balanceInitialPageviewFired)){d.current=l,console.log("[BalanceAnalytics] Skipping initial pageview (script already fired)");return}let b=window.location.href,m=document.title;d.current=l,window.balance.page({url:b,title:m})},[t,i]),null}function j(){return u.default.createElement(u.Suspense,{fallback:null},u.default.createElement(ue,null))}(function(){let t=document.currentScript,i=t?.dataset.artistId,o=t?.dataset.projectId,d=t?.dataset.emulator==="true",l=t?.dataset.debug==="true";if(!i){console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");return}let b="session_id",m="session_timestamp",N="attribution",R="fan_id_hash",O="balance_consent",H=60*60*1e3,E="balance_",A=d?"http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint":"https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint",S=null,g=null,c=null,w={},f=[],C=null,p="session",s=(...e)=>{l&&console.log("[BALANCE Pixel]",...e)};function B(){try{return p==="local"?localStorage:sessionStorage}catch{return null}}function v(e){let n=B();if(!n)return null;try{let r=E+e,a=n.getItem(r);if(!a&&p==="session")try{a=localStorage.getItem(r)}catch{}return a}catch{return null}}function y(e,n){let r=B();if(r)try{r.setItem(E+e,n)}catch{}}function k(){if(p!=="local"){s("Upgrading storage tier: session -> local");try{let e=[];for(let n=0;n<sessionStorage.length;n++){let r=sessionStorage.key(n);r?.startsWith(E)&&e.push(r)}for(let n of e){let r=sessionStorage.getItem(n);r&&localStorage.setItem(n,r)}for(let n of e)sessionStorage.removeItem(n);p="local",s(`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 n=Math.random()*16|0;return(e==="x"?n:n&3|8).toString(16)})}function M(){try{let e=v(b),n=v(m);if(e&&n&&Date.now()-parseInt(n,10)<H)return y(m,Date.now().toString()),e;let r=F();return y(b,r),y(m,Date.now().toString()),r}catch{return F()}}function K(){let e=new URLSearchParams(window.location.search),n={};return["source","medium","campaign","content","term"].forEach(r=>{let a=e.get(`utm_${r}`);a&&(n[`utm_${r}`]=a)}),n}function J(){try{let e=v(N);if(e){w=JSON.parse(e),s("Loaded attribution:",w);return}let n=K();Object.keys(n).length>0&&(w=n,y(N,JSON.stringify(n)),s("Captured attribution:",w))}catch{}}function Y(){try{g=v(R)}catch{}}function q(){try{let e=localStorage.getItem(O);e&&(c=JSON.parse(e).preferences||null,s("Loaded consent:",c))}catch{}}function $(e){let n=c;c=e;try{let a={preferences:e,method:"explicit",version:1};localStorage.setItem(O,JSON.stringify(a)),s("Consent saved:",e)}catch(a){console.error("[BALANCE Pixel] Could not save consent:",a)}e.analytics===!0&&k();let r=h({event_name:"consent_updated",metadata:{consent_preferences:e,consent_method:"explicit",previous_consent:n||void 0}});x(r)}function G(){return c}function W(e){return c?.[e]===!0}async function Q(e){let n=e.toLowerCase().trim(),a=new TextEncoder().encode(n),T=await crypto.subtle.digest("SHA-256",a);return Array.from(new Uint8Array(T)).map(ne=>ne.toString(16).padStart(2,"0")).join("")}function h(e){let n={artist_id:i,fan_session_id:S,fan_id_hash:g||void 0,timestamp:new Date().toISOString(),source_url:window.location.href,referrer_url:document.referrer||void 0,user_agent:navigator.userAgent,...e,...w};return o&&!e.projectId&&(n.projectId=o),n}function x(e){f.push(e),s("Event queued:",e.event_name,"(queue:",f.length,")"),f.length>=10&&I()}async function I(){if(f.length===0)return;let e=[...f];f=[],s("Flushing",e.length,"events to",A);try{let n=await fetch(A,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({events:e}),keepalive:!0});if(!n.ok)throw new Error(`HTTP ${n.status}`);s("Events sent successfully")}catch(n){console.error("[BALANCE Pixel] Failed to send events:",n),f.length<50&&f.push(...e)}}function V(){C&&clearInterval(C),C=window.setInterval(()=>{f.length>0&&I()},5e3)}function L(e={}){let n=h({event_name:"pageview",page_title:e.title||document.title,source_url:e.url||window.location.href});x(n)}function X(e,n={}){let r=h({event_name:"custom",metadata:{event_type:e,...n}});x(r)}async function Z(e,n={}){try{if(c&&c.analytics===!1){s("Identify skipped - user declined analytics consent");return}g=await Q(e),c?.analytics===!0&&k(),y(R,g);let r=e.split("@"),a=r[0].charAt(0)+"***@"+(r[1]||"");s("Fan identified:",{name:n.name||"(no name)",email:a,hash:g.substring(0,16)+"...",traits:n,storageTier:p});let T=h({event_name:"identify",fan_id_hash:g,metadata:{email_sha256:g,traits:n,consent_preferences:c||void 0,storage_tier:p}});x(T)}catch(r){console.error("[BALANCE Pixel] Failed to identify:",r)}}function ee(e,n="USD",r={}){let a=h({event_name:"purchase",metadata:{revenue:e,currency:n,...r}});x(a)}function D(){q(),c?.analytics===!0?(p="local",s("Storage tier: local (analytics consent granted)")):(p="session",s("Storage tier: session (privacy by default)")),S=M(),Y(),c||(c={analytics:!0,marketing:!0,personalization:!0,timestamp:new Date().toISOString()},s("Default consent enabled (all tracking):",c),k()),J(),V(),s("Initialized",{artistId:i,projectId:o||"(none - will track to all projects)",sessionId:S,fanIdHash:g,consent:c,storageTier:p,useEmulator:d,endpoint:A}),window._balanceInitialPageviewFired=!0,L(),window.addEventListener("beforeunload",()=>{I()}),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&I()})}window.balance={track:X,identify:Z,page:L,purchase:ee,getSessionId:()=>S,getFanIdHash:()=>g,getAttribution:()=>w,setConsent:$,getConsent:G,hasConsent:W},document.readyState==="loading"?document.addEventListener("DOMContentLoaded",D):D(),s("Pixel script loaded")})();var de=(t,i)=>{typeof window>"u"||(window.balance?window.balance.track(t,i):console.warn("[Balance Pixel] track() called before pixel initialized:",t))},ge=(t,i)=>typeof window>"u"?Promise.resolve():window.balance?window.balance.identify(t,i):(console.warn("[Balance Pixel] identify() called before pixel initialized"),Promise.resolve()),fe=t=>{typeof window>"u"||(window.balance?window.balance.page(t):console.warn("[Balance Pixel] page() called before pixel initialized"))},pe=(t,i,o)=>{typeof window>"u"||(window.balance?window.balance.purchase(t,i,o):console.warn("[Balance Pixel] purchase() called before pixel initialized"))},we=()=>typeof window>"u"?null:window.balance?.getSessionId()??null,me=()=>typeof window>"u"?null:window.balance?.getFanIdHash()??null,ye=()=>typeof window>"u"?{}:window.balance?.getAttribution()??{},he=t=>{typeof window>"u"||(window.balance?window.balance.setConsent(t):console.warn("[Balance Pixel] setConsent() called before pixel initialized"))},xe=()=>typeof window>"u"?null:window.balance?.getConsent()??null,be=t=>typeof window>"u"?!1:window.balance?.hasConsent(t)??!1;return le(Se);})();
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@hifilabs/pixel",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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"