@particle-academy/agent-integrations 0.18.0 → 0.19.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/chunk-7R6RMROL.js +110 -0
- package/dist/chunk-7R6RMROL.js.map +1 -0
- package/dist/heuristics/sink.d.cts +78 -0
- package/dist/heuristics/sink.d.ts +78 -0
- package/dist/heuristics.cjs +115 -0
- package/dist/heuristics.cjs.map +1 -0
- package/dist/heuristics.js +6 -0
- package/dist/heuristics.js.map +1 -0
- package/dist/index.cjs +107 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { onActivity } from './chunk-C3TYI5TJ.js';
|
|
2
|
+
|
|
3
|
+
// src/heuristics/sink.ts
|
|
4
|
+
function numericMeta(meta, key) {
|
|
5
|
+
const v = meta?.[key];
|
|
6
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
7
|
+
}
|
|
8
|
+
function mapActivityToEvent(e, path) {
|
|
9
|
+
const source = e.source ?? "agent";
|
|
10
|
+
const targetId = e.target?.elementId;
|
|
11
|
+
const label = e.target?.label;
|
|
12
|
+
const meta = {
|
|
13
|
+
action: e.action,
|
|
14
|
+
agentId: e.agentId,
|
|
15
|
+
source,
|
|
16
|
+
kind: e.target?.kind
|
|
17
|
+
};
|
|
18
|
+
const dwellMs = numericMeta(e.meta, "dwellMs");
|
|
19
|
+
if (dwellMs !== void 0) {
|
|
20
|
+
const ev2 = {
|
|
21
|
+
kind: "dwell",
|
|
22
|
+
actor: "agent",
|
|
23
|
+
path,
|
|
24
|
+
ts: e.timestamp,
|
|
25
|
+
dwellMs,
|
|
26
|
+
meta
|
|
27
|
+
};
|
|
28
|
+
if (targetId !== void 0) ev2.targetId = targetId;
|
|
29
|
+
if (label !== void 0) ev2.label = label;
|
|
30
|
+
return ev2;
|
|
31
|
+
}
|
|
32
|
+
const ev = {
|
|
33
|
+
kind: "click",
|
|
34
|
+
actor: "agent",
|
|
35
|
+
path,
|
|
36
|
+
ts: e.timestamp,
|
|
37
|
+
x: numericMeta(e.meta, "x") ?? 0,
|
|
38
|
+
y: numericMeta(e.meta, "y") ?? 0,
|
|
39
|
+
vw: numericMeta(e.meta, "vw") ?? 0,
|
|
40
|
+
vh: numericMeta(e.meta, "vh") ?? 0,
|
|
41
|
+
meta
|
|
42
|
+
};
|
|
43
|
+
if (targetId !== void 0) ev.targetId = targetId;
|
|
44
|
+
if (label !== void 0) ev.label = label;
|
|
45
|
+
return ev;
|
|
46
|
+
}
|
|
47
|
+
function randomId() {
|
|
48
|
+
return Math.random().toString(36).slice(2, 10);
|
|
49
|
+
}
|
|
50
|
+
function attachHeuristicsSink(opts) {
|
|
51
|
+
if (typeof window === "undefined") return () => {
|
|
52
|
+
};
|
|
53
|
+
const endpoint = opts.endpoint.replace(/\/$/, "");
|
|
54
|
+
const url = `${endpoint}/collect`;
|
|
55
|
+
const siteKey = opts.siteKey;
|
|
56
|
+
const sessionId = opts.sessionId ?? `agent-${randomId()}`;
|
|
57
|
+
const getPath = opts.path ?? (() => location.pathname);
|
|
58
|
+
const source = opts.source ?? "all";
|
|
59
|
+
const batchMs = opts.batchMs ?? 2e3;
|
|
60
|
+
const filter = source === "all" ? void 0 : { source };
|
|
61
|
+
let buffer = [];
|
|
62
|
+
function flush() {
|
|
63
|
+
if (buffer.length === 0) return;
|
|
64
|
+
const events = buffer;
|
|
65
|
+
buffer = [];
|
|
66
|
+
const batch = { siteKey, sessionId, events };
|
|
67
|
+
const body = JSON.stringify(batch);
|
|
68
|
+
try {
|
|
69
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
70
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
71
|
+
navigator.sendBeacon(url, blob);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
void fetch(url, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "content-type": "application/json" },
|
|
80
|
+
body,
|
|
81
|
+
keepalive: true
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const unsubscribeActivity = onActivity((e) => {
|
|
87
|
+
try {
|
|
88
|
+
buffer.push(mapActivityToEvent(e, getPath()));
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
}, filter);
|
|
92
|
+
const timer = setInterval(flush, batchMs);
|
|
93
|
+
const onPageHide = () => flush();
|
|
94
|
+
const onVisibility = () => {
|
|
95
|
+
if (document.visibilityState === "hidden") flush();
|
|
96
|
+
};
|
|
97
|
+
window.addEventListener("pagehide", onPageHide);
|
|
98
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
99
|
+
return () => {
|
|
100
|
+
unsubscribeActivity();
|
|
101
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
102
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
103
|
+
clearInterval(timer);
|
|
104
|
+
flush();
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { attachHeuristicsSink, mapActivityToEvent };
|
|
109
|
+
//# sourceMappingURL=chunk-7R6RMROL.js.map
|
|
110
|
+
//# sourceMappingURL=chunk-7R6RMROL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/heuristics/sink.ts"],"names":["ev"],"mappings":";;;AAkEA,SAAS,WAAA,CACP,MACA,GAAA,EACoB;AACpB,EAAA,MAAM,CAAA,GAAI,OAAO,GAAG,CAAA;AACpB,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,IAAY,OAAO,QAAA,CAAS,CAAC,IAAI,CAAA,GAAI,MAAA;AAC3D;AAQO,SAAS,kBAAA,CACd,GACA,IAAA,EACiB;AACjB,EAAA,MAAM,MAAA,GAAS,EAAE,MAAA,IAAU,OAAA;AAC3B,EAAA,MAAM,QAAA,GAAW,EAAE,MAAA,EAAQ,SAAA;AAC3B,EAAA,MAAM,KAAA,GAAQ,EAAE,MAAA,EAAQ,KAAA;AACxB,EAAA,MAAM,IAAA,GAAgC;AAAA,IACpC,QAAQ,CAAA,CAAE,MAAA;AAAA,IACV,SAAS,CAAA,CAAE,OAAA;AAAA,IACX,MAAA;AAAA,IACA,IAAA,EAAM,EAAE,MAAA,EAAQ;AAAA,GAClB;AACA,EAAA,MAAM,OAAA,GAAU,WAAA,CAAY,CAAA,CAAE,IAAA,EAAM,SAAS,CAAA;AAE7C,EAAA,IAAI,YAAY,MAAA,EAAW;AACzB,IAAA,MAAMA,GAAAA,GAAsB;AAAA,MAC1B,IAAA,EAAM,OAAA;AAAA,MACN,KAAA,EAAO,OAAA;AAAA,MACP,IAAA;AAAA,MACA,IAAI,CAAA,CAAE,SAAA;AAAA,MACN,OAAA;AAAA,MACA;AAAA,KACF;AACA,IAAA,IAAI,QAAA,KAAa,MAAA,EAAWA,GAAAA,CAAG,QAAA,GAAW,QAAA;AAC1C,IAAA,IAAI,KAAA,KAAU,MAAA,EAAWA,GAAAA,CAAG,KAAA,GAAQ,KAAA;AACpC,IAAA,OAAOA,GAAAA;AAAA,EACT;AAEA,EAAA,MAAM,EAAA,GAAsB;AAAA,IAC1B,IAAA,EAAM,OAAA;AAAA,IACN,KAAA,EAAO,OAAA;AAAA,IACP,IAAA;AAAA,IACA,IAAI,CAAA,CAAE,SAAA;AAAA,IACN,CAAA,EAAG,WAAA,CAAY,CAAA,CAAE,IAAA,EAAM,GAAG,CAAA,IAAK,CAAA;AAAA,IAC/B,CAAA,EAAG,WAAA,CAAY,CAAA,CAAE,IAAA,EAAM,GAAG,CAAA,IAAK,CAAA;AAAA,IAC/B,EAAA,EAAI,WAAA,CAAY,CAAA,CAAE,IAAA,EAAM,IAAI,CAAA,IAAK,CAAA;AAAA,IACjC,EAAA,EAAI,WAAA,CAAY,CAAA,CAAE,IAAA,EAAM,IAAI,CAAA,IAAK,CAAA;AAAA,IACjC;AAAA,GACF;AACA,EAAA,IAAI,QAAA,KAAa,MAAA,EAAW,EAAA,CAAG,QAAA,GAAW,QAAA;AAC1C,EAAA,IAAI,KAAA,KAAU,MAAA,EAAW,EAAA,CAAG,KAAA,GAAQ,KAAA;AACpC,EAAA,OAAO,EAAA;AACT;AAEA,SAAS,QAAA,GAAmB;AAC1B,EAAA,OAAO,IAAA,CAAK,QAAO,CAAE,QAAA,CAAS,EAAE,CAAA,CAAE,KAAA,CAAM,GAAG,EAAE,CAAA;AAC/C;AAQO,SAAS,qBACd,IAAA,EACY;AAEZ,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,OAAO,MAAM;AAAA,EAAC,CAAA;AAEjD,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,QAAA,CAAS,OAAA,CAAQ,OAAO,EAAE,CAAA;AAChD,EAAA,MAAM,GAAA,GAAM,GAAG,QAAQ,CAAA,QAAA,CAAA;AACvB,EAAA,MAAM,UAAU,IAAA,CAAK,OAAA;AACrB,EAAA,MAAM,SAAA,GAAY,IAAA,CAAK,SAAA,IAAa,CAAA,MAAA,EAAS,UAAU,CAAA,CAAA;AACvD,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,IAAA,KAAS,MAAM,QAAA,CAAS,QAAA,CAAA;AAC7C,EAAA,MAAM,MAAA,GAAS,KAAK,MAAA,IAAU,KAAA;AAC9B,EAAA,MAAM,OAAA,GAAU,KAAK,OAAA,IAAW,GAAA;AAIhC,EAAA,MAAM,MAAA,GAAS,MAAA,KAAW,KAAA,GAAQ,MAAA,GAAY,EAAE,MAAA,EAAO;AAEvD,EAAA,IAAI,SAA4B,EAAC;AAEjC,EAAA,SAAS,KAAA,GAAc;AACrB,IAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AACzB,IAAA,MAAM,MAAA,GAAS,MAAA;AACf,IAAA,MAAA,GAAS,EAAC;AACV,IAAA,MAAM,KAAA,GAAsB,EAAE,OAAA,EAAS,SAAA,EAAW,MAAA,EAAO;AACzD,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AACjC,IAAA,IAAI;AACF,MAAA,IACE,OAAO,SAAA,KAAc,WAAA,IACrB,OAAO,SAAA,CAAU,eAAe,UAAA,EAChC;AACA,QAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,CAAC,IAAI,CAAA,EAAG,EAAE,IAAA,EAAM,kBAAA,EAAoB,CAAA;AAC1D,QAAA,SAAA,CAAU,UAAA,CAAW,KAAK,IAAI,CAAA;AAC9B,QAAA;AAAA,MACF;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,IAAI;AACF,MAAA,KAAK,MAAM,GAAA,EAAK;AAAA,QACd,MAAA,EAAQ,MAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA;AAAA,QACA,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF;AAEA,EAAA,MAAM,mBAAA,GAAsB,UAAA,CAAW,CAAC,CAAA,KAAM;AAC5C,IAAA,IAAI;AACF,MAAA,MAAA,CAAO,IAAA,CAAK,kBAAA,CAAmB,CAAA,EAAG,OAAA,EAAS,CAAC,CAAA;AAAA,IAC9C,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACF,GAAG,MAAM,CAAA;AAET,EAAA,MAAM,KAAA,GAAwC,WAAA,CAAY,KAAA,EAAO,OAAO,CAAA;AAExE,EAAA,MAAM,UAAA,GAAa,MAAM,KAAA,EAAM;AAC/B,EAAA,MAAM,eAAe,MAAM;AACzB,IAAA,IAAI,QAAA,CAAS,eAAA,KAAoB,QAAA,EAAU,KAAA,EAAM;AAAA,EACnD,CAAA;AACA,EAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,UAAU,CAAA;AAC9C,EAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,YAAY,CAAA;AAE1D,EAAA,OAAO,MAAM;AACX,IAAA,mBAAA,EAAoB;AACpB,IAAA,MAAA,CAAO,mBAAA,CAAoB,YAAY,UAAU,CAAA;AACjD,IAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAoB,YAAY,CAAA;AAC7D,IAAA,aAAA,CAAc,KAAK,CAAA;AACnB,IAAA,KAAA,EAAM;AAAA,EACR,CAAA;AACF","file":"chunk-7R6RMROL.js","sourcesContent":["/**\n * In-browser heuristics analytics sink for agent traffic.\n *\n * Agent bridge mutations emit `AutoActivityEvent`s into the in-page\n * `@particle-academy/fancy-auto-common` activity bus (presence / cursors).\n * This sink — living in THIS package so it shares the SAME bundled\n * `fancy-auto-common` module instance the bridges emit into — subscribes to\n * that bus and POSTs each event to the `fancy-heuristics` `/collect` endpoint\n * as `actor:\"agent\"`, so agent traffic shows up in the heuristics dashboard.\n *\n * It does NOT depend on `fancy-heuristics` / `fancy-heuristics-js`: it only\n * emits the frozen wire shape over HTTP. The mapping mirrors\n * `fancy-heuristics-js/src/agent.ts` `mapActivityToEvent` — keep parity.\n */\nimport { onActivity, type AgentActivityEvent } from \"../presence\";\n\n/** What kind of interaction an event captures (mirrors fancy-heuristics-js). */\ntype HeuristicsEventKind = \"pageview\" | \"click\" | \"scroll\" | \"pointer\" | \"dwell\";\n\n/** Who produced the interaction. */\ntype HeuristicsActor = \"human\" | \"agent\";\n\n/**\n * A single interaction event — mirrors `fancy-heuristics-js` `HeuristicsEvent`.\n * Optional fields are omitted (never `null`) when not relevant to the `kind`.\n */\nexport interface HeuristicsEvent {\n kind: HeuristicsEventKind;\n actor: HeuristicsActor;\n /** location.pathname at capture time. */\n path: string;\n /** ms epoch. */\n ts: number;\n x?: number;\n y?: number;\n vw?: number;\n vh?: number;\n scrollPct?: number;\n dwellMs?: number;\n targetId?: string;\n label?: string;\n meta?: Record<string, unknown>;\n}\n\n/** The batched POST body sent to `${endpoint}/collect`. */\nexport interface CollectBatch {\n siteKey: string;\n sessionId: string;\n events: HeuristicsEvent[];\n}\n\nexport interface AttachHeuristicsSinkOptions {\n /** Base URL, e.g. \"/heuristics\". POSTs to `${endpoint}/collect`. */\n endpoint: string;\n /** Identifies the site to the ingestion endpoint. */\n siteKey: string;\n /** Stable session id. Default: a generated \"agent-<rand>\" per attach. */\n sessionId?: string;\n /** Resolves the current path. Default: `() => location.pathname`. */\n path?: () => string;\n /** Which activity sources to record. Default \"all\". */\n source?: \"agent\" | \"flow\" | \"all\";\n /** Flush interval in ms. Default 2000. */\n batchMs?: number;\n}\n\nfunction numericMeta(\n meta: Record<string, unknown> | undefined,\n key: string,\n): number | undefined {\n const v = meta?.[key];\n return typeof v === \"number\" && Number.isFinite(v) ? v : undefined;\n}\n\n/**\n * Map one activity event to an `actor:\"agent\"` HeuristicsEvent. Activities\n * carrying a finite `meta.dwellMs` become `kind:\"dwell\"`; everything else is a\n * discrete `kind:\"click\"` with x/y/vw/vh pulled from numeric meta (default 0).\n * Mirrors `fancy-heuristics-js/src/agent.ts` `mapActivityToEvent`.\n */\nexport function mapActivityToEvent(\n e: AgentActivityEvent,\n path: string,\n): HeuristicsEvent {\n const source = e.source ?? \"agent\";\n const targetId = e.target?.elementId;\n const label = e.target?.label;\n const meta: Record<string, unknown> = {\n action: e.action,\n agentId: e.agentId,\n source,\n kind: e.target?.kind,\n };\n const dwellMs = numericMeta(e.meta, \"dwellMs\");\n\n if (dwellMs !== undefined) {\n const ev: HeuristicsEvent = {\n kind: \"dwell\",\n actor: \"agent\",\n path,\n ts: e.timestamp,\n dwellMs,\n meta,\n };\n if (targetId !== undefined) ev.targetId = targetId;\n if (label !== undefined) ev.label = label;\n return ev;\n }\n\n const ev: HeuristicsEvent = {\n kind: \"click\",\n actor: \"agent\",\n path,\n ts: e.timestamp,\n x: numericMeta(e.meta, \"x\") ?? 0,\n y: numericMeta(e.meta, \"y\") ?? 0,\n vw: numericMeta(e.meta, \"vw\") ?? 0,\n vh: numericMeta(e.meta, \"vh\") ?? 0,\n meta,\n };\n if (targetId !== undefined) ev.targetId = targetId;\n if (label !== undefined) ev.label = label;\n return ev;\n}\n\nfunction randomId(): string {\n return Math.random().toString(36).slice(2, 10);\n}\n\n/**\n * Subscribe to the in-page agent activity bus and forward each matching event\n * to the heuristics `/collect` endpoint as `actor:\"agent\"`. SSR-safe (returns a\n * no-op when there is no `window`). Returns an unsubscribe that flushes the\n * buffer and detaches all listeners/timers.\n */\nexport function attachHeuristicsSink(\n opts: AttachHeuristicsSinkOptions,\n): () => void {\n // Browser guard — SSR-safe no-op.\n if (typeof window === \"undefined\") return () => {};\n\n const endpoint = opts.endpoint.replace(/\\/$/, \"\");\n const url = `${endpoint}/collect`;\n const siteKey = opts.siteKey;\n const sessionId = opts.sessionId ?? `agent-${randomId()}`;\n const getPath = opts.path ?? (() => location.pathname);\n const source = opts.source ?? \"all\";\n const batchMs = opts.batchMs ?? 2000;\n\n // onActivity's `source` filter is strict-equality; only set it when we want a\n // single source. \"all\" subscribes unfiltered.\n const filter = source === \"all\" ? undefined : { source };\n\n let buffer: HeuristicsEvent[] = [];\n\n function flush(): void {\n if (buffer.length === 0) return;\n const events = buffer;\n buffer = [];\n const batch: CollectBatch = { siteKey, sessionId, events };\n const body = JSON.stringify(batch);\n try {\n if (\n typeof navigator !== \"undefined\" &&\n typeof navigator.sendBeacon === \"function\"\n ) {\n const blob = new Blob([body], { type: \"application/json\" });\n navigator.sendBeacon(url, blob);\n return;\n }\n } catch {\n // fall through to fetch\n }\n try {\n void fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body,\n keepalive: true,\n });\n } catch {\n // never throw from a flush\n }\n }\n\n const unsubscribeActivity = onActivity((e) => {\n try {\n buffer.push(mapActivityToEvent(e, getPath()));\n } catch {\n // never let a malformed event break the bus\n }\n }, filter);\n\n const timer: ReturnType<typeof setInterval> = setInterval(flush, batchMs);\n\n const onPageHide = () => flush();\n const onVisibility = () => {\n if (document.visibilityState === \"hidden\") flush();\n };\n window.addEventListener(\"pagehide\", onPageHide);\n document.addEventListener(\"visibilitychange\", onVisibility);\n\n return () => {\n unsubscribeActivity();\n window.removeEventListener(\"pagehide\", onPageHide);\n document.removeEventListener(\"visibilitychange\", onVisibility);\n clearInterval(timer);\n flush();\n };\n}\n"]}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AutoActivityEvent } from '@particle-academy/fancy-auto-common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-browser heuristics analytics sink for agent traffic.
|
|
5
|
+
*
|
|
6
|
+
* Agent bridge mutations emit `AutoActivityEvent`s into the in-page
|
|
7
|
+
* `@particle-academy/fancy-auto-common` activity bus (presence / cursors).
|
|
8
|
+
* This sink — living in THIS package so it shares the SAME bundled
|
|
9
|
+
* `fancy-auto-common` module instance the bridges emit into — subscribes to
|
|
10
|
+
* that bus and POSTs each event to the `fancy-heuristics` `/collect` endpoint
|
|
11
|
+
* as `actor:"agent"`, so agent traffic shows up in the heuristics dashboard.
|
|
12
|
+
*
|
|
13
|
+
* It does NOT depend on `fancy-heuristics` / `fancy-heuristics-js`: it only
|
|
14
|
+
* emits the frozen wire shape over HTTP. The mapping mirrors
|
|
15
|
+
* `fancy-heuristics-js/src/agent.ts` `mapActivityToEvent` — keep parity.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** What kind of interaction an event captures (mirrors fancy-heuristics-js). */
|
|
19
|
+
type HeuristicsEventKind = "pageview" | "click" | "scroll" | "pointer" | "dwell";
|
|
20
|
+
/** Who produced the interaction. */
|
|
21
|
+
type HeuristicsActor = "human" | "agent";
|
|
22
|
+
/**
|
|
23
|
+
* A single interaction event — mirrors `fancy-heuristics-js` `HeuristicsEvent`.
|
|
24
|
+
* Optional fields are omitted (never `null`) when not relevant to the `kind`.
|
|
25
|
+
*/
|
|
26
|
+
interface HeuristicsEvent {
|
|
27
|
+
kind: HeuristicsEventKind;
|
|
28
|
+
actor: HeuristicsActor;
|
|
29
|
+
/** location.pathname at capture time. */
|
|
30
|
+
path: string;
|
|
31
|
+
/** ms epoch. */
|
|
32
|
+
ts: number;
|
|
33
|
+
x?: number;
|
|
34
|
+
y?: number;
|
|
35
|
+
vw?: number;
|
|
36
|
+
vh?: number;
|
|
37
|
+
scrollPct?: number;
|
|
38
|
+
dwellMs?: number;
|
|
39
|
+
targetId?: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
meta?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
/** The batched POST body sent to `${endpoint}/collect`. */
|
|
44
|
+
interface CollectBatch {
|
|
45
|
+
siteKey: string;
|
|
46
|
+
sessionId: string;
|
|
47
|
+
events: HeuristicsEvent[];
|
|
48
|
+
}
|
|
49
|
+
interface AttachHeuristicsSinkOptions {
|
|
50
|
+
/** Base URL, e.g. "/heuristics". POSTs to `${endpoint}/collect`. */
|
|
51
|
+
endpoint: string;
|
|
52
|
+
/** Identifies the site to the ingestion endpoint. */
|
|
53
|
+
siteKey: string;
|
|
54
|
+
/** Stable session id. Default: a generated "agent-<rand>" per attach. */
|
|
55
|
+
sessionId?: string;
|
|
56
|
+
/** Resolves the current path. Default: `() => location.pathname`. */
|
|
57
|
+
path?: () => string;
|
|
58
|
+
/** Which activity sources to record. Default "all". */
|
|
59
|
+
source?: "agent" | "flow" | "all";
|
|
60
|
+
/** Flush interval in ms. Default 2000. */
|
|
61
|
+
batchMs?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Map one activity event to an `actor:"agent"` HeuristicsEvent. Activities
|
|
65
|
+
* carrying a finite `meta.dwellMs` become `kind:"dwell"`; everything else is a
|
|
66
|
+
* discrete `kind:"click"` with x/y/vw/vh pulled from numeric meta (default 0).
|
|
67
|
+
* Mirrors `fancy-heuristics-js/src/agent.ts` `mapActivityToEvent`.
|
|
68
|
+
*/
|
|
69
|
+
declare function mapActivityToEvent(e: AutoActivityEvent, path: string): HeuristicsEvent;
|
|
70
|
+
/**
|
|
71
|
+
* Subscribe to the in-page agent activity bus and forward each matching event
|
|
72
|
+
* to the heuristics `/collect` endpoint as `actor:"agent"`. SSR-safe (returns a
|
|
73
|
+
* no-op when there is no `window`). Returns an unsubscribe that flushes the
|
|
74
|
+
* buffer and detaches all listeners/timers.
|
|
75
|
+
*/
|
|
76
|
+
declare function attachHeuristicsSink(opts: AttachHeuristicsSinkOptions): () => void;
|
|
77
|
+
|
|
78
|
+
export { type AttachHeuristicsSinkOptions, type CollectBatch, type HeuristicsEvent, attachHeuristicsSink, mapActivityToEvent };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AutoActivityEvent } from '@particle-academy/fancy-auto-common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-browser heuristics analytics sink for agent traffic.
|
|
5
|
+
*
|
|
6
|
+
* Agent bridge mutations emit `AutoActivityEvent`s into the in-page
|
|
7
|
+
* `@particle-academy/fancy-auto-common` activity bus (presence / cursors).
|
|
8
|
+
* This sink — living in THIS package so it shares the SAME bundled
|
|
9
|
+
* `fancy-auto-common` module instance the bridges emit into — subscribes to
|
|
10
|
+
* that bus and POSTs each event to the `fancy-heuristics` `/collect` endpoint
|
|
11
|
+
* as `actor:"agent"`, so agent traffic shows up in the heuristics dashboard.
|
|
12
|
+
*
|
|
13
|
+
* It does NOT depend on `fancy-heuristics` / `fancy-heuristics-js`: it only
|
|
14
|
+
* emits the frozen wire shape over HTTP. The mapping mirrors
|
|
15
|
+
* `fancy-heuristics-js/src/agent.ts` `mapActivityToEvent` — keep parity.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** What kind of interaction an event captures (mirrors fancy-heuristics-js). */
|
|
19
|
+
type HeuristicsEventKind = "pageview" | "click" | "scroll" | "pointer" | "dwell";
|
|
20
|
+
/** Who produced the interaction. */
|
|
21
|
+
type HeuristicsActor = "human" | "agent";
|
|
22
|
+
/**
|
|
23
|
+
* A single interaction event — mirrors `fancy-heuristics-js` `HeuristicsEvent`.
|
|
24
|
+
* Optional fields are omitted (never `null`) when not relevant to the `kind`.
|
|
25
|
+
*/
|
|
26
|
+
interface HeuristicsEvent {
|
|
27
|
+
kind: HeuristicsEventKind;
|
|
28
|
+
actor: HeuristicsActor;
|
|
29
|
+
/** location.pathname at capture time. */
|
|
30
|
+
path: string;
|
|
31
|
+
/** ms epoch. */
|
|
32
|
+
ts: number;
|
|
33
|
+
x?: number;
|
|
34
|
+
y?: number;
|
|
35
|
+
vw?: number;
|
|
36
|
+
vh?: number;
|
|
37
|
+
scrollPct?: number;
|
|
38
|
+
dwellMs?: number;
|
|
39
|
+
targetId?: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
meta?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
/** The batched POST body sent to `${endpoint}/collect`. */
|
|
44
|
+
interface CollectBatch {
|
|
45
|
+
siteKey: string;
|
|
46
|
+
sessionId: string;
|
|
47
|
+
events: HeuristicsEvent[];
|
|
48
|
+
}
|
|
49
|
+
interface AttachHeuristicsSinkOptions {
|
|
50
|
+
/** Base URL, e.g. "/heuristics". POSTs to `${endpoint}/collect`. */
|
|
51
|
+
endpoint: string;
|
|
52
|
+
/** Identifies the site to the ingestion endpoint. */
|
|
53
|
+
siteKey: string;
|
|
54
|
+
/** Stable session id. Default: a generated "agent-<rand>" per attach. */
|
|
55
|
+
sessionId?: string;
|
|
56
|
+
/** Resolves the current path. Default: `() => location.pathname`. */
|
|
57
|
+
path?: () => string;
|
|
58
|
+
/** Which activity sources to record. Default "all". */
|
|
59
|
+
source?: "agent" | "flow" | "all";
|
|
60
|
+
/** Flush interval in ms. Default 2000. */
|
|
61
|
+
batchMs?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Map one activity event to an `actor:"agent"` HeuristicsEvent. Activities
|
|
65
|
+
* carrying a finite `meta.dwellMs` become `kind:"dwell"`; everything else is a
|
|
66
|
+
* discrete `kind:"click"` with x/y/vw/vh pulled from numeric meta (default 0).
|
|
67
|
+
* Mirrors `fancy-heuristics-js/src/agent.ts` `mapActivityToEvent`.
|
|
68
|
+
*/
|
|
69
|
+
declare function mapActivityToEvent(e: AutoActivityEvent, path: string): HeuristicsEvent;
|
|
70
|
+
/**
|
|
71
|
+
* Subscribe to the in-page agent activity bus and forward each matching event
|
|
72
|
+
* to the heuristics `/collect` endpoint as `actor:"agent"`. SSR-safe (returns a
|
|
73
|
+
* no-op when there is no `window`). Returns an unsubscribe that flushes the
|
|
74
|
+
* buffer and detaches all listeners/timers.
|
|
75
|
+
*/
|
|
76
|
+
declare function attachHeuristicsSink(opts: AttachHeuristicsSinkOptions): () => void;
|
|
77
|
+
|
|
78
|
+
export { type AttachHeuristicsSinkOptions, type CollectBatch, type HeuristicsEvent, attachHeuristicsSink, mapActivityToEvent };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fancyAutoCommon = require('@particle-academy/fancy-auto-common');
|
|
4
|
+
|
|
5
|
+
// src/presence/registry.ts
|
|
6
|
+
|
|
7
|
+
// src/heuristics/sink.ts
|
|
8
|
+
function numericMeta(meta, key) {
|
|
9
|
+
const v = meta?.[key];
|
|
10
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
11
|
+
}
|
|
12
|
+
function mapActivityToEvent(e, path) {
|
|
13
|
+
const source = e.source ?? "agent";
|
|
14
|
+
const targetId = e.target?.elementId;
|
|
15
|
+
const label = e.target?.label;
|
|
16
|
+
const meta = {
|
|
17
|
+
action: e.action,
|
|
18
|
+
agentId: e.agentId,
|
|
19
|
+
source,
|
|
20
|
+
kind: e.target?.kind
|
|
21
|
+
};
|
|
22
|
+
const dwellMs = numericMeta(e.meta, "dwellMs");
|
|
23
|
+
if (dwellMs !== void 0) {
|
|
24
|
+
const ev2 = {
|
|
25
|
+
kind: "dwell",
|
|
26
|
+
actor: "agent",
|
|
27
|
+
path,
|
|
28
|
+
ts: e.timestamp,
|
|
29
|
+
dwellMs,
|
|
30
|
+
meta
|
|
31
|
+
};
|
|
32
|
+
if (targetId !== void 0) ev2.targetId = targetId;
|
|
33
|
+
if (label !== void 0) ev2.label = label;
|
|
34
|
+
return ev2;
|
|
35
|
+
}
|
|
36
|
+
const ev = {
|
|
37
|
+
kind: "click",
|
|
38
|
+
actor: "agent",
|
|
39
|
+
path,
|
|
40
|
+
ts: e.timestamp,
|
|
41
|
+
x: numericMeta(e.meta, "x") ?? 0,
|
|
42
|
+
y: numericMeta(e.meta, "y") ?? 0,
|
|
43
|
+
vw: numericMeta(e.meta, "vw") ?? 0,
|
|
44
|
+
vh: numericMeta(e.meta, "vh") ?? 0,
|
|
45
|
+
meta
|
|
46
|
+
};
|
|
47
|
+
if (targetId !== void 0) ev.targetId = targetId;
|
|
48
|
+
if (label !== void 0) ev.label = label;
|
|
49
|
+
return ev;
|
|
50
|
+
}
|
|
51
|
+
function randomId() {
|
|
52
|
+
return Math.random().toString(36).slice(2, 10);
|
|
53
|
+
}
|
|
54
|
+
function attachHeuristicsSink(opts) {
|
|
55
|
+
if (typeof window === "undefined") return () => {
|
|
56
|
+
};
|
|
57
|
+
const endpoint = opts.endpoint.replace(/\/$/, "");
|
|
58
|
+
const url = `${endpoint}/collect`;
|
|
59
|
+
const siteKey = opts.siteKey;
|
|
60
|
+
const sessionId = opts.sessionId ?? `agent-${randomId()}`;
|
|
61
|
+
const getPath = opts.path ?? (() => location.pathname);
|
|
62
|
+
const source = opts.source ?? "all";
|
|
63
|
+
const batchMs = opts.batchMs ?? 2e3;
|
|
64
|
+
const filter = source === "all" ? void 0 : { source };
|
|
65
|
+
let buffer = [];
|
|
66
|
+
function flush() {
|
|
67
|
+
if (buffer.length === 0) return;
|
|
68
|
+
const events = buffer;
|
|
69
|
+
buffer = [];
|
|
70
|
+
const batch = { siteKey, sessionId, events };
|
|
71
|
+
const body = JSON.stringify(batch);
|
|
72
|
+
try {
|
|
73
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
74
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
75
|
+
navigator.sendBeacon(url, blob);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
void fetch(url, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "content-type": "application/json" },
|
|
84
|
+
body,
|
|
85
|
+
keepalive: true
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const unsubscribeActivity = fancyAutoCommon.onActivity((e) => {
|
|
91
|
+
try {
|
|
92
|
+
buffer.push(mapActivityToEvent(e, getPath()));
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}, filter);
|
|
96
|
+
const timer = setInterval(flush, batchMs);
|
|
97
|
+
const onPageHide = () => flush();
|
|
98
|
+
const onVisibility = () => {
|
|
99
|
+
if (document.visibilityState === "hidden") flush();
|
|
100
|
+
};
|
|
101
|
+
window.addEventListener("pagehide", onPageHide);
|
|
102
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
103
|
+
return () => {
|
|
104
|
+
unsubscribeActivity();
|
|
105
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
106
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
107
|
+
clearInterval(timer);
|
|
108
|
+
flush();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
exports.attachHeuristicsSink = attachHeuristicsSink;
|
|
113
|
+
exports.mapActivityToEvent = mapActivityToEvent;
|
|
114
|
+
//# sourceMappingURL=heuristics.cjs.map
|
|
115
|
+
//# sourceMappingURL=heuristics.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/heuristics/sink.ts"],"names":["ev","onActivity"],"mappings":"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","file":"heuristics.cjs","sourcesContent":["/**\n * In-browser heuristics analytics sink for agent traffic.\n *\n * Agent bridge mutations emit `AutoActivityEvent`s into the in-page\n * `@particle-academy/fancy-auto-common` activity bus (presence / cursors).\n * This sink — living in THIS package so it shares the SAME bundled\n * `fancy-auto-common` module instance the bridges emit into — subscribes to\n * that bus and POSTs each event to the `fancy-heuristics` `/collect` endpoint\n * as `actor:\"agent\"`, so agent traffic shows up in the heuristics dashboard.\n *\n * It does NOT depend on `fancy-heuristics` / `fancy-heuristics-js`: it only\n * emits the frozen wire shape over HTTP. The mapping mirrors\n * `fancy-heuristics-js/src/agent.ts` `mapActivityToEvent` — keep parity.\n */\nimport { onActivity, type AgentActivityEvent } from \"../presence\";\n\n/** What kind of interaction an event captures (mirrors fancy-heuristics-js). */\ntype HeuristicsEventKind = \"pageview\" | \"click\" | \"scroll\" | \"pointer\" | \"dwell\";\n\n/** Who produced the interaction. */\ntype HeuristicsActor = \"human\" | \"agent\";\n\n/**\n * A single interaction event — mirrors `fancy-heuristics-js` `HeuristicsEvent`.\n * Optional fields are omitted (never `null`) when not relevant to the `kind`.\n */\nexport interface HeuristicsEvent {\n kind: HeuristicsEventKind;\n actor: HeuristicsActor;\n /** location.pathname at capture time. */\n path: string;\n /** ms epoch. */\n ts: number;\n x?: number;\n y?: number;\n vw?: number;\n vh?: number;\n scrollPct?: number;\n dwellMs?: number;\n targetId?: string;\n label?: string;\n meta?: Record<string, unknown>;\n}\n\n/** The batched POST body sent to `${endpoint}/collect`. */\nexport interface CollectBatch {\n siteKey: string;\n sessionId: string;\n events: HeuristicsEvent[];\n}\n\nexport interface AttachHeuristicsSinkOptions {\n /** Base URL, e.g. \"/heuristics\". POSTs to `${endpoint}/collect`. */\n endpoint: string;\n /** Identifies the site to the ingestion endpoint. */\n siteKey: string;\n /** Stable session id. Default: a generated \"agent-<rand>\" per attach. */\n sessionId?: string;\n /** Resolves the current path. Default: `() => location.pathname`. */\n path?: () => string;\n /** Which activity sources to record. Default \"all\". */\n source?: \"agent\" | \"flow\" | \"all\";\n /** Flush interval in ms. Default 2000. */\n batchMs?: number;\n}\n\nfunction numericMeta(\n meta: Record<string, unknown> | undefined,\n key: string,\n): number | undefined {\n const v = meta?.[key];\n return typeof v === \"number\" && Number.isFinite(v) ? v : undefined;\n}\n\n/**\n * Map one activity event to an `actor:\"agent\"` HeuristicsEvent. Activities\n * carrying a finite `meta.dwellMs` become `kind:\"dwell\"`; everything else is a\n * discrete `kind:\"click\"` with x/y/vw/vh pulled from numeric meta (default 0).\n * Mirrors `fancy-heuristics-js/src/agent.ts` `mapActivityToEvent`.\n */\nexport function mapActivityToEvent(\n e: AgentActivityEvent,\n path: string,\n): HeuristicsEvent {\n const source = e.source ?? \"agent\";\n const targetId = e.target?.elementId;\n const label = e.target?.label;\n const meta: Record<string, unknown> = {\n action: e.action,\n agentId: e.agentId,\n source,\n kind: e.target?.kind,\n };\n const dwellMs = numericMeta(e.meta, \"dwellMs\");\n\n if (dwellMs !== undefined) {\n const ev: HeuristicsEvent = {\n kind: \"dwell\",\n actor: \"agent\",\n path,\n ts: e.timestamp,\n dwellMs,\n meta,\n };\n if (targetId !== undefined) ev.targetId = targetId;\n if (label !== undefined) ev.label = label;\n return ev;\n }\n\n const ev: HeuristicsEvent = {\n kind: \"click\",\n actor: \"agent\",\n path,\n ts: e.timestamp,\n x: numericMeta(e.meta, \"x\") ?? 0,\n y: numericMeta(e.meta, \"y\") ?? 0,\n vw: numericMeta(e.meta, \"vw\") ?? 0,\n vh: numericMeta(e.meta, \"vh\") ?? 0,\n meta,\n };\n if (targetId !== undefined) ev.targetId = targetId;\n if (label !== undefined) ev.label = label;\n return ev;\n}\n\nfunction randomId(): string {\n return Math.random().toString(36).slice(2, 10);\n}\n\n/**\n * Subscribe to the in-page agent activity bus and forward each matching event\n * to the heuristics `/collect` endpoint as `actor:\"agent\"`. SSR-safe (returns a\n * no-op when there is no `window`). Returns an unsubscribe that flushes the\n * buffer and detaches all listeners/timers.\n */\nexport function attachHeuristicsSink(\n opts: AttachHeuristicsSinkOptions,\n): () => void {\n // Browser guard — SSR-safe no-op.\n if (typeof window === \"undefined\") return () => {};\n\n const endpoint = opts.endpoint.replace(/\\/$/, \"\");\n const url = `${endpoint}/collect`;\n const siteKey = opts.siteKey;\n const sessionId = opts.sessionId ?? `agent-${randomId()}`;\n const getPath = opts.path ?? (() => location.pathname);\n const source = opts.source ?? \"all\";\n const batchMs = opts.batchMs ?? 2000;\n\n // onActivity's `source` filter is strict-equality; only set it when we want a\n // single source. \"all\" subscribes unfiltered.\n const filter = source === \"all\" ? undefined : { source };\n\n let buffer: HeuristicsEvent[] = [];\n\n function flush(): void {\n if (buffer.length === 0) return;\n const events = buffer;\n buffer = [];\n const batch: CollectBatch = { siteKey, sessionId, events };\n const body = JSON.stringify(batch);\n try {\n if (\n typeof navigator !== \"undefined\" &&\n typeof navigator.sendBeacon === \"function\"\n ) {\n const blob = new Blob([body], { type: \"application/json\" });\n navigator.sendBeacon(url, blob);\n return;\n }\n } catch {\n // fall through to fetch\n }\n try {\n void fetch(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body,\n keepalive: true,\n });\n } catch {\n // never throw from a flush\n }\n }\n\n const unsubscribeActivity = onActivity((e) => {\n try {\n buffer.push(mapActivityToEvent(e, getPath()));\n } catch {\n // never let a malformed event break the bus\n }\n }, filter);\n\n const timer: ReturnType<typeof setInterval> = setInterval(flush, batchMs);\n\n const onPageHide = () => flush();\n const onVisibility = () => {\n if (document.visibilityState === \"hidden\") flush();\n };\n window.addEventListener(\"pagehide\", onPageHide);\n document.addEventListener(\"visibilitychange\", onVisibility);\n\n return () => {\n unsubscribeActivity();\n window.removeEventListener(\"pagehide\", onPageHide);\n document.removeEventListener(\"visibilitychange\", onVisibility);\n clearInterval(timer);\n flush();\n };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"heuristics.js"}
|
package/dist/index.cjs
CHANGED
|
@@ -3557,6 +3557,111 @@ function SimulateUsersButton({
|
|
|
3557
3557
|
|
|
3558
3558
|
// src/presence/index.ts
|
|
3559
3559
|
init_registry();
|
|
3560
|
+
|
|
3561
|
+
// src/heuristics/sink.ts
|
|
3562
|
+
function numericMeta(meta, key) {
|
|
3563
|
+
const v = meta?.[key];
|
|
3564
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
3565
|
+
}
|
|
3566
|
+
function mapActivityToEvent(e, path) {
|
|
3567
|
+
const source = e.source ?? "agent";
|
|
3568
|
+
const targetId = e.target?.elementId;
|
|
3569
|
+
const label = e.target?.label;
|
|
3570
|
+
const meta = {
|
|
3571
|
+
action: e.action,
|
|
3572
|
+
agentId: e.agentId,
|
|
3573
|
+
source,
|
|
3574
|
+
kind: e.target?.kind
|
|
3575
|
+
};
|
|
3576
|
+
const dwellMs = numericMeta(e.meta, "dwellMs");
|
|
3577
|
+
if (dwellMs !== void 0) {
|
|
3578
|
+
const ev2 = {
|
|
3579
|
+
kind: "dwell",
|
|
3580
|
+
actor: "agent",
|
|
3581
|
+
path,
|
|
3582
|
+
ts: e.timestamp,
|
|
3583
|
+
dwellMs,
|
|
3584
|
+
meta
|
|
3585
|
+
};
|
|
3586
|
+
if (targetId !== void 0) ev2.targetId = targetId;
|
|
3587
|
+
if (label !== void 0) ev2.label = label;
|
|
3588
|
+
return ev2;
|
|
3589
|
+
}
|
|
3590
|
+
const ev = {
|
|
3591
|
+
kind: "click",
|
|
3592
|
+
actor: "agent",
|
|
3593
|
+
path,
|
|
3594
|
+
ts: e.timestamp,
|
|
3595
|
+
x: numericMeta(e.meta, "x") ?? 0,
|
|
3596
|
+
y: numericMeta(e.meta, "y") ?? 0,
|
|
3597
|
+
vw: numericMeta(e.meta, "vw") ?? 0,
|
|
3598
|
+
vh: numericMeta(e.meta, "vh") ?? 0,
|
|
3599
|
+
meta
|
|
3600
|
+
};
|
|
3601
|
+
if (targetId !== void 0) ev.targetId = targetId;
|
|
3602
|
+
if (label !== void 0) ev.label = label;
|
|
3603
|
+
return ev;
|
|
3604
|
+
}
|
|
3605
|
+
function randomId2() {
|
|
3606
|
+
return Math.random().toString(36).slice(2, 10);
|
|
3607
|
+
}
|
|
3608
|
+
function attachHeuristicsSink(opts) {
|
|
3609
|
+
if (typeof window === "undefined") return () => {
|
|
3610
|
+
};
|
|
3611
|
+
const endpoint = opts.endpoint.replace(/\/$/, "");
|
|
3612
|
+
const url = `${endpoint}/collect`;
|
|
3613
|
+
const siteKey = opts.siteKey;
|
|
3614
|
+
const sessionId = opts.sessionId ?? `agent-${randomId2()}`;
|
|
3615
|
+
const getPath = opts.path ?? (() => location.pathname);
|
|
3616
|
+
const source = opts.source ?? "all";
|
|
3617
|
+
const batchMs = opts.batchMs ?? 2e3;
|
|
3618
|
+
const filter = source === "all" ? void 0 : { source };
|
|
3619
|
+
let buffer = [];
|
|
3620
|
+
function flush() {
|
|
3621
|
+
if (buffer.length === 0) return;
|
|
3622
|
+
const events = buffer;
|
|
3623
|
+
buffer = [];
|
|
3624
|
+
const batch = { siteKey, sessionId, events };
|
|
3625
|
+
const body = JSON.stringify(batch);
|
|
3626
|
+
try {
|
|
3627
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
3628
|
+
const blob = new Blob([body], { type: "application/json" });
|
|
3629
|
+
navigator.sendBeacon(url, blob);
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
} catch {
|
|
3633
|
+
}
|
|
3634
|
+
try {
|
|
3635
|
+
void fetch(url, {
|
|
3636
|
+
method: "POST",
|
|
3637
|
+
headers: { "content-type": "application/json" },
|
|
3638
|
+
body,
|
|
3639
|
+
keepalive: true
|
|
3640
|
+
});
|
|
3641
|
+
} catch {
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
const unsubscribeActivity = fancyAutoCommon.onActivity((e) => {
|
|
3645
|
+
try {
|
|
3646
|
+
buffer.push(mapActivityToEvent(e, getPath()));
|
|
3647
|
+
} catch {
|
|
3648
|
+
}
|
|
3649
|
+
}, filter);
|
|
3650
|
+
const timer = setInterval(flush, batchMs);
|
|
3651
|
+
const onPageHide = () => flush();
|
|
3652
|
+
const onVisibility = () => {
|
|
3653
|
+
if (document.visibilityState === "hidden") flush();
|
|
3654
|
+
};
|
|
3655
|
+
window.addEventListener("pagehide", onPageHide);
|
|
3656
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
3657
|
+
return () => {
|
|
3658
|
+
unsubscribeActivity();
|
|
3659
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
3660
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
3661
|
+
clearInterval(timer);
|
|
3662
|
+
flush();
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
3560
3665
|
function useUndoStack(agentId, intervalMs = 500) {
|
|
3561
3666
|
const [history, setHistory] = react.useState(() => fancyAutoCommon.readHistory(agentId));
|
|
3562
3667
|
react.useEffect(() => {
|
|
@@ -3702,6 +3807,7 @@ exports.SseRelayTransport = SseRelayTransport;
|
|
|
3702
3807
|
exports.ToolRegistry = ToolRegistry;
|
|
3703
3808
|
exports.VscodeMark = VscodeMark;
|
|
3704
3809
|
exports.WrenchMark = WrenchMark;
|
|
3810
|
+
exports.attachHeuristicsSink = attachHeuristicsSink;
|
|
3705
3811
|
exports.attachInProcess = attachInProcess;
|
|
3706
3812
|
exports.attachRelay = attachRelay;
|
|
3707
3813
|
exports.attachSseRelay = attachSseRelay;
|
|
@@ -3720,6 +3826,7 @@ exports.describeSession = describeSession;
|
|
|
3720
3826
|
exports.encodeBase64Json = encodeBase64Json;
|
|
3721
3827
|
exports.ensureUndoToolsRegistered = ensureUndoToolsRegistered;
|
|
3722
3828
|
exports.errorResult = errorResult;
|
|
3829
|
+
exports.mapActivityToHeuristicsEvent = mapActivityToEvent;
|
|
3723
3830
|
exports.readSessionFromUrl = readSessionFromUrl;
|
|
3724
3831
|
exports.registerChartsBridge = registerChartsBridge;
|
|
3725
3832
|
exports.registerCodeBridge = registerCodeBridge;
|