@entreprenoid/analytics 0.1.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 (44) hide show
  1. package/README.md +82 -0
  2. package/dist/chunk-5NZNPRMN.js +355 -0
  3. package/dist/chunk-5NZNPRMN.js.map +1 -0
  4. package/dist/chunk-EVNUKN5A.js +114 -0
  5. package/dist/chunk-EVNUKN5A.js.map +1 -0
  6. package/dist/chunk-GKFYGAKF.js +104 -0
  7. package/dist/chunk-GKFYGAKF.js.map +1 -0
  8. package/dist/chunk-IHZCOC2U.js +690 -0
  9. package/dist/chunk-IHZCOC2U.js.map +1 -0
  10. package/dist/core/breaker.d.ts +33 -0
  11. package/dist/core/collector.d.ts +40 -0
  12. package/dist/core/config.d.ts +101 -0
  13. package/dist/core/encode.d.ts +32 -0
  14. package/dist/core/queue.d.ts +39 -0
  15. package/dist/core/safe.d.ts +17 -0
  16. package/dist/core/transport.d.ts +45 -0
  17. package/dist/express.cjs +1042 -0
  18. package/dist/express.cjs.map +1 -0
  19. package/dist/express.d.ts +51 -0
  20. package/dist/express.js +4 -0
  21. package/dist/express.js.map +1 -0
  22. package/dist/index.cjs +1289 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.d.ts +17 -0
  25. package/dist/index.js +6 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/next.cjs +801 -0
  28. package/dist/next.cjs.map +1 -0
  29. package/dist/next.d.ts +90 -0
  30. package/dist/next.js +4 -0
  31. package/dist/next.js.map +1 -0
  32. package/dist/observe/redact.d.ts +58 -0
  33. package/dist/observe/request.d.ts +57 -0
  34. package/dist/observe/response.d.ts +24 -0
  35. package/dist/runtime.d.ts +27 -0
  36. package/dist/serve/accept.d.ts +7 -0
  37. package/dist/serve/discovery.d.ts +45 -0
  38. package/dist/serve/twin.d.ts +102 -0
  39. package/dist/web.cjs +790 -0
  40. package/dist/web.cjs.map +1 -0
  41. package/dist/web.d.ts +43 -0
  42. package/dist/web.js +4 -0
  43. package/dist/web.js.map +1 -0
  44. package/package.json +67 -0
package/dist/next.cjs ADDED
@@ -0,0 +1,801 @@
1
+ 'use strict';
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 DEFAULTS = {
41
+ batchSize: 20,
42
+ maxBatchSize: 100,
43
+ flushIntervalMs: 2e3,
44
+ maxQueueEvents: 1e3,
45
+ maxBodyBytes: 512 * 1024,
46
+ requestTimeoutMs: 2e3
47
+ };
48
+ var PUBLIC_ENV_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "PUBLIC_", "REACT_APP_", "NUXT_PUBLIC_"];
49
+ var EntreprenoidConfigError = class extends Error {
50
+ constructor(message) {
51
+ super(message);
52
+ this.name = "EntreprenoidConfigError";
53
+ }
54
+ };
55
+ function clamp(value, min, max) {
56
+ return Math.min(max, Math.max(min, value));
57
+ }
58
+ function boolFromEnv(raw) {
59
+ if (raw === void 0) return void 0;
60
+ const v = raw.trim().toLowerCase();
61
+ if (v === "1" || v === "true" || v === "yes") return true;
62
+ if (v === "0" || v === "false" || v === "no") return false;
63
+ return void 0;
64
+ }
65
+ function resolveConfig(options = {}, env = typeof process === "undefined" ? {} : process.env) {
66
+ for (const prefix of PUBLIC_ENV_PREFIXES) {
67
+ const name = `${prefix}ENTREPRENOID_SERVER_KEY`;
68
+ if (env[name]) {
69
+ throw new EntreprenoidConfigError(
70
+ `${name} is set. The "${prefix}" prefix inlines a value into client-side JavaScript, so this key is already public. Revoke it, then set ENTREPRENOID_SERVER_KEY instead (no prefix) so it stays on the server.`
71
+ );
72
+ }
73
+ }
74
+ const ingestUrl = options.ingestUrl ?? env["ENTREPRENOID_INGEST_URL"] ?? "";
75
+ const serverKey = options.serverKey ?? env["ENTREPRENOID_SERVER_KEY"] ?? "";
76
+ const siteId = options.siteId ?? env["ENTREPRENOID_SITE_ID"] ?? UNSET_SITE_ID;
77
+ const enabled = options.enabled ?? boolFromEnv(env["ENTREPRENOID_ENABLED"]) ?? true;
78
+ const debug = options.debug ?? boolFromEnv(env["ENTREPRENOID_DEBUG"]) ?? false;
79
+ let disabled = null;
80
+ if (typeof window !== "undefined") {
81
+ disabled = "browser-environment";
82
+ } else if (!enabled) {
83
+ disabled = "explicitly-disabled";
84
+ } else if (!ingestUrl) {
85
+ disabled = "missing-url";
86
+ } else if (!serverKey) {
87
+ disabled = "missing-key";
88
+ }
89
+ return Object.freeze({
90
+ ingestUrl,
91
+ serverKey,
92
+ siteId,
93
+ debug,
94
+ batchSize: clamp(options.batchSize ?? DEFAULTS.batchSize, 1, DEFAULTS.maxBatchSize),
95
+ flushIntervalMs: clamp(options.flushIntervalMs ?? DEFAULTS.flushIntervalMs, 100, 6e4),
96
+ maxQueueEvents: clamp(options.maxQueueEvents ?? DEFAULTS.maxQueueEvents, 1, 1e5),
97
+ maxBodyBytes: clamp(options.maxBodyBytes ?? DEFAULTS.maxBodyBytes, 1024, DEFAULTS.maxBodyBytes),
98
+ requestTimeoutMs: clamp(options.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs, 100, 3e4),
99
+ routeTemplate: options.routeTemplate,
100
+ redactPatterns: options.redactPatterns ?? [],
101
+ // ⚠️ Defaults to TRUE. An `?? true` that a refactor turns into `?? false`
102
+ // is the entire bug returning, so `config.test.ts` asserts the default.
103
+ redactHighEntropyPaths: options.redactHighEntropyPaths ?? true,
104
+ isInternal: options.isInternal,
105
+ disabled
106
+ });
107
+ }
108
+ function redact(text) {
109
+ return text.replace(/\bep_(live|test)_server_[A-Za-z0-9_-]+/g, "ep_$1_server_[redacted]");
110
+ }
111
+
112
+ // src/core/encode.ts
113
+ var MAX_BODY_BYTES = 512 * 1024;
114
+ var OPEN = '{"events":[';
115
+ var CLOSE = "]}";
116
+ var byteLength = typeof TextEncoder === "function" ? (s) => new TextEncoder().encode(s).length : (
117
+ // Node 18+ always has TextEncoder; this is here so the module cannot
118
+ // throw at import time on an exotic runtime, which would take the host
119
+ // application down with it.
120
+ (s) => s.length
121
+ );
122
+ function encodeBatch(events, maxBytes = MAX_BODY_BYTES) {
123
+ if (events.length === 0) {
124
+ return { body: OPEN + CLOSE, taken: 0, oversized: false };
125
+ }
126
+ const overhead = byteLength(OPEN) + byteLength(CLOSE);
127
+ let used = overhead;
128
+ const parts = [];
129
+ for (const event of events) {
130
+ const encoded = JSON.stringify(event);
131
+ const cost = byteLength(encoded) + (parts.length > 0 ? 1 : 0);
132
+ if (used + cost > maxBytes) break;
133
+ parts.push(encoded);
134
+ used += cost;
135
+ }
136
+ return {
137
+ body: OPEN + parts.join(",") + CLOSE,
138
+ taken: parts.length,
139
+ oversized: parts.length === 0
140
+ };
141
+ }
142
+
143
+ // src/core/queue.ts
144
+ var BoundedQueue = class {
145
+ capacity;
146
+ #items;
147
+ #head = 0;
148
+ #size = 0;
149
+ #dropped = 0;
150
+ constructor(capacity) {
151
+ if (!Number.isInteger(capacity) || capacity < 1) {
152
+ throw new TypeError(`capacity must be a positive integer, got ${capacity}`);
153
+ }
154
+ this.capacity = capacity;
155
+ this.#items = new Array(capacity);
156
+ }
157
+ get size() {
158
+ return this.#size;
159
+ }
160
+ /** How many events have been discarded because the buffer was full. */
161
+ get dropped() {
162
+ return this.#dropped;
163
+ }
164
+ push(item) {
165
+ if (this.#size === this.capacity) {
166
+ this.#items[this.#head] = item;
167
+ this.#head = (this.#head + 1) % this.capacity;
168
+ this.#dropped += 1;
169
+ return;
170
+ }
171
+ this.#items[(this.#head + this.#size) % this.capacity] = item;
172
+ this.#size += 1;
173
+ }
174
+ /** The first `max` items, without removing them. */
175
+ peek(max) {
176
+ const n = Math.min(max, this.#size);
177
+ const out = new Array(n);
178
+ for (let i = 0; i < n; i += 1) {
179
+ out[i] = this.#items[(this.#head + i) % this.capacity];
180
+ }
181
+ return out;
182
+ }
183
+ /** Remove the first `n` items. Called only after they are safely sent. */
184
+ commit(n) {
185
+ const count = Math.min(n, this.#size);
186
+ for (let i = 0; i < count; i += 1) {
187
+ this.#items[(this.#head + i) % this.capacity] = void 0;
188
+ }
189
+ this.#head = (this.#head + count) % this.capacity;
190
+ this.#size -= count;
191
+ }
192
+ /** Reset the drop counter, once the count has been reported on the wire. */
193
+ clearDropped() {
194
+ this.#dropped = 0;
195
+ }
196
+ };
197
+
198
+ // src/core/safe.ts
199
+ function safe(fn, onError) {
200
+ try {
201
+ fn();
202
+ } catch (error) {
203
+ try {
204
+ onError?.(error);
205
+ } catch {
206
+ }
207
+ }
208
+ }
209
+ async function safeAsync(fn, onError) {
210
+ try {
211
+ await fn();
212
+ } catch (error) {
213
+ try {
214
+ onError?.(error);
215
+ } catch {
216
+ }
217
+ }
218
+ }
219
+
220
+ // src/core/transport.ts
221
+ function outcomeForStatus(status, retryAfterMs) {
222
+ if (status >= 200 && status < 300) return { outcome: "accepted", status };
223
+ if (status === 408 || status === 429 || status >= 500) {
224
+ return retryAfterMs === void 0 ? { outcome: "retryable", status } : { outcome: "retryable", status, retryAfterMs };
225
+ }
226
+ return { outcome: "rejected", status };
227
+ }
228
+ function parseRetryAfter(value) {
229
+ if (!value) return void 0;
230
+ const seconds = Number(value);
231
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
232
+ const date = Date.parse(value);
233
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
234
+ return void 0;
235
+ }
236
+ function fetchTransport(url) {
237
+ return {
238
+ async send(body, headers, signal) {
239
+ try {
240
+ const response = await fetch(url, {
241
+ method: "POST",
242
+ headers,
243
+ body,
244
+ signal,
245
+ // Never follow a redirect: this request carries a bearer token, and
246
+ // a redirect is an instruction from the network to send that
247
+ // credential somewhere we did not choose.
248
+ redirect: "error",
249
+ keepalive: false
250
+ });
251
+ return outcomeForStatus(
252
+ response.status,
253
+ parseRetryAfter(response.headers.get("retry-after"))
254
+ );
255
+ } catch {
256
+ return { outcome: "retryable" };
257
+ }
258
+ }
259
+ };
260
+ }
261
+
262
+ // src/core/collector.ts
263
+ var MAX_RETRIES = 2;
264
+ function defaultSetTimer(fn, ms) {
265
+ const handle = setInterval(fn, ms);
266
+ handle.unref?.();
267
+ return { cancel: () => clearInterval(handle) };
268
+ }
269
+ function createCollector(config, deps = {}) {
270
+ const log = deps.log ?? ((message) => console.warn(redact(message)));
271
+ const debug = (message) => {
272
+ if (config.debug) log(`[entreprenoid] ${message}`);
273
+ };
274
+ if (config.disabled) {
275
+ debug(`collection disabled: ${config.disabled}`);
276
+ const noop = {
277
+ record: () => {
278
+ },
279
+ flush: async () => {
280
+ },
281
+ stats: { queued: 0, dropped: 0, sent: 0, failed: 0, breaker: "closed" },
282
+ close: () => {
283
+ }
284
+ };
285
+ return noop;
286
+ }
287
+ const queue = new BoundedQueue(config.maxQueueEvents);
288
+ const transport = deps.transport ?? fetchTransport(config.ingestUrl);
289
+ const breaker = deps.breaker ?? createBreaker();
290
+ const random = deps.random ?? Math.random;
291
+ const setTimer = deps.setTimer ?? defaultSetTimer;
292
+ let sent = 0;
293
+ let failed = 0;
294
+ let inFlight = null;
295
+ const timer = setTimer(() => {
296
+ void flush();
297
+ }, config.flushIntervalMs);
298
+ function headers() {
299
+ return {
300
+ "content-type": "application/json",
301
+ authorization: `Bearer ${config.serverKey}`
302
+ };
303
+ }
304
+ async function sendOnce(body) {
305
+ const controller = new AbortController();
306
+ const timeout = setTimeout(() => controller.abort(), config.requestTimeoutMs);
307
+ timeout.unref?.();
308
+ try {
309
+ return await transport.send(body, headers(), controller.signal);
310
+ } finally {
311
+ clearTimeout(timeout);
312
+ }
313
+ }
314
+ function backoffMs(attempt, retryAfterMs) {
315
+ if (retryAfterMs !== void 0) return Math.min(retryAfterMs, 3e4);
316
+ return random() * Math.min(5e3, 200 * 3 ** attempt);
317
+ }
318
+ const sleep = (ms) => new Promise((resolve) => {
319
+ const t = setTimeout(resolve, ms);
320
+ t.unref?.();
321
+ });
322
+ async function flushOnce() {
323
+ if (queue.size === 0) return;
324
+ if (!breaker.allow()) {
325
+ debug("breaker open, skipping flush");
326
+ return;
327
+ }
328
+ const candidates = queue.peek(config.batchSize);
329
+ const droppedSoFar = queue.dropped;
330
+ if (droppedSoFar > 0 && candidates[0]) {
331
+ candidates[0] = {
332
+ ...candidates[0],
333
+ sdk: { ...candidates[0].sdk, dropped: droppedSoFar }
334
+ };
335
+ }
336
+ const encoded = encodeBatch(candidates, config.maxBodyBytes);
337
+ if (encoded.oversized) {
338
+ queue.commit(1);
339
+ debug("dropped one event larger than the body limit");
340
+ return;
341
+ }
342
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
343
+ const result = await sendOnce(encoded.body);
344
+ if (result.outcome === "accepted") {
345
+ queue.commit(encoded.taken);
346
+ if (droppedSoFar > 0) queue.clearDropped();
347
+ sent += encoded.taken;
348
+ breaker.success();
349
+ return;
350
+ }
351
+ if (result.outcome === "rejected") {
352
+ queue.commit(encoded.taken);
353
+ failed += encoded.taken;
354
+ debug(`batch rejected with ${result.status}; dropped ${encoded.taken} event(s)`);
355
+ return;
356
+ }
357
+ if (attempt === MAX_RETRIES) break;
358
+ await sleep(backoffMs(attempt, result.retryAfterMs));
359
+ }
360
+ failed += encoded.taken;
361
+ breaker.failure();
362
+ debug(`batch failed after ${MAX_RETRIES + 1} attempt(s); ${queue.size} event(s) still buffered`);
363
+ }
364
+ function flush() {
365
+ if (inFlight) return inFlight;
366
+ inFlight = safeAsync(flushOnce, (error) => debug(`flush failed: ${String(error)}`)).finally(
367
+ () => {
368
+ inFlight = null;
369
+ }
370
+ );
371
+ return inFlight;
372
+ }
373
+ return {
374
+ record(event) {
375
+ try {
376
+ queue.push(event);
377
+ } catch {
378
+ }
379
+ },
380
+ flush,
381
+ get stats() {
382
+ return {
383
+ queued: queue.size,
384
+ dropped: queue.dropped,
385
+ sent,
386
+ failed,
387
+ breaker: breaker.state
388
+ };
389
+ },
390
+ close() {
391
+ timer.cancel();
392
+ }
393
+ };
394
+ }
395
+
396
+ // src/observe/redact.ts
397
+ var MIN_LENGTH = 16;
398
+ var MIN_ENTROPY_BITS = 3;
399
+ var PLACEHOLDER = /^[:*[{<]/;
400
+ var HAS_EXTENSION = /\.[A-Za-z0-9]{1,8}$/;
401
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
402
+ var CUID = /^c[a-z0-9]{20,31}$/;
403
+ var JWT = /^ey[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
404
+ var LONG_HEX = /^[0-9a-f]{16,}$/i;
405
+ var ALL_LOWER_ALPHA = /^[a-z]+$/;
406
+ function entropyBits(value) {
407
+ const counts = /* @__PURE__ */ new Map();
408
+ for (const ch of value) counts.set(ch, (counts.get(ch) ?? 0) + 1);
409
+ let bits = 0;
410
+ for (const n of counts.values()) {
411
+ const p = n / value.length;
412
+ bits -= p * Math.log2(p);
413
+ }
414
+ return bits;
415
+ }
416
+ function dense(value) {
417
+ if (value.length < MIN_LENGTH) return false;
418
+ if (ALL_LOWER_ALPHA.test(value)) return false;
419
+ const classes = Number(/[a-z]/.test(value)) + Number(/[A-Z]/.test(value)) + Number(/[0-9]/.test(value)) + Number(/[^A-Za-z0-9]/.test(value));
420
+ if (classes < 3) return false;
421
+ return entropyBits(value) >= MIN_ENTROPY_BITS;
422
+ }
423
+ function slugLike(segment) {
424
+ const parts = segment.split(/[-_]/).filter(Boolean);
425
+ if (parts.length < 2) return false;
426
+ const tidy = parts.every((p) => /^[a-z]+$/.test(p) || /^[0-9]+$/.test(p));
427
+ const hasWord = parts.some((p) => /^[a-z]{2,}$/.test(p));
428
+ return tidy && hasWord;
429
+ }
430
+ function looksLikeSecret(segment) {
431
+ if (segment.length < MIN_LENGTH) return false;
432
+ if (PLACEHOLDER.test(segment)) return false;
433
+ if (HAS_EXTENSION.test(segment)) return false;
434
+ if (UUID.test(segment) || CUID.test(segment) || JWT.test(segment) || LONG_HEX.test(segment)) {
435
+ return true;
436
+ }
437
+ for (const part of segment.split(/[-_.]/)) {
438
+ if (dense(part)) return true;
439
+ }
440
+ if (slugLike(segment)) return false;
441
+ return dense(segment);
442
+ }
443
+ var REDACTED = "[redacted]";
444
+ function redactPath(path, patterns, useDefault) {
445
+ if (patterns.length === 0 && !useDefault) return { path, redacted: false };
446
+ let redacted = false;
447
+ const out = path.split("/").map((segment) => {
448
+ if (!segment) return segment;
449
+ const matchesCustom = patterns.some((re) => {
450
+ re.lastIndex = 0;
451
+ return re.test(segment);
452
+ });
453
+ if (matchesCustom || useDefault && looksLikeSecret(segment)) {
454
+ redacted = true;
455
+ return REDACTED;
456
+ }
457
+ return segment;
458
+ }).join("/");
459
+ return { path: out, redacted };
460
+ }
461
+
462
+ // src/observe/request.ts
463
+ var CAMPAIGN_PARAMS = ["utm_source", "utm_medium", "utm_campaign", "utm_content"];
464
+ var CLICK_ID_PARAMS = ["gclid", "fbclid", "msclkid"];
465
+ function normalisePath(rawPath, redactPatterns = []) {
466
+ const path = cleanPath(rawPath);
467
+ return redactPatterns.length > 0 ? redactPath(path, redactPatterns, false).path : path;
468
+ }
469
+ function cleanPath(rawPath) {
470
+ let path = rawPath;
471
+ const hash = path.indexOf("#");
472
+ if (hash !== -1) path = path.slice(0, hash);
473
+ const query = path.indexOf("?");
474
+ if (query !== -1) path = path.slice(0, query);
475
+ if (path === "") path = "/";
476
+ if (!path.startsWith("/")) path = `/${path}`;
477
+ path = path.replace(/\/{2,}/g, "/");
478
+ if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
479
+ return path.length > 1024 ? path.slice(0, 1024) : path;
480
+ }
481
+ function referrerOrigin(referer) {
482
+ if (!referer) return void 0;
483
+ try {
484
+ const url = new URL(referer);
485
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
486
+ return url.origin.slice(0, 255);
487
+ } catch {
488
+ return void 0;
489
+ }
490
+ }
491
+ function campaignFrom(params) {
492
+ let found = false;
493
+ const campaign = {};
494
+ for (const name of CAMPAIGN_PARAMS) {
495
+ const value = params.get(name);
496
+ if (value) {
497
+ campaign[name.slice(4)] = value.slice(0, 120);
498
+ found = true;
499
+ }
500
+ }
501
+ return found ? campaign : void 0;
502
+ }
503
+ function clickIdFrom(params) {
504
+ for (const name of CLICK_ID_PARAMS) {
505
+ if (params.has(name)) return name;
506
+ }
507
+ return void 0;
508
+ }
509
+ function observeRequest(facts, config) {
510
+ const cleaned = cleanPath(facts.url);
511
+ let params = new URLSearchParams();
512
+ try {
513
+ params = new URL(facts.url, "http://x").searchParams;
514
+ } catch {
515
+ }
516
+ const templated = config.routeTemplate?.(cleaned);
517
+ const { path, redacted } = redactPath(
518
+ cleaned,
519
+ config.redactPatterns,
520
+ config.redactHighEntropyPaths
521
+ );
522
+ const observed = {
523
+ method: facts.method.slice(0, 10).toUpperCase(),
524
+ path
525
+ };
526
+ if (templated) {
527
+ const safeRoute = redactPath(templated, config.redactPatterns, config.redactHighEntropyPaths);
528
+ observed.route = safeRoute.path.slice(0, 512);
529
+ if (safeRoute.redacted) observed.pathRedacted = true;
530
+ }
531
+ if (redacted) observed.pathRedacted = true;
532
+ if (facts.host) observed.host = facts.host.slice(0, 253);
533
+ if (facts.protocol) observed.protocol = facts.protocol;
534
+ if (facts.userAgent) observed.userAgent = facts.userAgent.slice(0, 512);
535
+ const origin = referrerOrigin(facts.referer);
536
+ if (origin) observed.referrerOrigin = origin;
537
+ const campaign = campaignFrom(params);
538
+ if (campaign) observed.campaign = campaign;
539
+ const clickIdType = clickIdFrom(params);
540
+ if (clickIdType) observed.clickIdType = clickIdType;
541
+ if (config.isInternal?.(observed)) observed.internal = true;
542
+ return observed;
543
+ }
544
+
545
+ // src/observe/response.ts
546
+ function observeResponse(facts) {
547
+ const response = { observation: facts.observation };
548
+ if (facts.observation === "unknown") {
549
+ if (typeof facts.latencyMs === "number" && Number.isFinite(facts.latencyMs)) {
550
+ response.latencyMs = Math.max(0, Math.round(facts.latencyMs));
551
+ }
552
+ return response;
553
+ }
554
+ if (typeof facts.status === "number" && facts.status >= 100 && facts.status <= 599) {
555
+ response.status = facts.status;
556
+ }
557
+ if (facts.contentType) {
558
+ response.contentType = String(facts.contentType).slice(0, 255);
559
+ }
560
+ const length = typeof facts.contentLength === "string" ? Number(facts.contentLength) : facts.contentLength;
561
+ if (typeof length === "number" && Number.isFinite(length) && length >= 0) {
562
+ response.contentLength = Math.round(length);
563
+ }
564
+ if (typeof facts.latencyMs === "number" && Number.isFinite(facts.latencyMs)) {
565
+ response.latencyMs = Math.max(0, Math.round(facts.latencyMs));
566
+ }
567
+ return response;
568
+ }
569
+
570
+ // src/runtime.ts
571
+ var SDK_NAME = "@entreprenoid/analytics";
572
+ var SDK_VERSION = "0.1.0";
573
+ function runtimeName() {
574
+ try {
575
+ const g = globalThis;
576
+ const deno = g["Deno"];
577
+ if (deno?.version?.deno) return `deno-${deno.version.deno}`;
578
+ const bun = g["Bun"];
579
+ if (bun?.version) return `bun-${bun.version}`;
580
+ if (typeof process !== "undefined" && process.versions?.node) {
581
+ return `node-${process.versions.node}`;
582
+ }
583
+ const nav = g["navigator"];
584
+ if (nav?.userAgent?.includes("Cloudflare-Workers")) return "workerd";
585
+ } catch {
586
+ }
587
+ return "unknown";
588
+ }
589
+ function newEventId() {
590
+ try {
591
+ const c = globalThis.crypto;
592
+ if (typeof c?.randomUUID === "function") return c.randomUUID();
593
+ } catch {
594
+ }
595
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
596
+ }
597
+
598
+ // src/serve/accept.ts
599
+ function parseAccept(header) {
600
+ if (!header) return [];
601
+ const ranges = [];
602
+ for (const part of header.split(",")) {
603
+ const segments = part.trim().split(";");
604
+ const type = segments[0]?.trim().toLowerCase();
605
+ if (!type) continue;
606
+ let q = 1;
607
+ for (const segment of segments.slice(1)) {
608
+ const [key, value] = segment.split("=").map((s) => s.trim().toLowerCase());
609
+ if (key === "q") {
610
+ const parsed = Number(value);
611
+ q = Number.isFinite(parsed) ? Math.min(1, Math.max(0, parsed)) : 1;
612
+ }
613
+ }
614
+ ranges.push({ type, q });
615
+ }
616
+ return ranges;
617
+ }
618
+ function exactQ(ranges, type) {
619
+ let best = 0;
620
+ for (const range of ranges) {
621
+ if (range.type === type) best = Math.max(best, range.q);
622
+ }
623
+ return best;
624
+ }
625
+ function effectiveQ(ranges, type) {
626
+ const prefix = `${type.split("/")[0]}/*`;
627
+ let best = 0;
628
+ for (const range of ranges) {
629
+ if (range.type === type || range.type === prefix || range.type === "*/*") {
630
+ best = Math.max(best, range.q);
631
+ }
632
+ }
633
+ return best;
634
+ }
635
+ var MARKDOWN_TYPES = ["text/markdown", "text/x-markdown"];
636
+ function prefersMarkdown(header) {
637
+ const ranges = parseAccept(header);
638
+ if (ranges.length === 0) return false;
639
+ let markdown = 0;
640
+ for (const type of MARKDOWN_TYPES) {
641
+ markdown = Math.max(markdown, exactQ(ranges, type));
642
+ }
643
+ if (markdown === 0) return false;
644
+ const html = effectiveQ(ranges, "text/html");
645
+ return markdown >= html;
646
+ }
647
+
648
+ // src/serve/twin.ts
649
+ var DEFAULT_TWIN_CONTENT_TYPE = "text/markdown; charset=utf-8";
650
+ var DEFAULT_TWIN_CACHE_CONTROL = "public, max-age=3600, s-maxage=86400";
651
+ function decideTwin(input) {
652
+ const method = input.method.toUpperCase();
653
+ if (method !== "GET" && method !== "HEAD") {
654
+ return { action: "pass", reason: "not_get" };
655
+ }
656
+ if (input.path.endsWith(".md")) {
657
+ return { action: "serve", lookupPath: stripMdSuffix(input.path), reason: "md_path" };
658
+ }
659
+ if (prefersMarkdown(input.accept)) {
660
+ return { action: "serve", lookupPath: input.path, reason: "accept_header" };
661
+ }
662
+ return { action: "pass", reason: "no_signal" };
663
+ }
664
+ function stripMdSuffix(path) {
665
+ const withoutSuffix = path.slice(0, -3);
666
+ if (withoutSuffix === "" || withoutSuffix === "/index") return "/";
667
+ return withoutSuffix;
668
+ }
669
+ function twinPathFor(path) {
670
+ if (path === "/") return "/index.md";
671
+ return `${path}.md`;
672
+ }
673
+ function buildTwinResponse(twin, decision, options) {
674
+ const headers = {
675
+ "content-type": twin.contentType ?? DEFAULT_TWIN_CONTENT_TYPE,
676
+ "cache-control": options.cacheControl ?? DEFAULT_TWIN_CACHE_CONTROL
677
+ };
678
+ if (decision.reason === "accept_header") {
679
+ headers["vary"] = "Accept";
680
+ }
681
+ if (twin.etag) headers["etag"] = twin.etag;
682
+ if (twin.lastModified) headers["last-modified"] = twin.lastModified;
683
+ return { status: 200, headers, body: twin.body };
684
+ }
685
+ function advertiseHeader(path) {
686
+ return `<${twinPathFor(path)}>; rel="alternate"; type="text/markdown"`;
687
+ }
688
+
689
+ // src/next.ts
690
+ function proxy(options = {}) {
691
+ const config = resolveConfig(options);
692
+ const collector = options.collector ?? createCollector(config);
693
+ return async (request) => {
694
+ const startedAt = Date.now();
695
+ let served;
696
+ let twinResponse;
697
+ if (options.twin) {
698
+ try {
699
+ const url = new URL(request.url);
700
+ const decision = decideTwin({
701
+ method: request.method,
702
+ path: normalisePath(url.pathname),
703
+ accept: request.headers.get("accept")
704
+ });
705
+ if (decision.action === "serve") {
706
+ const found = await options.twin.resolve(decision.lookupPath);
707
+ if (found) {
708
+ const built = buildTwinResponse(found, decision, options.twin);
709
+ served = {
710
+ decision: "served",
711
+ reason: decision.reason,
712
+ format: built.headers["content-type"] ?? "text/markdown"
713
+ };
714
+ twinResponse = new Response(
715
+ request.method.toUpperCase() === "HEAD" ? null : built.body,
716
+ { status: built.status, headers: built.headers }
717
+ );
718
+ } else {
719
+ served = { decision: "fell_through", reason: "no_twin" };
720
+ }
721
+ } else {
722
+ served = { decision: "fell_through", reason: decision.reason };
723
+ }
724
+ } catch {
725
+ served = { decision: "error", reason: "resolver_error" };
726
+ }
727
+ }
728
+ const record = () => {
729
+ safe(() => {
730
+ if (config.disabled) return;
731
+ const url = new URL(request.url);
732
+ const observed = observeRequest(
733
+ {
734
+ method: request.method,
735
+ url: url.pathname + url.search,
736
+ host: url.host,
737
+ protocol: url.protocol === "https:" ? "https" : "http",
738
+ userAgent: request.headers.get("user-agent") ?? void 0,
739
+ referer: request.headers.get("referer") ?? void 0
740
+ },
741
+ config
742
+ );
743
+ const event = {
744
+ ...observed,
745
+ eventId: newEventId(),
746
+ // ⚠️ Emitted so a post-response observation CAN be merged later.
747
+ // Nothing merges it today, and the docblock above says so plainly
748
+ // rather than letting the field imply otherwise.
749
+ requestId: newEventId(),
750
+ siteId: config.siteId,
751
+ observedAt: new Date(startedAt).toISOString(),
752
+ ...served ? { serve: served } : {},
753
+ // ⚠️ **The response object is present ONLY when we built it.**
754
+ //
755
+ // If the twin was served, this proxy IS the responder and measured it.
756
+ // Otherwise the route has not run yet and there is nothing to
757
+ // observe — so the key is OMITTED, not set to a guess, not set to
758
+ // `observation: "unknown"` with a status (which the schema refuses),
759
+ // and not set to a latency-only object (we did not wait for the
760
+ // response, so we did not measure a latency either).
761
+ ...twinResponse ? {
762
+ response: observeResponse({
763
+ status: twinResponse.status,
764
+ contentType: twinResponse.headers.get("content-type"),
765
+ contentLength: twinResponse.headers.get("content-length"),
766
+ latencyMs: Date.now() - startedAt,
767
+ observation: "measured"
768
+ })
769
+ } : {},
770
+ sdk: {
771
+ name: SDK_NAME,
772
+ version: SDK_VERSION,
773
+ // ⚠️ `next-proxy`, which is the name the WIRE SCHEMA already uses
774
+ // in its own docblock (`event.ts:157`). Its own name matters here
775
+ // more than for any other adapter: this is the one whose events
776
+ // legitimately carry no response, and the dashboard must not
777
+ // present an adapter's blind spot as a fact about the traffic.
778
+ adapter: "next-proxy",
779
+ runtime: runtimeName()
780
+ }
781
+ };
782
+ collector.record(event);
783
+ });
784
+ };
785
+ if (options.after) {
786
+ try {
787
+ options.after(record);
788
+ } catch {
789
+ record();
790
+ }
791
+ } else {
792
+ record();
793
+ }
794
+ return twinResponse;
795
+ };
796
+ }
797
+
798
+ exports.advertiseHeader = advertiseHeader;
799
+ exports.proxy = proxy;
800
+ //# sourceMappingURL=next.cjs.map
801
+ //# sourceMappingURL=next.cjs.map