@agentstats/sdk 0.0.0-stage → 0.4.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 ADDED
@@ -0,0 +1,800 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ //#region src/web.ts
6
+ const VISIT_COOKIE = "_al_v";
7
+ /** What Claude in Chrome puts on the page while it's in control (its stop button). */
8
+ const AGENT_CONTROL_MARKER = "#claude-agent-stop-container";
9
+ const LOCATION_CHANGE = "agentstats:locationchange";
10
+ const PATCHED$1 = Symbol.for("agentstats.patched");
11
+ const FILE_TYPES = [
12
+ "pdf",
13
+ "xlsx",
14
+ "docx",
15
+ "txt",
16
+ "rtf",
17
+ "csv",
18
+ "exe",
19
+ "key",
20
+ "pps",
21
+ "ppt",
22
+ "pptx",
23
+ "7z",
24
+ "pkg",
25
+ "rar",
26
+ "gz",
27
+ "zip",
28
+ "avi",
29
+ "mov",
30
+ "mp4",
31
+ "mpeg",
32
+ "wmv",
33
+ "midi",
34
+ "mp3",
35
+ "wav",
36
+ "wma",
37
+ "dmg",
38
+ "iso",
39
+ "msi"
40
+ ];
41
+ function readCookie(name) {
42
+ try {
43
+ return document.cookie.match(new RegExp(`(?:^|; )${name}=([\\w-]{8,64})`))?.[1];
44
+ } catch {
45
+ return;
46
+ }
47
+ }
48
+ /** The AI agent browsing this page, if one is: in control right now (Claude in Chrome), or the browser itself is one. */
49
+ function agentInBrowser() {
50
+ if (typeof document === "undefined" || typeof navigator === "undefined") return null;
51
+ if (document.querySelector?.("#claude-agent-stop-container")) return {
52
+ name: "claude-in-chrome",
53
+ via: "dom"
54
+ };
55
+ const ua = String(navigator.userAgent ?? "");
56
+ if (/\bClaude\/\d/.test(ua)) return {
57
+ name: "claude-browser",
58
+ via: "user-agent"
59
+ };
60
+ if (readCookie("_al_v")) return {
61
+ name: "verified-visit",
62
+ via: "visit"
63
+ };
64
+ if (navigator.webdriver === true || /HeadlessChrome/.test(ua)) return {
65
+ name: "automation",
66
+ via: "webdriver"
67
+ };
68
+ return null;
69
+ }
70
+ /** Announce client-side navigation as a LOCATION_CHANGE event. History is patched once per page, for every client on it. */
71
+ function watchHistory() {
72
+ if (typeof history === "undefined") return;
73
+ for (const m of ["pushState", "replaceState"]) {
74
+ const original = history[m];
75
+ if (typeof original !== "function" || original[PATCHED$1]) continue;
76
+ const patched = function(...args) {
77
+ const r = original.apply(this, args);
78
+ window.dispatchEvent(new Event(LOCATION_CHANGE));
79
+ return r;
80
+ };
81
+ Object.defineProperty(patched, PATCHED$1, { value: true });
82
+ history[m] = patched;
83
+ }
84
+ }
85
+ /** Start recording pageviews; returns `track()` for custom events. Does nothing outside a browser. */
86
+ function webAnalytics(o) {
87
+ const noop = {
88
+ pageview() {},
89
+ track() {},
90
+ stop() {}
91
+ };
92
+ if (typeof window === "undefined" || typeof document === "undefined") return noop;
93
+ const log = (...a) => o.debug && console.debug("[agentstats]", ...a);
94
+ const local = /^localhost$|^127(\.\d+){3}$|^\[::1?\]$|\.local$|^$/.test(location.hostname) || location.protocol === "file:";
95
+ let ignored = false;
96
+ try {
97
+ ignored = localStorage.getItem("agentstats_ignore") === "true";
98
+ } catch {}
99
+ const send = (name, props) => {
100
+ if (ignored) return log("ignored: agentstats_ignore is set");
101
+ if (local && !o.allowLocalhost) return log("ignored on localhost:", name);
102
+ const body = JSON.stringify({
103
+ k: o.key,
104
+ n: name,
105
+ u: location.href,
106
+ r: document.referrer || null,
107
+ w: window.innerWidth,
108
+ p: props,
109
+ a: agentInBrowser()?.name,
110
+ h: o.hashRouting ? 1 : void 0
111
+ });
112
+ log("send", name, props ?? "");
113
+ try {
114
+ if (navigator.sendBeacon?.(o.endpoint, body)) return;
115
+ } catch {}
116
+ fetch(o.endpoint, {
117
+ method: "POST",
118
+ body,
119
+ keepalive: true,
120
+ headers: { "Content-Type": "text/plain" }
121
+ }).catch(() => {});
122
+ };
123
+ const ctl = new AbortController();
124
+ const on = { signal: ctl.signal };
125
+ let last = "";
126
+ const pageview = () => {
127
+ if (ctl.signal.aborted) return;
128
+ const page = location.pathname + (o.hashRouting ? location.hash : "");
129
+ if (page === last) return;
130
+ last = page;
131
+ send("pageview");
132
+ };
133
+ const start = () => {
134
+ if (document.visibilityState !== "prerender") return pageview();
135
+ document.addEventListener("visibilitychange", function shown() {
136
+ if (document.visibilityState === "prerender") return;
137
+ document.removeEventListener("visibilitychange", shown);
138
+ pageview();
139
+ }, on);
140
+ };
141
+ start();
142
+ watchHistory();
143
+ const nav = () => setTimeout(pageview, 0);
144
+ window.addEventListener(LOCATION_CHANGE, nav, on);
145
+ window.addEventListener("popstate", nav, on);
146
+ if (o.hashRouting) window.addEventListener("hashchange", nav, on);
147
+ if (o.outboundLinks || o.fileDownloads) {
148
+ const types = Array.isArray(o.fileDownloads) ? o.fileDownloads : o.fileDownloads ? FILE_TYPES : [];
149
+ const clicked = (e) => {
150
+ if (e.type === "auxclick" && e.button !== 1) return;
151
+ const a = e.target?.closest?.("a[href]");
152
+ if (!a) return;
153
+ let url;
154
+ try {
155
+ url = new URL(a.href, location.href);
156
+ } catch {
157
+ return;
158
+ }
159
+ const ext = url.pathname.split(".").pop()?.toLowerCase() ?? "";
160
+ if (types.includes(ext)) send("File Download", { url: url.href.split("?")[0] });
161
+ else if (o.outboundLinks && /^https?:$/.test(url.protocol) && url.host !== location.host) send("Outbound Link: Click", { url: url.href.split("?")[0] });
162
+ };
163
+ document.addEventListener("click", clicked, {
164
+ capture: true,
165
+ signal: ctl.signal
166
+ });
167
+ document.addEventListener("auxclick", clicked, {
168
+ capture: true,
169
+ signal: ctl.signal
170
+ });
171
+ }
172
+ return {
173
+ pageview,
174
+ track: (name, props) => send(String(name), props),
175
+ stop: () => ctl.abort()
176
+ };
177
+ }
178
+ //#endregion
179
+ //#region src/index.ts
180
+ /**
181
+ * AgentStats SDK — analytics for WebMCP tool calls, and for the people visiting the site.
182
+ *
183
+ * Works in any framework: it patches `document.modelContext.registerTool`
184
+ * (and legacy `provideContext` / `unregisterTool` / `clearContext`) so every
185
+ * tool registered *after* `init()` is instrumented automatically. Tools you
186
+ * register yourself can also be wrapped explicitly with `wrap()`.
187
+ *
188
+ * It also records pageviews (Visitors, see ./web.ts): no cookies, no personal data. Turn that
189
+ * off with `trackPageviews: false`.
190
+ */
191
+ const SDK_VERSION = "0.4.1";
192
+ const MAX_ARG_BYTES = 8192;
193
+ const MAX_RESULT_CHARS = 2048;
194
+ function uuid() {
195
+ const c = globalThis.crypto;
196
+ if (c?.randomUUID) return c.randomUUID();
197
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
198
+ const r = Math.random() * 16 | 0;
199
+ return (ch === "x" ? r : r & 3 | 8).toString(16);
200
+ });
201
+ }
202
+ function now() {
203
+ return Date.now();
204
+ }
205
+ function perfNow() {
206
+ return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
207
+ }
208
+ function isAbortError(err) {
209
+ return !!err && typeof err === "object" && err.name === "AbortError";
210
+ }
211
+ function redact(value, keys, depth = 0) {
212
+ if (depth > 6) return "[depth]";
213
+ if (Array.isArray(value)) return value.slice(0, 100).map((v) => redact(v, keys, depth + 1));
214
+ if (value && typeof value === "object") {
215
+ const out = {};
216
+ for (const [k, v] of Object.entries(value)) {
217
+ const lk = k.toLowerCase();
218
+ out[k] = keys.some((rk) => lk.includes(rk)) ? "[redacted]" : redact(v, keys, depth + 1);
219
+ }
220
+ return out;
221
+ }
222
+ if (typeof value === "string" && value.length > 1e3) return value.slice(0, 1e3) + "…";
223
+ return value;
224
+ }
225
+ function safeStringify(value, max) {
226
+ let s;
227
+ try {
228
+ s = typeof value === "string" ? value : JSON.stringify(value);
229
+ } catch {
230
+ s = String(value);
231
+ }
232
+ if (s === void 0) s = "";
233
+ return s.length > max ? s.slice(0, max) + "…" : s;
234
+ }
235
+ function byteSize(value) {
236
+ try {
237
+ return JSON.stringify(value)?.length ?? 0;
238
+ } catch {
239
+ return 0;
240
+ }
241
+ }
242
+ function toDescriptor(tool) {
243
+ const d = { name: String(tool.name ?? "") };
244
+ if (tool.title) d.title = String(tool.title);
245
+ if (tool.description) d.description = String(tool.description);
246
+ if (tool.inputSchema !== void 0) d.inputSchema = tool.inputSchema;
247
+ if (tool.annotations) d.annotations = { ...tool.annotations };
248
+ if (tool.origin) d.origin = String(tool.origin);
249
+ return d;
250
+ }
251
+ function detectEndpoint() {
252
+ if (typeof document !== "undefined") {
253
+ const cs = document.currentScript;
254
+ if (cs?.src) try {
255
+ return new URL("/api/ingest", cs.src).toString();
256
+ } catch {}
257
+ }
258
+ return "https://agentstats.io/api/ingest";
259
+ }
260
+ const AI_REFERRER = /(^|\.)(chatgpt\.com|chat\.openai\.com|perplexity\.ai|claude\.ai|gemini\.google\.com|bard\.google\.com|copilot\.microsoft\.com|copilot\.cloud\.microsoft|copilot\.com|meta\.ai|deepseek\.com|grok\.com|chat\.mistral\.ai|you\.com|phind\.com|poe\.com|duck\.ai|chat\.qwen\.ai|kimi\.com|kimi\.ai|doubao\.com)$/i;
261
+ const AI_UTM = /[?&]utm_source=(chatgpt(\.com)?|openai|perplexity(\.ai)?|claude(\.ai)?|gemini|copilot(\.com)?|meta\.?ai|deepseek|grok|mistral|le-?chat)(&|$)/i;
262
+ const PATCHED = Symbol.for("agentstats.patched");
263
+ const WRAPPED = Symbol.for("agentstats.wrapped");
264
+ var AgentStatsClient = class {
265
+ constructor(opts) {
266
+ this.queue = [];
267
+ this.timer = null;
268
+ this.snapshotTimer = null;
269
+ this.pollTimer = null;
270
+ this.sessionId = "";
271
+ this.lastActivity = 0;
272
+ this.lastSnapshotHash = "";
273
+ this.inflight = Promise.resolve();
274
+ this.web = null;
275
+ this.agentPage = "";
276
+ this.controlStarted = 0;
277
+ if (!opts || !opts.key) throw new Error("[agentstats] `key` is required");
278
+ this.options = {
279
+ endpoint: detectEndpoint(),
280
+ captureArgs: true,
281
+ captureResults: false,
282
+ captureErrors: true,
283
+ redactKeys: [],
284
+ sessionTimeoutMs: 18e5,
285
+ flushIntervalMs: 2e3,
286
+ maxBatchSize: 25,
287
+ autoInstrument: true,
288
+ trackAgentBrowsers: true,
289
+ trackPageviews: true,
290
+ debug: false,
291
+ disabled: false,
292
+ ...Object.fromEntries(Object.entries(opts).filter(([, v]) => v !== void 0))
293
+ };
294
+ this._fetch = opts.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : (() => Promise.reject(/* @__PURE__ */ new Error("no fetch"))));
295
+ this.sessionId = this.loadSession();
296
+ if (!this.options.disabled) {
297
+ if (this.options.autoInstrument) this.instrument();
298
+ this.bindLifecycle();
299
+ this.reportReferral();
300
+ if (this.options.trackAgentBrowsers) this.watchAgentBrowsers();
301
+ if (this.options.trackPageviews) {
302
+ const o = this.options;
303
+ this.web = webAnalytics({
304
+ key: o.key,
305
+ endpoint: `${o.endpoint.replace(/\/+$/, "")}/web`,
306
+ hashRouting: o.hashRouting,
307
+ outboundLinks: o.outboundLinks,
308
+ fileDownloads: o.fileDownloads,
309
+ allowLocalhost: o.allowLocalhost,
310
+ debug: o.debug
311
+ });
312
+ }
313
+ }
314
+ this.log("initialised", {
315
+ endpoint: this.options.endpoint,
316
+ session: this.sessionId
317
+ });
318
+ }
319
+ /** Wrap a WebMCP tool definition so its `execute` is tracked. Safe to call on already-wrapped tools. */
320
+ wrap(tool) {
321
+ if (!tool || typeof tool.execute !== "function") return tool;
322
+ if (tool[WRAPPED]) return tool;
323
+ const original = tool.execute;
324
+ const name = String(tool.name);
325
+ const client = this;
326
+ const wrapped = async function(input, options) {
327
+ const start = perfNow();
328
+ const signal = options?.signal;
329
+ try {
330
+ const result = await original.call(this, input, options);
331
+ client.recordCall(name, "success", perfNow() - start, input, result, void 0, signal);
332
+ return result;
333
+ } catch (err) {
334
+ const cancelled = signal?.aborted || isAbortError(err);
335
+ client.recordCall(name, cancelled ? "cancelled" : "error", perfNow() - start, input, void 0, err, signal);
336
+ throw err;
337
+ }
338
+ };
339
+ Object.defineProperty(wrapped, WRAPPED, { value: true });
340
+ const out = Object.assign({}, tool, { execute: wrapped });
341
+ Object.defineProperty(out, WRAPPED, { value: true });
342
+ return out;
343
+ }
344
+ /**
345
+ * Track a custom event (e.g. a conversion). It counts for the agent session it happens in, and
346
+ * as a goal in Visitors when a person does it (string, number and boolean props only there).
347
+ */
348
+ track(name, props) {
349
+ const clean = props ? redact(props, this.redactKeys()) : void 0;
350
+ this.enqueue({
351
+ type: "custom",
352
+ id: uuid(),
353
+ ts: now(),
354
+ name,
355
+ props: clean,
356
+ url: this.url()
357
+ });
358
+ this.web?.track(name, clean);
359
+ }
360
+ /** Record a pageview now, e.g. after a client-side navigation the SDK can't see. Pageviews are otherwise automatic. */
361
+ pageview() {
362
+ this.web?.pageview();
363
+ }
364
+ /** Take a snapshot of the currently registered tools now (also happens automatically). */
365
+ async snapshot() {
366
+ const mc = this.modelContext();
367
+ if (!mc?.getTools) return [];
368
+ let tools = [];
369
+ try {
370
+ tools = (await mc.getTools() ?? []).map((t) => toDescriptor(t));
371
+ } catch (err) {
372
+ this.log("getTools failed", err);
373
+ return [];
374
+ }
375
+ const hash = safeStringify(tools.map((t) => [
376
+ t.name,
377
+ t.description,
378
+ t.inputSchema,
379
+ t.annotations
380
+ ]), 1e7);
381
+ if (hash !== this.lastSnapshotHash) {
382
+ this.lastSnapshotHash = hash;
383
+ this.enqueue({
384
+ type: "tools_snapshot",
385
+ id: uuid(),
386
+ ts: now(),
387
+ tools,
388
+ url: this.url()
389
+ });
390
+ }
391
+ return tools;
392
+ }
393
+ /** Send everything queued right now. */
394
+ flush(useBeacon = false) {
395
+ if (this.timer) {
396
+ clearTimeout(this.timer);
397
+ this.timer = null;
398
+ }
399
+ if (!this.queue.length) return this.inflight;
400
+ const events = this.queue.splice(0, this.queue.length);
401
+ const payload = {
402
+ key: this.options.key,
403
+ sdk: `js/${SDK_VERSION}`,
404
+ session_id: this.sessionId,
405
+ sent_at: now(),
406
+ metadata: this.options.metadata,
407
+ visit: typeof document !== "undefined" ? readCookie(VISIT_COOKIE) : void 0,
408
+ client: this.clientHints(),
409
+ events
410
+ };
411
+ const body = JSON.stringify(payload);
412
+ this.log("flush", events.length, "events");
413
+ if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) try {
414
+ if (navigator.sendBeacon(this.options.endpoint, new Blob([body], { type: "text/plain" }))) return Promise.resolve();
415
+ } catch {}
416
+ const p = this._fetch(this.options.endpoint, {
417
+ method: "POST",
418
+ body,
419
+ keepalive: true,
420
+ headers: { "Content-Type": "text/plain" }
421
+ }).then(async (res) => {
422
+ if (!res.ok) this.log("ingest responded", res.status);
423
+ }).catch((err) => {
424
+ this.log("ingest failed", err);
425
+ });
426
+ this.inflight = p;
427
+ return p;
428
+ }
429
+ /** Remove patches and stop timers. */
430
+ destroy() {
431
+ if (this.timer) clearTimeout(this.timer);
432
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
433
+ if (this.pollTimer) clearInterval(this.pollTimer);
434
+ this.timer = this.snapshotTimer = this.pollTimer = null;
435
+ this.web?.stop();
436
+ this.web = null;
437
+ }
438
+ get session() {
439
+ return this.sessionId;
440
+ }
441
+ log(...args) {
442
+ if (this.options.debug) console.debug("[agentstats]", ...args);
443
+ }
444
+ url() {
445
+ return typeof location !== "undefined" ? location.href : void 0;
446
+ }
447
+ redactKeys() {
448
+ return (this.options.redactKeys ?? []).map((k) => k.toLowerCase()).filter(Boolean);
449
+ }
450
+ modelContext() {
451
+ const g = globalThis;
452
+ return g.document?.modelContext ?? g.navigator?.modelContext;
453
+ }
454
+ captureArgs(tool, input) {
455
+ const c = this.options.captureArgs;
456
+ if (c === false) return void 0;
457
+ let v = typeof c === "function" ? this.callOption("captureArgs", () => c(tool, input)) : redact(input, this.redactKeys());
458
+ if (v !== void 0 && byteSize(v) > MAX_ARG_BYTES) v = {
459
+ _truncated: true,
460
+ preview: safeStringify(v, 1e3)
461
+ };
462
+ return v;
463
+ }
464
+ captureResult(tool, result) {
465
+ const c = this.options.captureResults;
466
+ if (!c || result === void 0) return void 0;
467
+ const v = typeof c === "function" ? this.callOption("captureResults", () => c(tool, result)) : result;
468
+ return v === void 0 ? void 0 : safeStringify(v, MAX_RESULT_CHARS);
469
+ }
470
+ captureError(tool, err) {
471
+ const c = this.options.captureErrors;
472
+ if (c === false) return void 0;
473
+ const e = err;
474
+ const message = typeof e?.message === "string" ? e.message : safeStringify(err, 1e3);
475
+ if (typeof c !== "function") return safeStringify(message, 1e3);
476
+ const code = typeof e?.code === "string" || typeof e?.code === "number" ? e.code : null;
477
+ const out = this.callOption("captureErrors", () => c({
478
+ tool,
479
+ message,
480
+ name: typeof e?.name === "string" ? e.name : null,
481
+ code,
482
+ error: err
483
+ }));
484
+ return typeof out === "string" && out ? safeStringify(out, 1e3) : void 0;
485
+ }
486
+ /** Runs a user-supplied capture function. One that throws keeps nothing rather than breaking the tool call. */
487
+ callOption(option, fn) {
488
+ try {
489
+ return fn();
490
+ } catch (err) {
491
+ this.log(`${option} threw`, err);
492
+ return;
493
+ }
494
+ }
495
+ recordCall(tool, status, duration, input, result, err, _signal) {
496
+ const ev = {
497
+ type: "tool_call",
498
+ id: uuid(),
499
+ ts: now(),
500
+ tool,
501
+ status,
502
+ duration_ms: Math.round(duration * 100) / 100,
503
+ url: this.url()
504
+ };
505
+ const args = this.captureArgs(tool, input);
506
+ if (args !== void 0) ev.args = args;
507
+ const res = this.captureResult(tool, result);
508
+ if (res !== void 0) ev.result = res;
509
+ if (err !== void 0) {
510
+ const message = this.captureError(tool, err);
511
+ if (message !== void 0) ev.error = message;
512
+ const name = err?.name;
513
+ if (name) ev.error_name = String(name).slice(0, 100);
514
+ }
515
+ this.enqueue(ev);
516
+ }
517
+ enqueue(ev) {
518
+ if (this.options.disabled) return;
519
+ this.touchSession();
520
+ this.queue.push(ev);
521
+ this.log("event", ev.type, ev.tool ?? ev.name ?? "");
522
+ if (this.queue.length >= this.options.maxBatchSize) this.flush();
523
+ else if (!this.timer) this.timer = setTimeout(() => void this.flush(), this.options.flushIntervalMs);
524
+ }
525
+ scheduleSnapshot(delay = 250) {
526
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
527
+ this.snapshotTimer = setTimeout(() => {
528
+ this.snapshotTimer = null;
529
+ this.snapshot();
530
+ }, delay);
531
+ }
532
+ instrument() {
533
+ const tryPatch = () => {
534
+ const mc = this.modelContext();
535
+ if (!mc) return false;
536
+ this.patch(mc);
537
+ return true;
538
+ };
539
+ if (tryPatch()) return;
540
+ let attempts = 0;
541
+ this.pollTimer = setInterval(() => {
542
+ attempts++;
543
+ if (tryPatch() || attempts > 40) {
544
+ if (this.pollTimer) clearInterval(this.pollTimer);
545
+ this.pollTimer = null;
546
+ }
547
+ }, 250);
548
+ }
549
+ patch(mc) {
550
+ if (mc[PATCHED]) {
551
+ this.log("modelContext already patched");
552
+ this.scheduleSnapshot(0);
553
+ return;
554
+ }
555
+ Object.defineProperty(mc, PATCHED, { value: true });
556
+ const client = this;
557
+ if (typeof mc.registerTool === "function") {
558
+ const orig = mc.registerTool;
559
+ mc.registerTool = function(tool, options) {
560
+ const wrapped = client.wrap(tool);
561
+ client.enqueue({
562
+ type: "tool_registered",
563
+ id: uuid(),
564
+ ts: now(),
565
+ tool: String(tool?.name),
566
+ descriptor: toDescriptor(tool),
567
+ url: client.url()
568
+ });
569
+ client.scheduleSnapshot();
570
+ return orig.call(this, wrapped, options);
571
+ };
572
+ }
573
+ if (typeof mc.provideContext === "function") {
574
+ const orig = mc.provideContext;
575
+ mc.provideContext = function(ctx) {
576
+ const tools = (ctx?.tools ?? []).map((t) => {
577
+ client.enqueue({
578
+ type: "tool_registered",
579
+ id: uuid(),
580
+ ts: now(),
581
+ tool: String(t?.name),
582
+ descriptor: toDescriptor(t),
583
+ url: client.url()
584
+ });
585
+ return client.wrap(t);
586
+ });
587
+ client.scheduleSnapshot();
588
+ return orig.call(this, {
589
+ ...ctx,
590
+ tools
591
+ });
592
+ };
593
+ }
594
+ if (typeof mc.unregisterTool === "function") {
595
+ const orig = mc.unregisterTool;
596
+ mc.unregisterTool = function(name) {
597
+ client.enqueue({
598
+ type: "tool_unregistered",
599
+ id: uuid(),
600
+ ts: now(),
601
+ tool: String(name),
602
+ url: client.url()
603
+ });
604
+ client.scheduleSnapshot();
605
+ return orig.call(this, name);
606
+ };
607
+ }
608
+ if (typeof mc.clearContext === "function") {
609
+ const orig = mc.clearContext;
610
+ mc.clearContext = function() {
611
+ client.scheduleSnapshot();
612
+ return orig.call(this);
613
+ };
614
+ }
615
+ if (typeof mc.addEventListener === "function") try {
616
+ mc.addEventListener("toolchange", () => client.scheduleSnapshot());
617
+ } catch {}
618
+ this.log("modelContext patched");
619
+ this.scheduleSnapshot(0);
620
+ }
621
+ bindLifecycle() {
622
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
623
+ const onHide = () => void this.flush(true);
624
+ window.addEventListener("pagehide", onHide);
625
+ document.addEventListener("visibilitychange", () => {
626
+ if (document.visibilityState === "hidden") onHide();
627
+ });
628
+ }
629
+ /** User agent + webdriver flag, so a session can be attributed to an agent that browses with a real browser. */
630
+ clientHints() {
631
+ if (typeof navigator === "undefined") return void 0;
632
+ const ua = String(navigator.userAgent ?? "").slice(0, 1e3);
633
+ const webdriver = navigator.webdriver === true;
634
+ return ua || webdriver ? {
635
+ ua: ua || void 0,
636
+ webdriver: webdriver || void 0
637
+ } : void 0;
638
+ }
639
+ /** The agent browsing this page, if one is: in control right now (Claude in Chrome), or the browser itself is one. */
640
+ detectAgentBrowser() {
641
+ return agentInBrowser();
642
+ }
643
+ /** Record agent page views (on load and on client-side navigation) and Claude in Chrome taking and giving back control. */
644
+ watchAgentBrowsers() {
645
+ if (typeof document === "undefined" || typeof window === "undefined") return;
646
+ const pageview = () => {
647
+ const agent = this.detectAgentBrowser();
648
+ const url = this.url();
649
+ if (!agent || url === this.agentPage) return;
650
+ this.agentPage = url ?? "";
651
+ this.enqueue({
652
+ type: "agent_pageview",
653
+ id: uuid(),
654
+ ts: now(),
655
+ name: agent.name,
656
+ props: { via: agent.via },
657
+ url
658
+ });
659
+ };
660
+ const ready = () => {
661
+ pageview();
662
+ if (typeof MutationObserver === "undefined" || !document.body) return;
663
+ new MutationObserver(() => {
664
+ const active = !!document.querySelector(AGENT_CONTROL_MARKER);
665
+ if (active && !this.controlStarted) {
666
+ this.controlStarted = perfNow();
667
+ this.enqueue({
668
+ type: "agent_control",
669
+ id: uuid(),
670
+ ts: now(),
671
+ name: "claude-in-chrome",
672
+ props: {
673
+ via: "dom",
674
+ phase: "start"
675
+ },
676
+ url: this.url()
677
+ });
678
+ pageview();
679
+ } else if (!active && this.controlStarted) {
680
+ const duration = Math.round(perfNow() - this.controlStarted);
681
+ this.controlStarted = 0;
682
+ this.enqueue({
683
+ type: "agent_control",
684
+ id: uuid(),
685
+ ts: now(),
686
+ name: "claude-in-chrome",
687
+ props: {
688
+ via: "dom",
689
+ phase: "stop",
690
+ duration_ms: duration
691
+ },
692
+ url: this.url()
693
+ });
694
+ }
695
+ }).observe(document.body, { childList: true });
696
+ };
697
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", ready, { once: true });
698
+ else ready();
699
+ const nav = () => setTimeout(pageview, 0);
700
+ window.addEventListener("popstate", nav);
701
+ window.addEventListener(LOCATION_CHANGE, nav);
702
+ watchHistory();
703
+ }
704
+ /** If this page was reached from an AI assistant, tell the server once per session. */
705
+ reportReferral() {
706
+ if (typeof document === "undefined" || typeof location === "undefined") return;
707
+ let refHost = "";
708
+ try {
709
+ refHost = document.referrer ? new URL(document.referrer).hostname.replace(/^www\./, "") : "";
710
+ } catch {}
711
+ if (!(refHost && AI_REFERRER.test(refHost)) && !AI_UTM.test(location.search)) return;
712
+ const flag = `__agentstats_ref_${this.sessionId}`;
713
+ try {
714
+ if (sessionStorage.getItem(flag)) return;
715
+ sessionStorage.setItem(flag, "1");
716
+ } catch {}
717
+ this.enqueue({
718
+ type: "referral",
719
+ id: uuid(),
720
+ ts: now(),
721
+ url: this.url() ?? location.href,
722
+ referrer: refHost ? `https://${refHost}/` : null
723
+ });
724
+ }
725
+ loadSession() {
726
+ try {
727
+ const raw = sessionStorage.getItem("__agentstats_session");
728
+ if (raw) {
729
+ const parsed = JSON.parse(raw);
730
+ if (parsed?.id && now() - parsed.last < this.options.sessionTimeoutMs) {
731
+ this.lastActivity = parsed.last;
732
+ return parsed.id;
733
+ }
734
+ }
735
+ } catch {}
736
+ return uuid();
737
+ }
738
+ touchSession() {
739
+ const t = now();
740
+ if (this.lastActivity && t - this.lastActivity > this.options.sessionTimeoutMs) {
741
+ this.sessionId = uuid();
742
+ this.lastSnapshotHash = "";
743
+ this.log("new session", this.sessionId);
744
+ }
745
+ this.lastActivity = t;
746
+ try {
747
+ sessionStorage.setItem("__agentstats_session", JSON.stringify({
748
+ id: this.sessionId,
749
+ last: t
750
+ }));
751
+ } catch {}
752
+ }
753
+ };
754
+ let instance = null;
755
+ /** Initialise AgentStats. Call once, as early as possible (before registering tools). */
756
+ function init(options) {
757
+ if (instance) instance.destroy();
758
+ instance = new AgentStatsClient(options);
759
+ return instance;
760
+ }
761
+ function getClient() {
762
+ return instance;
763
+ }
764
+ function track(name, props) {
765
+ instance?.track(name, props);
766
+ }
767
+ function wrap(tool) {
768
+ return instance ? instance.wrap(tool) : tool;
769
+ }
770
+ function flush() {
771
+ return instance?.flush() ?? Promise.resolve();
772
+ }
773
+ function snapshot() {
774
+ return instance?.snapshot() ?? Promise.resolve([]);
775
+ }
776
+ function pageview() {
777
+ instance?.pageview();
778
+ }
779
+ const AgentStats = {
780
+ init,
781
+ track,
782
+ pageview,
783
+ wrap,
784
+ flush,
785
+ snapshot,
786
+ getClient,
787
+ version: SDK_VERSION
788
+ };
789
+ //#endregion
790
+ exports.AgentStats = AgentStats;
791
+ exports.default = AgentStats;
792
+ exports.AgentStatsClient = AgentStatsClient;
793
+ exports.SDK_VERSION = SDK_VERSION;
794
+ exports.flush = flush;
795
+ exports.getClient = getClient;
796
+ exports.init = init;
797
+ exports.pageview = pageview;
798
+ exports.snapshot = snapshot;
799
+ exports.track = track;
800
+ exports.wrap = wrap;