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 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
- | Agent | Signal |
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 shouldTrack(accept) {
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 (!shouldTrack(options.accept)) {
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  trackVisit
3
- } from "./chunk-Z3U3YF4H.mjs";
3
+ } from "./chunk-2UMSOEKA.mjs";
4
4
  export {
5
5
  trackVisit
6
6
  };
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 shouldTrack(accept) {
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 (!shouldTrack(options.accept)) {
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
- trackVisit({
77
- host: request.headers.get("host") || request.nextUrl.host,
78
- path: request.nextUrl.pathname,
79
- userAgent: request.headers.get("user-agent") || "",
80
- accept: request.headers.get("accept") || "",
81
- country: request.geo?.country || "unknown"
82
- }).catch(() => {
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-Z3U3YF4H.mjs";
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
- trackVisit({
11
- host: request.headers.get("host") || request.nextUrl.host,
12
- path: request.nextUrl.pathname,
13
- userAgent: request.headers.get("user-agent") || "",
14
- accept: request.headers.get("accept") || "",
15
- country: request.geo?.country || "unknown"
16
- }).catch(() => {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2027-track",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Track AI coding agents visiting your documentation",
5
5
  "repository": {
6
6
  "type": "git",