@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.
- package/dist/index.esm.d.ts +40 -0
- package/dist/index.esm.js +362 -6
- package/dist/index.js +266 -20
- package/dist/index.min.js +1 -1
- package/package.json +2 -2
package/dist/index.esm.d.ts
CHANGED
|
@@ -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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 = "
|
|
84
|
-
const SESSION_TIMESTAMP_KEY = "
|
|
85
|
-
const ATTRIBUTION_KEY = "
|
|
86
|
-
const FAN_ID_KEY = "
|
|
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 =
|
|
113
|
-
const timestamp =
|
|
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
|
-
|
|
248
|
+
storageSet(SESSION_TIMESTAMP_KEY, Date.now().toString());
|
|
118
249
|
return stored;
|
|
119
250
|
}
|
|
120
251
|
}
|
|
121
252
|
const newId = generateUUID();
|
|
122
|
-
|
|
123
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
9
|
+
"access": "restricted"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist"
|