@particle-academy/fancy-pixel 0.1.0 → 0.2.0

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,463 @@ function sendPixelBeacon(endpoint, payload) {
31
31
  return false;
32
32
  }
33
33
 
34
+ // node_modules/@particle-academy/fancy-heuristics-js/dist/chunk-3XXNC3HT.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.send = opts.send ?? defaultSend;
150
+ }
151
+ /** Queue one event for the next flush. */
152
+ add(event) {
153
+ this.events.push(event);
154
+ }
155
+ /** Number of buffered (un-flushed) events. */
156
+ get size() {
157
+ return this.events.length;
158
+ }
159
+ /** The collect URL this buffer ships to. */
160
+ get collectUrl() {
161
+ return this.url;
162
+ }
163
+ /**
164
+ * Ship the buffered events as one batch. No-op when empty. On a failed
165
+ * handoff the events are kept so the next flush retries them.
166
+ */
167
+ flush() {
168
+ if (this.events.length === 0) return;
169
+ const batch = {
170
+ siteKey: this.siteKey,
171
+ sessionId: this.sessionId,
172
+ events: this.events
173
+ };
174
+ const body = JSON.stringify(batch);
175
+ const ok = this.send(this.url, body);
176
+ if (ok) this.events = [];
177
+ }
178
+ };
179
+ function joinCollect(endpoint) {
180
+ return endpoint.replace(/\/+$/, "") + "/collect";
181
+ }
182
+ var defaultSend = (url, body) => {
183
+ const nav = typeof navigator !== "undefined" ? navigator : void 0;
184
+ if (nav && typeof nav.sendBeacon === "function") {
185
+ try {
186
+ const blob = typeof Blob !== "undefined" ? new Blob([body], { type: "application/json" }) : body;
187
+ if (nav.sendBeacon(url, blob)) return true;
188
+ } catch {
189
+ }
190
+ }
191
+ if (typeof fetch === "function") {
192
+ try {
193
+ void fetch(url, {
194
+ method: "POST",
195
+ body,
196
+ headers: { "Content-Type": "application/json" },
197
+ keepalive: true,
198
+ // Cross-origin ingestion: the PHP routes are CSRF-exempt; we don't read
199
+ // the response, so an opaque mode is fine.
200
+ mode: "cors",
201
+ credentials: "omit"
202
+ }).catch(() => {
203
+ });
204
+ return true;
205
+ } catch {
206
+ return false;
207
+ }
208
+ }
209
+ return false;
210
+ };
211
+ var SESSION_KEY = "fancy-heuristics:sid";
212
+ var fallbackCounter = 0;
213
+ function generateSessionId() {
214
+ const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
215
+ if (c && typeof c.randomUUID === "function") {
216
+ return c.randomUUID();
217
+ }
218
+ if (c && typeof c.getRandomValues === "function") {
219
+ const bytes = new Uint8Array(16);
220
+ c.getRandomValues(bytes);
221
+ bytes[6] = bytes[6] & 15 | 64;
222
+ bytes[8] = bytes[8] & 63 | 128;
223
+ const hex = [];
224
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
225
+ 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("");
226
+ }
227
+ fallbackCounter = fallbackCounter + 1 >>> 0;
228
+ return `fh-${Date.now().toString(36)}-${fallbackCounter.toString(36)}`;
229
+ }
230
+ function resolveSessionId(store) {
231
+ const s = safeSessionStorage();
232
+ if (!s) return generateSessionId();
233
+ try {
234
+ const existing = s.getItem(SESSION_KEY);
235
+ if (existing) return existing;
236
+ const fresh = generateSessionId();
237
+ s.setItem(SESSION_KEY, fresh);
238
+ return fresh;
239
+ } catch {
240
+ return generateSessionId();
241
+ }
242
+ }
243
+ function safeSessionStorage() {
244
+ try {
245
+ const ss = globalThis.sessionStorage;
246
+ if (!ss) return void 0;
247
+ ss.getItem(SESSION_KEY);
248
+ return ss;
249
+ } catch {
250
+ return void 0;
251
+ }
252
+ }
253
+ var DEFAULT_FLUSH_MS = 1500;
254
+ var DEFAULT_POINTER_THROTTLE_MS = 120;
255
+ var DEFAULT_SCROLL_THROTTLE_MS = 200;
256
+ function resolveTrack(track) {
257
+ return {
258
+ pageview: track?.pageview ?? true,
259
+ dwell: track?.dwell ?? true,
260
+ click: track?.click ?? true,
261
+ scroll: track?.scroll ?? true,
262
+ pointer: track?.pointer ?? true,
263
+ agent: track?.agent ?? true
264
+ };
265
+ }
266
+ function createCollector(opts) {
267
+ if (!opts || !opts.siteKey || !opts.endpoint) {
268
+ throw new Error("createCollector: { siteKey, endpoint } are required");
269
+ }
270
+ const track = resolveTrack(opts.track);
271
+ const flushMs = opts.flushMs ?? DEFAULT_FLUSH_MS;
272
+ const pointerThrottleMs = opts.pointerThrottleMs ?? DEFAULT_POINTER_THROTTLE_MS;
273
+ const scrollThrottleMs = opts.scrollThrottleMs ?? DEFAULT_SCROLL_THROTTLE_MS;
274
+ const sessionId = resolveSessionId();
275
+ const buffer = new EventBuffer({
276
+ siteKey: opts.siteKey,
277
+ endpoint: opts.endpoint,
278
+ sessionId
279
+ });
280
+ let started = false;
281
+ let flushTimer;
282
+ let agentUnsub;
283
+ const teardown = [];
284
+ let visibleSince = 0;
285
+ let accumulatedDwell = 0;
286
+ let maxScrollPct = 0;
287
+ let lastScrollAt = 0;
288
+ let lastPointerAt = 0;
289
+ const path = () => typeof location !== "undefined" && location ? location.pathname : "/";
290
+ const now = opts.now ?? (() => Date.now());
291
+ const vw = () => typeof window !== "undefined" ? window.innerWidth || 0 : 0;
292
+ const vh = () => typeof window !== "undefined" ? window.innerHeight || 0 : 0;
293
+ const emit = (e) => buffer.add(e);
294
+ const onClick = (ev) => {
295
+ const me = ev;
296
+ const targetEl = ev.target;
297
+ const { id, label } = describeTarget(targetEl);
298
+ emit(
299
+ buildClick({
300
+ path: path(),
301
+ ts: now(),
302
+ x: Math.round(me.clientX ?? 0),
303
+ y: Math.round(me.clientY ?? 0),
304
+ vw: vw(),
305
+ vh: vh(),
306
+ targetId: id,
307
+ label
308
+ })
309
+ );
310
+ };
311
+ const onScroll = () => {
312
+ const t = now();
313
+ if (t - lastScrollAt < scrollThrottleMs) return;
314
+ lastScrollAt = t;
315
+ const doc = typeof document !== "undefined" ? document.documentElement : void 0;
316
+ if (!doc) return;
317
+ const scrollTop = (typeof window !== "undefined" ? window.scrollY : 0) || doc.scrollTop || 0;
318
+ const scrollable = doc.scrollHeight - doc.clientHeight;
319
+ const pct = scrollPercent(scrollTop, scrollable);
320
+ if (pct > maxScrollPct) {
321
+ maxScrollPct = pct;
322
+ emit(buildScroll({ path: path(), ts: t, scrollPct: maxScrollPct }));
323
+ }
324
+ };
325
+ const onPointerMove = (ev) => {
326
+ const t = now();
327
+ if (t - lastPointerAt < pointerThrottleMs) return;
328
+ lastPointerAt = t;
329
+ const pe = ev;
330
+ emit(
331
+ buildPointer({
332
+ path: path(),
333
+ ts: t,
334
+ x: Math.round(pe.clientX ?? 0),
335
+ y: Math.round(pe.clientY ?? 0),
336
+ vw: vw(),
337
+ vh: vh()
338
+ })
339
+ );
340
+ };
341
+ const isVisible = () => typeof document === "undefined" || document.visibilityState !== "hidden";
342
+ const openDwellWindow = () => {
343
+ if (visibleSince === 0) visibleSince = now();
344
+ };
345
+ const closeDwellWindow = () => {
346
+ if (visibleSince !== 0) {
347
+ accumulatedDwell += now() - visibleSince;
348
+ visibleSince = 0;
349
+ }
350
+ };
351
+ const onVisibilityChange = () => {
352
+ if (isVisible()) openDwellWindow();
353
+ else closeDwellWindow();
354
+ };
355
+ const emitDwell = () => {
356
+ closeDwellWindow();
357
+ if (accumulatedDwell > 0) {
358
+ emit(buildDwell({ path: path(), ts: now(), dwellMs: accumulatedDwell }));
359
+ accumulatedDwell = 0;
360
+ }
361
+ if (isVisible()) openDwellWindow();
362
+ };
363
+ const onPageHide = () => {
364
+ if (track.dwell) {
365
+ closeDwellWindow();
366
+ if (accumulatedDwell > 0) {
367
+ emit(buildDwell({ path: path(), ts: now(), dwellMs: accumulatedDwell }));
368
+ accumulatedDwell = 0;
369
+ }
370
+ }
371
+ buffer.flush();
372
+ };
373
+ const addListener = (target, type, handler, options) => {
374
+ if (!target || typeof target.addEventListener !== "function") return;
375
+ target.addEventListener(type, handler, options);
376
+ teardown.push(() => target.removeEventListener(type, handler, options));
377
+ };
378
+ return {
379
+ start() {
380
+ if (started) return;
381
+ started = true;
382
+ const doc = typeof document !== "undefined" ? document : void 0;
383
+ const win = typeof window !== "undefined" ? window : void 0;
384
+ if (track.pageview) {
385
+ emit(buildPageview(path(), now()));
386
+ }
387
+ if (track.dwell) {
388
+ openDwellWindow();
389
+ addListener(doc, "visibilitychange", onVisibilityChange);
390
+ }
391
+ if (track.click) {
392
+ addListener(doc, "click", onClick, { capture: true, passive: true });
393
+ }
394
+ if (track.scroll) {
395
+ addListener(win, "scroll", onScroll, { passive: true });
396
+ }
397
+ if (track.pointer) {
398
+ addListener(win, "pointermove", onPointerMove, { passive: true });
399
+ }
400
+ addListener(win, "pagehide", onPageHide);
401
+ if (track.agent) {
402
+ void tapAgentActivity({ path, emit }).then((unsub) => {
403
+ if (started) agentUnsub = unsub;
404
+ else unsub();
405
+ });
406
+ }
407
+ if (typeof setInterval === "function") {
408
+ flushTimer = setInterval(() => {
409
+ if (track.dwell) emitDwell();
410
+ buffer.flush();
411
+ }, flushMs);
412
+ }
413
+ },
414
+ stop() {
415
+ if (!started) return;
416
+ started = false;
417
+ if (flushTimer !== void 0) {
418
+ clearInterval(flushTimer);
419
+ flushTimer = void 0;
420
+ }
421
+ for (const fn of teardown.splice(0)) {
422
+ try {
423
+ fn();
424
+ } catch {
425
+ }
426
+ }
427
+ if (agentUnsub) {
428
+ try {
429
+ agentUnsub();
430
+ } catch {
431
+ }
432
+ agentUnsub = void 0;
433
+ }
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
+ flush() {
444
+ buffer.flush();
445
+ }
446
+ };
447
+ }
448
+ function describeTarget(el) {
449
+ if (!el) return {};
450
+ let node = el;
451
+ let id;
452
+ for (let depth = 0; node && depth < 5; depth++) {
453
+ const candidate = node.getAttribute?.("data-fancy-id") ?? node.getAttribute?.("data-testid") ?? (node.id || void 0);
454
+ if (candidate) {
455
+ id = candidate;
456
+ break;
457
+ }
458
+ node = node.parentElement;
459
+ }
460
+ const labelSource = node ?? el;
461
+ const label = readLabel(labelSource);
462
+ const out = {};
463
+ if (id !== void 0) out.id = id;
464
+ if (label !== void 0) out.label = label;
465
+ return out;
466
+ }
467
+ function readLabel(el) {
468
+ const aria = el.getAttribute?.("aria-label");
469
+ if (aria) return trim(aria);
470
+ const text = el.textContent;
471
+ if (text) {
472
+ const t = trim(text);
473
+ if (t) return t;
474
+ }
475
+ const tag = el.tagName ? el.tagName.toLowerCase() : void 0;
476
+ return tag;
477
+ }
478
+ function trim(s) {
479
+ return s.replace(/\s+/g, " ").trim().slice(0, 120);
480
+ }
481
+
482
+ // src/collector.ts
483
+ var factory = (opts) => createCollector({ siteKey: opts.siteKey, endpoint: opts.endpoint });
484
+ function createPixelCollector(opts) {
485
+ return factory(opts);
486
+ }
487
+ function __setCollectorFactory(next) {
488
+ factory = next ?? ((opts) => createCollector({ siteKey: opts.siteKey, endpoint: opts.endpoint }));
489
+ }
490
+
34
491
  // src/render.ts
35
492
  function logoGlyph(size) {
36
493
  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 +648,7 @@ function mountPixel(opts = {}) {
191
648
  const mode = MODES.includes(opts.mode) ? opts.mode : "floating";
192
649
  const siteKey = opts.siteKey ?? "";
193
650
  const endpoint = opts.endpoint ?? "";
651
+ const collect = opts.collect !== false;
194
652
  const href = opts.href ?? DEFAULT_HREF;
195
653
  const target = opts.target ?? null;
196
654
  const host = createPixelHost(style, mode, href);
@@ -244,11 +702,21 @@ function mountPixel(opts = {}) {
244
702
  setVisible(true);
245
703
  }
246
704
  beacon();
705
+ let collector = null;
706
+ if (endpoint && collect) {
707
+ try {
708
+ collector = createPixelCollector({ siteKey, endpoint });
709
+ collector.start();
710
+ } catch {
711
+ collector = null;
712
+ }
713
+ }
247
714
  const resolved = {
248
715
  style,
249
716
  mode,
250
717
  siteKey,
251
718
  endpoint,
719
+ collect,
252
720
  href,
253
721
  target
254
722
  };
@@ -257,12 +725,22 @@ function mountPixel(opts = {}) {
257
725
  get visible() {
258
726
  return visible;
259
727
  },
728
+ get collector() {
729
+ return collector;
730
+ },
260
731
  options: resolved,
261
732
  destroy() {
262
733
  if (destroyed) return;
263
734
  destroyed = true;
264
735
  observer?.disconnect();
265
736
  observer = null;
737
+ if (collector) {
738
+ try {
739
+ collector.stop();
740
+ } catch {
741
+ }
742
+ collector = null;
743
+ }
266
744
  host.remove();
267
745
  }
268
746
  };
@@ -280,14 +758,16 @@ function readScriptConfig() {
280
758
  }
281
759
  if (!el) return null;
282
760
  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) {
761
+ 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
762
  return null;
285
763
  }
764
+ const collect = ds.collect !== void 0 ? ds.collect !== "false" : void 0;
286
765
  return {
287
766
  style: ds.style,
288
767
  mode: ds.mode,
289
768
  siteKey: ds.site,
290
769
  endpoint: ds.endpoint,
770
+ collect,
291
771
  target: ds.target ?? null,
292
772
  href: ds.href
293
773
  };
@@ -310,7 +790,9 @@ function autoInit() {
310
790
  return run();
311
791
  }
312
792
 
793
+ exports.__setCollectorFactory = __setCollectorFactory;
313
794
  exports.autoInit = autoInit;
795
+ exports.createPixelCollector = createPixelCollector;
314
796
  exports.createPixelHost = createPixelHost;
315
797
  exports.hostStyleFor = hostStyleFor;
316
798
  exports.mountPixel = mountPixel;