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