@hifilabs/pixel 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,554 +1,643 @@
1
- var BalancePixel = (() => {
2
- // src/browser.ts
3
- (function() {
4
- function parseUserAgent(ua) {
5
- let device_type = "desktop";
6
- if (/ipad|tablet|android(?!.*mobile)/i.test(ua))
7
- device_type = "tablet";
8
- else if (/mobile|iphone|android.*mobile|blackberry|iemobile/i.test(ua))
9
- device_type = "mobile";
10
- let browser = "Unknown";
11
- if (/edg/i.test(ua))
12
- browser = "Edge";
13
- else if (/opr|opera/i.test(ua))
14
- browser = "Opera";
15
- else if (/firefox/i.test(ua))
16
- browser = "Firefox";
17
- else if (/chrome/i.test(ua))
18
- browser = "Chrome";
19
- else if (/safari/i.test(ua))
20
- browser = "Safari";
21
- let os = "Unknown";
22
- if (/iphone|ipad/i.test(ua))
23
- os = "iOS";
24
- else if (/android/i.test(ua))
25
- os = "Android";
26
- else if (/windows/i.test(ua))
27
- os = "Windows";
28
- else if (/mac os/i.test(ua))
29
- os = "macOS";
30
- else if (/linux/i.test(ua))
31
- os = "Linux";
32
- else if (/cros/i.test(ua))
33
- os = "ChromeOS";
34
- return { device_type, browser, os };
35
- }
36
- let cachedDeviceInfo = null;
37
- function getDeviceInfo() {
38
- if (!cachedDeviceInfo) {
39
- try {
40
- cachedDeviceInfo = parseUserAgent(navigator.userAgent);
41
- } catch {
42
- cachedDeviceInfo = { device_type: "desktop", browser: "Unknown", os: "Unknown" };
43
- }
44
- }
45
- return cachedDeviceInfo;
46
- }
47
- const currentScript = document.currentScript;
48
- const artistId = currentScript?.dataset.artistId;
49
- const projectId = currentScript?.dataset.projectId;
50
- const useEmulator = currentScript?.dataset.emulator === "true";
51
- const debug = currentScript?.dataset.debug === "true";
52
- const heartbeatInterval = parseInt(currentScript?.dataset.heartbeatInterval || "30000", 10);
53
- const heartbeatEnabled = currentScript?.dataset.heartbeat !== "false";
54
- const explicitSource = currentScript?.dataset.source;
55
- function detectTrackingSource() {
56
- if (explicitSource)
57
- return explicitSource;
58
- const hasDataLayer = typeof window.dataLayer !== "undefined" && Array.isArray(window.dataLayer);
59
- const hasGtag = typeof window.gtag === "function";
60
- if (hasDataLayer && hasGtag) {
61
- return "gtm";
62
- }
63
- return "pixel";
64
- }
65
- let trackingSource = "pixel";
66
- if (!artistId) {
67
- console.error("[BALANCE Pixel] Error: data-artist-id attribute is required");
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var __publicField = (obj, key, value) => {
30
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
31
+ return value;
32
+ };
33
+
34
+ // src/index.ts
35
+ var src_exports = {};
36
+ __export(src_exports, {
37
+ BalanceAnalytics: () => BalanceAnalytics,
38
+ DEFAULT_GTM_CONSENT: () => DEFAULT_GTM_CONSENT,
39
+ GTMProvider: () => GTMProvider,
40
+ StorageManager: () => StorageManager,
41
+ getAttribution: () => getAttribution,
42
+ getConsent: () => getConsent,
43
+ getFanIdHash: () => getFanIdHash,
44
+ getSessionId: () => getSessionId,
45
+ getStorageManager: () => getStorageManager,
46
+ hasConsent: () => hasConsent,
47
+ identify: () => identify,
48
+ initStorageWithConsent: () => initStorageWithConsent,
49
+ page: () => page,
50
+ purchase: () => purchase,
51
+ setConsent: () => setConsent,
52
+ track: () => track,
53
+ useBalanceIdentify: () => useBalanceIdentify,
54
+ useGTMConsent: () => useGTMConsent
55
+ });
56
+ module.exports = __toCommonJS(src_exports);
57
+
58
+ // src/react/BalanceAnalytics.tsx
59
+ var import_react = __toESM(require("react"));
60
+ var import_navigation = require("next/navigation");
61
+ function BalanceAnalyticsInner() {
62
+ const pathname = (0, import_navigation.usePathname)();
63
+ const searchParams = (0, import_navigation.useSearchParams)();
64
+ const isFirstRender = (0, import_react.useRef)(true);
65
+ const lastTrackedPath = (0, import_react.useRef)(null);
66
+ (0, import_react.useEffect)(() => {
67
+ if (typeof window === "undefined" || !window.balance)
68
+ return;
69
+ const currentPath = pathname + (searchParams?.toString() || "");
70
+ if (lastTrackedPath.current === currentPath) {
68
71
  return;
69
72
  }
70
- const SESSION_KEY = "session_id";
71
- const SESSION_TIMESTAMP_KEY = "session_timestamp";
72
- const ATTRIBUTION_KEY = "attribution";
73
- const FAN_ID_KEY = "fan_id_hash";
74
- const CONSENT_STORAGE_KEY = "balance_consent";
75
- const SESSION_DURATION = 60 * 60 * 1e3;
76
- const STORAGE_PREFIX = "balance_";
77
- const API_ENDPOINT = useEmulator ? `http://localhost:5001/artist-os-distro/us-central1/pixelEndpoint` : `https://us-central1-artist-os-distro.cloudfunctions.net/pixelEndpoint`;
78
- let sessionId = null;
79
- let fanIdHash = null;
80
- let consent = null;
81
- let attribution = {};
82
- let eventQueue = [];
83
- let flushTimer = null;
84
- let currentStorageTier = "session";
85
- let heartbeatTimer = null;
86
- let pageStartTime = 0;
87
- let activeTime = 0;
88
- let lastActiveTimestamp = 0;
89
- let isPageVisible = true;
90
- const IDLE_TIMEOUT = 2 * 60 * 1e3;
91
- let lastActivityTime = Date.now();
92
- let isIdle = false;
93
- const log = (...args) => {
94
- if (debug)
95
- console.log("[BALANCE Pixel]", ...args);
96
- };
97
- function getStorage() {
98
- try {
99
- return currentStorageTier === "local" ? localStorage : sessionStorage;
100
- } catch {
101
- return null;
102
- }
103
- }
104
- function storageGet(key) {
105
- const storage = getStorage();
106
- if (!storage)
107
- return null;
108
- try {
109
- const fullKey = STORAGE_PREFIX + key;
110
- let value = storage.getItem(fullKey);
111
- if (!value && currentStorageTier === "session") {
112
- try {
113
- value = localStorage.getItem(fullKey);
114
- } catch {
115
- }
116
- }
117
- return value;
118
- } catch {
119
- return null;
120
- }
121
- }
122
- function storageSet(key, value) {
123
- const storage = getStorage();
124
- if (!storage)
125
- return;
126
- try {
127
- storage.setItem(STORAGE_PREFIX + key, value);
128
- } catch {
129
- }
130
- }
131
- function upgradeStorageTier() {
132
- if (currentStorageTier === "local")
73
+ if (isFirstRender.current) {
74
+ isFirstRender.current = false;
75
+ if (window._balanceInitialPageviewFired) {
76
+ lastTrackedPath.current = currentPath;
77
+ console.log("[BalanceAnalytics] Skipping initial pageview (script already fired)");
133
78
  return;
134
- log("Upgrading storage tier: session -> local");
135
- try {
136
- const keysToMigrate = [];
137
- for (let i = 0; i < sessionStorage.length; i++) {
138
- const key = sessionStorage.key(i);
139
- if (key?.startsWith(STORAGE_PREFIX)) {
140
- keysToMigrate.push(key);
141
- }
142
- }
143
- for (const key of keysToMigrate) {
144
- const value = sessionStorage.getItem(key);
145
- if (value) {
146
- localStorage.setItem(key, value);
147
- }
148
- }
149
- for (const key of keysToMigrate) {
150
- sessionStorage.removeItem(key);
151
- }
152
- currentStorageTier = "local";
153
- log(`Storage tier upgraded, migrated ${keysToMigrate.length} items`);
154
- } catch (error) {
155
- console.error("[BALANCE Pixel] Storage migration failed:", error);
156
79
  }
157
80
  }
158
- function generateUUID() {
159
- if (crypto && crypto.randomUUID) {
160
- return crypto.randomUUID();
161
- }
162
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
163
- const r = Math.random() * 16 | 0;
164
- const v = c === "x" ? r : r & 3 | 8;
165
- return v.toString(16);
166
- });
167
- }
168
- function getOrCreateSession() {
169
- try {
170
- const stored = storageGet(SESSION_KEY);
171
- const timestamp = storageGet(SESSION_TIMESTAMP_KEY);
172
- if (stored && timestamp) {
173
- const age = Date.now() - parseInt(timestamp, 10);
174
- if (age < SESSION_DURATION) {
175
- storageSet(SESSION_TIMESTAMP_KEY, Date.now().toString());
176
- return stored;
177
- }
178
- }
179
- const newId = generateUUID();
180
- storageSet(SESSION_KEY, newId);
181
- storageSet(SESSION_TIMESTAMP_KEY, Date.now().toString());
182
- return newId;
183
- } catch (e) {
184
- return generateUUID();
185
- }
186
- }
187
- function extractUTM() {
188
- const params = new URLSearchParams(window.location.search);
189
- const utm = {};
190
- ["source", "medium", "campaign", "content", "term"].forEach((param) => {
191
- const value = params.get(`utm_${param}`);
192
- if (value)
193
- utm[`utm_${param}`] = value;
194
- });
195
- return utm;
196
- }
197
- function captureAttribution() {
198
- try {
199
- const stored = storageGet(ATTRIBUTION_KEY);
200
- if (stored) {
201
- attribution = JSON.parse(stored);
202
- log("Loaded attribution:", attribution);
203
- return;
204
- }
205
- const utm = extractUTM();
206
- if (Object.keys(utm).length > 0) {
207
- attribution = utm;
208
- storageSet(ATTRIBUTION_KEY, JSON.stringify(utm));
209
- log("Captured attribution:", attribution);
81
+ const url = window.location.href;
82
+ const title = document.title;
83
+ lastTrackedPath.current = currentPath;
84
+ window.balance.page({ url, title });
85
+ }, [pathname, searchParams]);
86
+ return null;
87
+ }
88
+ function BalanceAnalytics() {
89
+ return /* @__PURE__ */ import_react.default.createElement(import_react.Suspense, { fallback: null }, /* @__PURE__ */ import_react.default.createElement(BalanceAnalyticsInner, null));
90
+ }
91
+
92
+ // src/react/useBalanceIdentify.ts
93
+ var import_react2 = require("react");
94
+
95
+ // src/storage/StorageManager.ts
96
+ var DEFAULT_PREFIX = "balance_";
97
+ var StorageManager = class {
98
+ constructor(config) {
99
+ __publicField(this, "prefix");
100
+ __publicField(this, "currentTier");
101
+ this.prefix = config?.prefix ?? DEFAULT_PREFIX;
102
+ this.currentTier = config?.tier ?? "session";
103
+ }
104
+ /**
105
+ * Get the appropriate storage based on current tier
106
+ */
107
+ getStorage() {
108
+ if (typeof window === "undefined")
109
+ return null;
110
+ try {
111
+ return this.currentTier === "local" ? localStorage : sessionStorage;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+ /**
117
+ * Get current storage tier
118
+ */
119
+ getTier() {
120
+ return this.currentTier;
121
+ }
122
+ /**
123
+ * Upgrade storage tier (e.g., when user gives consent)
124
+ * Automatically migrates data from sessionStorage to localStorage
125
+ */
126
+ upgradeTier(newTier) {
127
+ if (typeof window === "undefined")
128
+ return;
129
+ if (this.currentTier === newTier)
130
+ return;
131
+ const oldTier = this.currentTier;
132
+ if (oldTier === "session" && newTier === "local") {
133
+ this.migrateToLocal();
134
+ }
135
+ this.currentTier = newTier;
136
+ console.log(`[StorageManager] Upgraded tier: ${oldTier} -> ${newTier}`);
137
+ }
138
+ /**
139
+ * Downgrade storage tier (e.g., when user revokes consent)
140
+ * Clears localStorage data and falls back to sessionStorage
141
+ */
142
+ downgradeTier(newTier) {
143
+ if (typeof window === "undefined")
144
+ return;
145
+ if (this.currentTier === newTier)
146
+ return;
147
+ const oldTier = this.currentTier;
148
+ if (oldTier === "local" && newTier === "session") {
149
+ this.clearLocalStorage();
150
+ }
151
+ this.currentTier = newTier;
152
+ console.log(`[StorageManager] Downgraded tier: ${oldTier} -> ${newTier}`);
153
+ }
154
+ /**
155
+ * Migrate all prefixed data from sessionStorage to localStorage
156
+ */
157
+ migrateToLocal() {
158
+ if (typeof window === "undefined")
159
+ return;
160
+ try {
161
+ const keysToMigrate = [];
162
+ for (let i = 0; i < sessionStorage.length; i++) {
163
+ const key = sessionStorage.key(i);
164
+ if (key?.startsWith(this.prefix)) {
165
+ keysToMigrate.push(key);
210
166
  }
211
- } catch (e) {
212
167
  }
213
- }
214
- function loadFanId() {
215
- try {
216
- fanIdHash = storageGet(FAN_ID_KEY);
217
- } catch (e) {
218
- }
219
- }
220
- function loadConsent() {
221
- try {
222
- const stored = localStorage.getItem(CONSENT_STORAGE_KEY);
223
- if (stored) {
224
- const parsed = JSON.parse(stored);
225
- consent = parsed.preferences || null;
226
- log("Loaded consent:", consent);
168
+ for (const key of keysToMigrate) {
169
+ const value = sessionStorage.getItem(key);
170
+ if (value) {
171
+ localStorage.setItem(key, value);
172
+ console.log(`[StorageManager] Migrated: ${key}`);
227
173
  }
228
- } catch (e) {
229
- }
230
- }
231
- function setConsent(preferences) {
232
- const previousConsent = consent;
233
- consent = preferences;
234
- try {
235
- const storage = {
236
- preferences,
237
- method: "explicit",
238
- version: 1
239
- };
240
- localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(storage));
241
- log("Consent saved:", preferences);
242
- } catch (e) {
243
- console.error("[BALANCE Pixel] Could not save consent:", e);
244
174
  }
245
- if (preferences.analytics === true) {
246
- upgradeStorageTier();
175
+ for (const key of keysToMigrate) {
176
+ sessionStorage.removeItem(key);
247
177
  }
248
- const event = buildEvent({
249
- event_name: "consent_updated",
250
- metadata: {
251
- consent_preferences: preferences,
252
- consent_method: "explicit",
253
- previous_consent: previousConsent || void 0
178
+ console.log(`[StorageManager] Migration complete: ${keysToMigrate.length} items`);
179
+ } catch (error) {
180
+ console.error("[StorageManager] Migration failed:", error);
181
+ }
182
+ }
183
+ /**
184
+ * Clear all prefixed data from localStorage
185
+ */
186
+ clearLocalStorage() {
187
+ if (typeof window === "undefined")
188
+ return;
189
+ try {
190
+ const keysToRemove = [];
191
+ for (let i = 0; i < localStorage.length; i++) {
192
+ const key = localStorage.key(i);
193
+ if (key?.startsWith(this.prefix)) {
194
+ keysToRemove.push(key);
254
195
  }
255
- });
256
- enqueueEvent(event);
257
- }
258
- function getConsent() {
259
- return consent;
260
- }
261
- function hasConsent(type) {
262
- return consent?.[type] === true;
263
- }
264
- async function hashEmail(email) {
265
- const normalized = email.toLowerCase().trim();
266
- const encoder = new TextEncoder();
267
- const data = encoder.encode(normalized);
268
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
269
- const hashArray = Array.from(new Uint8Array(hashBuffer));
270
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
271
- }
272
- function buildEvent(partial) {
273
- const deviceInfo = getDeviceInfo();
274
- const base = {
275
- artist_id: artistId,
276
- fan_session_id: sessionId,
277
- fan_id_hash: fanIdHash || void 0,
278
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
279
- source_url: window.location.href,
280
- referrer_url: document.referrer || void 0,
281
- user_agent: navigator.userAgent,
282
- // Device info (parsed client-side)
283
- device_type: deviceInfo.device_type,
284
- browser: deviceInfo.browser,
285
- os: deviceInfo.os,
286
- // Tracking source (gtm, pixel, etc.) - enables filtering in artistHQ
287
- tracking_source: trackingSource,
288
- ...partial,
289
- ...attribution
290
- };
291
- if (projectId && !partial.projectId) {
292
- base.projectId = projectId;
293
196
  }
294
- return base;
295
- }
296
- function enqueueEvent(event) {
297
- eventQueue.push(event);
298
- log("Event queued:", event.event_name, "(queue:", eventQueue.length, ")");
299
- if (eventQueue.length >= 10) {
300
- flush();
197
+ for (const key of keysToRemove) {
198
+ localStorage.removeItem(key);
301
199
  }
302
- }
303
- async function flush() {
304
- if (eventQueue.length === 0)
305
- return;
306
- const events = [...eventQueue];
307
- eventQueue = [];
308
- log("Flushing", events.length, "events to", API_ENDPOINT);
309
- try {
310
- const response = await fetch(API_ENDPOINT, {
311
- method: "POST",
312
- headers: { "Content-Type": "application/json" },
313
- body: JSON.stringify({ events }),
314
- keepalive: true
315
- // Send even if page is closing
316
- });
317
- if (!response.ok) {
318
- throw new Error(`HTTP ${response.status}`);
319
- }
320
- log("Events sent successfully");
321
- } catch (error) {
322
- console.error("[BALANCE Pixel] Failed to send events:", error);
323
- if (eventQueue.length < 50) {
324
- eventQueue.push(...events);
200
+ console.log(`[StorageManager] Cleared ${keysToRemove.length} items from localStorage`);
201
+ } catch (error) {
202
+ console.error("[StorageManager] Clear failed:", error);
203
+ }
204
+ }
205
+ /**
206
+ * Get item from current storage tier
207
+ */
208
+ getItem(key) {
209
+ const storage = this.getStorage();
210
+ if (!storage)
211
+ return null;
212
+ try {
213
+ const fullKey = this.prefix + key;
214
+ let value = storage.getItem(fullKey);
215
+ if (!value && this.currentTier === "session") {
216
+ try {
217
+ value = localStorage.getItem(fullKey);
218
+ } catch {
325
219
  }
326
220
  }
327
- }
328
- function startFlushTimer() {
329
- if (flushTimer)
330
- clearInterval(flushTimer);
331
- flushTimer = window.setInterval(() => {
332
- if (eventQueue.length > 0)
333
- flush();
334
- }, 5e3);
335
- }
336
- function startActiveTimeTracking() {
337
- if (!lastActiveTimestamp) {
338
- pageStartTime = Date.now();
339
- }
340
- lastActiveTimestamp = Date.now();
341
- isPageVisible = true;
342
- log("Active time tracking started/resumed");
343
- }
344
- function pauseActiveTimeTracking() {
345
- if (lastActiveTimestamp && isPageVisible) {
346
- activeTime += Date.now() - lastActiveTimestamp;
347
- }
348
- isPageVisible = false;
349
- log("Active time tracking paused, accumulated:", activeTime, "ms");
350
- }
351
- function getCumulativeActiveTime() {
352
- let total = activeTime;
353
- if (isPageVisible && lastActiveTimestamp) {
354
- total += Date.now() - lastActiveTimestamp;
355
- }
356
- return total;
357
- }
358
- function resetIdleTimer() {
359
- lastActivityTime = Date.now();
360
- if (isIdle) {
361
- isIdle = false;
362
- startActiveTimeTracking();
363
- log("User returned from idle");
364
- }
365
- }
366
- function sendHeartbeat() {
367
- if (document.visibilityState === "hidden") {
368
- log("Skipping heartbeat - tab hidden");
221
+ return value;
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+ /**
227
+ * Set item in current storage tier
228
+ */
229
+ setItem(key, value) {
230
+ const storage = this.getStorage();
231
+ if (!storage)
232
+ return;
233
+ try {
234
+ const fullKey = this.prefix + key;
235
+ storage.setItem(fullKey, value);
236
+ } catch (error) {
237
+ console.error("[StorageManager] setItem failed:", error);
238
+ }
239
+ }
240
+ /**
241
+ * Remove item from all storage tiers
242
+ */
243
+ removeItem(key) {
244
+ if (typeof window === "undefined")
245
+ return;
246
+ const fullKey = this.prefix + key;
247
+ try {
248
+ sessionStorage.removeItem(fullKey);
249
+ } catch {
250
+ }
251
+ try {
252
+ localStorage.removeItem(fullKey);
253
+ } catch {
254
+ }
255
+ }
256
+ /**
257
+ * Get JSON-parsed item
258
+ */
259
+ getJSON(key) {
260
+ const value = this.getItem(key);
261
+ if (!value)
262
+ return null;
263
+ try {
264
+ return JSON.parse(value);
265
+ } catch {
266
+ return null;
267
+ }
268
+ }
269
+ /**
270
+ * Set JSON-stringified item
271
+ */
272
+ setJSON(key, value) {
273
+ try {
274
+ this.setItem(key, JSON.stringify(value));
275
+ } catch {
276
+ }
277
+ }
278
+ };
279
+ var storageManagerInstance = null;
280
+ function getStorageManager() {
281
+ if (!storageManagerInstance) {
282
+ storageManagerInstance = new StorageManager();
283
+ }
284
+ return storageManagerInstance;
285
+ }
286
+ function initStorageWithConsent(hasAnalyticsConsent) {
287
+ const manager = getStorageManager();
288
+ if (hasAnalyticsConsent) {
289
+ manager.upgradeTier("local");
290
+ } else {
291
+ manager.downgradeTier("session");
292
+ }
293
+ return manager;
294
+ }
295
+
296
+ // src/react/useBalanceIdentify.ts
297
+ var PENDING_IDENTIFY_KEY = "pending_identify";
298
+ var MAX_PENDING_AGE_MS = 30 * 60 * 1e3;
299
+ function checkAnalyticsConsent() {
300
+ if (typeof window === "undefined")
301
+ return false;
302
+ try {
303
+ if (window.balance?.hasConsent) {
304
+ return window.balance.hasConsent("analytics");
305
+ }
306
+ const stored = localStorage.getItem("balance_consent");
307
+ if (stored) {
308
+ const consent = JSON.parse(stored);
309
+ return consent?.preferences?.analytics !== false;
310
+ }
311
+ return true;
312
+ } catch {
313
+ return true;
314
+ }
315
+ }
316
+ function useBalanceIdentify() {
317
+ const pollIntervalRef = (0, import_react2.useRef)(null);
318
+ const hasProcessedPending = (0, import_react2.useRef)(false);
319
+ const storageManager = typeof window !== "undefined" ? getStorageManager() : null;
320
+ const getPendingIdentify = (0, import_react2.useCallback)(() => {
321
+ if (!storageManager)
322
+ return null;
323
+ const pending = storageManager.getJSON(PENDING_IDENTIFY_KEY);
324
+ if (!pending)
325
+ return null;
326
+ if (Date.now() - pending.timestamp > MAX_PENDING_AGE_MS) {
327
+ storageManager.removeItem(PENDING_IDENTIFY_KEY);
328
+ return null;
329
+ }
330
+ return pending;
331
+ }, [storageManager]);
332
+ const setPendingIdentify = (0, import_react2.useCallback)((email, traits) => {
333
+ if (!storageManager)
334
+ return;
335
+ storageManager.setJSON(PENDING_IDENTIFY_KEY, {
336
+ email,
337
+ traits,
338
+ timestamp: Date.now()
339
+ });
340
+ }, [storageManager]);
341
+ const clearPendingIdentify = (0, import_react2.useCallback)(() => {
342
+ if (!storageManager)
343
+ return;
344
+ storageManager.removeItem(PENDING_IDENTIFY_KEY);
345
+ }, [storageManager]);
346
+ const identify2 = (0, import_react2.useCallback)((email, traits) => {
347
+ const hasConsent2 = checkAnalyticsConsent();
348
+ if (!hasConsent2) {
349
+ console.log("[useBalanceIdentify] Skipping identify - user declined analytics consent");
350
+ return false;
351
+ }
352
+ if (storageManager) {
353
+ initStorageWithConsent(hasConsent2);
354
+ }
355
+ if (typeof window !== "undefined" && window.balance?.identify) {
356
+ window.balance.identify(email, traits);
357
+ console.log("[useBalanceIdentify] User identified:", email.split("@")[0] + "***");
358
+ clearPendingIdentify();
359
+ return true;
360
+ }
361
+ console.log("[useBalanceIdentify] Pixel not ready, queueing identify");
362
+ setPendingIdentify(email, traits);
363
+ return false;
364
+ }, [storageManager, clearPendingIdentify, setPendingIdentify]);
365
+ const processPendingIdentify = (0, import_react2.useCallback)(() => {
366
+ if (hasProcessedPending.current)
367
+ return;
368
+ const pending = getPendingIdentify();
369
+ if (!pending)
370
+ return;
371
+ if (typeof window !== "undefined" && window.balance?.identify) {
372
+ const hasConsent2 = checkAnalyticsConsent();
373
+ if (!hasConsent2) {
374
+ console.log("[useBalanceIdentify] Clearing pending identify - user declined analytics consent");
375
+ clearPendingIdentify();
376
+ hasProcessedPending.current = true;
369
377
  return;
370
378
  }
371
- if (Date.now() - lastActivityTime > IDLE_TIMEOUT) {
372
- if (!isIdle) {
373
- isIdle = true;
374
- pauseActiveTimeTracking();
375
- log("User idle - pausing heartbeat");
376
- }
377
- return;
379
+ if (storageManager) {
380
+ initStorageWithConsent(hasConsent2);
378
381
  }
379
- const timeOnPage = getCumulativeActiveTime();
380
- const timeOnPageSeconds = Math.round(timeOnPage / 1e3);
381
- const event = buildEvent({
382
- event_name: "engagement_heartbeat",
383
- metadata: {
384
- time_on_page_seconds: timeOnPageSeconds,
385
- time_on_page_ms: timeOnPage,
386
- heartbeat_interval_ms: heartbeatInterval,
387
- is_active: isPageVisible && !isIdle
382
+ console.log("[useBalanceIdentify] Processing pending identify");
383
+ window.balance.identify(pending.email, pending.traits);
384
+ clearPendingIdentify();
385
+ hasProcessedPending.current = true;
386
+ }
387
+ }, [getPendingIdentify, clearPendingIdentify, storageManager]);
388
+ (0, import_react2.useEffect)(() => {
389
+ processPendingIdentify();
390
+ const pending = getPendingIdentify();
391
+ if (pending && !hasProcessedPending.current) {
392
+ pollIntervalRef.current = setInterval(() => {
393
+ processPendingIdentify();
394
+ if (hasProcessedPending.current || !getPendingIdentify()) {
395
+ if (pollIntervalRef.current) {
396
+ clearInterval(pollIntervalRef.current);
397
+ pollIntervalRef.current = null;
398
+ }
388
399
  }
389
- });
390
- enqueueEvent(event);
391
- log("Heartbeat sent:", timeOnPageSeconds, "seconds active");
400
+ }, 500);
392
401
  }
393
- function startHeartbeat() {
394
- if (!heartbeatEnabled) {
395
- log('Heartbeat disabled via data-heartbeat="false"');
396
- return;
397
- }
398
- if (heartbeatTimer)
399
- clearInterval(heartbeatTimer);
400
- startActiveTimeTracking();
401
- heartbeatTimer = window.setInterval(() => {
402
- sendHeartbeat();
403
- }, heartbeatInterval);
404
- log("Heartbeat started with interval:", heartbeatInterval, "ms");
405
- }
406
- function stopHeartbeat() {
407
- if (heartbeatTimer) {
408
- clearInterval(heartbeatTimer);
409
- heartbeatTimer = null;
402
+ return () => {
403
+ if (pollIntervalRef.current) {
404
+ clearInterval(pollIntervalRef.current);
405
+ pollIntervalRef.current = null;
410
406
  }
411
- if (heartbeatEnabled) {
412
- sendHeartbeat();
413
- log("Heartbeat stopped, final time sent");
414
- }
415
- }
416
- function trackPageView(options = {}) {
417
- const event = buildEvent({
418
- event_name: "pageview",
419
- page_title: options.title || document.title,
420
- source_url: options.url || window.location.href
421
- });
422
- enqueueEvent(event);
423
- }
424
- function track(eventName, properties = {}) {
425
- const event = buildEvent({
426
- event_name: "custom",
427
- metadata: {
428
- event_type: eventName,
429
- ...properties
430
- }
431
- });
432
- enqueueEvent(event);
433
- }
434
- async function identify(email, traits = {}) {
435
- try {
436
- if (consent && consent.analytics === false) {
437
- log("Identify skipped - user declined analytics consent");
438
- return;
439
- }
440
- fanIdHash = await hashEmail(email);
441
- if (consent?.analytics === true) {
442
- upgradeStorageTier();
443
- }
444
- storageSet(FAN_ID_KEY, fanIdHash);
445
- const emailParts = email.split("@");
446
- const maskedEmail = emailParts[0].charAt(0) + "***@" + (emailParts[1] || "");
447
- log("Fan identified:", {
448
- name: traits.name || "(no name)",
449
- email: maskedEmail,
450
- hash: fanIdHash.substring(0, 16) + "...",
451
- traits,
452
- storageTier: currentStorageTier
453
- });
454
- const event = buildEvent({
455
- event_name: "identify",
456
- fan_id_hash: fanIdHash,
457
- metadata: {
458
- email_sha256: fanIdHash,
459
- traits,
460
- consent_preferences: consent || void 0,
461
- storage_tier: currentStorageTier
462
- }
463
- });
464
- enqueueEvent(event);
465
- } catch (error) {
466
- console.error("[BALANCE Pixel] Failed to identify:", error);
407
+ };
408
+ }, [processPendingIdentify, getPendingIdentify]);
409
+ return {
410
+ /**
411
+ * Identify a user with BALANCE pixel.
412
+ * Handles consent checks, queuing, and storage tier automatically.
413
+ */
414
+ identify: identify2,
415
+ /**
416
+ * Check if analytics consent is currently granted
417
+ */
418
+ hasAnalyticsConsent: checkAnalyticsConsent
419
+ };
420
+ }
421
+
422
+ // src/react/GTMProvider.tsx
423
+ var import_react4 = __toESM(require("react"));
424
+ var import_script = __toESM(require("next/script"));
425
+
426
+ // src/react/useGTMConsent.ts
427
+ var import_react3 = require("react");
428
+ function useGTMConsent(options = {}) {
429
+ const { pollInterval = 1e3, debug = false, enabled = true } = options;
430
+ const lastConsentRef = (0, import_react3.useRef)("");
431
+ const log = (0, import_react3.useCallback)(
432
+ (...args) => {
433
+ if (debug) {
434
+ console.log("[useGTMConsent]", ...args);
467
435
  }
436
+ },
437
+ [debug]
438
+ );
439
+ const mapPixelConsentToGTM = (0, import_react3.useCallback)(() => {
440
+ if (typeof window === "undefined" || !window.balance) {
441
+ return {
442
+ ad_storage: "denied",
443
+ ad_user_data: "denied",
444
+ ad_personalization: "denied",
445
+ analytics_storage: "denied"
446
+ };
468
447
  }
469
- function purchase(revenue, currency = "USD", properties = {}) {
470
- const event = buildEvent({
471
- event_name: "purchase",
472
- metadata: {
473
- revenue,
474
- currency,
475
- ...properties
476
- }
477
- });
478
- enqueueEvent(event);
479
- }
480
- function init() {
481
- trackingSource = detectTrackingSource();
482
- log("Tracking source detected:", trackingSource);
483
- loadConsent();
484
- if (consent?.analytics === true) {
485
- currentStorageTier = "local";
486
- log("Storage tier: local (analytics consent granted)");
487
- } else {
488
- currentStorageTier = "session";
489
- log("Storage tier: session (privacy by default)");
490
- }
491
- sessionId = getOrCreateSession();
492
- loadFanId();
493
- if (!consent) {
494
- consent = {
495
- analytics: true,
496
- marketing: true,
497
- personalization: true,
498
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
448
+ const hasAnalytics = window.balance.hasConsent("analytics");
449
+ const hasMarketing = window.balance.hasConsent("marketing");
450
+ const analyticsState = hasAnalytics ? "granted" : "denied";
451
+ const marketingState = hasMarketing ? "granted" : "denied";
452
+ return {
453
+ analytics_storage: analyticsState,
454
+ ad_storage: marketingState,
455
+ ad_user_data: marketingState,
456
+ ad_personalization: marketingState
457
+ };
458
+ }, []);
459
+ const updateGTMConsent = (0, import_react3.useCallback)(
460
+ (consentConfig) => {
461
+ if (typeof window === "undefined")
462
+ return;
463
+ window.dataLayer = window.dataLayer || [];
464
+ if (typeof window.gtag !== "function") {
465
+ window.gtag = function gtag(...args) {
466
+ window.dataLayer.push(args);
499
467
  };
500
- log("Default consent enabled (all tracking):", consent);
501
- upgradeStorageTier();
502
468
  }
503
- captureAttribution();
504
- startFlushTimer();
505
- log("Initialized", {
506
- artistId,
507
- projectId: projectId || "(none - will track to all projects)",
508
- sessionId,
509
- fanIdHash,
510
- consent,
511
- storageTier: currentStorageTier,
512
- trackingSource,
513
- useEmulator,
514
- endpoint: API_ENDPOINT
515
- });
516
- window._balanceInitialPageviewFired = true;
517
- trackPageView();
518
- startHeartbeat();
519
- ["mousemove", "keydown", "scroll", "touchstart"].forEach((eventType) => {
520
- document.addEventListener(eventType, resetIdleTimer, { passive: true });
521
- });
522
- window.addEventListener("beforeunload", () => {
523
- stopHeartbeat();
524
- flush();
525
- });
526
- document.addEventListener("visibilitychange", () => {
527
- if (document.visibilityState === "hidden") {
528
- pauseActiveTimeTracking();
529
- flush();
530
- } else {
531
- startActiveTimeTracking();
532
- }
533
- });
534
- }
535
- window.balance = {
536
- track,
537
- identify,
538
- page: trackPageView,
539
- purchase,
540
- getSessionId: () => sessionId,
541
- getFanIdHash: () => fanIdHash,
542
- getAttribution: () => attribution,
543
- setConsent,
544
- getConsent,
545
- hasConsent
469
+ window.gtag("consent", "update", consentConfig);
470
+ log("Pushed consent update to GTM:", consentConfig);
471
+ },
472
+ [log]
473
+ );
474
+ const syncConsent = (0, import_react3.useCallback)(() => {
475
+ const currentConsent = mapPixelConsentToGTM();
476
+ const consentKey = JSON.stringify(currentConsent);
477
+ if (consentKey !== lastConsentRef.current) {
478
+ lastConsentRef.current = consentKey;
479
+ updateGTMConsent(currentConsent);
480
+ }
481
+ }, [mapPixelConsentToGTM, updateGTMConsent]);
482
+ (0, import_react3.useEffect)(() => {
483
+ if (typeof window === "undefined" || !enabled)
484
+ return;
485
+ syncConsent();
486
+ const intervalId = setInterval(syncConsent, pollInterval);
487
+ const handleStorageChange = (e) => {
488
+ if (e.key === "balance_consent") {
489
+ log("Consent changed in another tab, syncing...");
490
+ syncConsent();
491
+ }
492
+ };
493
+ window.addEventListener("storage", handleStorageChange);
494
+ return () => {
495
+ clearInterval(intervalId);
496
+ window.removeEventListener("storage", handleStorageChange);
546
497
  };
547
- if (document.readyState === "loading") {
548
- document.addEventListener("DOMContentLoaded", init);
549
- } else {
550
- init();
498
+ }, [syncConsent, pollInterval, log, enabled]);
499
+ return {
500
+ /**
501
+ * Manually trigger a consent sync
502
+ */
503
+ syncConsent,
504
+ /**
505
+ * Get current consent config
506
+ */
507
+ getConsentConfig: mapPixelConsentToGTM
508
+ };
509
+ }
510
+
511
+ // src/types/gtm.ts
512
+ var DEFAULT_GTM_CONSENT = {
513
+ ad_storage: "denied",
514
+ ad_user_data: "denied",
515
+ ad_personalization: "denied",
516
+ analytics_storage: "denied"
517
+ };
518
+
519
+ // src/react/GTMProvider.tsx
520
+ function GTMProvider({ gtmId, children, debug = false }) {
521
+ const resolvedGtmId = gtmId || process.env.NEXT_PUBLIC_GTM_ID;
522
+ useGTMConsent({ debug, enabled: !!resolvedGtmId });
523
+ if (!resolvedGtmId) {
524
+ if (debug) {
525
+ console.log("[GTMProvider] No GTM ID configured, skipping GTM initialization");
526
+ }
527
+ return /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, children);
528
+ }
529
+ const consentJson = JSON.stringify(DEFAULT_GTM_CONSENT);
530
+ const consentScript = `
531
+ window.dataLayer = window.dataLayer || [];
532
+ function gtag(){dataLayer.push(arguments);}
533
+ window.gtag = gtag;
534
+ gtag('consent', 'default', ${consentJson});
535
+ gtag('set', 'wait_for_update', 500);
536
+ ${debug ? "console.log('[GTM] Default consent initialized (denied)');" : ""}
537
+ `;
538
+ return /* @__PURE__ */ import_react4.default.createElement(import_react4.default.Fragment, null, /* @__PURE__ */ import_react4.default.createElement(
539
+ import_script.default,
540
+ {
541
+ id: "gtm-consent-default",
542
+ strategy: "beforeInteractive",
543
+ dangerouslySetInnerHTML: { __html: consentScript }
544
+ }
545
+ ), /* @__PURE__ */ import_react4.default.createElement(
546
+ import_script.default,
547
+ {
548
+ id: "gtm-script",
549
+ strategy: "afterInteractive",
550
+ dangerouslySetInnerHTML: {
551
+ __html: `
552
+ (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
553
+ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
554
+ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
555
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
556
+ })(window,document,'script','dataLayer','${resolvedGtmId}');
557
+ `
558
+ }
551
559
  }
552
- log("Pixel script loaded");
553
- })();
554
- })();
560
+ ), /* @__PURE__ */ import_react4.default.createElement("noscript", null, /* @__PURE__ */ import_react4.default.createElement(
561
+ "iframe",
562
+ {
563
+ src: `https://www.googletagmanager.com/ns.html?id=${resolvedGtmId}`,
564
+ height: "0",
565
+ width: "0",
566
+ style: { display: "none", visibility: "hidden" },
567
+ title: "GTM"
568
+ }
569
+ )), children);
570
+ }
571
+
572
+ // src/index.esm.ts
573
+ var track = (eventName, properties) => {
574
+ if (typeof window === "undefined")
575
+ return;
576
+ if (window.balance) {
577
+ window.balance.track(eventName, properties);
578
+ } else {
579
+ console.warn("[Balance Pixel] track() called before pixel initialized:", eventName);
580
+ }
581
+ };
582
+ var identify = (email, traits) => {
583
+ if (typeof window === "undefined")
584
+ return Promise.resolve();
585
+ if (window.balance) {
586
+ return window.balance.identify(email, traits);
587
+ } else {
588
+ console.warn("[Balance Pixel] identify() called before pixel initialized");
589
+ return Promise.resolve();
590
+ }
591
+ };
592
+ var page = (options) => {
593
+ if (typeof window === "undefined")
594
+ return;
595
+ if (window.balance) {
596
+ window.balance.page(options);
597
+ } else {
598
+ console.warn("[Balance Pixel] page() called before pixel initialized");
599
+ }
600
+ };
601
+ var purchase = (revenue, currency, properties) => {
602
+ if (typeof window === "undefined")
603
+ return;
604
+ if (window.balance) {
605
+ window.balance.purchase(revenue, currency, properties);
606
+ } else {
607
+ console.warn("[Balance Pixel] purchase() called before pixel initialized");
608
+ }
609
+ };
610
+ var getSessionId = () => {
611
+ if (typeof window === "undefined")
612
+ return null;
613
+ return window.balance?.getSessionId() ?? null;
614
+ };
615
+ var getFanIdHash = () => {
616
+ if (typeof window === "undefined")
617
+ return null;
618
+ return window.balance?.getFanIdHash() ?? null;
619
+ };
620
+ var getAttribution = () => {
621
+ if (typeof window === "undefined")
622
+ return {};
623
+ return window.balance?.getAttribution() ?? {};
624
+ };
625
+ var setConsent = (preferences) => {
626
+ if (typeof window === "undefined")
627
+ return;
628
+ if (window.balance) {
629
+ window.balance.setConsent(preferences);
630
+ } else {
631
+ console.warn("[Balance Pixel] setConsent() called before pixel initialized");
632
+ }
633
+ };
634
+ var getConsent = () => {
635
+ if (typeof window === "undefined")
636
+ return null;
637
+ return window.balance?.getConsent() ?? null;
638
+ };
639
+ var hasConsent = (type) => {
640
+ if (typeof window === "undefined")
641
+ return false;
642
+ return window.balance?.hasConsent(type) ?? false;
643
+ };