2027-track 0.1.0 → 0.1.2
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 +35 -5
- package/dist/chunk-2UMSOEKA.mjs +46 -0
- package/dist/index.js +2 -2
- package/dist/index.mjs +1 -1
- package/dist/next.js +12 -10
- package/dist/next.mjs +11 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,37 @@ await trackVisit({
|
|
|
34
34
|
});
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## Route Filtering
|
|
38
|
+
|
|
39
|
+
**Important:** Only track public documentation routes. Exclude private endpoints to avoid leaking sensitive paths.
|
|
40
|
+
|
|
41
|
+
### Next.js matcher (recommended)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// middleware.ts
|
|
45
|
+
import { withAIAnalytics } from "2027-track/next";
|
|
46
|
+
|
|
47
|
+
export default withAIAnalytics();
|
|
48
|
+
|
|
49
|
+
export const config = {
|
|
50
|
+
matcher: [
|
|
51
|
+
// Exclude private routes, track everything else
|
|
52
|
+
"/((?!api|_next|app|admin|dashboard|auth|login|signup).*)",
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Manual filtering
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { trackVisit } from "2027-track";
|
|
61
|
+
|
|
62
|
+
// Only track if path starts with /docs
|
|
63
|
+
if (path.startsWith("/docs")) {
|
|
64
|
+
await trackVisit({ host, path, userAgent, accept });
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
37
68
|
## Configuration
|
|
38
69
|
|
|
39
70
|
### Kill switch
|
|
@@ -49,13 +80,12 @@ Set `AI_ANALYTICS_ENDPOINT` to your own endpoint URL.
|
|
|
49
80
|
- Events are sent **server-side** from Vercel Edge (or your server)
|
|
50
81
|
- Visitor IP addresses **never reach** the analytics endpoint
|
|
51
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.
|
|
52
86
|
|
|
53
87
|
## Detection
|
|
54
88
|
|
|
55
|
-
|
|
56
|
-
|-------|--------|
|
|
57
|
-
| Claude Code | `axios` user-agent + `text/markdown` accept |
|
|
58
|
-
| OpenCode | `text/markdown` accept with `q=` weights |
|
|
59
|
-
| Codex | `ChatGPT-User` user-agent |
|
|
89
|
+
Agent classification (user-agent and accept header matching) happens server-side at the analytics endpoint — the middleware only forwards raw headers.
|
|
60
90
|
|
|
61
91
|
Dashboard: https://ai-docs-analytics.vercel.app
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var DEFAULT_ENDPOINT = "https://ai-docs-analytics-api.theisease.workers.dev/track";
|
|
3
|
+
var TIMEOUT_MS = 2500;
|
|
4
|
+
function getEndpoint() {
|
|
5
|
+
const env = typeof process !== "undefined" ? process.env.AI_ANALYTICS_ENDPOINT : void 0;
|
|
6
|
+
if (env === "") return null;
|
|
7
|
+
return env || DEFAULT_ENDPOINT;
|
|
8
|
+
}
|
|
9
|
+
function isPageView(accept) {
|
|
10
|
+
const a = accept.toLowerCase();
|
|
11
|
+
return a.includes("text/html") || a.includes("text/markdown");
|
|
12
|
+
}
|
|
13
|
+
async function trackVisit(options) {
|
|
14
|
+
const endpoint = getEndpoint();
|
|
15
|
+
if (!endpoint) {
|
|
16
|
+
return { ok: true, skipped: "disabled" };
|
|
17
|
+
}
|
|
18
|
+
if (!isPageView(options.accept)) {
|
|
19
|
+
return { ok: true, skipped: "not-page-view" };
|
|
20
|
+
}
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(endpoint, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
host: options.host,
|
|
29
|
+
path: options.path,
|
|
30
|
+
user_agent: options.userAgent,
|
|
31
|
+
accept: options.accept,
|
|
32
|
+
country: options.country || "unknown"
|
|
33
|
+
}),
|
|
34
|
+
signal: controller.signal
|
|
35
|
+
});
|
|
36
|
+
clearTimeout(timeoutId);
|
|
37
|
+
return await response.json();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
return { ok: false, error: e instanceof Error ? e.message : "unknown error" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
trackVisit
|
|
46
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ function getEndpoint() {
|
|
|
30
30
|
if (env === "") return null;
|
|
31
31
|
return env || DEFAULT_ENDPOINT;
|
|
32
32
|
}
|
|
33
|
-
function
|
|
33
|
+
function isPageView(accept) {
|
|
34
34
|
const a = accept.toLowerCase();
|
|
35
35
|
return a.includes("text/html") || a.includes("text/markdown");
|
|
36
36
|
}
|
|
@@ -39,7 +39,7 @@ async function trackVisit(options) {
|
|
|
39
39
|
if (!endpoint) {
|
|
40
40
|
return { ok: true, skipped: "disabled" };
|
|
41
41
|
}
|
|
42
|
-
if (!
|
|
42
|
+
if (!isPageView(options.accept)) {
|
|
43
43
|
return { ok: true, skipped: "not-page-view" };
|
|
44
44
|
}
|
|
45
45
|
const controller = new AbortController();
|
package/dist/index.mjs
CHANGED
package/dist/next.js
CHANGED
|
@@ -34,7 +34,7 @@ function getEndpoint() {
|
|
|
34
34
|
if (env === "") return null;
|
|
35
35
|
return env || DEFAULT_ENDPOINT;
|
|
36
36
|
}
|
|
37
|
-
function
|
|
37
|
+
function isPageView(accept) {
|
|
38
38
|
const a = accept.toLowerCase();
|
|
39
39
|
return a.includes("text/html") || a.includes("text/markdown");
|
|
40
40
|
}
|
|
@@ -43,7 +43,7 @@ async function trackVisit(options) {
|
|
|
43
43
|
if (!endpoint) {
|
|
44
44
|
return { ok: true, skipped: "disabled" };
|
|
45
45
|
}
|
|
46
|
-
if (!
|
|
46
|
+
if (!isPageView(options.accept)) {
|
|
47
47
|
return { ok: true, skipped: "not-page-view" };
|
|
48
48
|
}
|
|
49
49
|
const controller = new AbortController();
|
|
@@ -73,14 +73,16 @@ async function trackVisit(options) {
|
|
|
73
73
|
function withAIAnalytics(middleware) {
|
|
74
74
|
return async (request, event) => {
|
|
75
75
|
const response = middleware ? await middleware(request, event) : import_server.NextResponse.next();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
event.waitUntil(
|
|
77
|
+
trackVisit({
|
|
78
|
+
host: request.headers.get("host") || request.nextUrl.host,
|
|
79
|
+
path: request.nextUrl.pathname,
|
|
80
|
+
userAgent: request.headers.get("user-agent") || "",
|
|
81
|
+
accept: request.headers.get("accept") || "",
|
|
82
|
+
country: request.geo?.country || "unknown"
|
|
83
|
+
}).catch(() => {
|
|
84
|
+
})
|
|
85
|
+
);
|
|
84
86
|
return response;
|
|
85
87
|
};
|
|
86
88
|
}
|
package/dist/next.mjs
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
trackVisit
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-2UMSOEKA.mjs";
|
|
4
4
|
|
|
5
5
|
// src/next.ts
|
|
6
6
|
import { NextResponse } from "next/server";
|
|
7
7
|
function withAIAnalytics(middleware) {
|
|
8
8
|
return async (request, event) => {
|
|
9
9
|
const response = middleware ? await middleware(request, event) : NextResponse.next();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
event.waitUntil(
|
|
11
|
+
trackVisit({
|
|
12
|
+
host: request.headers.get("host") || request.nextUrl.host,
|
|
13
|
+
path: request.nextUrl.pathname,
|
|
14
|
+
userAgent: request.headers.get("user-agent") || "",
|
|
15
|
+
accept: request.headers.get("accept") || "",
|
|
16
|
+
country: request.geo?.country || "unknown"
|
|
17
|
+
}).catch(() => {
|
|
18
|
+
})
|
|
19
|
+
);
|
|
18
20
|
return response;
|
|
19
21
|
};
|
|
20
22
|
}
|