@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.js CHANGED
@@ -29,6 +29,463 @@ function sendPixelBeacon(endpoint, payload) {
29
29
  return false;
30
30
  }
31
31
 
32
+ // node_modules/@particle-academy/fancy-heuristics-js/dist/chunk-3XXNC3HT.js
33
+ function base(kind, actor, path, ts) {
34
+ return { kind, actor, path, ts };
35
+ }
36
+ function buildPageview(path, ts, actor = "human") {
37
+ return base("pageview", actor, path, ts);
38
+ }
39
+ function buildClick(args) {
40
+ const e = base("click", args.actor ?? "human", args.path, args.ts);
41
+ e.x = args.x;
42
+ e.y = args.y;
43
+ e.vw = args.vw;
44
+ e.vh = args.vh;
45
+ if (args.targetId !== void 0) e.targetId = args.targetId;
46
+ if (args.label !== void 0) e.label = args.label;
47
+ if (args.meta !== void 0) e.meta = args.meta;
48
+ return e;
49
+ }
50
+ function buildScroll(args) {
51
+ const e = base("scroll", args.actor ?? "human", args.path, args.ts);
52
+ e.scrollPct = args.scrollPct;
53
+ return e;
54
+ }
55
+ function buildPointer(args) {
56
+ const e = base("pointer", args.actor ?? "human", args.path, args.ts);
57
+ e.x = args.x;
58
+ e.y = args.y;
59
+ e.vw = args.vw;
60
+ e.vh = args.vh;
61
+ return e;
62
+ }
63
+ function buildDwell(args) {
64
+ const e = base("dwell", args.actor ?? "human", args.path, args.ts);
65
+ e.dwellMs = args.dwellMs;
66
+ return e;
67
+ }
68
+ function scrollPercent(scrollTop, scrollable) {
69
+ if (scrollable <= 0) return 100;
70
+ const pct = scrollTop / scrollable * 100;
71
+ if (pct <= 0) return 0;
72
+ if (pct >= 100) return 100;
73
+ return Math.round(pct);
74
+ }
75
+ function mapActivityToEvent(e, path) {
76
+ const source = e.source ?? "agent";
77
+ if (source !== "agent" && source !== "flow") return void 0;
78
+ const targetId = e.target?.elementId;
79
+ const label = e.target?.label;
80
+ const dwellMs = numericMeta(e.meta, "dwellMs");
81
+ if (dwellMs !== void 0) {
82
+ const ev2 = buildDwell({ path, ts: e.timestamp, dwellMs, actor: "agent" });
83
+ if (targetId !== void 0) ev2.targetId = targetId;
84
+ if (label !== void 0) ev2.label = label;
85
+ ev2.meta = { action: e.action, agentId: e.agentId, source };
86
+ return ev2;
87
+ }
88
+ const ev = buildClick({
89
+ path,
90
+ ts: e.timestamp,
91
+ x: numericMeta(e.meta, "x") ?? 0,
92
+ y: numericMeta(e.meta, "y") ?? 0,
93
+ vw: numericMeta(e.meta, "vw") ?? 0,
94
+ vh: numericMeta(e.meta, "vh") ?? 0,
95
+ actor: "agent",
96
+ targetId,
97
+ label,
98
+ meta: { action: e.action, agentId: e.agentId, source }
99
+ });
100
+ return ev;
101
+ }
102
+ function numericMeta(meta, key) {
103
+ const v = meta?.[key];
104
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
105
+ }
106
+ async function tapAgentActivity(opts) {
107
+ const load = opts.loadModule ?? defaultLoad;
108
+ let mod;
109
+ try {
110
+ mod = await load();
111
+ } catch {
112
+ mod = void 0;
113
+ }
114
+ if (!mod || typeof mod.onActivity !== "function") return () => {
115
+ };
116
+ try {
117
+ return mod.onActivity((e) => {
118
+ const ev = mapActivityToEvent(e, opts.path());
119
+ if (ev) opts.emit(ev);
120
+ });
121
+ } catch {
122
+ return () => {
123
+ };
124
+ }
125
+ }
126
+ async function defaultLoad() {
127
+ const spec = "@particle-academy/fancy-auto-common";
128
+ try {
129
+ const mod = await import(
130
+ /* @vite-ignore */
131
+ spec
132
+ );
133
+ if (mod && typeof mod.onActivity === "function") {
134
+ return mod;
135
+ }
136
+ return void 0;
137
+ } catch {
138
+ return void 0;
139
+ }
140
+ }
141
+ var EventBuffer = class {
142
+ constructor(opts) {
143
+ this.events = [];
144
+ this.siteKey = opts.siteKey;
145
+ this.url = joinCollect(opts.endpoint);
146
+ this.sessionId = opts.sessionId;
147
+ this.send = opts.send ?? defaultSend;
148
+ }
149
+ /** Queue one event for the next flush. */
150
+ add(event) {
151
+ this.events.push(event);
152
+ }
153
+ /** Number of buffered (un-flushed) events. */
154
+ get size() {
155
+ return this.events.length;
156
+ }
157
+ /** The collect URL this buffer ships to. */
158
+ get collectUrl() {
159
+ return this.url;
160
+ }
161
+ /**
162
+ * Ship the buffered events as one batch. No-op when empty. On a failed
163
+ * handoff the events are kept so the next flush retries them.
164
+ */
165
+ flush() {
166
+ if (this.events.length === 0) return;
167
+ const batch = {
168
+ siteKey: this.siteKey,
169
+ sessionId: this.sessionId,
170
+ events: this.events
171
+ };
172
+ const body = JSON.stringify(batch);
173
+ const ok = this.send(this.url, body);
174
+ if (ok) this.events = [];
175
+ }
176
+ };
177
+ function joinCollect(endpoint) {
178
+ return endpoint.replace(/\/+$/, "") + "/collect";
179
+ }
180
+ var defaultSend = (url, body) => {
181
+ const nav = typeof navigator !== "undefined" ? navigator : void 0;
182
+ if (nav && typeof nav.sendBeacon === "function") {
183
+ try {
184
+ const blob = typeof Blob !== "undefined" ? new Blob([body], { type: "application/json" }) : body;
185
+ if (nav.sendBeacon(url, blob)) return true;
186
+ } catch {
187
+ }
188
+ }
189
+ if (typeof fetch === "function") {
190
+ try {
191
+ void fetch(url, {
192
+ method: "POST",
193
+ body,
194
+ headers: { "Content-Type": "application/json" },
195
+ keepalive: true,
196
+ // Cross-origin ingestion: the PHP routes are CSRF-exempt; we don't read
197
+ // the response, so an opaque mode is fine.
198
+ mode: "cors",
199
+ credentials: "omit"
200
+ }).catch(() => {
201
+ });
202
+ return true;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+ return false;
208
+ };
209
+ var SESSION_KEY = "fancy-heuristics:sid";
210
+ var fallbackCounter = 0;
211
+ function generateSessionId() {
212
+ const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
213
+ if (c && typeof c.randomUUID === "function") {
214
+ return c.randomUUID();
215
+ }
216
+ if (c && typeof c.getRandomValues === "function") {
217
+ const bytes = new Uint8Array(16);
218
+ c.getRandomValues(bytes);
219
+ bytes[6] = bytes[6] & 15 | 64;
220
+ bytes[8] = bytes[8] & 63 | 128;
221
+ const hex = [];
222
+ for (let i = 0; i < 16; i++) hex.push(bytes[i].toString(16).padStart(2, "0"));
223
+ 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("");
224
+ }
225
+ fallbackCounter = fallbackCounter + 1 >>> 0;
226
+ return `fh-${Date.now().toString(36)}-${fallbackCounter.toString(36)}`;
227
+ }
228
+ function resolveSessionId(store) {
229
+ const s = safeSessionStorage();
230
+ if (!s) return generateSessionId();
231
+ try {
232
+ const existing = s.getItem(SESSION_KEY);
233
+ if (existing) return existing;
234
+ const fresh = generateSessionId();
235
+ s.setItem(SESSION_KEY, fresh);
236
+ return fresh;
237
+ } catch {
238
+ return generateSessionId();
239
+ }
240
+ }
241
+ function safeSessionStorage() {
242
+ try {
243
+ const ss = globalThis.sessionStorage;
244
+ if (!ss) return void 0;
245
+ ss.getItem(SESSION_KEY);
246
+ return ss;
247
+ } catch {
248
+ return void 0;
249
+ }
250
+ }
251
+ var DEFAULT_FLUSH_MS = 1500;
252
+ var DEFAULT_POINTER_THROTTLE_MS = 120;
253
+ var DEFAULT_SCROLL_THROTTLE_MS = 200;
254
+ function resolveTrack(track) {
255
+ return {
256
+ pageview: track?.pageview ?? true,
257
+ dwell: track?.dwell ?? true,
258
+ click: track?.click ?? true,
259
+ scroll: track?.scroll ?? true,
260
+ pointer: track?.pointer ?? true,
261
+ agent: track?.agent ?? true
262
+ };
263
+ }
264
+ function createCollector(opts) {
265
+ if (!opts || !opts.siteKey || !opts.endpoint) {
266
+ throw new Error("createCollector: { siteKey, endpoint } are required");
267
+ }
268
+ const track = resolveTrack(opts.track);
269
+ const flushMs = opts.flushMs ?? DEFAULT_FLUSH_MS;
270
+ const pointerThrottleMs = opts.pointerThrottleMs ?? DEFAULT_POINTER_THROTTLE_MS;
271
+ const scrollThrottleMs = opts.scrollThrottleMs ?? DEFAULT_SCROLL_THROTTLE_MS;
272
+ const sessionId = resolveSessionId();
273
+ const buffer = new EventBuffer({
274
+ siteKey: opts.siteKey,
275
+ endpoint: opts.endpoint,
276
+ sessionId
277
+ });
278
+ let started = false;
279
+ let flushTimer;
280
+ let agentUnsub;
281
+ const teardown = [];
282
+ let visibleSince = 0;
283
+ let accumulatedDwell = 0;
284
+ let maxScrollPct = 0;
285
+ let lastScrollAt = 0;
286
+ let lastPointerAt = 0;
287
+ const path = () => typeof location !== "undefined" && location ? location.pathname : "/";
288
+ const now = opts.now ?? (() => Date.now());
289
+ const vw = () => typeof window !== "undefined" ? window.innerWidth || 0 : 0;
290
+ const vh = () => typeof window !== "undefined" ? window.innerHeight || 0 : 0;
291
+ const emit = (e) => buffer.add(e);
292
+ const onClick = (ev) => {
293
+ const me = ev;
294
+ const targetEl = ev.target;
295
+ const { id, label } = describeTarget(targetEl);
296
+ emit(
297
+ buildClick({
298
+ path: path(),
299
+ ts: now(),
300
+ x: Math.round(me.clientX ?? 0),
301
+ y: Math.round(me.clientY ?? 0),
302
+ vw: vw(),
303
+ vh: vh(),
304
+ targetId: id,
305
+ label
306
+ })
307
+ );
308
+ };
309
+ const onScroll = () => {
310
+ const t = now();
311
+ if (t - lastScrollAt < scrollThrottleMs) return;
312
+ lastScrollAt = t;
313
+ const doc = typeof document !== "undefined" ? document.documentElement : void 0;
314
+ if (!doc) return;
315
+ const scrollTop = (typeof window !== "undefined" ? window.scrollY : 0) || doc.scrollTop || 0;
316
+ const scrollable = doc.scrollHeight - doc.clientHeight;
317
+ const pct = scrollPercent(scrollTop, scrollable);
318
+ if (pct > maxScrollPct) {
319
+ maxScrollPct = pct;
320
+ emit(buildScroll({ path: path(), ts: t, scrollPct: maxScrollPct }));
321
+ }
322
+ };
323
+ const onPointerMove = (ev) => {
324
+ const t = now();
325
+ if (t - lastPointerAt < pointerThrottleMs) return;
326
+ lastPointerAt = t;
327
+ const pe = ev;
328
+ emit(
329
+ buildPointer({
330
+ path: path(),
331
+ ts: t,
332
+ x: Math.round(pe.clientX ?? 0),
333
+ y: Math.round(pe.clientY ?? 0),
334
+ vw: vw(),
335
+ vh: vh()
336
+ })
337
+ );
338
+ };
339
+ const isVisible = () => typeof document === "undefined" || document.visibilityState !== "hidden";
340
+ const openDwellWindow = () => {
341
+ if (visibleSince === 0) visibleSince = now();
342
+ };
343
+ const closeDwellWindow = () => {
344
+ if (visibleSince !== 0) {
345
+ accumulatedDwell += now() - visibleSince;
346
+ visibleSince = 0;
347
+ }
348
+ };
349
+ const onVisibilityChange = () => {
350
+ if (isVisible()) openDwellWindow();
351
+ else closeDwellWindow();
352
+ };
353
+ const emitDwell = () => {
354
+ closeDwellWindow();
355
+ if (accumulatedDwell > 0) {
356
+ emit(buildDwell({ path: path(), ts: now(), dwellMs: accumulatedDwell }));
357
+ accumulatedDwell = 0;
358
+ }
359
+ if (isVisible()) openDwellWindow();
360
+ };
361
+ const onPageHide = () => {
362
+ if (track.dwell) {
363
+ closeDwellWindow();
364
+ if (accumulatedDwell > 0) {
365
+ emit(buildDwell({ path: path(), ts: now(), dwellMs: accumulatedDwell }));
366
+ accumulatedDwell = 0;
367
+ }
368
+ }
369
+ buffer.flush();
370
+ };
371
+ const addListener = (target, type, handler, options) => {
372
+ if (!target || typeof target.addEventListener !== "function") return;
373
+ target.addEventListener(type, handler, options);
374
+ teardown.push(() => target.removeEventListener(type, handler, options));
375
+ };
376
+ return {
377
+ start() {
378
+ if (started) return;
379
+ started = true;
380
+ const doc = typeof document !== "undefined" ? document : void 0;
381
+ const win = typeof window !== "undefined" ? window : void 0;
382
+ if (track.pageview) {
383
+ emit(buildPageview(path(), now()));
384
+ }
385
+ if (track.dwell) {
386
+ openDwellWindow();
387
+ addListener(doc, "visibilitychange", onVisibilityChange);
388
+ }
389
+ if (track.click) {
390
+ addListener(doc, "click", onClick, { capture: true, passive: true });
391
+ }
392
+ if (track.scroll) {
393
+ addListener(win, "scroll", onScroll, { passive: true });
394
+ }
395
+ if (track.pointer) {
396
+ addListener(win, "pointermove", onPointerMove, { passive: true });
397
+ }
398
+ addListener(win, "pagehide", onPageHide);
399
+ if (track.agent) {
400
+ void tapAgentActivity({ path, emit }).then((unsub) => {
401
+ if (started) agentUnsub = unsub;
402
+ else unsub();
403
+ });
404
+ }
405
+ if (typeof setInterval === "function") {
406
+ flushTimer = setInterval(() => {
407
+ if (track.dwell) emitDwell();
408
+ buffer.flush();
409
+ }, flushMs);
410
+ }
411
+ },
412
+ stop() {
413
+ if (!started) return;
414
+ started = false;
415
+ if (flushTimer !== void 0) {
416
+ clearInterval(flushTimer);
417
+ flushTimer = void 0;
418
+ }
419
+ for (const fn of teardown.splice(0)) {
420
+ try {
421
+ fn();
422
+ } catch {
423
+ }
424
+ }
425
+ if (agentUnsub) {
426
+ try {
427
+ agentUnsub();
428
+ } catch {
429
+ }
430
+ agentUnsub = void 0;
431
+ }
432
+ if (track.dwell) {
433
+ closeDwellWindow();
434
+ if (accumulatedDwell > 0) {
435
+ emit(buildDwell({ path: path(), ts: now(), dwellMs: accumulatedDwell }));
436
+ accumulatedDwell = 0;
437
+ }
438
+ }
439
+ buffer.flush();
440
+ },
441
+ flush() {
442
+ buffer.flush();
443
+ }
444
+ };
445
+ }
446
+ function describeTarget(el) {
447
+ if (!el) return {};
448
+ let node = el;
449
+ let id;
450
+ for (let depth = 0; node && depth < 5; depth++) {
451
+ const candidate = node.getAttribute?.("data-fancy-id") ?? node.getAttribute?.("data-testid") ?? (node.id || void 0);
452
+ if (candidate) {
453
+ id = candidate;
454
+ break;
455
+ }
456
+ node = node.parentElement;
457
+ }
458
+ const labelSource = node ?? el;
459
+ const label = readLabel(labelSource);
460
+ const out = {};
461
+ if (id !== void 0) out.id = id;
462
+ if (label !== void 0) out.label = label;
463
+ return out;
464
+ }
465
+ function readLabel(el) {
466
+ const aria = el.getAttribute?.("aria-label");
467
+ if (aria) return trim(aria);
468
+ const text = el.textContent;
469
+ if (text) {
470
+ const t = trim(text);
471
+ if (t) return t;
472
+ }
473
+ const tag = el.tagName ? el.tagName.toLowerCase() : void 0;
474
+ return tag;
475
+ }
476
+ function trim(s) {
477
+ return s.replace(/\s+/g, " ").trim().slice(0, 120);
478
+ }
479
+
480
+ // src/collector.ts
481
+ var factory = (opts) => createCollector({ siteKey: opts.siteKey, endpoint: opts.endpoint });
482
+ function createPixelCollector(opts) {
483
+ return factory(opts);
484
+ }
485
+ function __setCollectorFactory(next) {
486
+ factory = next ?? ((opts) => createCollector({ siteKey: opts.siteKey, endpoint: opts.endpoint }));
487
+ }
488
+
32
489
  // src/render.ts
33
490
  function logoGlyph(size) {
34
491
  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;">
@@ -189,6 +646,7 @@ function mountPixel(opts = {}) {
189
646
  const mode = MODES.includes(opts.mode) ? opts.mode : "floating";
190
647
  const siteKey = opts.siteKey ?? "";
191
648
  const endpoint = opts.endpoint ?? "";
649
+ const collect = opts.collect !== false;
192
650
  const href = opts.href ?? DEFAULT_HREF;
193
651
  const target = opts.target ?? null;
194
652
  const host = createPixelHost(style, mode, href);
@@ -242,11 +700,21 @@ function mountPixel(opts = {}) {
242
700
  setVisible(true);
243
701
  }
244
702
  beacon();
703
+ let collector = null;
704
+ if (endpoint && collect) {
705
+ try {
706
+ collector = createPixelCollector({ siteKey, endpoint });
707
+ collector.start();
708
+ } catch {
709
+ collector = null;
710
+ }
711
+ }
245
712
  const resolved = {
246
713
  style,
247
714
  mode,
248
715
  siteKey,
249
716
  endpoint,
717
+ collect,
250
718
  href,
251
719
  target
252
720
  };
@@ -255,12 +723,22 @@ function mountPixel(opts = {}) {
255
723
  get visible() {
256
724
  return visible;
257
725
  },
726
+ get collector() {
727
+ return collector;
728
+ },
258
729
  options: resolved,
259
730
  destroy() {
260
731
  if (destroyed) return;
261
732
  destroyed = true;
262
733
  observer?.disconnect();
263
734
  observer = null;
735
+ if (collector) {
736
+ try {
737
+ collector.stop();
738
+ } catch {
739
+ }
740
+ collector = null;
741
+ }
264
742
  host.remove();
265
743
  }
266
744
  };
@@ -278,14 +756,16 @@ function readScriptConfig() {
278
756
  }
279
757
  if (!el) return null;
280
758
  const ds = el.dataset;
281
- if (ds.style === void 0 && ds.mode === void 0 && ds.site === void 0 && ds.endpoint === void 0 && ds.fancyPixel === void 0) {
759
+ 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) {
282
760
  return null;
283
761
  }
762
+ const collect = ds.collect !== void 0 ? ds.collect !== "false" : void 0;
284
763
  return {
285
764
  style: ds.style,
286
765
  mode: ds.mode,
287
766
  siteKey: ds.site,
288
767
  endpoint: ds.endpoint,
768
+ collect,
289
769
  target: ds.target ?? null,
290
770
  href: ds.href
291
771
  };
@@ -308,6 +788,6 @@ function autoInit() {
308
788
  return run();
309
789
  }
310
790
 
311
- export { autoInit, createPixelHost, hostStyleFor, mountPixel, sendPixelBeacon };
791
+ export { __setCollectorFactory, autoInit, createPixelCollector, createPixelHost, hostStyleFor, mountPixel, sendPixelBeacon };
312
792
  //# sourceMappingURL=index.js.map
313
793
  //# sourceMappingURL=index.js.map