@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/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @entreprenoid/analytics
2
+
3
+ Analytics for the agentic web — and the other half nobody ships: **serving those agents a clean
4
+ markdown twin from the same middleware**.
5
+
6
+ One `app.use`. It observes every request at the server boundary, so it counts the clients that
7
+ never run JavaScript, and it can serve a markdown representation of a page to anything that asks
8
+ for one.
9
+
10
+ ```ts
11
+ import { entreprenoid } from "@entreprenoid/analytics/express";
12
+
13
+ app.use(entreprenoid());
14
+ ```
15
+
16
+ Nothing loads in a visitor's browser. No client script, no cookies.
17
+
18
+ ## Installing with a coding agent
19
+
20
+ Point it at **<https://entreprenoid.com/install.md>** — the same guide this package serves from
21
+ your own middleware. It is written for an agent rather than a person: imperative, naming the
22
+ failure modes, and telling the agent to stop rather than guess at a credential.
23
+
24
+ ## The three properties worth knowing before you install
25
+
26
+ **It never blocks and never fails your responses.** `record()` is synchronous and returns `void`,
27
+ every adapter entry point wears one `safe()` wrapper, and nothing awaits the network on the
28
+ request path. The serve path fails open to *content*: any throw, timeout or manifest miss falls
29
+ through to your own handler with the response unchanged. Each adapter has a test asserting
30
+ byte-identical output with and without the middleware — including with a collector injected to
31
+ throw on every call.
32
+
33
+ **The response is inspected, never consumed.** `res` is a single-read stream, and monkey-patching
34
+ `write`/`end` to observe a body adds latency, adds memory, and can corrupt what you send. This
35
+ reads what is already in the headers.
36
+
37
+ **⚠️ The markdown twin is gated on `Accept`, never on identity.** A crawler sending
38
+ `Accept: */*` receives exactly what a browser receives. Serving different content by User-Agent
39
+ is cloaking, it forces `Vary: User-Agent` (which disables shared caching), and a User-Agent is a
40
+ claim rather than proof.
41
+
42
+ ## ⚠️ Look at your routes before you go live
43
+
44
+ The path is sent as it arrives. The query string is dropped before anything parses it and the
45
+ `Referer` is reduced to an origin — but plenty of sites put secrets in the **path**:
46
+ `/reveal/<token>`, `/join/<code>`, `/confirm/<ticket>`.
47
+
48
+ ```ts
49
+ app.use(entreprenoid({
50
+ routeTemplate: (path) => path.replace(/\/\d+(?=\/|$)/g, "/:id"),
51
+ redactPatterns: [/^[A-Za-z0-9_-]{20,}$/],
52
+ isInternal: (req) => req.path.startsWith("/_health"),
53
+ }));
54
+ ```
55
+
56
+ A default backstop redacts segments that look like credentials, and records that it did. It
57
+ cannot catch a short token and it does not know which of your ids are sensitive. Only your routes
58
+ do. `redactHighEntropyPaths: false` turns the backstop off; your own `redactPatterns` always
59
+ apply.
60
+
61
+ ## ⚠️ The key is a server credential
62
+
63
+ `ENTREPRENOID_SERVER_KEY` can write events for every site it owns. It must never reach a browser.
64
+ Every export declares `"browser": null`, and the package **refuses to start** if it finds the key
65
+ under `NEXT_PUBLIC_`, `VITE_`, `PUBLIC_` or `REACT_APP_` — those prefixes inline a value into
66
+ client JavaScript, which publishes the key to everyone who loads the page.
67
+
68
+ ## Zero runtime dependencies
69
+
70
+ Deliberately, and enforced: a build gate fails if `dependencies` is non-empty or a bare import
71
+ survives into `dist/`. It holds a credential and runs inside other people's request paths; its
72
+ supply chain is its own.
73
+
74
+ ## Documentation
75
+
76
+ - Install guide, for agents: <https://entreprenoid.com/install.md>
77
+ - What is collected, generated from the wire schema and kept honest by a test:
78
+ [`collected-fields.md`](https://github.com/Fifth-Mind/entreprenoid/blob/main/docs/design/collected-fields.md)
79
+
80
+ ## Licence
81
+
82
+ UNLICENSED — see the repository.
@@ -0,0 +1,355 @@
1
+ import { resolveConfig, createCollector, safe, normalisePath, DISCOVERY_PATHS, decideTwin, advertiseHeader, buildTwinResponse, observeRequest, runtimeName, observeResponse, newEventId, SDK_VERSION, SDK_NAME } from './chunk-IHZCOC2U.js';
2
+
3
+ // src/serve/discovery.ts
4
+ function titleFor(entry) {
5
+ if (entry.title) return entry.title;
6
+ if (entry.path === "/") return "Home";
7
+ const last = entry.path.split("/").filter(Boolean).pop() ?? entry.path;
8
+ return last.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
9
+ }
10
+ var twinPath = (path) => path === "/" ? "/index.md" : `${path}.md`;
11
+ function renderLlmsTxt(options) {
12
+ const origin = options.origin ?? "";
13
+ const lines = [`# ${options.siteName}`, ""];
14
+ if (options.description) {
15
+ for (const line of options.description.split("\n")) lines.push(`> ${line}`);
16
+ lines.push("");
17
+ }
18
+ lines.push(
19
+ "Every page listed here is also available as markdown: append `.md` to the path",
20
+ "(`/` becomes `/index.md`), or send `Accept: text/markdown`.",
21
+ "",
22
+ "## Pages",
23
+ ""
24
+ );
25
+ for (const entry of options.entries) {
26
+ const href = `${origin}${twinPath(entry.path)}`;
27
+ lines.push(`- [${titleFor(entry)}](${href})${entry.summary ? `: ${entry.summary}` : ""}`);
28
+ }
29
+ lines.push("");
30
+ return lines.join("\n");
31
+ }
32
+ async function renderLlmsFullTxt(options, resolve) {
33
+ const parts = [`# ${options.siteName}`, ""];
34
+ if (options.description) {
35
+ for (const line of options.description.split("\n")) parts.push(`> ${line}`);
36
+ parts.push("");
37
+ }
38
+ for (const entry of options.entries) {
39
+ let twin;
40
+ try {
41
+ twin = await resolve(entry.path);
42
+ } catch {
43
+ continue;
44
+ }
45
+ if (!twin) continue;
46
+ parts.push(`---`, ``, `## ${titleFor(entry)}`, ``, `Source: ${entry.path}`, ``, twin.body.trim(), ``);
47
+ }
48
+ return parts.join("\n");
49
+ }
50
+ function renderInstallMd(options = {}) {
51
+ const pkg = options.packageName ?? "@entreprenoid/analytics";
52
+ return `# entreprenoid install guide (for AI coding agents)
53
+
54
+ You are installing **entreprenoid** \u2014 analytics for the agentic web. It records every
55
+ request a site serves, especially from AI agents, and can serve those agents a clean
56
+ markdown twin of each page. Collection is fire-and-forget: it never blocks and never
57
+ fails the user's responses.
58
+
59
+ ## Before you start
60
+
61
+ Every environment variable this package reads:
62
+
63
+ | Variable | Required | What it is |
64
+ | --- | --- | --- |
65
+ | \`ENTREPRENOID_INGEST_URL\` | **yes** | The ingest endpoint. \u26A0\uFE0F It must end in \`/v1/events\` \u2014 the SDK POSTs this URL verbatim and appends nothing, so a bare origin sends every batch to \`POST /\`, which is rejected. The SDK fails open, so you would see nothing at all. |
66
+ | \`ENTREPRENOID_SERVER_KEY\` | **yes** | The site's key, starting with \`ep_live_server_\`. |
67
+ | \`ENTREPRENOID_SITE_ID\` | no | The dashboard shows it beside the key. Safe to set, safe to omit: the server resolves the site from the credential and ignores this value. |
68
+ | \`ENTREPRENOID_ENABLED\` | no | \`false\` makes collection a transparent no-op without removing any code. Useful per environment. |
69
+ | \`ENTREPRENOID_DEBUG\` | no | \`true\` prints one startup line saying whether collection is on, and why not if it is off. |
70
+
71
+ **If either required value is missing, STOP and ask.** Do not invent them, and do not
72
+ leave a placeholder \u2014 a config that looks complete and is not is worse than no config.
73
+
74
+ Safety rules that apply throughout:
75
+
76
+ - **Never hardcode the key**, never print it, never commit it. Environment variables only.
77
+ - Before writing an env file, make sure it is gitignored.
78
+ - \u26A0\uFE0F **Never prefix the key with \`NEXT_PUBLIC_\`, \`VITE_\`, \`PUBLIC_\` or \`REACT_APP_\`.**
79
+ Those prefixes inline a value into client-side JavaScript, which publishes the key to
80
+ everyone who loads the page. The SDK refuses to start if it finds one.
81
+ - Only touch the files this install needs.
82
+
83
+ ## Step 1 \u2014 install the package
84
+
85
+ Detect the package manager from the lockfile:
86
+
87
+ | Lockfile | Command |
88
+ | --- | --- |
89
+ | \`pnpm-lock.yaml\` | \`pnpm add ${pkg}\` |
90
+ | \`yarn.lock\` | \`yarn add ${pkg}\` |
91
+ | \`bun.lock\` | \`bun add ${pkg}\` |
92
+ | \`package-lock.json\` or none | \`npm install ${pkg}\` |
93
+
94
+ ## Step 2 \u2014 wire up the collector (pick exactly ONE)
95
+
96
+ ### Express
97
+
98
+ \`\`\`ts
99
+ import { entreprenoid } from "${pkg}/express";
100
+
101
+ app.use(entreprenoid());
102
+ \`\`\`
103
+
104
+ Add it **before** your routes so it observes all of them.
105
+
106
+ ### Next.js (App Router, 14+)
107
+
108
+ Use the **Next adapter**, not the web one. In \`proxy.ts\` at the project root
109
+ (\`middleware.ts\` on Next 15 and earlier \u2014 same file, renamed in Next 16):
110
+
111
+ \`\`\`ts
112
+ import { after } from "next/server";
113
+ import { proxy } from "${pkg}/next";
114
+
115
+ export default proxy({ after });
116
+
117
+ export const config = {
118
+ matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
119
+ };
120
+ \`\`\`
121
+
122
+ \u26A0\uFE0F **Do not use \`${pkg}/web\` in a Next proxy.** It runs and it lies. A proxy
123
+ executes *before* the route and hands control onward with a sentinel response \u2014 status
124
+ 200, no real content type \u2014 so the web adapter would record a **measured 200 for every
125
+ request**, including the ones your routes render as 404 or 500.
126
+
127
+ The Next adapter emits only what a proxy can actually know, and **omits the response
128
+ entirely** rather than guessing at it. Your dashboard will show those requests with no
129
+ status, which is the truth: nothing observed one.
130
+
131
+ \u26A0\uFE0F **Pass \`after\`.** Without it the collector relies on its own timer, and a serverless
132
+ invocation can be frozen before that timer fires \u2014 events are simply lost, silently.
133
+
134
+ ### Web-standard runtimes (Cloudflare Workers, Deno, Bun, Hono)
135
+
136
+ \`\`\`ts
137
+ import { observe } from "${pkg}/web";
138
+
139
+ export default {
140
+ fetch: observe(handler, { waitUntil: (p) => ctx.waitUntil(p) }),
141
+ };
142
+ \`\`\`
143
+
144
+ Pass \`waitUntil\` where the runtime offers one, or a serverless invocation can be
145
+ frozen before the events are sent.
146
+
147
+ ## Step 3 \u2014 optional: serve a markdown twin
148
+
149
+ Agents pay for every token they read, and most of a modern page is markup they do not
150
+ want. Supply a resolver and the same middleware will serve clean markdown when a client
151
+ asks for it:
152
+
153
+ \`\`\`ts
154
+ app.use(entreprenoid({
155
+ twin: { resolve: (path) => markdownFor(path) },
156
+ }));
157
+ \`\`\`
158
+
159
+ \u26A0\uFE0F **A browser never receives markdown.** The twin is served only when the path ends in
160
+ \`.md\` or the \`Accept\` header explicitly prefers \`text/markdown\` \u2014 never based on the
161
+ User-Agent, which would be cloaking and would break shared caching.
162
+
163
+ ## Step 4 \u2014 \u26A0\uFE0F look at the routes before you go live
164
+
165
+ **Do not skip this one.** The path is sent as it arrives. The query string is dropped
166
+ before anything parses it, and the \`Referer\` is reduced to an origin \u2014 but the path
167
+ itself is data, and on a lot of sites the path carries secrets:
168
+
169
+ \`\`\`
170
+ /reveal/<single-use-token> /join/<invite-code>
171
+ /confirm/<token> /upload/<ticket>
172
+ \`\`\`
173
+
174
+ Read the project's routes. For each one, decide:
175
+
176
+ \`\`\`ts
177
+ app.use(entreprenoid({
178
+ // Collapse identifiers so analytics never sees a per-user value, and so one
179
+ // route does not become ten thousand rows.
180
+ routeTemplate: (path) => path.replace(/\\/\\d+(?=\\/|$)/g, "/:id"),
181
+
182
+ // Replace any segment that is a token, code or personal identifier.
183
+ redactPatterns: [/^[A-Za-z0-9_-]{20,}$/],
184
+
185
+ // Traffic you do not want counted: health checks, your own office, previews.
186
+ isInternal: (req) => req.path.startsWith("/_health"),
187
+ }));
188
+ \`\`\`
189
+
190
+ \u26A0\uFE0F **A default backstop already runs, and you should not rely on it.** Segments that
191
+ look like credentials \u2014 uuids, cuids, JWTs, long hex, dense mixed-case strings \u2014 are
192
+ replaced with \`[redacted]\` before the event is sent, and the event records that it
193
+ happened. It cannot catch a short token like \`/j/aB3xK9\`, and it does not know which of
194
+ this project's ids are sensitive. **Only the routes tell you that.** Set
195
+ \`redactHighEntropyPaths: false\` to turn the backstop off; that never disables
196
+ \`redactPatterns\`, which are yours.
197
+
198
+ If you are unsure whether a path segment is a secret, treat it as one and say so in your
199
+ summary to the user.
200
+
201
+ ## Step 5 \u2014 verify
202
+
203
+ Start the app and make one request. Within a few seconds the dashboard should show it.
204
+ If nothing arrives:
205
+
206
+ - check the key is set in the server's environment, not the client's
207
+ - check \`ENTREPRENOID_INGEST_URL\` ends in \`/v1/events\`
208
+ - set \`ENTREPRENOID_DEBUG=true\` and read the startup line
209
+
210
+ **Do not add retry logic, queues or error handling around the SDK.** It already buffers,
211
+ retries with backoff, and fails open. Wrapping it in a try/catch is harmless; awaiting it
212
+ is not, and would put analytics on your critical path.
213
+ `;
214
+ }
215
+
216
+ // src/express.ts
217
+ function header(req, name) {
218
+ const value = req.headers[name];
219
+ if (Array.isArray(value)) return value[0];
220
+ return value;
221
+ }
222
+ function headerString(res, name) {
223
+ const value = res.getHeader(name);
224
+ if (value === void 0 || value === null) return void 0;
225
+ return Array.isArray(value) ? value[0] : String(value);
226
+ }
227
+ function entreprenoid(options = {}) {
228
+ const config = resolveConfig(options);
229
+ const collector = options.collector ?? createCollector(config);
230
+ const twin = options.twin;
231
+ return function entreprenoidMiddleware(req, res, next) {
232
+ if (config.disabled && !twin) {
233
+ next();
234
+ return;
235
+ }
236
+ const startedAt = Date.now();
237
+ const startedHr = process.hrtime.bigint();
238
+ let recorded = false;
239
+ let served;
240
+ const record = () => {
241
+ if (recorded) return;
242
+ recorded = true;
243
+ safe(() => {
244
+ const observed = observeRequest(
245
+ {
246
+ method: req.method ?? "GET",
247
+ url: req.originalUrl ?? req.url ?? "/",
248
+ host: header(req, "host"),
249
+ protocol: req.secure || req.protocol === "https" ? "https" : "http",
250
+ userAgent: header(req, "user-agent"),
251
+ referer: header(req, "referer") ?? header(req, "referrer")
252
+ },
253
+ config
254
+ );
255
+ const finished = res.writableFinished !== false;
256
+ const latencyMs = Number(process.hrtime.bigint() - startedHr) / 1e6;
257
+ const event = {
258
+ ...observed,
259
+ eventId: newEventId(),
260
+ siteId: config.siteId,
261
+ observedAt: new Date(startedAt).toISOString(),
262
+ response: finished ? observeResponse({
263
+ status: res.statusCode,
264
+ contentType: headerString(res, "content-type"),
265
+ contentLength: headerString(res, "content-length"),
266
+ latencyMs,
267
+ observation: "measured"
268
+ }) : observeResponse({ latencyMs, observation: "unknown" }),
269
+ ...served ? { serve: served } : {},
270
+ sdk: {
271
+ name: SDK_NAME,
272
+ version: SDK_VERSION,
273
+ adapter: "express",
274
+ runtime: runtimeName()
275
+ }
276
+ };
277
+ collector.record(event);
278
+ });
279
+ };
280
+ safe(() => {
281
+ res.once("finish", record);
282
+ res.once("close", record);
283
+ });
284
+ if (!twin) {
285
+ next();
286
+ return;
287
+ }
288
+ const path = normalisePath(req.originalUrl ?? req.url ?? "/", config.redactPatterns);
289
+ if (twin.discovery && DISCOVERY_PATHS.includes(path)) {
290
+ const method = (req.method ?? "GET").toUpperCase();
291
+ if (method === "GET" || method === "HEAD") {
292
+ void (async () => {
293
+ try {
294
+ const body = path === "/install.md" ? renderInstallMd() : path === "/llms.txt" ? renderLlmsTxt(twin.discovery) : await renderLlmsFullTxt(twin.discovery, twin.resolve);
295
+ res.setHeader("content-type", "text/markdown; charset=utf-8");
296
+ res.setHeader("cache-control", twin.cacheControl ?? "public, max-age=3600, s-maxage=86400");
297
+ res.statusCode = 200;
298
+ served = { decision: "served", reason: "md_path", format: "text/markdown; charset=utf-8" };
299
+ if (method === "HEAD") res.end();
300
+ else res.end(body);
301
+ } catch {
302
+ served = { decision: "error", reason: "resolver_error" };
303
+ if (!res.headersSent) next();
304
+ }
305
+ })();
306
+ return;
307
+ }
308
+ }
309
+ const decision = decideTwin({
310
+ method: req.method ?? "GET",
311
+ path,
312
+ accept: header(req, "accept")
313
+ });
314
+ if (decision.action === "pass") {
315
+ if (twin.advertise !== false) {
316
+ void Promise.resolve().then(() => twin.resolve(path)).then((found) => {
317
+ if (found && !res.headersSent) {
318
+ safe(() => res.setHeader("link", advertiseHeader(path)));
319
+ served = { decision: "advertised", reason: "no_twin" };
320
+ }
321
+ }).catch(() => void 0).finally(() => next());
322
+ return;
323
+ }
324
+ served = { decision: "fell_through", reason: decision.reason };
325
+ next();
326
+ return;
327
+ }
328
+ void Promise.resolve().then(() => twin.resolve(decision.lookupPath)).then((found) => {
329
+ if (!found) {
330
+ served = { decision: "fell_through", reason: "no_twin" };
331
+ next();
332
+ return;
333
+ }
334
+ const built = buildTwinResponse(found, decision, twin);
335
+ served = {
336
+ decision: "served",
337
+ reason: decision.reason,
338
+ format: built.headers["content-type"] ?? "text/markdown"
339
+ };
340
+ for (const [name, value] of Object.entries(built.headers)) {
341
+ res.setHeader(name, value);
342
+ }
343
+ res.statusCode = built.status;
344
+ if ((req.method ?? "GET").toUpperCase() === "HEAD") res.end();
345
+ else res.end(built.body);
346
+ }).catch(() => {
347
+ served = { decision: "error", reason: "resolver_error" };
348
+ if (!res.headersSent) next();
349
+ });
350
+ };
351
+ }
352
+
353
+ export { entreprenoid, renderInstallMd, renderLlmsFullTxt, renderLlmsTxt };
354
+ //# sourceMappingURL=chunk-5NZNPRMN.js.map
355
+ //# sourceMappingURL=chunk-5NZNPRMN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/serve/discovery.ts","../src/express.ts"],"names":[],"mappings":";;;AAkCA,SAAS,SAAS,KAAA,EAA0B;AAC1C,EAAA,IAAI,KAAA,CAAM,KAAA,EAAO,OAAO,KAAA,CAAM,KAAA;AAC9B,EAAA,IAAI,KAAA,CAAM,IAAA,KAAS,GAAA,EAAK,OAAO,MAAA;AAC/B,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE,GAAA,EAAI,IAAK,KAAA,CAAM,IAAA;AAClE,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,GAAG,CAAA,CAAE,OAAA,CAAQ,OAAA,EAAS,CAAC,CAAA,KAAM,CAAA,CAAE,WAAA,EAAa,CAAA;AAC3E;AAEA,IAAM,WAAW,CAAC,IAAA,KAAkB,SAAS,GAAA,GAAM,WAAA,GAAc,GAAG,IAAI,CAAA,GAAA,CAAA;AAGjE,SAAS,cAAc,OAAA,EAAmC;AAC/D,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,EAAA;AACjC,EAAA,MAAM,QAAkB,CAAC,CAAA,EAAA,EAAK,OAAA,CAAQ,QAAQ,IAAI,EAAE,CAAA;AAEpD,EAAA,IAAI,QAAQ,WAAA,EAAa;AACvB,IAAA,KAAA,MAAW,IAAA,IAAQ,OAAA,CAAQ,WAAA,CAAY,KAAA,CAAM,IAAI,GAAG,KAAA,CAAM,IAAA,CAAK,CAAA,EAAA,EAAK,IAAI,CAAA,CAAE,CAAA;AAC1E,IAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AAAA,EACf;AAEA,EAAA,KAAA,CAAM,IAAA;AAAA,IACJ,gFAAA;AAAA,IACA,6DAAA;AAAA,IACA,EAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,KAAA,MAAW,KAAA,IAAS,QAAQ,OAAA,EAAS;AACnC,IAAA,MAAM,OAAO,CAAA,EAAG,MAAM,GAAG,QAAA,CAAS,KAAA,CAAM,IAAI,CAAC,CAAA,CAAA;AAC7C,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,GAAA,EAAM,QAAA,CAAS,KAAK,CAAC,CAAA,EAAA,EAAK,IAAI,CAAA,CAAA,EAAI,KAAA,CAAM,UAAU,CAAA,EAAA,EAAK,KAAA,CAAM,OAAO,CAAA,CAAA,GAAK,EAAE,CAAA,CAAE,CAAA;AAAA,EAC1F;AAEA,EAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AACb,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB;AAGA,eAAsB,iBAAA,CACpB,SACA,OAAA,EACiB;AACjB,EAAA,MAAM,QAAkB,CAAC,CAAA,EAAA,EAAK,OAAA,CAAQ,QAAQ,IAAI,EAAE,CAAA;AACpD,EAAA,IAAI,QAAQ,WAAA,EAAa;AACvB,IAAA,KAAA,MAAW,IAAA,IAAQ,OAAA,CAAQ,WAAA,CAAY,KAAA,CAAM,IAAI,GAAG,KAAA,CAAM,IAAA,CAAK,CAAA,EAAA,EAAK,IAAI,CAAA,CAAE,CAAA;AAC1E,IAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AAAA,EACf;AAEA,EAAA,KAAA,MAAW,KAAA,IAAS,QAAQ,OAAA,EAAS;AACnC,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI;AACF,MAAA,IAAA,GAAO,MAAM,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA;AAAA,IACjC,CAAA,CAAA,MAAQ;AAGN,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,KAAA,CAAM,KAAK,CAAA,GAAA,CAAA,EAAO,CAAA,CAAA,EAAI,MAAM,QAAA,CAAS,KAAK,CAAC,CAAA,CAAA,EAAI,CAAA,CAAA,EAAI,CAAA,QAAA,EAAW,KAAA,CAAM,IAAI,CAAA,CAAA,EAAI,CAAA,CAAA,EAAI,KAAK,IAAA,CAAK,IAAA,IAAQ,CAAA,CAAE,CAAA;AAAA,EACtG;AAEA,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB;AAUO,SAAS,eAAA,CAAgB,OAAA,GAAoC,EAAC,EAAW;AAC9E,EAAA,MAAM,GAAA,GAAM,QAAQ,WAAA,IAAe,yBAAA;AACnC,EAAA,OAAO,CAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;AAAA,kCAAA,EAqC2B,GAAG,CAAA;AAAA,6BAAA,EACR,GAAG,CAAA;AAAA,2BAAA,EACL,GAAG,CAAA;AAAA,gDAAA,EACkB,GAAG,CAAA;;AAAA;;AAAA;;AAAA;AAAA,8BAAA,EAOrB,GAAG,CAAA;;AAAA;AAAA;;AAAA;;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA,uBAAA,EAcV,GAAG,CAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA,4BAAA,EASR,GAAG,CAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA,yBAAA,EAeI,GAAG,CAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA,CAAA;AA6E9B;;;ACpMA,SAAS,MAAA,CAAO,KAAU,IAAA,EAAkC;AAC1D,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAA;AAC9B,EAAA,IAAI,MAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,MAAM,CAAC,CAAA;AACxC,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,YAAA,CAAa,KAAU,IAAA,EAAkC;AAChE,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,SAAA,CAAU,IAAI,CAAA;AAChC,EAAA,IAAI,KAAA,KAAU,MAAA,IAAa,KAAA,KAAU,IAAA,EAAM,OAAO,MAAA;AAClD,EAAA,OAAO,KAAA,CAAM,QAAQ,KAAK,CAAA,GAAI,MAAM,CAAC,CAAA,GAAI,OAAO,KAAK,CAAA;AACvD;AAEO,SAAS,YAAA,CAAa,OAAA,GAA0B,EAAC,EAAG;AACzD,EAAA,MAAM,MAAA,GAAS,cAAc,OAAO,CAAA;AACpC,EAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,SAAA,IAAa,eAAA,CAAgB,MAAM,CAAA;AAE7D,EAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AAErB,EAAA,OAAO,SAAS,sBAAA,CAAuB,GAAA,EAAU,GAAA,EAAU,IAAA,EAAkB;AAC3E,IAAA,IAAI,MAAA,CAAO,QAAA,IAAY,CAAC,IAAA,EAAM;AAC5B,MAAA,IAAA,EAAK;AACL,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,MAAA,CAAO,MAAA,EAAO;AACxC,IAAA,IAAI,QAAA,GAAW,KAAA;AACf,IAAA,IAAI,MAAA;AAEJ,IAAA,MAAM,SAAS,MAAY;AACzB,MAAA,IAAI,QAAA,EAAU;AACd,MAAA,QAAA,GAAW,IAAA;AAEX,MAAA,IAAA,CAAK,MAAM;AACT,QAAA,MAAM,QAAA,GAAW,cAAA;AAAA,UACf;AAAA,YACE,MAAA,EAAQ,IAAI,MAAA,IAAU,KAAA;AAAA,YACtB,GAAA,EAAK,GAAA,CAAI,WAAA,IAAe,GAAA,CAAI,GAAA,IAAO,GAAA;AAAA,YACnC,IAAA,EAAM,MAAA,CAAO,GAAA,EAAK,MAAM,CAAA;AAAA,YACxB,UAAU,GAAA,CAAI,MAAA,IAAU,GAAA,CAAI,QAAA,KAAa,UAAU,OAAA,GAAU,MAAA;AAAA,YAC7D,SAAA,EAAW,MAAA,CAAO,GAAA,EAAK,YAAY,CAAA;AAAA,YACnC,SAAS,MAAA,CAAO,GAAA,EAAK,SAAS,CAAA,IAAK,MAAA,CAAO,KAAK,UAAU;AAAA,WAC3D;AAAA,UACA;AAAA,SACF;AAOA,QAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,KAAqB,KAAA;AAC1C,QAAA,MAAM,YAAY,MAAA,CAAO,OAAA,CAAQ,OAAO,MAAA,EAAO,GAAI,SAAS,CAAA,GAAI,GAAA;AAEhE,QAAA,MAAM,KAAA,GAAsB;AAAA,UAC1B,GAAG,QAAA;AAAA,UACH,SAAS,UAAA,EAAW;AAAA,UACpB,QAAQ,MAAA,CAAO,MAAA;AAAA,UACf,UAAA,EAAY,IAAI,IAAA,CAAK,SAAS,EAAE,WAAA,EAAY;AAAA,UAC5C,QAAA,EAAU,WACN,eAAA,CAAgB;AAAA,YACd,QAAQ,GAAA,CAAI,UAAA;AAAA,YACZ,WAAA,EAAa,YAAA,CAAa,GAAA,EAAK,cAAc,CAAA;AAAA,YAC7C,aAAA,EAAe,YAAA,CAAa,GAAA,EAAK,gBAAgB,CAAA;AAAA,YACjD,SAAA;AAAA,YACA,WAAA,EAAa;AAAA,WACd,CAAA,GACD,eAAA,CAAgB,EAAE,SAAA,EAAW,WAAA,EAAa,WAAW,CAAA;AAAA,UACzD,GAAI,MAAA,GAAS,EAAE,KAAA,EAAO,MAAA,KAAW,EAAC;AAAA,UAClC,GAAA,EAAK;AAAA,YACH,IAAA,EAAM,QAAA;AAAA,YACN,OAAA,EAAS,WAAA;AAAA,YACT,OAAA,EAAS,SAAA;AAAA,YACT,SAAS,WAAA;AAAY;AACvB,SACF;AAEA,QAAA,SAAA,CAAU,OAAO,KAAK,CAAA;AAAA,MACxB,CAAC,CAAA;AAAA,IACH,CAAA;AAIA,IAAA,IAAA,CAAK,MAAM;AACT,MAAA,GAAA,CAAI,IAAA,CAAK,UAAU,MAAM,CAAA;AACzB,MAAA,GAAA,CAAI,IAAA,CAAK,SAAS,MAAM,CAAA;AAAA,IAC1B,CAAC,CAAA;AAED,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,IAAA,EAAK;AACL,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,cAAc,GAAA,CAAI,WAAA,IAAe,IAAI,GAAA,IAAO,GAAA,EAAK,OAAO,cAAc,CAAA;AAMnF,IAAA,IAAI,IAAA,CAAK,SAAA,IAAa,eAAA,CAAgB,QAAA,CAAS,IAAa,CAAA,EAAG;AAC7D,MAAA,MAAM,MAAA,GAAA,CAAU,GAAA,CAAI,MAAA,IAAU,KAAA,EAAO,WAAA,EAAY;AACjD,MAAA,IAAI,MAAA,KAAW,KAAA,IAAS,MAAA,KAAW,MAAA,EAAQ;AACzC,QAAA,KAAA,CAAM,YAAY;AAChB,UAAA,IAAI;AACF,YAAA,MAAM,OACJ,IAAA,KAAS,aAAA,GACL,eAAA,EAAgB,GAChB,SAAS,WAAA,GACP,aAAA,CAAc,IAAA,CAAK,SAAU,IAC7B,MAAM,iBAAA,CAAkB,IAAA,CAAK,SAAA,EAAY,KAAK,OAAO,CAAA;AAC7D,YAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,8BAA8B,CAAA;AAC5D,YAAA,GAAA,CAAI,SAAA,CAAU,eAAA,EAAiB,IAAA,CAAK,YAAA,IAAgB,sCAAsC,CAAA;AAC1F,YAAA,GAAA,CAAI,UAAA,GAAa,GAAA;AACjB,YAAA,MAAA,GAAS,EAAE,QAAA,EAAU,QAAA,EAAU,MAAA,EAAQ,SAAA,EAAW,QAAQ,8BAAA,EAA+B;AACzF,YAAA,IAAI,MAAA,KAAW,MAAA,EAAQ,GAAA,CAAI,GAAA,EAAI;AAAA,iBAC1B,GAAA,CAAI,IAAI,IAAI,CAAA;AAAA,UACnB,CAAA,CAAA,MAAQ;AAEN,YAAA,MAAA,GAAS,EAAE,QAAA,EAAU,OAAA,EAAS,MAAA,EAAQ,gBAAA,EAAiB;AACvD,YAAA,IAAI,CAAC,GAAA,CAAI,WAAA,EAAa,IAAA,EAAK;AAAA,UAC7B;AAAA,QACF,CAAA,GAAG;AACH,QAAA;AAAA,MACF;AAAA,IACF;AAOA,IAAA,MAAM,WAAW,UAAA,CAAW;AAAA,MAC1B,MAAA,EAAQ,IAAI,MAAA,IAAU,KAAA;AAAA,MACtB,IAAA;AAAA,MACA,MAAA,EAAQ,MAAA,CAAO,GAAA,EAAK,QAAQ;AAAA,KAC7B,CAAA;AAED,IAAA,IAAI,QAAA,CAAS,WAAW,MAAA,EAAQ;AAC9B,MAAA,IAAI,IAAA,CAAK,cAAc,KAAA,EAAO;AAC5B,QAAA,KAAK,OAAA,CAAQ,OAAA,EAAQ,CAClB,IAAA,CAAK,MAAM,IAAA,CAAK,OAAA,CAAQ,IAAI,CAAC,CAAA,CAC7B,IAAA,CAAK,CAAC,KAAA,KAAU;AACf,UAAA,IAAI,KAAA,IAAS,CAAC,GAAA,CAAI,WAAA,EAAa;AAC7B,YAAA,IAAA,CAAK,MAAM,GAAA,CAAI,SAAA,CAAU,QAAQ,eAAA,CAAgB,IAAI,CAAC,CAAC,CAAA;AACvD,YAAA,MAAA,GAAS,EAAE,QAAA,EAAU,YAAA,EAAc,MAAA,EAAQ,SAAA,EAAU;AAAA,UACvD;AAAA,QACF,CAAC,EACA,KAAA,CAAM,MAAM,MAAS,CAAA,CACrB,OAAA,CAAQ,MAAM,IAAA,EAAM,CAAA;AACvB,QAAA;AAAA,MACF;AACA,MAAA,MAAA,GAAS,EAAE,QAAA,EAAU,cAAA,EAAgB,MAAA,EAAQ,SAAS,MAAA,EAAO;AAC7D,MAAA,IAAA,EAAK;AACL,MAAA;AAAA,IACF;AAEA,IAAA,KAAK,OAAA,CAAQ,OAAA,EAAQ,CAClB,IAAA,CAAK,MAAM,IAAA,CAAK,OAAA,CAAQ,QAAA,CAAS,UAAU,CAAC,CAAA,CAC5C,IAAA,CAAK,CAAC,KAAA,KAAU;AACf,MAAA,IAAI,CAAC,KAAA,EAAO;AACV,QAAA,MAAA,GAAS,EAAE,QAAA,EAAU,cAAA,EAAgB,MAAA,EAAQ,SAAA,EAAU;AACvD,QAAA,IAAA,EAAK;AACL,QAAA;AAAA,MACF;AACA,MAAA,MAAM,KAAA,GAAQ,iBAAA,CAAkB,KAAA,EAAO,QAAA,EAAU,IAAI,CAAA;AACrD,MAAA,MAAA,GAAS;AAAA,QACP,QAAA,EAAU,QAAA;AAAA,QACV,QAAQ,QAAA,CAAS,MAAA;AAAA,QACjB,MAAA,EAAQ,KAAA,CAAM,OAAA,CAAQ,cAAc,CAAA,IAAK;AAAA,OAC3C;AACA,MAAA,KAAA,MAAW,CAAC,MAAM,KAAK,CAAA,IAAK,OAAO,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA,EAAG;AACzD,QAAA,GAAA,CAAI,SAAA,CAAU,MAAM,KAAK,CAAA;AAAA,MAC3B;AACA,MAAA,GAAA,CAAI,aAAa,KAAA,CAAM,MAAA;AAIvB,MAAA,IAAA,CAAK,IAAI,MAAA,IAAU,KAAA,EAAO,aAAY,KAAM,MAAA,MAAY,GAAA,EAAI;AAAA,WACvD,GAAA,CAAI,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA;AAAA,IACzB,CAAC,CAAA,CACA,KAAA,CAAM,MAAM;AAEX,MAAA,MAAA,GAAS,EAAE,QAAA,EAAU,OAAA,EAAS,MAAA,EAAQ,gBAAA,EAAiB;AACvD,MAAA,IAAI,CAAC,GAAA,CAAI,WAAA,EAAa,IAAA,EAAK;AAAA,IAC7B,CAAC,CAAA;AAAA,EACL,CAAA;AACF","file":"chunk-5NZNPRMN.js","sourcesContent":["import type { Twin, TwinOptions } from \"./twin.js\";\n\n/**\n * The three files that let an agent find a site's content without crawling it\n * blindly: `/llms.txt`, `/llms-full.txt` and `/install.md`.\n *\n * ── Why generate them rather than let customers write them ───────────────────\n * A hand-written index is wrong the first time a page is added, and an index\n * that lists pages which no longer exist is worse than none -- an agent spends\n * its budget on 404s and concludes the site is broken. These are derived from\n * the same manifest that serves the twins, so they cannot disagree with what is\n * actually servable.\n *\n * `llms.txt` is the convention proposed by Answer.AI in 2024: a markdown index\n * of a site's high-value content. `llms-full.txt` is the same set with the\n * bodies inline, for an agent that would rather make one request than a hundred.\n */\n\nexport interface TwinEntry {\n /** The page path, e.g. `/docs/intro`. */\n path: string;\n /** One line. This is what an agent reads when deciding whether to fetch. */\n summary?: string;\n title?: string;\n}\n\nexport interface DiscoveryOptions {\n siteName: string;\n /** One or two sentences. Appears under the heading in `llms.txt`. */\n description?: string;\n origin?: string;\n entries: readonly TwinEntry[];\n}\n\nfunction titleFor(entry: TwinEntry): string {\n if (entry.title) return entry.title;\n if (entry.path === \"/\") return \"Home\";\n const last = entry.path.split(\"/\").filter(Boolean).pop() ?? entry.path;\n return last.replace(/[-_]/g, \" \").replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nconst twinPath = (path: string) => (path === \"/\" ? \"/index.md\" : `${path}.md`);\n\n/** The index: every page, one line each. */\nexport function renderLlmsTxt(options: DiscoveryOptions): string {\n const origin = options.origin ?? \"\";\n const lines: string[] = [`# ${options.siteName}`, \"\"];\n\n if (options.description) {\n for (const line of options.description.split(\"\\n\")) lines.push(`> ${line}`);\n lines.push(\"\");\n }\n\n lines.push(\n \"Every page listed here is also available as markdown: append `.md` to the path\",\n \"(`/` becomes `/index.md`), or send `Accept: text/markdown`.\",\n \"\",\n \"## Pages\",\n \"\",\n );\n\n for (const entry of options.entries) {\n const href = `${origin}${twinPath(entry.path)}`;\n lines.push(`- [${titleFor(entry)}](${href})${entry.summary ? `: ${entry.summary}` : \"\"}`);\n }\n\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/** The bulk file: one request instead of a hundred. */\nexport async function renderLlmsFullTxt(\n options: DiscoveryOptions,\n resolve: TwinOptions[\"resolve\"],\n): Promise<string> {\n const parts: string[] = [`# ${options.siteName}`, \"\"];\n if (options.description) {\n for (const line of options.description.split(\"\\n\")) parts.push(`> ${line}`);\n parts.push(\"\");\n }\n\n for (const entry of options.entries) {\n let twin: Twin | null | undefined;\n try {\n twin = await resolve(entry.path);\n } catch {\n // ⚠️ A resolver that throws on one page must not empty the whole file.\n // Skipping the page is a smaller lie than serving an index of nothing.\n continue;\n }\n if (!twin) continue;\n parts.push(`---`, ``, `## ${titleFor(entry)}`, ``, `Source: ${entry.path}`, ``, twin.body.trim(), ``);\n }\n\n return parts.join(\"\\n\");\n}\n\n/**\n * The install guide, written for an AI coding agent rather than a person.\n *\n * ⚠️ This is the install path for a product whose users install things by\n * asking a model. It is deliberately imperative, names the exact failure modes,\n * and tells the agent to STOP rather than guess at a missing value -- because an\n * agent that guesses a credential writes a broken config that looks finished.\n */\nexport function renderInstallMd(options: { packageName?: string } = {}): string {\n const pkg = options.packageName ?? \"@entreprenoid/analytics\";\n return `# entreprenoid install guide (for AI coding agents)\n\nYou are installing **entreprenoid** — analytics for the agentic web. It records every\nrequest a site serves, especially from AI agents, and can serve those agents a clean\nmarkdown twin of each page. Collection is fire-and-forget: it never blocks and never\nfails the user's responses.\n\n## Before you start\n\nEvery environment variable this package reads:\n\n| Variable | Required | What it is |\n| --- | --- | --- |\n| \\`ENTREPRENOID_INGEST_URL\\` | **yes** | The ingest endpoint. ⚠️ It must end in \\`/v1/events\\` — the SDK POSTs this URL verbatim and appends nothing, so a bare origin sends every batch to \\`POST /\\`, which is rejected. The SDK fails open, so you would see nothing at all. |\n| \\`ENTREPRENOID_SERVER_KEY\\` | **yes** | The site's key, starting with \\`ep_live_server_\\`. |\n| \\`ENTREPRENOID_SITE_ID\\` | no | The dashboard shows it beside the key. Safe to set, safe to omit: the server resolves the site from the credential and ignores this value. |\n| \\`ENTREPRENOID_ENABLED\\` | no | \\`false\\` makes collection a transparent no-op without removing any code. Useful per environment. |\n| \\`ENTREPRENOID_DEBUG\\` | no | \\`true\\` prints one startup line saying whether collection is on, and why not if it is off. |\n\n**If either required value is missing, STOP and ask.** Do not invent them, and do not\nleave a placeholder — a config that looks complete and is not is worse than no config.\n\nSafety rules that apply throughout:\n\n- **Never hardcode the key**, never print it, never commit it. Environment variables only.\n- Before writing an env file, make sure it is gitignored.\n- ⚠️ **Never prefix the key with \\`NEXT_PUBLIC_\\`, \\`VITE_\\`, \\`PUBLIC_\\` or \\`REACT_APP_\\`.**\n Those prefixes inline a value into client-side JavaScript, which publishes the key to\n everyone who loads the page. The SDK refuses to start if it finds one.\n- Only touch the files this install needs.\n\n## Step 1 — install the package\n\nDetect the package manager from the lockfile:\n\n| Lockfile | Command |\n| --- | --- |\n| \\`pnpm-lock.yaml\\` | \\`pnpm add ${pkg}\\` |\n| \\`yarn.lock\\` | \\`yarn add ${pkg}\\` |\n| \\`bun.lock\\` | \\`bun add ${pkg}\\` |\n| \\`package-lock.json\\` or none | \\`npm install ${pkg}\\` |\n\n## Step 2 — wire up the collector (pick exactly ONE)\n\n### Express\n\n\\`\\`\\`ts\nimport { entreprenoid } from \"${pkg}/express\";\n\napp.use(entreprenoid());\n\\`\\`\\`\n\nAdd it **before** your routes so it observes all of them.\n\n### Next.js (App Router, 14+)\n\nUse the **Next adapter**, not the web one. In \\`proxy.ts\\` at the project root\n(\\`middleware.ts\\` on Next 15 and earlier — same file, renamed in Next 16):\n\n\\`\\`\\`ts\nimport { after } from \"next/server\";\nimport { proxy } from \"${pkg}/next\";\n\nexport default proxy({ after });\n\nexport const config = {\n matcher: [\"/((?!_next/static|_next/image|favicon.ico).*)\"],\n};\n\\`\\`\\`\n\n⚠️ **Do not use \\`${pkg}/web\\` in a Next proxy.** It runs and it lies. A proxy\nexecutes *before* the route and hands control onward with a sentinel response — status\n200, no real content type — so the web adapter would record a **measured 200 for every\nrequest**, including the ones your routes render as 404 or 500.\n\nThe Next adapter emits only what a proxy can actually know, and **omits the response\nentirely** rather than guessing at it. Your dashboard will show those requests with no\nstatus, which is the truth: nothing observed one.\n\n⚠️ **Pass \\`after\\`.** Without it the collector relies on its own timer, and a serverless\ninvocation can be frozen before that timer fires — events are simply lost, silently.\n\n### Web-standard runtimes (Cloudflare Workers, Deno, Bun, Hono)\n\n\\`\\`\\`ts\nimport { observe } from \"${pkg}/web\";\n\nexport default {\n fetch: observe(handler, { waitUntil: (p) => ctx.waitUntil(p) }),\n};\n\\`\\`\\`\n\nPass \\`waitUntil\\` where the runtime offers one, or a serverless invocation can be\nfrozen before the events are sent.\n\n## Step 3 — optional: serve a markdown twin\n\nAgents pay for every token they read, and most of a modern page is markup they do not\nwant. Supply a resolver and the same middleware will serve clean markdown when a client\nasks for it:\n\n\\`\\`\\`ts\napp.use(entreprenoid({\n twin: { resolve: (path) => markdownFor(path) },\n}));\n\\`\\`\\`\n\n⚠️ **A browser never receives markdown.** The twin is served only when the path ends in\n\\`.md\\` or the \\`Accept\\` header explicitly prefers \\`text/markdown\\` — never based on the\nUser-Agent, which would be cloaking and would break shared caching.\n\n## Step 4 — ⚠️ look at the routes before you go live\n\n**Do not skip this one.** The path is sent as it arrives. The query string is dropped\nbefore anything parses it, and the \\`Referer\\` is reduced to an origin — but the path\nitself is data, and on a lot of sites the path carries secrets:\n\n\\`\\`\\`\n/reveal/<single-use-token> /join/<invite-code>\n/confirm/<token> /upload/<ticket>\n\\`\\`\\`\n\nRead the project's routes. For each one, decide:\n\n\\`\\`\\`ts\napp.use(entreprenoid({\n // Collapse identifiers so analytics never sees a per-user value, and so one\n // route does not become ten thousand rows.\n routeTemplate: (path) => path.replace(/\\\\/\\\\d+(?=\\\\/|$)/g, \"/:id\"),\n\n // Replace any segment that is a token, code or personal identifier.\n redactPatterns: [/^[A-Za-z0-9_-]{20,}$/],\n\n // Traffic you do not want counted: health checks, your own office, previews.\n isInternal: (req) => req.path.startsWith(\"/_health\"),\n}));\n\\`\\`\\`\n\n⚠️ **A default backstop already runs, and you should not rely on it.** Segments that\nlook like credentials — uuids, cuids, JWTs, long hex, dense mixed-case strings — are\nreplaced with \\`[redacted]\\` before the event is sent, and the event records that it\nhappened. It cannot catch a short token like \\`/j/aB3xK9\\`, and it does not know which of\nthis project's ids are sensitive. **Only the routes tell you that.** Set\n\\`redactHighEntropyPaths: false\\` to turn the backstop off; that never disables\n\\`redactPatterns\\`, which are yours.\n\nIf you are unsure whether a path segment is a secret, treat it as one and say so in your\nsummary to the user.\n\n## Step 5 — verify\n\nStart the app and make one request. Within a few seconds the dashboard should show it.\nIf nothing arrives:\n\n- check the key is set in the server's environment, not the client's\n- check \\`ENTREPRENOID_INGEST_URL\\` ends in \\`/v1/events\\`\n- set \\`ENTREPRENOID_DEBUG=true\\` and read the startup line\n\n**Do not add retry logic, queues or error handling around the SDK.** It already buffers,\nretries with backoff, and fails open. Wrapping it in a try/catch is harmless; awaiting it\nis not, and would put analytics on your critical path.\n`;\n}\n","import type { RequestEvent } from \"@entreprenoid/event-schema\";\nimport { createCollector, type Collector } from \"./core/collector.js\";\nimport { resolveConfig, type EntreprenoidConfig } from \"./core/config.js\";\nimport { safe } from \"./core/safe.js\";\nimport { normalisePath, observeRequest } from \"./observe/request.js\";\nimport { observeResponse } from \"./observe/response.js\";\nimport { SDK_NAME, SDK_VERSION, newEventId, runtimeName } from \"./runtime.js\";\nimport {\n renderInstallMd,\n renderLlmsFullTxt,\n renderLlmsTxt,\n} from \"./serve/discovery.js\";\nimport {\n advertiseHeader,\n buildTwinResponse,\n decideTwin,\n DISCOVERY_PATHS,\n type TwinOptions,\n} from \"./serve/twin.js\";\n\n/**\n * Express middleware.\n *\n * app.use(entreprenoid());\n *\n * ── How it observes a completed response without touching it ─────────────────\n * By listening, not by wrapping. `res` is a writable stream and the tempting\n * approach -- monkey-patching `res.write` and `res.end` to inspect what goes\n * out -- is wrong three ways: it adds a function call to every chunk, it holds\n * the bytes in memory, and any mistake in re-emitting them corrupts the\n * customer's response. Everything we need is already in the headers by the time\n * the response finishes.\n *\n * ⚠️ **`finish` and `close`, with a latch.** `finish` fires when the response\n * completed. `close` fires when the socket went away, which for an aborted\n * request is the ONLY event that fires -- and an aborted request is exactly the\n * kind an AI client makes when it has read enough. Listening to `finish` alone\n * silently loses them. Both are registered and a latch makes sure one event is\n * recorded, not two.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any -- these are Express's\n types without taking a dependency on Express. The adapter must compile for a\n customer who has express installed and for one who does not. */\ntype Req = {\n method?: string;\n originalUrl?: string;\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n protocol?: string;\n secure?: boolean;\n [key: string]: any;\n};\ntype Res = {\n statusCode: number;\n writableFinished?: boolean;\n getHeader(name: string): number | string | string[] | undefined;\n once(event: string, listener: () => void): unknown;\n [key: string]: any;\n};\ntype Next = (error?: unknown) => void;\n\nexport interface ExpressOptions extends EntreprenoidConfig {\n collector?: Collector;\n /**\n * Serve a markdown twin. Omit it and this middleware is observation-only,\n * byte-for-byte as before -- the serve half is opt-in, because it is the half\n * that can change what a visitor receives.\n */\n twin?: TwinOptions;\n}\n\n\nfunction header(req: Req, name: string): string | undefined {\n const value = req.headers[name];\n if (Array.isArray(value)) return value[0];\n return value;\n}\n\nfunction headerString(res: Res, name: string): string | undefined {\n const value = res.getHeader(name);\n if (value === undefined || value === null) return undefined;\n return Array.isArray(value) ? value[0] : String(value);\n}\n\nexport function entreprenoid(options: ExpressOptions = {}) {\n const config = resolveConfig(options);\n const collector = options.collector ?? createCollector(config);\n\n const twin = options.twin;\n\n return function entreprenoidMiddleware(req: Req, res: Res, next: Next): void {\n if (config.disabled && !twin) {\n next();\n return;\n }\n\n const startedAt = Date.now();\n const startedHr = process.hrtime.bigint();\n let recorded = false;\n let served: RequestEvent[\"serve\"];\n\n const record = (): void => {\n if (recorded) return;\n recorded = true;\n\n safe(() => {\n const observed = observeRequest(\n {\n method: req.method ?? \"GET\",\n url: req.originalUrl ?? req.url ?? \"/\",\n host: header(req, \"host\"),\n protocol: req.secure || req.protocol === \"https\" ? \"https\" : \"http\",\n userAgent: header(req, \"user-agent\"),\n referer: header(req, \"referer\") ?? header(req, \"referrer\"),\n },\n config,\n );\n\n // ⚠️ If the response did not finish, we did not measure it. Reporting a\n // status we never saw the client receive would put a guess in the same\n // column as a measurement, which is the one thing this product must not\n // do. An aborted request is recorded with `observation: \"unknown\"` --\n // still counted, honestly labelled.\n const finished = res.writableFinished !== false;\n const latencyMs = Number(process.hrtime.bigint() - startedHr) / 1e6;\n\n const event: RequestEvent = {\n ...observed,\n eventId: newEventId(),\n siteId: config.siteId,\n observedAt: new Date(startedAt).toISOString(),\n response: finished\n ? observeResponse({\n status: res.statusCode,\n contentType: headerString(res, \"content-type\"),\n contentLength: headerString(res, \"content-length\"),\n latencyMs,\n observation: \"measured\",\n })\n : observeResponse({ latencyMs, observation: \"unknown\" }),\n ...(served ? { serve: served } : {}),\n sdk: {\n name: SDK_NAME,\n version: SDK_VERSION,\n adapter: \"express\",\n runtime: runtimeName(),\n },\n };\n\n collector.record(event);\n });\n };\n\n // Registered before next(), so a synchronous handler that responds\n // immediately is still observed.\n safe(() => {\n res.once(\"finish\", record);\n res.once(\"close\", record);\n });\n\n if (!twin) {\n next();\n return;\n }\n\n const path = normalisePath(req.originalUrl ?? req.url ?? \"/\", config.redactPatterns);\n\n // ── Discovery, before anything else ──────────────────────────────────────\n // These are the three files that let an agent find the content at all, and\n // they are cheap: one is a constant, two come from the manifest already in\n // memory.\n if (twin.discovery && DISCOVERY_PATHS.includes(path as never)) {\n const method = (req.method ?? \"GET\").toUpperCase();\n if (method === \"GET\" || method === \"HEAD\") {\n void (async () => {\n try {\n const body =\n path === \"/install.md\"\n ? renderInstallMd()\n : path === \"/llms.txt\"\n ? renderLlmsTxt(twin.discovery!)\n : await renderLlmsFullTxt(twin.discovery!, twin.resolve);\n res.setHeader(\"content-type\", \"text/markdown; charset=utf-8\");\n res.setHeader(\"cache-control\", twin.cacheControl ?? \"public, max-age=3600, s-maxage=86400\");\n res.statusCode = 200;\n served = { decision: \"served\", reason: \"md_path\", format: \"text/markdown; charset=utf-8\" };\n if (method === \"HEAD\") res.end();\n else res.end(body);\n } catch {\n // Their site must still work.\n served = { decision: \"error\", reason: \"resolver_error\" };\n if (!res.headersSent) next();\n }\n })();\n return;\n }\n }\n\n // ── The serve half ───────────────────────────────────────────────────────\n // ⚠️ Everything below can change what the visitor receives, so every exit\n // leads to `next()` unless a twin was actually written. A throw, a rejected\n // promise, a missing twin: all fall through to the customer's own handler\n // with the response untouched.\n const decision = decideTwin({\n method: req.method ?? \"GET\",\n path,\n accept: header(req, \"accept\"),\n });\n\n if (decision.action === \"pass\") {\n if (twin.advertise !== false) {\n void Promise.resolve()\n .then(() => twin.resolve(path))\n .then((found) => {\n if (found && !res.headersSent) {\n safe(() => res.setHeader(\"link\", advertiseHeader(path)));\n served = { decision: \"advertised\", reason: \"no_twin\" };\n }\n })\n .catch(() => undefined)\n .finally(() => next());\n return;\n }\n served = { decision: \"fell_through\", reason: decision.reason };\n next();\n return;\n }\n\n void Promise.resolve()\n .then(() => twin.resolve(decision.lookupPath))\n .then((found) => {\n if (!found) {\n served = { decision: \"fell_through\", reason: \"no_twin\" };\n next();\n return;\n }\n const built = buildTwinResponse(found, decision, twin);\n served = {\n decision: \"served\",\n reason: decision.reason,\n format: built.headers[\"content-type\"] ?? \"text/markdown\",\n };\n for (const [name, value] of Object.entries(built.headers)) {\n res.setHeader(name, value);\n }\n res.statusCode = built.status;\n // HEAD asks for the headers of what a GET would return, and nothing\n // else. Writing a body to it is a protocol violation some clients\n // handle by hanging.\n if ((req.method ?? \"GET\").toUpperCase() === \"HEAD\") res.end();\n else res.end(built.body);\n })\n .catch(() => {\n // The customer's resolver threw. Their site must still work.\n served = { decision: \"error\", reason: \"resolver_error\" };\n if (!res.headersSent) next();\n });\n };\n}\n"]}
@@ -0,0 +1,114 @@
1
+ import { resolveConfig, createCollector, decideTwin, normalisePath, buildTwinResponse, safe, observeRequest, runtimeName, observeResponse, newEventId, SDK_VERSION, SDK_NAME } from './chunk-IHZCOC2U.js';
2
+
3
+ // src/next.ts
4
+ function proxy(options = {}) {
5
+ const config = resolveConfig(options);
6
+ const collector = options.collector ?? createCollector(config);
7
+ return async (request) => {
8
+ const startedAt = Date.now();
9
+ let served;
10
+ let twinResponse;
11
+ if (options.twin) {
12
+ try {
13
+ const url = new URL(request.url);
14
+ const decision = decideTwin({
15
+ method: request.method,
16
+ path: normalisePath(url.pathname),
17
+ accept: request.headers.get("accept")
18
+ });
19
+ if (decision.action === "serve") {
20
+ const found = await options.twin.resolve(decision.lookupPath);
21
+ if (found) {
22
+ const built = buildTwinResponse(found, decision, options.twin);
23
+ served = {
24
+ decision: "served",
25
+ reason: decision.reason,
26
+ format: built.headers["content-type"] ?? "text/markdown"
27
+ };
28
+ twinResponse = new Response(
29
+ request.method.toUpperCase() === "HEAD" ? null : built.body,
30
+ { status: built.status, headers: built.headers }
31
+ );
32
+ } else {
33
+ served = { decision: "fell_through", reason: "no_twin" };
34
+ }
35
+ } else {
36
+ served = { decision: "fell_through", reason: decision.reason };
37
+ }
38
+ } catch {
39
+ served = { decision: "error", reason: "resolver_error" };
40
+ }
41
+ }
42
+ const record = () => {
43
+ safe(() => {
44
+ if (config.disabled) return;
45
+ const url = new URL(request.url);
46
+ const observed = observeRequest(
47
+ {
48
+ method: request.method,
49
+ url: url.pathname + url.search,
50
+ host: url.host,
51
+ protocol: url.protocol === "https:" ? "https" : "http",
52
+ userAgent: request.headers.get("user-agent") ?? void 0,
53
+ referer: request.headers.get("referer") ?? void 0
54
+ },
55
+ config
56
+ );
57
+ const event = {
58
+ ...observed,
59
+ eventId: newEventId(),
60
+ // ⚠️ Emitted so a post-response observation CAN be merged later.
61
+ // Nothing merges it today, and the docblock above says so plainly
62
+ // rather than letting the field imply otherwise.
63
+ requestId: newEventId(),
64
+ siteId: config.siteId,
65
+ observedAt: new Date(startedAt).toISOString(),
66
+ ...served ? { serve: served } : {},
67
+ // ⚠️ **The response object is present ONLY when we built it.**
68
+ //
69
+ // If the twin was served, this proxy IS the responder and measured it.
70
+ // Otherwise the route has not run yet and there is nothing to
71
+ // observe — so the key is OMITTED, not set to a guess, not set to
72
+ // `observation: "unknown"` with a status (which the schema refuses),
73
+ // and not set to a latency-only object (we did not wait for the
74
+ // response, so we did not measure a latency either).
75
+ ...twinResponse ? {
76
+ response: observeResponse({
77
+ status: twinResponse.status,
78
+ contentType: twinResponse.headers.get("content-type"),
79
+ contentLength: twinResponse.headers.get("content-length"),
80
+ latencyMs: Date.now() - startedAt,
81
+ observation: "measured"
82
+ })
83
+ } : {},
84
+ sdk: {
85
+ name: SDK_NAME,
86
+ version: SDK_VERSION,
87
+ // ⚠️ `next-proxy`, which is the name the WIRE SCHEMA already uses
88
+ // in its own docblock (`event.ts:157`). Its own name matters here
89
+ // more than for any other adapter: this is the one whose events
90
+ // legitimately carry no response, and the dashboard must not
91
+ // present an adapter's blind spot as a fact about the traffic.
92
+ adapter: "next-proxy",
93
+ runtime: runtimeName()
94
+ }
95
+ };
96
+ collector.record(event);
97
+ });
98
+ };
99
+ if (options.after) {
100
+ try {
101
+ options.after(record);
102
+ } catch {
103
+ record();
104
+ }
105
+ } else {
106
+ record();
107
+ }
108
+ return twinResponse;
109
+ };
110
+ }
111
+
112
+ export { proxy };
113
+ //# sourceMappingURL=chunk-EVNUKN5A.js.map
114
+ //# sourceMappingURL=chunk-EVNUKN5A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next.ts"],"names":[],"mappings":"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","file":"chunk-EVNUKN5A.js","sourcesContent":["import type { RequestEvent } from \"@entreprenoid/event-schema\";\nimport { createCollector, type Collector } from \"./core/collector.js\";\nimport { resolveConfig, type EntreprenoidConfig } from \"./core/config.js\";\nimport { safe } from \"./core/safe.js\";\nimport { observeRequest, normalisePath } from \"./observe/request.js\";\nimport { observeResponse } from \"./observe/response.js\";\nimport { newEventId, runtimeName, SDK_NAME, SDK_VERSION } from \"./runtime.js\";\nimport { advertiseHeader, buildTwinResponse, decideTwin, type TwinOptions } from \"./serve/twin.js\";\n\n/**\n * The Next.js adapter, for `proxy.ts` (called `middleware.ts` before Next 16).\n *\n * ── ⚠️ Why this exists rather than \"just use ./web\" ──────────────────────────\n * Because `./web` produces FALSE DATA in a Next proxy, and does it silently.\n *\n * A proxy runs **before the route**. It hands control onward by returning\n * `NextResponse.next()` — a sentinel with status 200, no real content-type and\n * no content-length. `./web` reads that sentinel and records\n * `{ status: 200, observation: \"measured\" }`, so **every event claims a\n * measured 200**, including requests the route renders as 404 or 500.\n *\n * ⚠️ **Our own schema already said this was wrong and refused it**:\n * `observation: \"unknown\"` may not carry a status, and the Next case is the one\n * where the `response` object is absent entirely — \"we never had a chance to\n * look\". Correct behaviour, specified, enforced by a `superRefine`, and until\n * now unreachable because no adapter emitted it. That reads as done, which is\n * worse than reading as missing.\n *\n * ── What this emits ──────────────────────────────────────────────────────────\n * Exactly what a proxy genuinely knows — method, path, host, user agent,\n * referrer origin, campaign — and **no `response` object at all**, unless it\n * served the twin itself, in which case it made the response and can measure it\n * honestly.\n *\n * ── ⚠️ The requestId merge is DESIGNED and NOT BUILT ─────────────────────────\n * `requestId` rides every event so a later post-response observation can be\n * merged onto it. **Nothing performs that merge today** — measured: nothing in\n * the worker or the database reads the field. Next offers no general\n * post-response hook (`onRequestError` fires only on errors; `after()` still\n * runs in the proxy, which cannot see the route's response), so the honest\n * second observation point is a wrapper around each route handler — a second\n * install step, easy to forget, and a half-installed product reports half its\n * traffic.\n *\n * So this ships the single honest observation. That is not a degraded event: a\n * proxy cannot see a response, and saying so precisely is the product's\n * differentiator rather than a shortfall.\n *\n * ── ⚠️ `after` is an OPTION, not an import ───────────────────────────────────\n * There is no `ctx.waitUntil` in a Next proxy; the equivalent is `after()` from\n * `next/server`. Importing it here — even dynamically — would put a bare\n * specifier in a package whose whole discipline is zero runtime dependencies,\n * and a dynamic `import()` slips past `check-sdk-artifact.mjs`'s regex, which\n * would be a gate quietly stopping working. So the customer passes it, exactly\n * as `observe()` already takes `waitUntil`:\n *\n * ```ts\n * import { after } from \"next/server\";\n * import { proxy } from \"@entreprenoid/analytics/next\";\n *\n * export default proxy({ after });\n * export const config = { matcher: [\"/((?!_next/static).*)\"] };\n * ```\n *\n * Without it the collector falls back to its own 2s timer, which a serverless\n * deployment can freeze before it fires.\n */\n\nexport interface ProxyOptions extends EntreprenoidConfig {\n /**\n * `after` from `next/server`. Passed rather than imported — see the docblock.\n * Omitting it is supported and costs reliability on serverless.\n */\n after?: (task: () => void | Promise<void>) => void;\n /** For tests, and for a host that already has a collector. */\n collector?: Collector;\n /**\n * Serve a markdown twin from the proxy.\n *\n * ⚠️ A proxy is a GOOD place for this, unlike the observation half: it runs\n * before the route, so returning the twin short-circuits rendering entirely\n * and the agent never pays for a React tree it discards. And because the\n * proxy built that response itself, the event can honestly say `measured`.\n */\n twin?: TwinOptions;\n}\n\n/**\n * ⚠️ Returns `undefined` to continue, rather than `NextResponse.next()`.\n *\n * Next treats a void return as \"carry on\", which means this adapter never has\n * to import `next/server` at all. It also removes the exact sentinel that made\n * `./web` lie here: there is no fake 200 to accidentally observe.\n */\nexport type ProxyResult = Response | undefined;\n\nexport function proxy(options: ProxyOptions = {}): (request: Request) => Promise<ProxyResult> {\n const config = resolveConfig(options);\n const collector = options.collector ?? createCollector(config);\n\n return async (request: Request): Promise<ProxyResult> => {\n // ⚠️ The clock starts OUTSIDE the try, exactly as in `./web`: if observation\n // setup throws, the request must still reach the route.\n const startedAt = Date.now();\n let served: RequestEvent[\"serve\"];\n let twinResponse: Response | undefined;\n\n // ── The serve half, before the route ─────────────────────────────────────\n // Every failure falls through to the customer's application unchanged.\n if (options.twin) {\n try {\n const url = new URL(request.url);\n const decision = decideTwin({\n method: request.method,\n path: normalisePath(url.pathname),\n accept: request.headers.get(\"accept\"),\n });\n\n if (decision.action === \"serve\") {\n const found = await options.twin.resolve(decision.lookupPath);\n if (found) {\n const built = buildTwinResponse(found, decision, options.twin);\n served = {\n decision: \"served\",\n reason: decision.reason,\n format: built.headers[\"content-type\"] ?? \"text/markdown\",\n };\n twinResponse = new Response(\n request.method.toUpperCase() === \"HEAD\" ? null : built.body,\n { status: built.status, headers: built.headers },\n );\n } else {\n served = { decision: \"fell_through\", reason: \"no_twin\" };\n }\n } else {\n served = { decision: \"fell_through\", reason: decision.reason };\n }\n } catch {\n served = { decision: \"error\", reason: \"resolver_error\" };\n }\n }\n\n // ⚠️ Advertising is deliberately NOT done here. It would mean appending a\n // `link` header to a response this proxy does not have — the route builds\n // it later. `./express` advertises because it sees the real response.\n // Claiming a twin on a response we cannot touch would be a header nobody\n // receives.\n\n const record = (): void => {\n safe(() => {\n if (config.disabled) return;\n\n const url = new URL(request.url);\n const observed = observeRequest(\n {\n method: request.method,\n url: url.pathname + url.search,\n host: url.host,\n protocol: url.protocol === \"https:\" ? \"https\" : \"http\",\n userAgent: request.headers.get(\"user-agent\") ?? undefined,\n referer: request.headers.get(\"referer\") ?? undefined,\n },\n config,\n );\n\n const event: RequestEvent = {\n ...observed,\n eventId: newEventId(),\n // ⚠️ Emitted so a post-response observation CAN be merged later.\n // Nothing merges it today, and the docblock above says so plainly\n // rather than letting the field imply otherwise.\n requestId: newEventId(),\n siteId: config.siteId,\n observedAt: new Date(startedAt).toISOString(),\n ...(served ? { serve: served } : {}),\n // ⚠️ **The response object is present ONLY when we built it.**\n //\n // If the twin was served, this proxy IS the responder and measured it.\n // Otherwise the route has not run yet and there is nothing to\n // observe — so the key is OMITTED, not set to a guess, not set to\n // `observation: \"unknown\"` with a status (which the schema refuses),\n // and not set to a latency-only object (we did not wait for the\n // response, so we did not measure a latency either).\n ...(twinResponse\n ? {\n response: observeResponse({\n status: twinResponse.status,\n contentType: twinResponse.headers.get(\"content-type\"),\n contentLength: twinResponse.headers.get(\"content-length\"),\n latencyMs: Date.now() - startedAt,\n observation: \"measured\",\n }),\n }\n : {}),\n sdk: {\n name: SDK_NAME,\n version: SDK_VERSION,\n // ⚠️ `next-proxy`, which is the name the WIRE SCHEMA already uses\n // in its own docblock (`event.ts:157`). Its own name matters here\n // more than for any other adapter: this is the one whose events\n // legitimately carry no response, and the dashboard must not\n // present an adapter's blind spot as a fact about the traffic.\n adapter: \"next-proxy\",\n runtime: runtimeName(),\n },\n };\n\n collector.record(event);\n });\n };\n\n // ⚠️ `after` runs the recording once the response is on its way, so nothing\n // here is on the path of the request. Without it, `record()` is still\n // synchronous and still non-blocking — `safe()` and the bounded queue see\n // to that — it is only the eventual FLUSH that risks being frozen.\n if (options.after) {\n try {\n options.after(record);\n } catch {\n // A hook that throws is not a reason to lose the event.\n record();\n }\n } else {\n record();\n }\n\n return twinResponse;\n };\n}\n\n/** Advertise a twin from a route handler, where the response actually exists. */\nexport { advertiseHeader };\n"]}