@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 +65 -0
- package/dist/beacon.d.ts +33 -0
- package/dist/beacon.d.ts.map +1 -0
- package/dist/beacon.js +26 -0
- package/dist/beacon.js.map +1 -0
- package/dist/classify.d.ts +26 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +187 -0
- package/dist/classify.js.map +1 -0
- package/dist/cloudflare.d.ts +29 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +42 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/core.d.ts +36 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +29 -0
- package/dist/core.js.map +1 -0
- package/dist/next.d.ts +27 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +43 -0
- package/dist/next.js.map +1 -0
- package/package.json +43 -0
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.
|
package/dist/beacon.d.ts
ADDED
|
@@ -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"}
|
package/dist/classify.js
ADDED
|
@@ -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
|
package/dist/core.js.map
ADDED
|
@@ -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
|
package/dist/next.js.map
ADDED
|
@@ -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
|
+
}
|