@caprail-dev/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 ADDED
@@ -0,0 +1,65 @@
1
+ # @caprail-dev/analytics
2
+
3
+ Edge collector for [Caprail](https://caprail.dev) — see which AI agents (Claude
4
+ Code, Codex, ChatGPT, Gemini, Perplexity, …) access your site, in real time.
5
+
6
+ One install, framework adapters as subpath exports. The collector only classifies
7
+ nothing locally and beacons each request to your Caprail ingest endpoint; the
8
+ server re-classifies the `User-Agent` authoritatively.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm i @caprail-dev/analytics
14
+ ```
15
+
16
+ Set `CAPRAIL_INGEST_URL` and `CAPRAIL_INGEST_KEY` (your site's `cap_live_…` key,
17
+ from `/dashboard/sites`) in your environment, or pass them explicitly.
18
+
19
+ ## Next.js
20
+
21
+ ```ts
22
+ // middleware.ts
23
+ import { createCaprailMiddleware } from "@caprail-dev/analytics/next";
24
+
25
+ export const middleware = createCaprailMiddleware(); // reads env, or pass { url, key }
26
+ export const config = {
27
+ matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
28
+ };
29
+ ```
30
+
31
+ Middleware runs *before* the response, so it cannot see the final `status` /
32
+ `Content-Type` — the markdown/html grade falls back to path inference.
33
+
34
+ ## Cloudflare Worker
35
+
36
+ ```ts
37
+ // worker.ts
38
+ import { withCaprail } from "@caprail-dev/analytics/cloudflare";
39
+
40
+ export default withCaprail({
41
+ async fetch(request) {
42
+ return fetch(request); // your origin
43
+ },
44
+ });
45
+ ```
46
+
47
+ The Worker wraps the real `Response`, so it reports the authoritative status,
48
+ content-type, and latency.
49
+
50
+ ## Exports
51
+
52
+ | Subpath | Export |
53
+ | -------------- | ------------------------- |
54
+ | `.` | `classify`, `collect`, `resolveConfig`, types |
55
+ | `./next` | `createCaprailMiddleware` |
56
+ | `./cloudflare` | `withCaprail` |
57
+
58
+ > Adapters for TanStack Start and Node (Express/Hono) are planned.
59
+
60
+ ## Publishing
61
+
62
+ `exports` currently points at TypeScript source so the Caprail monorepo can
63
+ dogfood the package with no build step (`transpilePackages`). Before publishing
64
+ to npm, run `bun run build` (emits `dist/`) and switch the `exports` map to the
65
+ compiled `./dist/*.js` + `./dist/*.d.ts` entries.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The beacon — the framework-agnostic half of the collector. Builds the event
3
+ * payload and POSTs it to the Caprail ingest API. The server re-classifies the
4
+ * `userAgent` authoritatively, so the collector deliberately sends only raw
5
+ * request facts and never its own classification (which would be spoofable).
6
+ */
7
+ /**
8
+ * A single collected request, matching the ingest API's accepted shape. Only
9
+ * `timestamp`/`method`/`path`/`status` are required; the rest are best-effort
10
+ * and depend on what the host framework can observe (e.g. middleware cannot see
11
+ * the final `status` / `contentType`; a Cloudflare Worker can).
12
+ */
13
+ export type CaprailEvent = {
14
+ /** ISO-8601 event time (collector clock). */
15
+ timestamp: string;
16
+ method: string;
17
+ /** Pathname; the server strips any querystring defensively. */
18
+ path: string;
19
+ status: number;
20
+ latencyMs?: number;
21
+ userAgent?: string;
22
+ contentType?: string;
23
+ country?: string;
24
+ ip?: string;
25
+ };
26
+ /**
27
+ * POST a batch of events to the ingest endpoint. Fire-and-forget: uses
28
+ * `keepalive` so it survives the request lifecycle and swallows every error —
29
+ * collection must never surface to (or block) the end user. Pass the result to
30
+ * `ev.waitUntil(...)` / `ctx.waitUntil(...)` so the runtime keeps it alive.
31
+ */
32
+ export declare function sendBeacon(url: string, key: string, events: CaprailEvent[]): Promise<void>;
33
+ //# sourceMappingURL=beacon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beacon.d.ts","sourceRoot":"","sources":["../src/beacon.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,6CAA6C;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,YAAY,EAAE,GACrB,OAAO,CAAC,IAAI,CAAC,CAYf"}
package/dist/beacon.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The beacon — the framework-agnostic half of the collector. Builds the event
3
+ * payload and POSTs it to the Caprail ingest API. The server re-classifies the
4
+ * `userAgent` authoritatively, so the collector deliberately sends only raw
5
+ * request facts and never its own classification (which would be spoofable).
6
+ */
7
+ /**
8
+ * POST a batch of events to the ingest endpoint. Fire-and-forget: uses
9
+ * `keepalive` so it survives the request lifecycle and swallows every error —
10
+ * collection must never surface to (or block) the end user. Pass the result to
11
+ * `ev.waitUntil(...)` / `ctx.waitUntil(...)` so the runtime keeps it alive.
12
+ */
13
+ export function sendBeacon(url, key, events) {
14
+ return fetch(url, {
15
+ method: "POST",
16
+ keepalive: true,
17
+ headers: {
18
+ "content-type": "application/json",
19
+ authorization: `Bearer ${key}`,
20
+ },
21
+ body: JSON.stringify({ events }),
22
+ })
23
+ .then(() => undefined)
24
+ .catch(() => undefined);
25
+ }
26
+ //# sourceMappingURL=beacon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beacon.js","sourceRoot":"","sources":["../src/beacon.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsBH;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,GAAW,EACX,GAAW,EACX,MAAsB;IAEtB,OAAO,KAAK,CAAC,GAAG,EAAE;QAChB,MAAM,EAAE,MAAM;QACd,SAAS,EAAE,IAAI;QACf,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,aAAa,EAAE,UAAU,GAAG,EAAE;SAC/B;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;KACjC,CAAC;SACC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;SACrB,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Agent classifier — the single source of truth shared by the Caprail Node
3
+ * ingest API and every edge collector adapter. Pure, dependency-free, and
4
+ * runtime-agnostic (no `db`, no Node-only APIs) so it runs unchanged in Node,
5
+ * the Edge runtime, and Cloudflare Workers.
6
+ */
7
+ /**
8
+ * One of the `agent_type` values. Mirrors the `agent_type` pgEnum in the Caprail
9
+ * app schema (`src/server/db/schema.ts`) — keep the two lists in sync.
10
+ */
11
+ export type AgentType = "crawler" | "fetcher" | "browser" | "search" | "human";
12
+ export type Classification = {
13
+ /** e.g. "ClaudeBot"; null = human / unknown. */
14
+ name: string | null;
15
+ /** e.g. "Anthropic"; null = human / unknown. */
16
+ vendor: string | null;
17
+ type: AgentType;
18
+ isAgent: boolean;
19
+ };
20
+ /**
21
+ * Classify a request by its `User-Agent`. Matches known agent tokens as
22
+ * case-insensitive substrings, most-specific-first; anything unrecognised (or
23
+ * missing) is treated as `human`.
24
+ */
25
+ export declare function classify(ua: string | null | undefined): Classification;
26
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE/E,MAAM,MAAM,cAAc,GAAG;IAC3B,gDAAgD;IAChD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,gDAAgD;IAChD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAgLF;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,cAAc,CAgBtE"}
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Agent classifier — the single source of truth shared by the Caprail Node
3
+ * ingest API and every edge collector adapter. Pure, dependency-free, and
4
+ * runtime-agnostic (no `db`, no Node-only APIs) so it runs unchanged in Node,
5
+ * the Edge runtime, and Cloudflare Workers.
6
+ */
7
+ /**
8
+ * Known agent tokens, **declared most-specific-first**. Matching walks this list
9
+ * in order and returns the first substring hit, so qualified tokens MUST precede
10
+ * their shorter relatives (e.g. `Claude-User` before `ClaudeBot`, `ChatGPT-User`
11
+ * before `ChatGPT Agent`, `meta-externalfetcher` before `meta-externalagent`,
12
+ * `Google-Extended` / `GoogleOther` before `Googlebot`). Preserve this ordering
13
+ * when editing. Canonical published tokens — refresh from knownagents.com /
14
+ * darkvisitors.com.
15
+ */
16
+ const AGENTS = [
17
+ // Anthropic
18
+ {
19
+ token: "Claude-SearchBot",
20
+ name: "Claude-SearchBot",
21
+ vendor: "Anthropic",
22
+ type: "search",
23
+ },
24
+ {
25
+ token: "Claude-User",
26
+ name: "Claude-User",
27
+ vendor: "Anthropic",
28
+ type: "fetcher",
29
+ },
30
+ {
31
+ token: "Claude-Web",
32
+ name: "Claude-Web",
33
+ vendor: "Anthropic",
34
+ type: "fetcher",
35
+ },
36
+ {
37
+ token: "ClaudeBot",
38
+ name: "ClaudeBot",
39
+ vendor: "Anthropic",
40
+ type: "crawler",
41
+ },
42
+ {
43
+ token: "anthropic-ai",
44
+ name: "anthropic-ai",
45
+ vendor: "Anthropic",
46
+ type: "crawler",
47
+ },
48
+ // Claude Code has no reliable UA token (a collector header is preferred); keep
49
+ // this as a best-effort fallback.
50
+ {
51
+ token: "claude-code",
52
+ name: "Claude Code",
53
+ vendor: "Anthropic",
54
+ type: "fetcher",
55
+ },
56
+ // OpenAI
57
+ {
58
+ token: "ChatGPT-User",
59
+ name: "ChatGPT-User",
60
+ vendor: "OpenAI",
61
+ type: "fetcher",
62
+ },
63
+ {
64
+ token: "OAI-SearchBot",
65
+ name: "OAI-SearchBot",
66
+ vendor: "OpenAI",
67
+ type: "search",
68
+ },
69
+ {
70
+ token: "ChatGPT Agent",
71
+ name: "ChatGPT Agent",
72
+ vendor: "OpenAI",
73
+ type: "browser",
74
+ },
75
+ { token: "GPTBot", name: "GPTBot", vendor: "OpenAI", type: "crawler" },
76
+ { token: "Codex", name: "Codex", vendor: "OpenAI", type: "fetcher" },
77
+ // Google
78
+ {
79
+ token: "Gemini-Deep-Research",
80
+ name: "Gemini-Deep-Research",
81
+ vendor: "Google",
82
+ type: "fetcher",
83
+ },
84
+ {
85
+ token: "Google-NotebookLM",
86
+ name: "Google-NotebookLM",
87
+ vendor: "Google",
88
+ type: "fetcher",
89
+ },
90
+ {
91
+ token: "Google-Extended",
92
+ name: "Google-Extended",
93
+ vendor: "Google",
94
+ type: "crawler",
95
+ },
96
+ {
97
+ token: "GoogleOther",
98
+ name: "GoogleOther",
99
+ vendor: "Google",
100
+ type: "crawler",
101
+ },
102
+ { token: "Googlebot", name: "Googlebot", vendor: "Google", type: "search" },
103
+ // Perplexity
104
+ {
105
+ token: "Perplexity-User",
106
+ name: "Perplexity-User",
107
+ vendor: "Perplexity",
108
+ type: "fetcher",
109
+ },
110
+ {
111
+ token: "PerplexityBot",
112
+ name: "PerplexityBot",
113
+ vendor: "Perplexity",
114
+ type: "search",
115
+ },
116
+ // Meta
117
+ {
118
+ token: "meta-externalfetcher",
119
+ name: "meta-externalfetcher",
120
+ vendor: "Meta",
121
+ type: "fetcher",
122
+ },
123
+ {
124
+ token: "meta-externalagent",
125
+ name: "meta-externalagent",
126
+ vendor: "Meta",
127
+ type: "crawler",
128
+ },
129
+ // Other
130
+ {
131
+ token: "Bytespider",
132
+ name: "Bytespider",
133
+ vendor: "ByteDance",
134
+ type: "crawler",
135
+ },
136
+ { token: "CCBot", name: "CCBot", vendor: "Common Crawl", type: "crawler" },
137
+ { token: "Amazonbot", name: "Amazonbot", vendor: "Amazon", type: "crawler" },
138
+ {
139
+ token: "Applebot-Extended",
140
+ name: "Applebot-Extended",
141
+ vendor: "Apple",
142
+ type: "crawler",
143
+ },
144
+ { token: "cohere-ai", name: "cohere-ai", vendor: "Cohere", type: "crawler" },
145
+ { token: "Diffbot", name: "Diffbot", vendor: "Diffbot", type: "crawler" },
146
+ {
147
+ token: "MistralAI-User",
148
+ name: "MistralAI-User",
149
+ vendor: "Mistral",
150
+ type: "fetcher",
151
+ },
152
+ {
153
+ token: "DuckAssistBot",
154
+ name: "DuckAssistBot",
155
+ vendor: "DuckDuckGo",
156
+ type: "fetcher",
157
+ },
158
+ { token: "Bingbot", name: "Bingbot", vendor: "Microsoft", type: "search" },
159
+ ];
160
+ const HUMAN = {
161
+ name: null,
162
+ vendor: null,
163
+ type: "human",
164
+ isAgent: false,
165
+ };
166
+ /**
167
+ * Classify a request by its `User-Agent`. Matches known agent tokens as
168
+ * case-insensitive substrings, most-specific-first; anything unrecognised (or
169
+ * missing) is treated as `human`.
170
+ */
171
+ export function classify(ua) {
172
+ if (!ua)
173
+ return HUMAN;
174
+ const haystack = ua.toLowerCase();
175
+ for (const agent of AGENTS) {
176
+ if (haystack.includes(agent.token.toLowerCase())) {
177
+ return {
178
+ name: agent.name,
179
+ vendor: agent.vendor,
180
+ type: agent.type,
181
+ isAgent: true,
182
+ };
183
+ }
184
+ }
185
+ return HUMAN;
186
+ }
187
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../src/classify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Cloudflare Worker adapter — `"@caprail-dev/analytics/cloudflare"`.
3
+ *
4
+ * `withCaprail(handler)` wraps a Worker `fetch` handler. Because it sees the
5
+ * real `Response`, it reports the **authoritative** `status` / `Content-Type`
6
+ * (which the Markdown✓/HTML✗ grade is derived from) and measures `latencyMs` —
7
+ * none of which Next.js middleware can observe.
8
+ *
9
+ * // worker.ts
10
+ * import { withCaprail } from "@caprail-dev/analytics/cloudflare";
11
+ * export default withCaprail({
12
+ * async fetch(request) { return fetch(request); },
13
+ * });
14
+ *
15
+ * Config resolves from the Worker `env` binding (`CAPRAIL_INGEST_URL` /
16
+ * `CAPRAIL_INGEST_KEY`); when either is missing it is a transparent no-op.
17
+ */
18
+ /** Minimal structural types so the adapter needs no `@cloudflare/workers-types`. */
19
+ type WaitUntilContext = {
20
+ waitUntil(promise: Promise<unknown>): void;
21
+ };
22
+ type WorkerEnv = Record<string, string | undefined>;
23
+ type FetchHandler = {
24
+ fetch(request: Request, env: WorkerEnv, ctx: WaitUntilContext): Response | Promise<Response>;
25
+ };
26
+ /** Wrap a Worker `fetch` handler so every request is beaconed to Caprail. */
27
+ export declare function withCaprail(handler: FetchHandler): FetchHandler;
28
+ export {};
29
+ //# sourceMappingURL=cloudflare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../src/cloudflare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,oFAAoF;AACpF,KAAK,gBAAgB,GAAG;IAAE,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,CAAC;AACvE,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEpD,KAAK,YAAY,GAAG;IAClB,KAAK,CACH,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,SAAS,EACd,GAAG,EAAE,gBAAgB,GACpB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACjC,CAAC;AAKF,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,CA4B/D"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Cloudflare Worker adapter — `"@caprail-dev/analytics/cloudflare"`.
3
+ *
4
+ * `withCaprail(handler)` wraps a Worker `fetch` handler. Because it sees the
5
+ * real `Response`, it reports the **authoritative** `status` / `Content-Type`
6
+ * (which the Markdown✓/HTML✗ grade is derived from) and measures `latencyMs` —
7
+ * none of which Next.js middleware can observe.
8
+ *
9
+ * // worker.ts
10
+ * import { withCaprail } from "@caprail-dev/analytics/cloudflare";
11
+ * export default withCaprail({
12
+ * async fetch(request) { return fetch(request); },
13
+ * });
14
+ *
15
+ * Config resolves from the Worker `env` binding (`CAPRAIL_INGEST_URL` /
16
+ * `CAPRAIL_INGEST_KEY`); when either is missing it is a transparent no-op.
17
+ */
18
+ import { collect, resolveConfig } from "./core";
19
+ /** Wrap a Worker `fetch` handler so every request is beaconed to Caprail. */
20
+ export function withCaprail(handler) {
21
+ return {
22
+ async fetch(request, env, ctx) {
23
+ const start = Date.now();
24
+ const res = await handler.fetch(request, env, ctx);
25
+ const config = resolveConfig(undefined, env);
26
+ if (config) {
27
+ ctx.waitUntil(collect({
28
+ timestamp: new Date().toISOString(),
29
+ method: request.method,
30
+ path: new URL(request.url).pathname,
31
+ status: res.status,
32
+ contentType: res.headers.get("content-type") ?? undefined,
33
+ userAgent: request.headers.get("user-agent") ?? undefined,
34
+ country: request.cf?.country,
35
+ latencyMs: Date.now() - start,
36
+ }, config));
37
+ }
38
+ return res;
39
+ },
40
+ };
41
+ }
42
+ //# sourceMappingURL=cloudflare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../src/cloudflare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAiBhD,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,OAAqB;IAC/C,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAgB,EAAE,GAAc,EAAE,GAAqB;YACjE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YAEnD,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;YAC7C,IAAI,MAAM,EAAE,CAAC;gBACX,GAAG,CAAC,SAAS,CACX,OAAO,CACL;oBACE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;oBACnC,MAAM,EAAE,OAAO,CAAC,MAAM;oBACtB,IAAI,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ;oBACnC,MAAM,EAAE,GAAG,CAAC,MAAM;oBAClB,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,SAAS;oBACzD,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,SAAS;oBACzD,OAAO,EAAG,OAAyB,CAAC,EAAE,EAAE,OAAO;oBAC/C,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;iBAC9B,EACD,MAAM,CACP,CACF,CAAC;YACJ,CAAC;YAED,OAAO,GAAG,CAAC;QACb,CAAC;KACF,CAAC;AACJ,CAAC"}
package/dist/core.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared core — the package's `.` entry point. Resolves configuration (explicit
3
+ * options ▸ environment) and exposes `collect()`, the single call every adapter
4
+ * funnels through. Also re-exports `classify` as the single source of truth so
5
+ * the Caprail ingest API can drop its duplicate copy.
6
+ */
7
+ import { type CaprailEvent } from "./beacon";
8
+ export { classify } from "./classify";
9
+ export type { AgentType, Classification } from "./classify";
10
+ export type { CaprailEvent } from "./beacon";
11
+ /** Explicit collector config; either field falls back to the matching env var. */
12
+ export type CaprailConfig = {
13
+ /** Ingest endpoint URL. Falls back to `CAPRAIL_INGEST_URL`. */
14
+ url?: string;
15
+ /** Site ingest key (`cap_live_…`). Falls back to `CAPRAIL_INGEST_KEY`. */
16
+ key?: string;
17
+ };
18
+ /** Fully-resolved config, or `null` when either value is missing (→ no-op). */
19
+ export type ResolvedConfig = {
20
+ url: string;
21
+ key: string;
22
+ };
23
+ /**
24
+ * Merge explicit options over an env source, returning `null` unless both `url`
25
+ * and `key` are present. `envSource` lets non-`process.env` runtimes (Cloudflare
26
+ * Workers, which pass `env` as a handler argument) supply their bindings; it
27
+ * defaults to `process.env` where available.
28
+ */
29
+ export declare function resolveConfig(options?: CaprailConfig, envSource?: Record<string, string | undefined>): ResolvedConfig | null;
30
+ /**
31
+ * Beacon a single collected event. Thin wrapper over `sendBeacon` that every
32
+ * adapter shares; the returned promise should be handed to the runtime's
33
+ * keep-alive hook (`waitUntil`).
34
+ */
35
+ export declare function collect(event: CaprailEvent, config: ResolvedConfig): Promise<void>;
36
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,YAAY,EAAc,MAAM,UAAU,CAAC;AAEzD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5D,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,kFAAkF;AAClF,MAAM,MAAM,aAAa,GAAG;IAC1B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,CAAC,EAAE,aAAa,EACvB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAC7C,cAAc,GAAG,IAAI,CAMvB;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CACrB,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,IAAI,CAAC,CAEf"}
package/dist/core.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared core — the package's `.` entry point. Resolves configuration (explicit
3
+ * options ▸ environment) and exposes `collect()`, the single call every adapter
4
+ * funnels through. Also re-exports `classify` as the single source of truth so
5
+ * the Caprail ingest API can drop its duplicate copy.
6
+ */
7
+ import { sendBeacon } from "./beacon";
8
+ export { classify } from "./classify";
9
+ /**
10
+ * Merge explicit options over an env source, returning `null` unless both `url`
11
+ * and `key` are present. `envSource` lets non-`process.env` runtimes (Cloudflare
12
+ * Workers, which pass `env` as a handler argument) supply their bindings; it
13
+ * defaults to `process.env` where available.
14
+ */
15
+ export function resolveConfig(options, envSource) {
16
+ const env = envSource ?? (typeof process !== "undefined" ? process.env : undefined);
17
+ const url = options?.url ?? env?.CAPRAIL_INGEST_URL;
18
+ const key = options?.key ?? env?.CAPRAIL_INGEST_KEY;
19
+ return url && key ? { url, key } : null;
20
+ }
21
+ /**
22
+ * Beacon a single collected event. Thin wrapper over `sendBeacon` that every
23
+ * adapter shares; the returned promise should be handed to the runtime's
24
+ * keep-alive hook (`waitUntil`).
25
+ */
26
+ export function collect(event, config) {
27
+ return sendBeacon(config.url, config.key, [event]);
28
+ }
29
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAqB,UAAU,EAAE,MAAM,UAAU,CAAC;AAEzD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAetC;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAuB,EACvB,SAA8C;IAE9C,MAAM,GAAG,GACP,SAAS,IAAI,CAAC,OAAO,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC1E,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC;IACpD,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC;IACpD,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,OAAO,CACrB,KAAmB,EACnB,MAAsB;IAEtB,OAAO,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;AACrD,CAAC"}
package/dist/next.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Next.js adapter — `"@caprail-dev/analytics/next"`.
3
+ *
4
+ * `createCaprailMiddleware()` returns a `middleware` function for `middleware.ts`
5
+ * that classifies nothing locally and beacons each request via `ev.waitUntil`,
6
+ * so it never blocks (or fails) the response.
7
+ *
8
+ * // middleware.ts
9
+ * import { createCaprailMiddleware } from "@caprail-dev/analytics/next";
10
+ * export const middleware = createCaprailMiddleware(); // reads env
11
+ * export const config = {
12
+ * matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
13
+ * };
14
+ *
15
+ * Limitation: middleware runs *before* the response, so it cannot observe the
16
+ * final `status` / `Content-Type` (true on both the Edge and Node runtimes).
17
+ * The Cloudflare adapter is authoritative for those — see `./cloudflare`.
18
+ */
19
+ import { NextResponse, type NextFetchEvent, type NextRequest } from "next/server";
20
+ import { type CaprailConfig } from "./core";
21
+ /**
22
+ * Build a Caprail collection middleware. Config resolves explicit `options` over
23
+ * `CAPRAIL_INGEST_URL` / `CAPRAIL_INGEST_KEY`; when neither source yields both a
24
+ * URL and key the middleware is a transparent no-op.
25
+ */
26
+ export declare function createCaprailMiddleware(options?: CaprailConfig): (req: NextRequest, ev: NextFetchEvent) => NextResponse<unknown>;
27
+ //# sourceMappingURL=next.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,KAAK,aAAa,EAA0B,MAAM,QAAQ,CAAC;AAEpE;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,aAAa,IAClC,KAAK,WAAW,EAAE,IAAI,cAAc,2BAsBhE"}
package/dist/next.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Next.js adapter — `"@caprail-dev/analytics/next"`.
3
+ *
4
+ * `createCaprailMiddleware()` returns a `middleware` function for `middleware.ts`
5
+ * that classifies nothing locally and beacons each request via `ev.waitUntil`,
6
+ * so it never blocks (or fails) the response.
7
+ *
8
+ * // middleware.ts
9
+ * import { createCaprailMiddleware } from "@caprail-dev/analytics/next";
10
+ * export const middleware = createCaprailMiddleware(); // reads env
11
+ * export const config = {
12
+ * matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
13
+ * };
14
+ *
15
+ * Limitation: middleware runs *before* the response, so it cannot observe the
16
+ * final `status` / `Content-Type` (true on both the Edge and Node runtimes).
17
+ * The Cloudflare adapter is authoritative for those — see `./cloudflare`.
18
+ */
19
+ import { NextResponse, } from "next/server";
20
+ import { collect, resolveConfig } from "./core";
21
+ /**
22
+ * Build a Caprail collection middleware. Config resolves explicit `options` over
23
+ * `CAPRAIL_INGEST_URL` / `CAPRAIL_INGEST_KEY`; when neither source yields both a
24
+ * URL and key the middleware is a transparent no-op.
25
+ */
26
+ export function createCaprailMiddleware(options) {
27
+ return function middleware(req, ev) {
28
+ const config = resolveConfig(options);
29
+ if (config) {
30
+ const ua = req.headers.get("user-agent");
31
+ ev.waitUntil(collect({
32
+ timestamp: new Date().toISOString(),
33
+ method: req.method,
34
+ path: req.nextUrl.pathname,
35
+ status: 200, // pre-response; the real status is unknown here.
36
+ userAgent: ua ?? undefined,
37
+ country: req.headers.get("x-vercel-ip-country") ?? undefined,
38
+ }, config));
39
+ }
40
+ return NextResponse.next();
41
+ };
42
+ }
43
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.js","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,YAAY,GAGb,MAAM,aAAa,CAAC;AAErB,OAAO,EAAsB,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAEpE;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAuB;IAC7D,OAAO,SAAS,UAAU,CAAC,GAAgB,EAAE,EAAkB;QAC7D,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QAEtC,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;YACzC,EAAE,CAAC,SAAS,CACV,OAAO,CACL;gBACE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,QAAQ;gBAC1B,MAAM,EAAE,GAAG,EAAE,iDAAiD;gBAC9D,SAAS,EAAE,EAAE,IAAI,SAAS;gBAC1B,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,IAAI,SAAS;aAC7D,EACD,MAAM,CACP,CACF,CAAC;QACJ,CAAC;QAED,OAAO,YAAY,CAAC,IAAI,EAAE,CAAC;IAC7B,CAAC,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@caprail-dev/analytics",
3
+ "version": "0.1.0",
4
+ "description": "Edge collector for Caprail — see which AI agents access your site.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "license": "MIT",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "main": "./dist/core.js",
13
+ "types": "./dist/core.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/core.d.ts",
17
+ "default": "./dist/core.js"
18
+ },
19
+ "./next": {
20
+ "types": "./dist/next.d.ts",
21
+ "default": "./dist/next.js"
22
+ },
23
+ "./cloudflare": {
24
+ "types": "./dist/cloudflare.d.ts",
25
+ "default": "./dist/cloudflare.js"
26
+ }
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json",
33
+ "test": "bun test"
34
+ },
35
+ "peerDependencies": {
36
+ "next": ">=14"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "next": {
40
+ "optional": true
41
+ }
42
+ }
43
+ }