2027-track 0.1.2 → 0.1.4

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 CHANGED
@@ -1,14 +1,16 @@
1
1
  # 2027-track
2
2
 
3
- Track AI coding agents (Claude Code, Codex, OpenCode) visiting your documentation.
3
+ **Know who's using your product: humans or agents**
4
4
 
5
- ## Installation
5
+ Lightweight middleware to detect and track AI coding agents visiting your docs.
6
+
7
+ ## Install
6
8
 
7
9
  ```bash
8
10
  npm install 2027-track
9
11
  ```
10
12
 
11
- ## Usage with Next.js
13
+ ## Next.js
12
14
 
13
15
  ```ts
14
16
  // middleware.ts
@@ -16,76 +18,103 @@ import { withAIAnalytics } from "2027-track/next";
16
18
 
17
19
  export default withAIAnalytics();
18
20
 
19
- // Or wrap your existing middleware:
20
- export default withAIAnalytics(yourExistingMiddleware);
21
+ export const config = {
22
+ matcher: ["/((?!api|_next|admin|auth).*)",],
23
+ };
21
24
  ```
22
25
 
23
- ## Usage (generic)
26
+ Or wrap existing middleware:
24
27
 
25
28
  ```ts
26
- import { trackVisit } from "2027-track";
27
-
28
- await trackVisit({
29
- host: "docs.example.com",
30
- path: "/api/getting-started",
31
- userAgent: request.headers.get("user-agent"),
32
- accept: request.headers.get("accept"),
33
- country: "US", // optional
34
- });
29
+ export default withAIAnalytics(yourMiddleware);
35
30
  ```
36
31
 
37
- ## Route Filtering
38
-
39
- **Important:** Only track public documentation routes. Exclude private endpoints to avoid leaking sensitive paths.
32
+ ## Vercel (any framework)
40
33
 
41
- ### Next.js matcher (recommended)
34
+ Works with Remix, SvelteKit, Astro, Nuxt, or any Vercel deployment:
42
35
 
43
36
  ```ts
44
37
  // middleware.ts
45
- import { withAIAnalytics } from "2027-track/next";
38
+ import { withAIAnalytics } from "2027-track/vercel";
46
39
 
47
40
  export default withAIAnalytics();
48
41
 
49
42
  export const config = {
50
- matcher: [
51
- // Exclude private routes, track everything else
52
- "/((?!api|_next|app|admin|dashboard|auth|login|signup).*)",
53
- ],
43
+ matcher: "/((?!_next|api|favicon.ico|assets|.*\\..*).*)",
54
44
  };
55
45
  ```
56
46
 
57
- ### Manual filtering
47
+ Or wrap existing middleware:
48
+
49
+ ```ts
50
+ export default withAIAnalytics(yourMiddleware);
51
+ ```
52
+
53
+ Or use `trackVisit` directly:
58
54
 
59
55
  ```ts
60
56
  import { trackVisit } from "2027-track";
61
57
 
62
- // Only track if path starts with /docs
63
- if (path.startsWith("/docs")) {
64
- await trackVisit({ host, path, userAgent, accept });
58
+ export const config = {
59
+ matcher: "/((?!_next|api|favicon.ico|assets|.*\\..*).*)",
60
+ };
61
+
62
+ export default function middleware(request: Request, context: { waitUntil: (promise: Promise<unknown>) => void }) {
63
+ const url = new URL(request.url);
64
+ context.waitUntil(
65
+ trackVisit({
66
+ host: url.hostname,
67
+ path: url.pathname,
68
+ userAgent: request.headers.get("user-agent") || "",
69
+ accept: request.headers.get("accept") || "",
70
+ country: request.headers.get("x-vercel-ip-country") || undefined,
71
+ }).catch(() => {})
72
+ );
65
73
  }
66
74
  ```
67
75
 
68
- ## Configuration
76
+ ## Generic Usage
77
+
78
+ ```ts
79
+ import { trackVisit } from "2027-track";
69
80
 
70
- ### Kill switch
81
+ await trackVisit({
82
+ host: "docs.example.com",
83
+ path: "/getting-started",
84
+ userAgent: request.headers.get("user-agent"),
85
+ accept: request.headers.get("accept"),
86
+ });
87
+ ```
71
88
 
72
- Set `AI_ANALYTICS_ENDPOINT=""` to disable tracking entirely.
89
+ ## Configuration
73
90
 
74
- ### Custom endpoint
91
+ ```bash
92
+ # Disable tracking
93
+ AI_ANALYTICS_ENDPOINT=""
75
94
 
76
- Set `AI_ANALYTICS_ENDPOINT` to your own endpoint URL.
95
+ # Self-host (optional)
96
+ AI_ANALYTICS_ENDPOINT="https://your-api.workers.dev/track"
97
+ ```
77
98
 
78
99
  ## Privacy
79
100
 
80
- - Events are sent **server-side** from Vercel Edge (or your server)
81
- - Visitor IP addresses **never reach** the analytics endpoint
82
- - Only headers (user-agent, accept) and page info (host, path) are sent
83
- - **No cookies, no fingerprinting, no personal identifiers**
84
-
85
- This middleware collects no personally identifiable information (PII). Because there are no cookies, no IP forwarding, and no user identifiers, it generally does not trigger privacy policy or cookie-consent requirements under GDPR, CCPA, or similar regulations. That said, you should verify with your own legal counsel, especially under strict EU interpretations.
101
+ - Events sent **server-side** visitor IPs never reach the analytics endpoint
102
+ - No cookies, no fingerprinting, no PII
103
+ - Fully open source [audit the code](https://github.com/team2027/track)
86
104
 
87
105
  ## Detection
88
106
 
89
- Agent classification (user-agent and accept header matching) happens server-side at the analytics endpoint — the middleware only forwards raw headers.
107
+ | Agent | Signal |
108
+ |-------|--------|
109
+ | Claude Code | `axios` + `text/markdown` |
110
+ | OpenCode | `text/markdown` with `q=` weights |
111
+ | Codex | `ChatGPT-User` user-agent |
112
+
113
+ ## Links
114
+
115
+ - [GitHub](https://github.com/team2027/track)
116
+ - [Test your agent](https://ai-docs-analytics-api.theisease.workers.dev/detect)
117
+
118
+ ## License
90
119
 
91
- Dashboard: https://ai-docs-analytics.vercel.app
120
+ MIT
@@ -0,0 +1,8 @@
1
+ export { trackVisit } from './index.mjs';
2
+
3
+ interface VercelContext {
4
+ waitUntil: (promise: Promise<unknown>) => void;
5
+ }
6
+ declare function withAIAnalytics(middleware?: (request: Request, context: VercelContext) => Response | undefined | Promise<Response | undefined>): (request: Request, context: VercelContext) => Promise<Response | undefined>;
7
+
8
+ export { withAIAnalytics };
@@ -0,0 +1,8 @@
1
+ export { trackVisit } from './index.js';
2
+
3
+ interface VercelContext {
4
+ waitUntil: (promise: Promise<unknown>) => void;
5
+ }
6
+ declare function withAIAnalytics(middleware?: (request: Request, context: VercelContext) => Response | undefined | Promise<Response | undefined>): (request: Request, context: VercelContext) => Promise<Response | undefined>;
7
+
8
+ export { withAIAnalytics };
package/dist/vercel.js ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/vercel.ts
21
+ var vercel_exports = {};
22
+ __export(vercel_exports, {
23
+ trackVisit: () => trackVisit,
24
+ withAIAnalytics: () => withAIAnalytics
25
+ });
26
+ module.exports = __toCommonJS(vercel_exports);
27
+
28
+ // src/index.ts
29
+ var DEFAULT_ENDPOINT = "https://ai-docs-analytics-api.theisease.workers.dev/track";
30
+ var TIMEOUT_MS = 2500;
31
+ function getEndpoint() {
32
+ const env = typeof process !== "undefined" ? process.env.AI_ANALYTICS_ENDPOINT : void 0;
33
+ if (env === "") return null;
34
+ return env || DEFAULT_ENDPOINT;
35
+ }
36
+ function isPageView(accept) {
37
+ const a = accept.toLowerCase();
38
+ return a.includes("text/html") || a.includes("text/markdown");
39
+ }
40
+ async function trackVisit(options) {
41
+ const endpoint = getEndpoint();
42
+ if (!endpoint) {
43
+ return { ok: true, skipped: "disabled" };
44
+ }
45
+ if (!isPageView(options.accept)) {
46
+ return { ok: true, skipped: "not-page-view" };
47
+ }
48
+ const controller = new AbortController();
49
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
50
+ try {
51
+ const response = await fetch(endpoint, {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({
55
+ host: options.host,
56
+ path: options.path,
57
+ user_agent: options.userAgent,
58
+ accept: options.accept,
59
+ country: options.country || "unknown"
60
+ }),
61
+ signal: controller.signal
62
+ });
63
+ clearTimeout(timeoutId);
64
+ return await response.json();
65
+ } catch (e) {
66
+ clearTimeout(timeoutId);
67
+ return { ok: false, error: e instanceof Error ? e.message : "unknown error" };
68
+ }
69
+ }
70
+
71
+ // src/vercel.ts
72
+ function withAIAnalytics(middleware) {
73
+ return async (request, context) => {
74
+ const response = middleware ? await middleware(request, context) : void 0;
75
+ const url = new URL(request.url);
76
+ context.waitUntil(
77
+ trackVisit({
78
+ host: url.hostname,
79
+ path: url.pathname,
80
+ userAgent: request.headers.get("user-agent") || "",
81
+ accept: request.headers.get("accept") || "",
82
+ country: request.headers.get("x-vercel-ip-country") || void 0
83
+ }).catch(() => {
84
+ })
85
+ );
86
+ return response;
87
+ };
88
+ }
89
+ // Annotate the CommonJS export names for ESM import in node:
90
+ 0 && (module.exports = {
91
+ trackVisit,
92
+ withAIAnalytics
93
+ });
@@ -0,0 +1,26 @@
1
+ import {
2
+ trackVisit
3
+ } from "./chunk-2UMSOEKA.mjs";
4
+
5
+ // src/vercel.ts
6
+ function withAIAnalytics(middleware) {
7
+ return async (request, context) => {
8
+ const response = middleware ? await middleware(request, context) : void 0;
9
+ const url = new URL(request.url);
10
+ context.waitUntil(
11
+ trackVisit({
12
+ host: url.hostname,
13
+ path: url.pathname,
14
+ userAgent: request.headers.get("user-agent") || "",
15
+ accept: request.headers.get("accept") || "",
16
+ country: request.headers.get("x-vercel-ip-country") || void 0
17
+ }).catch(() => {
18
+ })
19
+ );
20
+ return response;
21
+ };
22
+ }
23
+ export {
24
+ trackVisit,
25
+ withAIAnalytics
26
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2027-track",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Track AI coding agents visiting your documentation",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,14 +20,19 @@
20
20
  "types": "./dist/next.d.ts",
21
21
  "import": "./dist/next.mjs",
22
22
  "require": "./dist/next.js"
23
+ },
24
+ "./vercel": {
25
+ "types": "./dist/vercel.d.ts",
26
+ "import": "./dist/vercel.mjs",
27
+ "require": "./dist/vercel.js"
23
28
  }
24
29
  },
25
30
  "files": [
26
31
  "dist"
27
32
  ],
28
33
  "scripts": {
29
- "build": "tsup src/index.ts src/next.ts --format cjs,esm --dts",
30
- "dev": "tsup src/index.ts src/next.ts --format cjs,esm --dts --watch"
34
+ "build": "tsup src/index.ts src/next.ts src/vercel.ts --format cjs,esm --dts",
35
+ "dev": "tsup src/index.ts src/next.ts src/vercel.ts --format cjs,esm --dts --watch"
31
36
  },
32
37
  "keywords": [
33
38
  "ai",