@particle-academy/fancy-pixel 0.1.0 → 0.2.1

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.cjs CHANGED
@@ -31,6 +31,533 @@ function sendPixelBeacon(endpoint, payload) {
31
31
  return false;
32
32
  }
33
33
 
34
+ // node_modules/@particle-academy/fancy-heuristics-js/dist/chunk-3RKEJJ4Y.js
35
+ function base(kind, actor, path, ts) {
36
+ return { kind, actor, path, ts };
37
+ }
38
+ function buildPageview(path, ts, actor = "human") {
39
+ return base("pageview", actor, path, ts);
40
+ }
41
+ function buildClick(args) {
42
+ const e = base("click", args.actor ?? "human", args.path, args.ts);
43
+ e.x = args.x;
44
+ e.y = args.y;
45
+ e.vw = args.vw;
46
+ e.vh = args.vh;
47
+ if (args.targetId !== void 0) e.targetId = args.targetId;
48
+ if (args.label !== void 0) e.label = args.label;
49
+ if (args.meta !== void 0) e.meta = args.meta;
50
+ return e;
51
+ }
52
+ function buildScroll(args) {
53
+ const e = base("scroll", args.actor ?? "human", args.path, args.ts);
54
+ e.scrollPct = args.scrollPct;
55
+ return e;
56
+ }
57
+ function buildPointer(args) {
58
+ const e = base("pointer", args.actor ?? "human", args.path, args.ts);
59
+ e.x = args.x;
60
+ e.y = args.y;
61
+ e.vw = args.vw;
62
+ e.vh = args.vh;
63
+ return e;
64
+ }
65
+ function buildDwell(args) {
66
+ const e = base("dwell", args.actor ?? "human", args.path, args.ts);
67
+ e.dwellMs = args.dwellMs;
68
+ return e;
69
+ }
70
+ function scrollPercent(scrollTop, scrollable) {
71
+ if (scrollable <= 0) return 100;
72
+ const pct = scrollTop / scrollable * 100;
73
+ if (pct <= 0) return 0;
74
+ if (pct >= 100) return 100;
75
+ return Math.round(pct);
76
+ }
77
+ function mapActivityToEvent(e, path) {
78
+ const source = e.source ?? "agent";
79
+ if (source !== "agent" && source !== "flow") return void 0;
80
+ const targetId = e.target?.elementId;
81
+ const label = e.target?.label;
82
+ const dwellMs = numericMeta(e.meta, "dwellMs");
83
+ if (dwellMs !== void 0) {
84
+ const ev2 = buildDwell({ path, ts: e.timestamp, dwellMs, actor: "agent" });
85
+ if (targetId !== void 0) ev2.targetId = targetId;
86
+ if (label !== void 0) ev2.label = label;
87
+ ev2.meta = { action: e.action, agentId: e.agentId, source };
88
+ return ev2;
89
+ }
90
+ const ev = buildClick({
91
+ path,
92
+ ts: e.timestamp,
93
+ x: numericMeta(e.meta, "x") ?? 0,
94
+ y: numericMeta(e.meta, "y") ?? 0,
95
+ vw: numericMeta(e.meta, "vw") ?? 0,
96
+ vh: numericMeta(e.meta, "vh") ?? 0,
97
+ actor: "agent",
98
+ targetId,
99
+ label,
100
+ meta: { action: e.action, agentId: e.agentId, source }
101
+ });
102
+ return ev;
103
+ }
104
+ function numericMeta(meta, key) {
105
+ const v = meta?.[key];
106
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
107
+ }
108
+ async function tapAgentActivity(opts) {
109
+ const load = opts.loadModule ?? defaultLoad;
110
+ let mod;
111
+ try {
112
+ mod = await load();
113
+ } catch {
114
+ mod = void 0;
115
+ }
116
+ if (!mod || typeof mod.onActivity !== "function") return () => {
117
+ };
118
+ try {
119
+ return mod.onActivity((e) => {
120
+ const ev = mapActivityToEvent(e, opts.path());
121
+ if (ev) opts.emit(ev);
122
+ });
123
+ } catch {
124
+ return () => {
125
+ };
126
+ }
127
+ }
128
+ async function defaultLoad() {
129
+ const spec = "@particle-academy/fancy-auto-common";
130
+ try {
131
+ const mod = await import(
132
+ /* @vite-ignore */
133
+ spec
134
+ );
135
+ if (mod && typeof mod.onActivity === "function") {
136
+ return mod;
137
+ }
138
+ return void 0;
139
+ } catch {
140
+ return void 0;
141
+ }
142
+ }
143
+ var EventBuffer = class {
144
+ constructor(opts) {
145
+ this.events = [];
146
+ this.siteKey = opts.siteKey;
147
+ this.url = joinCollect(opts.endpoint);
148
+ this.sessionId = opts.sessionId;
149
+ this.context = opts.context;
150
+ this.send = opts.send ?? defaultSend;
151
+ }
152
+ /** Queue one event for the next flush. */
153
+ add(event) {
154
+ this.events.push(event);
155
+ }
156
+ /** Number of buffered (un-flushed) events. */
157
+ get size() {
158
+ return this.events.length;
159
+ }
160
+ /** The collect URL this buffer ships to. */
161
+ get collectUrl() {
162
+ return this.url;
163
+ }
164
+ /**
165
+ * Ship the buffered events as one batch. No-op when empty. On a failed
166
+ * handoff the events are kept so the next flush retries them.
167
+ */
168
+ flush() {
169
+ if (this.events.length === 0) return;
170
+ const batch = {
171
+ siteKey: this.siteKey,
172
+ sessionId: this.sessionId,
173
+ events: this.events
174
+ };
175
+ if (this.context !== void 0) batch.context = this.context;
176
+ const body = JSON.stringify(batch);
177
+ const ok = this.send(this.url, body);
178
+ if (ok) {
179
+ this.events = [];
180
+ this.context = void 0;
181
+ }
182
+ }
183
+ };
184
+ function joinCollect(endpoint) {
185
+ return endpoint.replace(/\/+$/, "") + "/collect";
186
+ }
187
+ var defaultSend = (url, body) => {
188
+ const nav = typeof navigator !== "undefined" ? navigator : void 0;
189
+ if (nav && typeof nav.sendBeacon === "function") {
190
+ try {
191
+ const blob = typeof Blob !== "undefined" ? new Blob([body], { type: "application/json" }) : body;
192
+ if (nav.sendBeacon(url, blob)) return true;
193
+ } catch {
194
+ }
195
+ }
196
+ if (typeof fetch === "function") {
197
+ try {
198
+ void fetch(url, {
199
+ method: "POST",
200
+ body,
201
+ headers: { "Content-Type": "application/json" },
202
+ keepalive: true,
203
+ // Cross-origin ingestion: the PHP routes are CSRF-exempt; we don't read
204
+ // the response, so an opaque mode is fine.
205
+ mode: "cors",
206
+ credentials: "omit"
207
+ }).catch(() => {
208
+ });
209
+ return true;
210
+ } catch {
211
+ return false;
212
+ }
213
+ }
214
+ return false;
215
+ };
216
+ var UTM_KEYS = [
217
+ ["utm_source", "source"],
218
+ ["utm_medium", "medium"],
219
+ ["utm_campaign", "campaign"],
220
+ ["utm_term", "term"],
221
+ ["utm_content", "content"]
222
+ ];
223
+ function buildContext() {
224
+ if (typeof document === "undefined" || !document) return void 0;
225
+ const ctx = {};
226
+ if (typeof document.referrer === "string") {
227
+ ctx.referrer = document.referrer;
228
+ }
229
+ const utm = parseUtm(
230
+ typeof location !== "undefined" && location ? location.search : void 0
231
+ );
232
+ if (utm) ctx.utm = utm;
233
+ const nav = typeof navigator !== "undefined" ? navigator : void 0;
234
+ if (nav && typeof nav.language === "string" && nav.language) {
235
+ ctx.lang = nav.language;
236
+ }
237
+ const tz = resolveTimeZone();
238
+ if (tz) ctx.tz = tz;
239
+ const scr = typeof screen !== "undefined" ? screen : void 0;
240
+ if (scr) {
241
+ if (typeof scr.width === "number") ctx.screenW = scr.width;
242
+ if (typeof scr.height === "number") ctx.screenH = scr.height;
243
+ }
244
+ if (typeof window !== "undefined" && typeof window.devicePixelRatio === "number") {
245
+ ctx.dpr = window.devicePixelRatio;
246
+ }
247
+ return ctx;
248
+ }
249
+ function parseUtm(search) {
250
+ if (!search || typeof URLSearchParams === "undefined") return void 0;
251
+ let params;
252
+ try {
253
+ params = new URLSearchParams(search);
254
+ } catch {
255
+ return void 0;
256
+ }
257
+ const utm = {};
258
+ let found = false;
259
+ for (const [param, field] of UTM_KEYS) {
260
+ const raw = params.get(param);
261
+ if (raw) {
262
+ utm[field] = raw;
263
+ found = true;
264
+ }
265
+ }
266
+ return found ? utm : void 0;
267
+ }
268
+ function resolveTimeZone() {
269
+ try {
270
+ if (typeof Intl === "undefined" || typeof Intl.DateTimeFormat !== "function") {
271
+ return void 0;
272
+ }
273
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
274
+ return tz || void 0;
275
+ } catch {
276
+ return void 0;
277
+ }
278
+ }
279
+ var SESSION_KEY = "fancy-heuristics:sid";
280
+ var fallbackCounter = 0;
281
+ function generateSessionId() {
282
+ const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
283
+ if (c && typeof c.randomUUID === "function") {
284
+ return c.randomUUID();
285
+ }
286
+ if (c && typeof c.getRandomValues === "function") {
287
+ const bytes = new Uint8Array(16);
288
+ c.getRandomValues(bytes);
289
+ bytes[6] = bytes[6] & 15 | 64;
290
+ bytes[8] = bytes[8] & 63 | 128;
291
+ const hex = [];
292
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
293
+ return hex.slice(0, 4).join("") + "-" + hex.slice(4, 6).join("") + "-" + hex.slice(6, 8).join("") + "-" + hex.slice(8, 10).join("") + "-" + hex.slice(10, 16).join("");
294
+ }
295
+ fallbackCounter = fallbackCounter + 1 >>> 0;
296
+ return `fh-${Date.now().toString(36)}-${fallbackCounter.toString(36)}`;
297
+ }
298
+ function resolveSessionId(store) {
299
+ const s = safeSessionStorage();
300
+ if (!s) return generateSessionId();
301
+ try {
302
+ const existing = s.getItem(SESSION_KEY);
303
+ if (existing) return existing;
304
+ const fresh = generateSessionId();
305
+ s.setItem(SESSION_KEY, fresh);
306
+ return fresh;
307
+ } catch {
308
+ return generateSessionId();
309
+ }
310
+ }
311
+ function safeSessionStorage() {
312
+ try {
313
+ const ss = globalThis.sessionStorage;
314
+ if (!ss) return void 0;
315
+ ss.getItem(SESSION_KEY);
316
+ return ss;
317
+ } catch {
318
+ return void 0;
319
+ }
320
+ }
321
+ var DEFAULT_FLUSH_MS = 1500;
322
+ var DEFAULT_POINTER_THROTTLE_MS = 120;
323
+ var DEFAULT_SCROLL_THROTTLE_MS = 200;
324
+ function resolveTrack(track) {
325
+ return {
326
+ pageview: track?.pageview ?? true,
327
+ dwell: track?.dwell ?? true,
328
+ click: track?.click ?? true,
329
+ scroll: track?.scroll ?? true,
330
+ pointer: track?.pointer ?? true,
331
+ agent: track?.agent ?? true
332
+ };
333
+ }
334
+ function createCollector(opts) {
335
+ if (!opts || !opts.siteKey || !opts.endpoint) {
336
+ throw new Error("createCollector: { siteKey, endpoint } are required");
337
+ }
338
+ const track = resolveTrack(opts.track);
339
+ const flushMs = opts.flushMs ?? DEFAULT_FLUSH_MS;
340
+ const pointerThrottleMs = opts.pointerThrottleMs ?? DEFAULT_POINTER_THROTTLE_MS;
341
+ const scrollThrottleMs = opts.scrollThrottleMs ?? DEFAULT_SCROLL_THROTTLE_MS;
342
+ const sessionId = resolveSessionId();
343
+ const context = buildContext();
344
+ const buffer = new EventBuffer({
345
+ siteKey: opts.siteKey,
346
+ endpoint: opts.endpoint,
347
+ sessionId,
348
+ ...context !== void 0 ? { context } : {}
349
+ });
350
+ let started = false;
351
+ let flushTimer;
352
+ let agentUnsub;
353
+ const teardown = [];
354
+ let visibleSince = 0;
355
+ let accumulatedDwell = 0;
356
+ let maxScrollPct = 0;
357
+ let lastScrollAt = 0;
358
+ let lastPointerAt = 0;
359
+ const path = () => typeof location !== "undefined" && location ? location.pathname : "/";
360
+ const now = opts.now ?? (() => Date.now());
361
+ const vw = () => typeof window !== "undefined" ? window.innerWidth || 0 : 0;
362
+ const vh = () => typeof window !== "undefined" ? window.innerHeight || 0 : 0;
363
+ const emit = (e) => buffer.add(e);
364
+ const onClick = (ev) => {
365
+ const me = ev;
366
+ const targetEl = ev.target;
367
+ const { id, label } = describeTarget(targetEl);
368
+ emit(
369
+ buildClick({
370
+ path: path(),
371
+ ts: now(),
372
+ x: Math.round(me.clientX ?? 0),
373
+ y: Math.round(me.clientY ?? 0),
374
+ vw: vw(),
375
+ vh: vh(),
376
+ targetId: id,
377
+ label
378
+ })
379
+ );
380
+ };
381
+ const onScroll = () => {
382
+ const t = now();
383
+ if (t - lastScrollAt < scrollThrottleMs) return;
384
+ lastScrollAt = t;
385
+ const doc = typeof document !== "undefined" ? document.documentElement : void 0;
386
+ if (!doc) return;
387
+ const scrollTop = (typeof window !== "undefined" ? window.scrollY : 0) || doc.scrollTop || 0;
388
+ const scrollable = doc.scrollHeight - doc.clientHeight;
389
+ const pct = scrollPercent(scrollTop, scrollable);
390
+ if (pct > maxScrollPct) {
391
+ maxScrollPct = pct;
392
+ emit(buildScroll({ path: path(), ts: t, scrollPct: maxScrollPct }));
393
+ }
394
+ };
395
+ const onPointerMove = (ev) => {
396
+ const t = now();
397
+ if (t - lastPointerAt < pointerThrottleMs) return;
398
+ lastPointerAt = t;
399
+ const pe = ev;
400
+ emit(
401
+ buildPointer({
402
+ path: path(),
403
+ ts: t,
404
+ x: Math.round(pe.clientX ?? 0),
405
+ y: Math.round(pe.clientY ?? 0),
406
+ vw: vw(),
407
+ vh: vh()
408
+ })
409
+ );
410
+ };
411
+ const isVisible = () => typeof document === "undefined" || document.visibilityState !== "hidden";
412
+ const openDwellWindow = () => {
413
+ if (visibleSince === 0) visibleSince = now();
414
+ };
415
+ const closeDwellWindow = () => {
416
+ if (visibleSince !== 0) {
417
+ accumulatedDwell += now() - visibleSince;
418
+ visibleSince = 0;
419
+ }
420
+ };
421
+ const onVisibilityChange = () => {
422
+ if (isVisible()) openDwellWindow();
423
+ else closeDwellWindow();
424
+ };
425
+ const emitDwell = () => {
426
+ closeDwellWindow();
427
+ if (accumulatedDwell > 0) {
428
+ emit(buildDwell({ path: path(), ts: now(), dwellMs: accumulatedDwell }));
429
+ accumulatedDwell = 0;
430
+ }
431
+ if (isVisible()) openDwellWindow();
432
+ };
433
+ const onPageHide = () => {
434
+ if (track.dwell) {
435
+ closeDwellWindow();
436
+ if (accumulatedDwell > 0) {
437
+ emit(buildDwell({ path: path(), ts: now(), dwellMs: accumulatedDwell }));
438
+ accumulatedDwell = 0;
439
+ }
440
+ }
441
+ buffer.flush();
442
+ };
443
+ const addListener = (target, type, handler, options) => {
444
+ if (!target || typeof target.addEventListener !== "function") return;
445
+ target.addEventListener(type, handler, options);
446
+ teardown.push(() => target.removeEventListener(type, handler, options));
447
+ };
448
+ return {
449
+ start() {
450
+ if (started) return;
451
+ started = true;
452
+ const doc = typeof document !== "undefined" ? document : void 0;
453
+ const win = typeof window !== "undefined" ? window : void 0;
454
+ if (track.pageview) {
455
+ emit(buildPageview(path(), now()));
456
+ }
457
+ if (track.dwell) {
458
+ openDwellWindow();
459
+ addListener(doc, "visibilitychange", onVisibilityChange);
460
+ }
461
+ if (track.click) {
462
+ addListener(doc, "click", onClick, { capture: true, passive: true });
463
+ }
464
+ if (track.scroll) {
465
+ addListener(win, "scroll", onScroll, { passive: true });
466
+ }
467
+ if (track.pointer) {
468
+ addListener(win, "pointermove", onPointerMove, { passive: true });
469
+ }
470
+ addListener(win, "pagehide", onPageHide);
471
+ if (track.agent) {
472
+ void tapAgentActivity({ path, emit }).then((unsub) => {
473
+ if (started) agentUnsub = unsub;
474
+ else unsub();
475
+ });
476
+ }
477
+ if (typeof setInterval === "function") {
478
+ flushTimer = setInterval(() => {
479
+ if (track.dwell) emitDwell();
480
+ buffer.flush();
481
+ }, flushMs);
482
+ }
483
+ },
484
+ stop() {
485
+ if (!started) return;
486
+ started = false;
487
+ if (flushTimer !== void 0) {
488
+ clearInterval(flushTimer);
489
+ flushTimer = void 0;
490
+ }
491
+ for (const fn of teardown.splice(0)) {
492
+ try {
493
+ fn();
494
+ } catch {
495
+ }
496
+ }
497
+ if (agentUnsub) {
498
+ try {
499
+ agentUnsub();
500
+ } catch {
501
+ }
502
+ agentUnsub = void 0;
503
+ }
504
+ if (track.dwell) {
505
+ closeDwellWindow();
506
+ if (accumulatedDwell > 0) {
507
+ emit(buildDwell({ path: path(), ts: now(), dwellMs: accumulatedDwell }));
508
+ accumulatedDwell = 0;
509
+ }
510
+ }
511
+ buffer.flush();
512
+ },
513
+ flush() {
514
+ buffer.flush();
515
+ }
516
+ };
517
+ }
518
+ function describeTarget(el) {
519
+ if (!el) return {};
520
+ let node = el;
521
+ let id;
522
+ for (let depth = 0; node && depth < 5; depth++) {
523
+ const candidate = node.getAttribute?.("data-fancy-id") ?? node.getAttribute?.("data-testid") ?? (node.id || void 0);
524
+ if (candidate) {
525
+ id = candidate;
526
+ break;
527
+ }
528
+ node = node.parentElement;
529
+ }
530
+ const labelSource = node ?? el;
531
+ const label = readLabel(labelSource);
532
+ const out = {};
533
+ if (id !== void 0) out.id = id;
534
+ if (label !== void 0) out.label = label;
535
+ return out;
536
+ }
537
+ function readLabel(el) {
538
+ const aria = el.getAttribute?.("aria-label");
539
+ if (aria) return trim(aria);
540
+ const text = el.textContent;
541
+ if (text) {
542
+ const t = trim(text);
543
+ if (t) return t;
544
+ }
545
+ const tag = el.tagName ? el.tagName.toLowerCase() : void 0;
546
+ return tag;
547
+ }
548
+ function trim(s) {
549
+ return s.replace(/\s+/g, " ").trim().slice(0, 120);
550
+ }
551
+
552
+ // src/collector.ts
553
+ var factory = (opts) => createCollector({ siteKey: opts.siteKey, endpoint: opts.endpoint });
554
+ function createPixelCollector(opts) {
555
+ return factory(opts);
556
+ }
557
+ function __setCollectorFactory(next) {
558
+ factory = next ?? ((opts) => createCollector({ siteKey: opts.siteKey, endpoint: opts.endpoint }));
559
+ }
560
+
34
561
  // src/render.ts
35
562
  function logoGlyph(size) {
36
563
  return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" style="display:block;flex:0 0 auto;">
@@ -191,6 +718,7 @@ function mountPixel(opts = {}) {
191
718
  const mode = MODES.includes(opts.mode) ? opts.mode : "floating";
192
719
  const siteKey = opts.siteKey ?? "";
193
720
  const endpoint = opts.endpoint ?? "";
721
+ const collect = opts.collect !== false;
194
722
  const href = opts.href ?? DEFAULT_HREF;
195
723
  const target = opts.target ?? null;
196
724
  const host = createPixelHost(style, mode, href);
@@ -244,11 +772,21 @@ function mountPixel(opts = {}) {
244
772
  setVisible(true);
245
773
  }
246
774
  beacon();
775
+ let collector = null;
776
+ if (endpoint && collect) {
777
+ try {
778
+ collector = createPixelCollector({ siteKey, endpoint });
779
+ collector.start();
780
+ } catch {
781
+ collector = null;
782
+ }
783
+ }
247
784
  const resolved = {
248
785
  style,
249
786
  mode,
250
787
  siteKey,
251
788
  endpoint,
789
+ collect,
252
790
  href,
253
791
  target
254
792
  };
@@ -257,12 +795,22 @@ function mountPixel(opts = {}) {
257
795
  get visible() {
258
796
  return visible;
259
797
  },
798
+ get collector() {
799
+ return collector;
800
+ },
260
801
  options: resolved,
261
802
  destroy() {
262
803
  if (destroyed) return;
263
804
  destroyed = true;
264
805
  observer?.disconnect();
265
806
  observer = null;
807
+ if (collector) {
808
+ try {
809
+ collector.stop();
810
+ } catch {
811
+ }
812
+ collector = null;
813
+ }
266
814
  host.remove();
267
815
  }
268
816
  };
@@ -280,14 +828,16 @@ function readScriptConfig() {
280
828
  }
281
829
  if (!el) return null;
282
830
  const ds = el.dataset;
283
- if (ds.style === void 0 && ds.mode === void 0 && ds.site === void 0 && ds.endpoint === void 0 && ds.fancyPixel === void 0) {
831
+ if (ds.style === void 0 && ds.mode === void 0 && ds.site === void 0 && ds.endpoint === void 0 && ds.collect === void 0 && ds.fancyPixel === void 0) {
284
832
  return null;
285
833
  }
834
+ const collect = ds.collect !== void 0 ? ds.collect !== "false" : void 0;
286
835
  return {
287
836
  style: ds.style,
288
837
  mode: ds.mode,
289
838
  siteKey: ds.site,
290
839
  endpoint: ds.endpoint,
840
+ collect,
291
841
  target: ds.target ?? null,
292
842
  href: ds.href
293
843
  };
@@ -310,7 +860,9 @@ function autoInit() {
310
860
  return run();
311
861
  }
312
862
 
863
+ exports.__setCollectorFactory = __setCollectorFactory;
313
864
  exports.autoInit = autoInit;
865
+ exports.createPixelCollector = createPixelCollector;
314
866
  exports.createPixelHost = createPixelHost;
315
867
  exports.hostStyleFor = hostStyleFor;
316
868
  exports.mountPixel = mountPixel;