@agenthoney/analytics 0.0.0-stage → 0.14.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -2
  3. package/dist/answers.cjs +20034 -0
  4. package/dist/answers.cjs.map +1 -0
  5. package/dist/answers.d.ts +148 -0
  6. package/dist/answers.js +139 -0
  7. package/dist/answers.js.map +1 -0
  8. package/dist/chunk-CD4WLJX7.js +48 -0
  9. package/dist/chunk-CD4WLJX7.js.map +1 -0
  10. package/dist/chunk-F3PRHEXB.js +20143 -0
  11. package/dist/chunk-F3PRHEXB.js.map +1 -0
  12. package/dist/chunk-L22VERBM.js +911 -0
  13. package/dist/chunk-L22VERBM.js.map +1 -0
  14. package/dist/chunk-OH4H2B7O.js +150 -0
  15. package/dist/chunk-OH4H2B7O.js.map +1 -0
  16. package/dist/chunk-R76CTIBG.js +701 -0
  17. package/dist/chunk-R76CTIBG.js.map +1 -0
  18. package/dist/chunk-UG3REZCJ.js +147 -0
  19. package/dist/chunk-UG3REZCJ.js.map +1 -0
  20. package/dist/core/breaker.d.ts +33 -0
  21. package/dist/core/collector.d.ts +51 -0
  22. package/dist/core/config.d.ts +124 -0
  23. package/dist/core/encode.d.ts +32 -0
  24. package/dist/core/queue.d.ts +39 -0
  25. package/dist/core/record-gate.d.ts +17 -0
  26. package/dist/core/safe.d.ts +17 -0
  27. package/dist/core/transport.d.ts +45 -0
  28. package/dist/express.cjs +21789 -0
  29. package/dist/express.cjs.map +1 -0
  30. package/dist/express.d.ts +65 -0
  31. package/dist/express.js +6 -0
  32. package/dist/express.js.map +1 -0
  33. package/dist/index.cjs +22118 -0
  34. package/dist/index.cjs.map +1 -0
  35. package/dist/index.d.ts +51 -0
  36. package/dist/index.js +8 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/next.cjs +21186 -0
  39. package/dist/next.cjs.map +1 -0
  40. package/dist/next.d.ts +90 -0
  41. package/dist/next.js +5 -0
  42. package/dist/next.js.map +1 -0
  43. package/dist/observe/client-ip.d.ts +109 -0
  44. package/dist/observe/next-router.d.ts +22 -0
  45. package/dist/observe/redact.d.ts +58 -0
  46. package/dist/observe/request.d.ts +75 -0
  47. package/dist/observe/response.d.ts +24 -0
  48. package/dist/runtime.d.ts +27 -0
  49. package/dist/serve/accept.d.ts +7 -0
  50. package/dist/serve/discovery.d.ts +56 -0
  51. package/dist/serve/hosted.d.ts +135 -0
  52. package/dist/serve/source.d.ts +48 -0
  53. package/dist/serve/tag-asset.generated.d.ts +14 -0
  54. package/dist/serve/tag.d.ts +131 -0
  55. package/dist/serve/twin.d.ts +162 -0
  56. package/dist/web.cjs +21225 -0
  57. package/dist/web.cjs.map +1 -0
  58. package/dist/web.d.ts +52 -0
  59. package/dist/web.js +6 -0
  60. package/dist/web.js.map +1 -0
  61. package/install.md +463 -0
  62. package/package.json +76 -4
@@ -0,0 +1,911 @@
1
+ import { shouldRecord, normalisePath } from './chunk-F3PRHEXB.js';
2
+
3
+ // src/core/breaker.ts
4
+ function createBreaker(options = {}) {
5
+ const failureThreshold = options.failureThreshold ?? 5;
6
+ const openMs = options.openMs ?? 3e4;
7
+ const now = options.now ?? (() => Date.now());
8
+ let consecutiveFailures = 0;
9
+ let openedAt = 0;
10
+ let state = "closed";
11
+ return {
12
+ get state() {
13
+ return state;
14
+ },
15
+ allow() {
16
+ if (state === "closed") return true;
17
+ if (state === "half-open") return true;
18
+ if (now() - openedAt >= openMs) {
19
+ state = "half-open";
20
+ return true;
21
+ }
22
+ return false;
23
+ },
24
+ success() {
25
+ consecutiveFailures = 0;
26
+ state = "closed";
27
+ },
28
+ failure() {
29
+ consecutiveFailures += 1;
30
+ if (state === "half-open" || consecutiveFailures >= failureThreshold) {
31
+ state = "open";
32
+ openedAt = now();
33
+ }
34
+ }
35
+ };
36
+ }
37
+
38
+ // src/core/config.ts
39
+ var UNSET_SITE_ID = "unset";
40
+ var KEY_FAMILY = /^ep_[A-Za-z0-9]+_[A-Za-z0-9]+_[A-Za-z0-9-]+_([A-Za-z0-9_-]+)$/;
41
+ var MIN_SECRET = 16;
42
+ function looksLikeKey(value) {
43
+ const match = KEY_FAMILY.exec(value.trim());
44
+ return match !== null && (match[1]?.length ?? 0) >= MIN_SECRET;
45
+ }
46
+ var DEFAULTS = {
47
+ batchSize: 20,
48
+ maxBatchSize: 100,
49
+ flushIntervalMs: 2e3,
50
+ maxQueueEvents: 1e3,
51
+ maxBodyBytes: 512 * 1024,
52
+ requestTimeoutMs: 2e3
53
+ };
54
+ var PUBLIC_ENV_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "PUBLIC_", "REACT_APP_", "NUXT_PUBLIC_"];
55
+ var AgentHoneyConfigError = class extends Error {
56
+ constructor(message) {
57
+ super(message);
58
+ this.name = "AgentHoneyConfigError";
59
+ }
60
+ };
61
+ function clamp(value, min, max) {
62
+ return Math.min(max, Math.max(min, value));
63
+ }
64
+ function boolFromEnv(raw) {
65
+ if (raw === void 0) return void 0;
66
+ const v = raw.trim().toLowerCase();
67
+ if (v === "1" || v === "true" || v === "yes") return true;
68
+ if (v === "0" || v === "false" || v === "no") return false;
69
+ return void 0;
70
+ }
71
+ function resolveConfig(options = {}, env = typeof process === "undefined" ? {} : process.env) {
72
+ for (const prefix of PUBLIC_ENV_PREFIXES) {
73
+ const name = `${prefix}AGENTHONEY_SERVER_KEY`;
74
+ if (env[name]) {
75
+ throw new AgentHoneyConfigError(
76
+ `${name} is set. The "${prefix}" prefix inlines a value into client-side JavaScript, so this key is already public. Revoke it, then set AGENTHONEY_SERVER_KEY instead (no prefix) so it stays on the server.`
77
+ );
78
+ }
79
+ }
80
+ const ingestUrl = options.ingestUrl ?? env["AGENTHONEY_INGEST_URL"] ?? "";
81
+ const serverKey = options.serverKey ?? env["AGENTHONEY_SERVER_KEY"] ?? "";
82
+ const siteId = options.siteId ?? env["AGENTHONEY_SITE_ID"] ?? UNSET_SITE_ID;
83
+ const enabled = options.enabled ?? boolFromEnv(env["AGENTHONEY_ENABLED"]) ?? true;
84
+ const debug = options.debug ?? boolFromEnv(env["AGENTHONEY_DEBUG"]) ?? false;
85
+ let disabled = null;
86
+ if (typeof window !== "undefined") {
87
+ disabled = "browser-environment";
88
+ } else if (!enabled) {
89
+ disabled = "explicitly-disabled";
90
+ } else if (!ingestUrl) {
91
+ disabled = "missing-url";
92
+ } else if (!serverKey) {
93
+ disabled = "missing-key";
94
+ } else if (!looksLikeKey(serverKey)) {
95
+ disabled = "malformed-key";
96
+ }
97
+ return Object.freeze({
98
+ ingestUrl,
99
+ serverKey,
100
+ siteId,
101
+ debug,
102
+ batchSize: clamp(options.batchSize ?? DEFAULTS.batchSize, 1, DEFAULTS.maxBatchSize),
103
+ flushIntervalMs: clamp(options.flushIntervalMs ?? DEFAULTS.flushIntervalMs, 100, 6e4),
104
+ maxQueueEvents: clamp(options.maxQueueEvents ?? DEFAULTS.maxQueueEvents, 1, 1e5),
105
+ maxBodyBytes: clamp(options.maxBodyBytes ?? DEFAULTS.maxBodyBytes, 1024, DEFAULTS.maxBodyBytes),
106
+ requestTimeoutMs: clamp(options.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs, 100, 3e4),
107
+ routeTemplate: options.routeTemplate,
108
+ redactPatterns: options.redactPatterns ?? [],
109
+ // ⚠️ Defaults to TRUE. An `?? true` that a refactor turns into `?? false`
110
+ // is the entire bug returning, so `config.test.ts` asserts the default.
111
+ redactHighEntropyPaths: options.redactHighEntropyPaths ?? true,
112
+ isInternal: options.isInternal,
113
+ clientIp: normaliseClientIpOption(options.clientIp) ?? clientIpFromEnv(env["AGENTHONEY_CLIENT_IP"]) ?? "platform",
114
+ disabled
115
+ });
116
+ }
117
+ function normaliseClientIpOption(value) {
118
+ if (value === void 0 || value === false || typeof value === "function") return value;
119
+ return value === "platform" || value === "forwarded" ? value : void 0;
120
+ }
121
+ function clientIpFromEnv(value) {
122
+ if (value === void 0) return void 0;
123
+ const normalised = value.trim().toLowerCase();
124
+ if (normalised === "off" || normalised === "false") return false;
125
+ if (normalised === "platform" || normalised === "forwarded") return normalised;
126
+ return void 0;
127
+ }
128
+ function redact(text) {
129
+ return text.replace(/\bep_(live|test)_server_[A-Za-z0-9_-]+/g, "ep_$1_server_[redacted]");
130
+ }
131
+
132
+ // src/core/encode.ts
133
+ var MAX_BODY_BYTES = 512 * 1024;
134
+ var OPEN = '{"events":[';
135
+ var CLOSE = "]}";
136
+ var byteLength = typeof TextEncoder === "function" ? (s) => new TextEncoder().encode(s).length : (
137
+ // Node 18+ always has TextEncoder; this is here so the module cannot
138
+ // throw at import time on an exotic runtime, which would take the host
139
+ // application down with it.
140
+ (s) => s.length
141
+ );
142
+ function encodeBatch(events, maxBytes = MAX_BODY_BYTES) {
143
+ if (events.length === 0) {
144
+ return { body: OPEN + CLOSE, taken: 0, oversized: false };
145
+ }
146
+ const overhead = byteLength(OPEN) + byteLength(CLOSE);
147
+ let used = overhead;
148
+ const parts = [];
149
+ for (const event of events) {
150
+ const encoded = JSON.stringify(event);
151
+ const cost = byteLength(encoded) + (parts.length > 0 ? 1 : 0);
152
+ if (used + cost > maxBytes) break;
153
+ parts.push(encoded);
154
+ used += cost;
155
+ }
156
+ return {
157
+ body: OPEN + parts.join(",") + CLOSE,
158
+ taken: parts.length,
159
+ oversized: parts.length === 0
160
+ };
161
+ }
162
+
163
+ // src/core/queue.ts
164
+ var BoundedQueue = class {
165
+ capacity;
166
+ #items;
167
+ #head = 0;
168
+ #size = 0;
169
+ #dropped = 0;
170
+ constructor(capacity) {
171
+ if (!Number.isInteger(capacity) || capacity < 1) {
172
+ throw new TypeError(`capacity must be a positive integer, got ${capacity}`);
173
+ }
174
+ this.capacity = capacity;
175
+ this.#items = new Array(capacity);
176
+ }
177
+ get size() {
178
+ return this.#size;
179
+ }
180
+ /** How many events have been discarded because the buffer was full. */
181
+ get dropped() {
182
+ return this.#dropped;
183
+ }
184
+ push(item) {
185
+ if (this.#size === this.capacity) {
186
+ this.#items[this.#head] = item;
187
+ this.#head = (this.#head + 1) % this.capacity;
188
+ this.#dropped += 1;
189
+ return;
190
+ }
191
+ this.#items[(this.#head + this.#size) % this.capacity] = item;
192
+ this.#size += 1;
193
+ }
194
+ /** The first `max` items, without removing them. */
195
+ peek(max) {
196
+ const n = Math.min(max, this.#size);
197
+ const out = new Array(n);
198
+ for (let i = 0; i < n; i += 1) {
199
+ out[i] = this.#items[(this.#head + i) % this.capacity];
200
+ }
201
+ return out;
202
+ }
203
+ /** Remove the first `n` items. Called only after they are safely sent. */
204
+ commit(n) {
205
+ const count = Math.min(n, this.#size);
206
+ for (let i = 0; i < count; i += 1) {
207
+ this.#items[(this.#head + i) % this.capacity] = void 0;
208
+ }
209
+ this.#head = (this.#head + count) % this.capacity;
210
+ this.#size -= count;
211
+ }
212
+ /** Reset the drop counter, once the count has been reported on the wire. */
213
+ clearDropped() {
214
+ this.#dropped = 0;
215
+ }
216
+ };
217
+
218
+ // src/observe/client-ip.ts
219
+ var PLATFORM_IP_HEADERS = [
220
+ "cf-connecting-ip",
221
+ // Cloudflare, including a tunnel
222
+ "true-client-ip",
223
+ // Cloudflare Enterprise, Akamai
224
+ "x-vercel-forwarded-for",
225
+ // Vercel
226
+ "fly-client-ip",
227
+ // Fly.io
228
+ "x-nf-client-connection-ip",
229
+ // Netlify
230
+ "x-azure-clientip"
231
+ // Azure Front Door
232
+ ];
233
+ var COUNTRY_HEADERS = [
234
+ "cf-ipcountry",
235
+ "x-vercel-ip-country",
236
+ "x-nf-geo-country",
237
+ "fastly-client-country",
238
+ "x-azure-clientip-country"
239
+ ];
240
+ var NON_COUNTRIES = /* @__PURE__ */ new Set(["XX", "T1", "A1", "A2", "AP", "EU"]);
241
+ var IPV4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
242
+ var IPV6_CHARS = /^[0-9a-f:.]+$/;
243
+ function normaliseIp(raw) {
244
+ if (!raw) return void 0;
245
+ let value = raw.trim().toLowerCase();
246
+ if (!value || value === "unknown") return void 0;
247
+ const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(value);
248
+ if (bracketed) value = bracketed[1];
249
+ else if (value.split(":").length === 2 && value.includes(".")) {
250
+ value = value.slice(0, value.indexOf(":"));
251
+ }
252
+ if (value.startsWith("::ffff:") && value.includes(".")) value = value.slice(7);
253
+ const v4 = IPV4.exec(value);
254
+ if (v4) {
255
+ return v4.slice(1).every((octet) => Number(octet) <= 255) ? value : void 0;
256
+ }
257
+ if (value.includes(":") && IPV6_CHARS.test(value) && value.length <= 45) return value;
258
+ return void 0;
259
+ }
260
+ function leftmost(chain) {
261
+ if (!chain) return void 0;
262
+ return normaliseIp(chain.split(",")[0]);
263
+ }
264
+ function rightmost(chain) {
265
+ if (!chain) return void 0;
266
+ const hops = chain.split(",");
267
+ return normaliseIp(hops[hops.length - 1]);
268
+ }
269
+ function isUnroutable(ip) {
270
+ if (ip === "::1" || ip.startsWith("fe80:") || ip.startsWith("fc") || ip.startsWith("fd")) {
271
+ return true;
272
+ }
273
+ const v4 = IPV4.exec(ip);
274
+ if (!v4) return false;
275
+ const [a, b] = [Number(v4[1]), Number(v4[2])];
276
+ return a === 10 || a === 127 || a === 0 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31 || a === 169 && b === 254 || a === 100 && b >= 64 && b <= 127;
277
+ }
278
+ function platformIp(facts) {
279
+ for (const name of PLATFORM_IP_HEADERS) {
280
+ const found = leftmost(facts.header(name));
281
+ if (found) return found;
282
+ }
283
+ return void 0;
284
+ }
285
+ function resolveClientIp(facts, source) {
286
+ if (source === false) return void 0;
287
+ if (typeof source === "function") {
288
+ try {
289
+ return normaliseIp(source(facts));
290
+ } catch {
291
+ return void 0;
292
+ }
293
+ }
294
+ if (source === "forwarded") {
295
+ const forwarded = rightmost(facts.header("x-forwarded-for"));
296
+ if (forwarded && !isUnroutable(forwarded)) return forwarded;
297
+ } else {
298
+ const platform = platformIp(facts);
299
+ if (platform) return platform;
300
+ }
301
+ const fallback = normaliseIp(facts.frameworkIp) ?? normaliseIp(facts.socketIp);
302
+ return fallback && !isUnroutable(fallback) ? fallback : void 0;
303
+ }
304
+ function resolveCountry(facts) {
305
+ for (const name of COUNTRY_HEADERS) {
306
+ const value = facts.header(name)?.trim().toUpperCase();
307
+ if (value && /^[A-Z]{2}$/.test(value) && !NON_COUNTRIES.has(value)) return value;
308
+ }
309
+ return void 0;
310
+ }
311
+ function ipSourceName(source) {
312
+ if (source === false) return "off";
313
+ if (typeof source === "function") return "custom";
314
+ return source;
315
+ }
316
+ function observeNetwork(facts, source) {
317
+ const ip = resolveClientIp(facts, source);
318
+ const countryCode = resolveCountry(facts);
319
+ return {
320
+ ...ip ? { ip } : {},
321
+ ...countryCode ? { countryCode } : {},
322
+ ipSource: ipSourceName(source)
323
+ };
324
+ }
325
+
326
+ // src/core/safe.ts
327
+ function safe(fn, onError) {
328
+ try {
329
+ fn();
330
+ } catch (error) {
331
+ try {
332
+ onError?.(error);
333
+ } catch {
334
+ }
335
+ }
336
+ }
337
+ async function safeAsync(fn, onError) {
338
+ try {
339
+ await fn();
340
+ } catch (error) {
341
+ try {
342
+ onError?.(error);
343
+ } catch {
344
+ }
345
+ }
346
+ }
347
+
348
+ // src/core/transport.ts
349
+ function outcomeForStatus(status, retryAfterMs) {
350
+ if (status >= 200 && status < 300) return { outcome: "accepted", status };
351
+ if (status === 408 || status === 429 || status >= 500) {
352
+ return retryAfterMs === void 0 ? { outcome: "retryable", status } : { outcome: "retryable", status, retryAfterMs };
353
+ }
354
+ return { outcome: "rejected", status };
355
+ }
356
+ function parseRetryAfter(value) {
357
+ if (!value) return void 0;
358
+ const seconds = Number(value);
359
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
360
+ const date = Date.parse(value);
361
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
362
+ return void 0;
363
+ }
364
+ function fetchTransport(url) {
365
+ return {
366
+ async send(body, headers, signal) {
367
+ try {
368
+ const response = await fetch(url, {
369
+ method: "POST",
370
+ headers,
371
+ body,
372
+ signal,
373
+ // Never follow a redirect: this request carries a bearer token, and
374
+ // a redirect is an instruction from the network to send that
375
+ // credential somewhere we did not choose.
376
+ redirect: "error",
377
+ keepalive: false
378
+ });
379
+ return outcomeForStatus(
380
+ response.status,
381
+ parseRetryAfter(response.headers.get("retry-after"))
382
+ );
383
+ } catch {
384
+ return { outcome: "retryable" };
385
+ }
386
+ }
387
+ };
388
+ }
389
+
390
+ // src/core/collector.ts
391
+ var ADDRESSLESS_WARN_AFTER = 20;
392
+ var MAX_RETRIES = 2;
393
+ var MAX_NOT_RECORDED_COUNT = 1e7;
394
+ function defaultSetTimer(fn, ms) {
395
+ const handle = setInterval(fn, ms);
396
+ handle.unref?.();
397
+ return { cancel: () => clearInterval(handle) };
398
+ }
399
+ function createCollector(config, deps = {}) {
400
+ const log = deps.log ?? ((message) => console.warn(redact(message)));
401
+ const debug = (message) => {
402
+ if (config.debug) log(`[AgentHoney] ${message}`);
403
+ };
404
+ if (config.disabled) {
405
+ debug(`collection disabled: ${config.disabled}`);
406
+ const noop = {
407
+ record: () => {
408
+ },
409
+ notRecorded: () => {
410
+ },
411
+ flush: async () => {
412
+ },
413
+ stats: { queued: 0, dropped: 0, sent: 0, failed: 0, notRecorded: 0, breaker: "closed" },
414
+ close: () => {
415
+ }
416
+ };
417
+ return noop;
418
+ }
419
+ debug(`collection on; client address from: ${ipSourceName(config.clientIp)}`);
420
+ const queue = new BoundedQueue(config.maxQueueEvents);
421
+ const transport = deps.transport ?? fetchTransport(config.ingestUrl);
422
+ const breaker = deps.breaker ?? createBreaker();
423
+ const random = deps.random ?? Math.random;
424
+ const setTimer = deps.setTimer ?? defaultSetTimer;
425
+ let sent = 0;
426
+ let failed = 0;
427
+ const tally = /* @__PURE__ */ new Map();
428
+ const tallyTotal = () => [...tally.values()].reduce((a, b) => a + b, 0);
429
+ const settle = (reported) => {
430
+ for (const [reason, n] of reported) {
431
+ const left = (tally.get(reason) ?? 0) - n;
432
+ if (left > 0) tally.set(reason, left);
433
+ else tally.delete(reason);
434
+ }
435
+ };
436
+ let inFlight = null;
437
+ let addressless = 0;
438
+ let warnedAddressless = false;
439
+ const watchAddress = (event) => {
440
+ if (warnedAddressless || config.clientIp === false) return;
441
+ if (event.network?.ip) {
442
+ warnedAddressless = true;
443
+ return;
444
+ }
445
+ if (++addressless < ADDRESSLESS_WARN_AFTER) return;
446
+ warnedAddressless = true;
447
+ log(
448
+ `[AgentHoney] ${addressless} requests have been recorded with no client address, so crawler verification cannot run and every bot will stay "claimed". This server is behind a proxy that writes no platform header. Set AGENTHONEY_CLIENT_IP=forwarded to read the hop your proxy already writes, or AGENTHONEY_CLIENT_IP=off to silence this and send no address at all.`
449
+ );
450
+ };
451
+ const timer = setTimer(() => {
452
+ void flush();
453
+ }, config.flushIntervalMs);
454
+ function headers() {
455
+ return {
456
+ "content-type": "application/json",
457
+ authorization: `Bearer ${config.serverKey}`
458
+ };
459
+ }
460
+ async function sendOnce(body) {
461
+ const controller = new AbortController();
462
+ const timeout = setTimeout(() => controller.abort(), config.requestTimeoutMs);
463
+ timeout.unref?.();
464
+ try {
465
+ return await transport.send(body, headers(), controller.signal);
466
+ } finally {
467
+ clearTimeout(timeout);
468
+ }
469
+ }
470
+ function backoffMs(attempt, retryAfterMs) {
471
+ if (retryAfterMs !== void 0) return Math.min(retryAfterMs, 3e4);
472
+ return random() * Math.min(5e3, 200 * 3 ** attempt);
473
+ }
474
+ const sleep = (ms) => new Promise((resolve) => {
475
+ const t = setTimeout(resolve, ms);
476
+ t.unref?.();
477
+ });
478
+ async function flushOnce() {
479
+ if (queue.size === 0 && tally.size === 0) return;
480
+ if (!breaker.allow()) {
481
+ debug("breaker open, skipping flush");
482
+ return;
483
+ }
484
+ const candidates = queue.peek(config.batchSize);
485
+ const droppedSoFar = queue.dropped;
486
+ if (droppedSoFar > 0 && candidates[0]) {
487
+ candidates[0] = {
488
+ ...candidates[0],
489
+ sdk: { ...candidates[0].sdk, dropped: droppedSoFar }
490
+ };
491
+ }
492
+ const reported = new Map(tally);
493
+ const encoded = encodeBatch(candidates, config.maxBodyBytes - (reported.size > 0 ? 512 : 0));
494
+ if (reported.size > 0) {
495
+ const counts = Object.fromEntries([...reported].map(([reason, n]) => [reason, Math.min(n, MAX_NOT_RECORDED_COUNT)]));
496
+ encoded.body = `${encoded.body.slice(0, -1)},"notRecorded":${JSON.stringify(counts)}}`;
497
+ }
498
+ if (candidates.length > 0 && encoded.oversized) {
499
+ queue.commit(1);
500
+ debug("dropped one event larger than the body limit");
501
+ return;
502
+ }
503
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
504
+ const result = await sendOnce(encoded.body);
505
+ if (result.outcome === "accepted") {
506
+ settle(reported);
507
+ queue.commit(encoded.taken);
508
+ if (droppedSoFar > 0) queue.clearDropped();
509
+ sent += encoded.taken;
510
+ breaker.success();
511
+ return;
512
+ }
513
+ if (result.outcome === "rejected") {
514
+ settle(reported);
515
+ queue.commit(encoded.taken);
516
+ failed += encoded.taken;
517
+ debug(`batch rejected with ${result.status}; dropped ${encoded.taken} event(s)`);
518
+ return;
519
+ }
520
+ if (attempt === MAX_RETRIES) break;
521
+ await sleep(backoffMs(attempt, result.retryAfterMs));
522
+ }
523
+ failed += encoded.taken;
524
+ breaker.failure();
525
+ debug(`batch failed after ${MAX_RETRIES + 1} attempt(s); ${queue.size} event(s) still buffered`);
526
+ }
527
+ function flush() {
528
+ if (inFlight) return inFlight;
529
+ inFlight = safeAsync(flushOnce, (error) => debug(`flush failed: ${String(error)}`)).finally(
530
+ () => {
531
+ inFlight = null;
532
+ }
533
+ );
534
+ return inFlight;
535
+ }
536
+ return {
537
+ record(event) {
538
+ try {
539
+ queue.push(event);
540
+ watchAddress(event);
541
+ } catch {
542
+ }
543
+ },
544
+ notRecorded(reason) {
545
+ try {
546
+ tally.set(reason, (tally.get(reason) ?? 0) + 1);
547
+ } catch {
548
+ }
549
+ },
550
+ flush,
551
+ get stats() {
552
+ return {
553
+ queued: queue.size,
554
+ dropped: queue.dropped,
555
+ sent,
556
+ failed,
557
+ notRecorded: tallyTotal(),
558
+ breaker: breaker.state
559
+ };
560
+ },
561
+ close() {
562
+ timer.cancel();
563
+ }
564
+ };
565
+ }
566
+
567
+ // src/serve/accept.ts
568
+ var QVALUE = /^\d*\.?\d+$/;
569
+ function parseAccept(header) {
570
+ if (!header) return [];
571
+ const ranges = [];
572
+ for (const part of header.split(",")) {
573
+ const segments = part.trim().split(";");
574
+ const type = segments[0]?.trim().toLowerCase();
575
+ if (!type) continue;
576
+ let q = 1;
577
+ for (const segment of segments.slice(1)) {
578
+ const eq = segment.indexOf("=");
579
+ const key = (eq === -1 ? segment : segment.slice(0, eq)).trim().toLowerCase();
580
+ if (key === "q") {
581
+ const value = eq === -1 ? void 0 : segment.slice(eq + 1).trim().toLowerCase();
582
+ q = value !== void 0 && QVALUE.test(value) ? Math.min(1, Math.max(0, Number(value))) : 1;
583
+ }
584
+ }
585
+ ranges.push({ type, q });
586
+ }
587
+ return ranges;
588
+ }
589
+ function exactQ(ranges, type) {
590
+ let best = 0;
591
+ for (const range of ranges) {
592
+ if (range.type === type) best = Math.max(best, range.q);
593
+ }
594
+ return best;
595
+ }
596
+ function effectiveQ(ranges, type) {
597
+ const prefix = `${type.split("/")[0]}/*`;
598
+ let best = 0;
599
+ for (const range of ranges) {
600
+ if (range.type === type || range.type === prefix || range.type === "*/*") {
601
+ best = Math.max(best, range.q);
602
+ }
603
+ }
604
+ return best;
605
+ }
606
+ var MARKDOWN_TYPES = ["text/markdown", "text/x-markdown"];
607
+ function prefersMarkdown(header) {
608
+ const ranges = parseAccept(header);
609
+ if (ranges.length === 0) return false;
610
+ let markdown = 0;
611
+ for (const type of MARKDOWN_TYPES) {
612
+ markdown = Math.max(markdown, exactQ(ranges, type));
613
+ }
614
+ if (markdown === 0) return false;
615
+ const html = effectiveQ(ranges, "text/html");
616
+ return markdown >= html;
617
+ }
618
+
619
+ // src/core/record-gate.ts
620
+ function recordOrCount(collector, event, accept, background) {
621
+ let decision;
622
+ try {
623
+ decision = shouldRecord({ ...event, acceptPrefersMarkdown: prefersMarkdown(accept), background });
624
+ } catch {
625
+ decision = { record: true, because: "page" };
626
+ }
627
+ if (decision.record) collector.record(event);
628
+ else collector.notRecorded?.(decision.reason);
629
+ }
630
+
631
+ // src/observe/response.ts
632
+ function observeResponse(facts) {
633
+ const response = { observation: facts.observation };
634
+ if (facts.observation === "unknown") {
635
+ if (typeof facts.latencyMs === "number" && Number.isFinite(facts.latencyMs)) {
636
+ response.latencyMs = Math.max(0, Math.round(facts.latencyMs));
637
+ }
638
+ return response;
639
+ }
640
+ if (typeof facts.status === "number" && facts.status >= 100 && facts.status <= 599) {
641
+ response.status = facts.status;
642
+ }
643
+ if (facts.contentType) {
644
+ response.contentType = String(facts.contentType).slice(0, 255);
645
+ }
646
+ const length = typeof facts.contentLength === "string" ? Number(facts.contentLength) : facts.contentLength;
647
+ if (typeof length === "number" && Number.isFinite(length) && length >= 0) {
648
+ response.contentLength = Math.round(length);
649
+ }
650
+ if (typeof facts.latencyMs === "number" && Number.isFinite(facts.latencyMs)) {
651
+ response.latencyMs = Math.max(0, Math.round(facts.latencyMs));
652
+ }
653
+ return response;
654
+ }
655
+
656
+ // src/runtime.ts
657
+ var SDK_NAME = "@agenthoney/analytics";
658
+ var SDK_VERSION = "0.14.0";
659
+ function runtimeName() {
660
+ try {
661
+ const g = globalThis;
662
+ const deno = g["Deno"];
663
+ if (deno?.version?.deno) return `deno-${deno.version.deno}`;
664
+ const bun = g["Bun"];
665
+ if (bun?.version) return `bun-${bun.version}`;
666
+ if (typeof process !== "undefined" && process.versions?.node) {
667
+ return `node-${process.versions.node}`;
668
+ }
669
+ const nav = g["navigator"];
670
+ if (nav?.userAgent?.includes("Cloudflare-Workers")) return "workerd";
671
+ } catch {
672
+ }
673
+ return "unknown";
674
+ }
675
+ function newEventId() {
676
+ try {
677
+ const c = globalThis.crypto;
678
+ if (typeof c?.randomUUID === "function") return c.randomUUID();
679
+ } catch {
680
+ }
681
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
682
+ }
683
+
684
+ // src/serve/hosted.ts
685
+ function originOf(url) {
686
+ try {
687
+ return new URL(url).origin;
688
+ } catch {
689
+ return void 0;
690
+ }
691
+ }
692
+ var PAGE_LIMIT = 2e3;
693
+ function hostedTwins(options) {
694
+ const base = originOf(options.ingestUrl ?? "https://ingest.agenthoney.ai/v1/events");
695
+ const refreshMs = options.refreshMs ?? 3e5;
696
+ const coldTimeoutMs = options.coldTimeoutMs ?? 750;
697
+ const failureBackoffMs = options.failureBackoffMs ?? 3e4;
698
+ const refreshTimeoutMs = options.refreshTimeoutMs ?? 5e3;
699
+ const fetchImpl = options.fetchImpl ?? fetch;
700
+ const now = options.now ?? (() => Date.now());
701
+ let twins = /* @__PURE__ */ new Map();
702
+ let fetchedAt = 0;
703
+ let failedAt = 0;
704
+ let inFlight;
705
+ function abortAfter(ms) {
706
+ const Controller = globalThis.AbortController;
707
+ if (!Controller) return { signal: void 0, done: () => {
708
+ } };
709
+ const controller = new Controller();
710
+ const timer = setTimeout(() => controller.abort(), ms);
711
+ timer.unref?.();
712
+ return { signal: controller.signal, done: () => clearTimeout(timer) };
713
+ }
714
+ async function refresh() {
715
+ if (!base) return;
716
+ const deadline = abortAfter(refreshTimeoutMs);
717
+ try {
718
+ const response = await fetchImpl(`${base}/v1/twins`, {
719
+ headers: { authorization: `Bearer ${options.serverKey}` },
720
+ ...deadline.signal ? { signal: deadline.signal } : {}
721
+ });
722
+ if (!response.ok) {
723
+ failedAt = now();
724
+ return;
725
+ }
726
+ const body = await response.json();
727
+ if (!Array.isArray(body.twins)) {
728
+ failedAt = now();
729
+ return;
730
+ }
731
+ if (body.twins.length === 0 && twins.size > 0) {
732
+ fetchedAt = now();
733
+ failedAt = 0;
734
+ return;
735
+ }
736
+ const next = /* @__PURE__ */ new Map();
737
+ for (const row of body.twins.slice(0, PAGE_LIMIT)) {
738
+ if (typeof row?.path !== "string" || typeof row.markdown !== "string") continue;
739
+ next.set(normalisePath(row.path), { body: row.markdown });
740
+ }
741
+ twins = next;
742
+ fetchedAt = now();
743
+ failedAt = 0;
744
+ } catch {
745
+ failedAt = now();
746
+ } finally {
747
+ deadline.done();
748
+ }
749
+ }
750
+ function refreshIfStale() {
751
+ if (inFlight) return inFlight;
752
+ if (fetchedAt !== 0 && now() - fetchedAt < refreshMs) return Promise.resolve();
753
+ if (failedAt !== 0 && now() - failedAt < failureBackoffMs) return Promise.resolve();
754
+ inFlight = refresh().finally(() => {
755
+ inFlight = void 0;
756
+ });
757
+ return inFlight;
758
+ }
759
+ function withDeadline(work) {
760
+ return new Promise((done) => {
761
+ let settled = false;
762
+ const finish = (value) => {
763
+ if (settled) return;
764
+ settled = true;
765
+ done(value);
766
+ };
767
+ const timer = setTimeout(() => finish(void 0), coldTimeoutMs);
768
+ timer.unref?.();
769
+ work.then(finish, () => finish(void 0)).finally(() => clearTimeout(timer));
770
+ });
771
+ }
772
+ const lookup = (path) => twins.get(normalisePath(path));
773
+ async function fetchOne(path) {
774
+ if (!base) return void 0;
775
+ try {
776
+ const url = `${base}/v1/twins?path=${encodeURIComponent(normalisePath(path))}`;
777
+ const deadline = abortAfter(refreshTimeoutMs);
778
+ let body;
779
+ try {
780
+ const response = await fetchImpl(url, {
781
+ headers: { authorization: `Bearer ${options.serverKey}` },
782
+ ...deadline.signal ? { signal: deadline.signal } : {}
783
+ });
784
+ if (!response.ok) return void 0;
785
+ body = await response.json();
786
+ } finally {
787
+ deadline.done();
788
+ }
789
+ const row = Array.isArray(body.twins) ? body.twins[0] : void 0;
790
+ if (!row || typeof row.markdown !== "string") return void 0;
791
+ return { body: row.markdown };
792
+ } catch {
793
+ return void 0;
794
+ }
795
+ }
796
+ return {
797
+ lookup,
798
+ fetchOne,
799
+ /**
800
+ * ⚠️ Returns a VALUE when warm and a PROMISE when cold. `TwinOptions.resolve`
801
+ * accepts either, so a warm lookup never introduces a microtask and a cold
802
+ * one never answers "no twin" just because this isolate is new.
803
+ *
804
+ * ⚠️ **Only call this where the client has already asked for markdown.** On
805
+ * a cold process it waits, and the adapters route the advertise path to
806
+ * `lookup` for exactly that reason. The wait is bounded by `coldTimeoutMs`
807
+ * and its worst case is "no twin", which is a page that works normally.
808
+ */
809
+ resolve: (path) => {
810
+ if (fetchedAt !== 0) return lookup(path);
811
+ if (failedAt !== 0 && now() - failedAt < failureBackoffMs) return void 0;
812
+ void refreshIfStale();
813
+ return withDeadline(fetchOne(path));
814
+ },
815
+ refresh,
816
+ refreshIfStale,
817
+ get size() {
818
+ return twins.size;
819
+ }
820
+ };
821
+ }
822
+
823
+ // src/serve/twin.ts
824
+ var DISCOVERY_PATHS = ["/llms.txt", "/llms-full.txt", "/install.md"];
825
+ var DEFAULT_TWIN_CONTENT_TYPE = "text/markdown; charset=utf-8";
826
+ var DEFAULT_TWIN_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
827
+ function decideTwin(input) {
828
+ const method = input.method.toUpperCase();
829
+ if (method !== "GET" && method !== "HEAD") {
830
+ return { action: "pass", reason: "not_get" };
831
+ }
832
+ if (input.path.endsWith(".md")) {
833
+ return { action: "serve", lookupPath: stripMdSuffix(input.path), reason: "md_path" };
834
+ }
835
+ if (prefersMarkdown(input.accept)) {
836
+ return { action: "serve", lookupPath: input.path, reason: "accept_header" };
837
+ }
838
+ return { action: "pass", reason: "no_signal" };
839
+ }
840
+ function stripMdSuffix(path) {
841
+ const withoutSuffix = path.slice(0, -3);
842
+ if (withoutSuffix === "" || withoutSuffix === "/index") return "/";
843
+ return withoutSuffix;
844
+ }
845
+ function twinPathFor(path) {
846
+ if (path === "/") return "/index.md";
847
+ return `${path}.md`;
848
+ }
849
+ function buildTwinResponse(twin, decision, options) {
850
+ const headers = {
851
+ "content-type": twin.contentType ?? DEFAULT_TWIN_CONTENT_TYPE,
852
+ "cache-control": options.cacheControl ?? DEFAULT_TWIN_CACHE_CONTROL
853
+ };
854
+ if (decision.reason === "accept_header") {
855
+ headers["vary"] = "Accept";
856
+ }
857
+ if (twin.etag) headers["etag"] = twin.etag;
858
+ if (twin.lastModified) headers["last-modified"] = twin.lastModified;
859
+ return { status: 200, headers, body: twin.body };
860
+ }
861
+ function isThenable(value) {
862
+ return typeof value === "object" && value !== null && typeof value.then === "function";
863
+ }
864
+ function advertiseHeader(path) {
865
+ return `<${twinPathFor(path)}>; rel="alternate"; type="text/markdown"`;
866
+ }
867
+
868
+ // src/serve/source.ts
869
+ var NOOP_WARM = async () => {
870
+ };
871
+ function twinSourceFor(twin, config) {
872
+ const hosted = hostedFor(twin, config);
873
+ const authored = twin.resolve;
874
+ const lookup = (path) => {
875
+ if (authored) {
876
+ try {
877
+ const found = authored(path);
878
+ if (!isThenable(found) && found) return found;
879
+ } catch {
880
+ }
881
+ }
882
+ return hosted?.lookup(path);
883
+ };
884
+ const resolve = (path) => {
885
+ if (!authored) return hosted ? hosted.resolve(path) : void 0;
886
+ if (!hosted) return authored(path);
887
+ const found = authored(path);
888
+ if (isThenable(found)) return found.then((t) => t ?? hosted.resolve(path));
889
+ return found ?? hosted.resolve(path);
890
+ };
891
+ return { lookup, resolve, warm: hosted ? () => hosted.refreshIfStale() : NOOP_WARM };
892
+ }
893
+ function hostedFor(twin, config) {
894
+ if (!twin.hosted) return void 0;
895
+ const opts = twin.hosted === true ? {} : twin.hosted;
896
+ const serverKey = opts.serverKey ?? config.serverKey;
897
+ if (!serverKey) return void 0;
898
+ return hostedTwins({
899
+ serverKey,
900
+ ingestUrl: opts.ingestUrl ?? config.ingestUrl,
901
+ ...opts.refreshMs === void 0 ? {} : { refreshMs: opts.refreshMs },
902
+ ...opts.coldTimeoutMs === void 0 ? {} : { coldTimeoutMs: opts.coldTimeoutMs },
903
+ ...opts.failureBackoffMs === void 0 ? {} : { failureBackoffMs: opts.failureBackoffMs },
904
+ ...opts.refreshTimeoutMs === void 0 ? {} : { refreshTimeoutMs: opts.refreshTimeoutMs },
905
+ ...opts.fetchImpl === void 0 ? {} : { fetchImpl: opts.fetchImpl }
906
+ });
907
+ }
908
+
909
+ export { AgentHoneyConfigError, BoundedQueue, DEFAULTS, DEFAULT_TWIN_CACHE_CONTROL, DEFAULT_TWIN_CONTENT_TYPE, DISCOVERY_PATHS, MARKDOWN_TYPES, MAX_BODY_BYTES, SDK_NAME, SDK_VERSION, UNSET_SITE_ID, advertiseHeader, buildTwinResponse, createBreaker, createCollector, decideTwin, encodeBatch, fetchTransport, hostedTwins, looksLikeKey, newEventId, observeNetwork, observeResponse, outcomeForStatus, parseAccept, prefersMarkdown, recordOrCount, redact, resolveConfig, runtimeName, safe, safeAsync, stripMdSuffix, twinPathFor, twinSourceFor };
910
+ //# sourceMappingURL=chunk-L22VERBM.js.map
911
+ //# sourceMappingURL=chunk-L22VERBM.js.map