@knownagents/sdk 2.1.0 → 2.3.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 CHANGED
@@ -22,48 +22,33 @@ import { KnownAgents } from "@knownagents/sdk"
22
22
  const knownAgents = new KnownAgents("YOUR_ACCESS_TOKEN")
23
23
  ```
24
24
 
25
- ## How To Set Up Agent & LLM Analytics ([Full Docs](https://knownagents.com/docs/analytics))
26
-
27
- Get realtime insight into how [AI agents and other bots](https://knownagents.com/agents) browse your web pages, call your [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) endpoints, and purchase products through your [ACP (Agentic Commerce Protocol)](https://www.agenticcommerce.dev/) and [UCP (Universal Commerce Protocol)](https://ucp.dev/) shopping flows. Measure human traffic from AI chat and search platforms like ChatGPT, Claude, and Gemini.
28
-
29
- ### Track Pageviews and REST Calls
30
-
31
- Call `trackPageviewOrRESTCall` with the incoming request and outgoing response. If you can, do this in middleware.
25
+ To batch visits, set the visit event queue size and flush interval:
32
26
 
33
27
  ```ts
34
- knownAgents.trackPageviewOrRESTCall(request, response)
28
+ const knownAgents = new KnownAgents("YOUR_ACCESS_TOKEN", {
29
+ flushQueueSize: 1000,
30
+ flushIntervalInMilliseconds: 10000
31
+ })
35
32
  ```
36
33
 
37
- MCP calls are skipped automatically by `trackPageviewOrRESTCall`. Use `trackMCPCall` to track MCP calls.
38
-
39
- High-traffic websites should not use this method. Instead, batch visits and send them periodically with `trackVisits` (see below).
34
+ The entire visit event queue is uploaded when it reaches `flushQueueSize` or when the flush interval elapses after the first visit event enters an empty queue.
40
35
 
41
- #### Agentic Commerce Calls
36
+ ## Send Visit Events ([Agent Analytics](https://knownagents.com/products/agent-analytics), [LLM Referral Tracking](https://knownagents.com/products/llm-referral-tracking))
42
37
 
43
- For REST implementations of ACP and UCP, include the response body.
38
+ Get realtime insight into how [AI agents and other bots](https://knownagents.com/agents) browse your web pages, call your [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) endpoints, and purchase products through your [ACP (Agentic Commerce Protocol)](https://www.agenticcommerce.dev/) and [UCP (Universal Commerce Protocol)](https://ucp.dev/) shopping flows. Measure human traffic from AI chat and search platforms like ChatGPT, Claude, and Gemini.
44
39
 
45
- ```ts
46
- // For ACP calls ...
40
+ ### Pageviews and REST Calls
47
41
 
48
- knownAgents.trackPageviewOrRESTCall(request, response, {
49
- acpResponseBody: acpResponseBody
50
- })
51
- ```
42
+ Call `trackPageviewOrRESTCall` with the incoming request and outgoing response. If you can, do this in middleware. This method skips MCP calls automatically.
52
43
 
53
44
  ```ts
54
- // For UCP calls ...
55
-
56
- knownAgents.trackPageviewOrRESTCall(request, response, {
57
- ucpResponseBody: ucpResponseBody
58
- })
45
+ knownAgents.trackPageviewOrRESTCall(request, response)
59
46
  ```
60
47
 
61
- ### Track MCP Calls
48
+ ### MCP Calls (Optional)
62
49
 
63
50
  For MCP servers that use `StreamableHTTPServerTransport`, call `trackMCPCall` after connecting the transport and before the transport handles the request.
64
51
 
65
- High-traffic websites, or those that use the same transport instance to handle multiple concurrent requests, should not use this method. Instead, batch visits and send them periodically with `trackVisits` (see below).
66
-
67
52
  ```ts
68
53
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
69
54
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
@@ -88,50 +73,28 @@ knownAgents.trackMCPCall(request, response, transport)
88
73
  await transport.handleRequest(request, response, parsedBody)
89
74
  ```
90
75
 
91
- #### Agentic Commerce Calls
92
-
93
- This method will track MCP implementations of ACP and UCP automatically.
94
-
95
- ### Batching for High-Traffic Websites
76
+ ### Agentic Commerce Interactions (Optional)
96
77
 
97
- High-traffic websites should batch multiple visits together and send them periodically (e.g. every 30 seconds) using `trackVisits`. This method can track pageviews, REST, MCP, ACP, and UCP calls. Here's an example with Express middleware:
78
+ For REST implementations of ACP and UCP, include the response body in `trackPageviewOrRESTCall`.
98
79
 
99
80
  ```ts
100
- import express from "express"
101
- import { KnownAgents, type VisitRequest } from "@knownagents/sdk"
102
-
103
- const app = express()
81
+ // For ACP calls ...
104
82
 
105
- const knownAgents = new KnownAgents("YOUR_ACCESS_TOKEN")
106
- const visits: VisitRequest[] = []
107
-
108
- app.use((request, response, next) => {
109
- const created = new Date()
110
-
111
- response.once("finish", () => {
112
- visits.push({
113
- request_path: request.url,
114
- request_method: request.method,
115
- request_headers: request.headers,
116
- response_status_code: response.statusCode,
117
- response_headers: response.getHeaders(),
118
- response_duration_in_milliseconds: Date.now() - created.getTime(),
119
- created: created.toISOString()
120
- })
121
- })
122
-
123
- next()
83
+ knownAgents.trackPageviewOrRESTCall(request, response, {
84
+ acpResponseBody: acpResponseBody
124
85
  })
86
+ ```
125
87
 
126
- setInterval(() => {
127
- const batch = visits.splice(0)
88
+ ```ts
89
+ // For UCP calls ...
128
90
 
129
- if (batch.length > 0) {
130
- knownAgents.trackVisits(batch)
131
- }
132
- }, 30000)
91
+ knownAgents.trackPageviewOrRESTCall(request, response, {
92
+ ucpResponseBody: ucpResponseBody
93
+ })
133
94
  ```
134
95
 
96
+ `trackMCPCall` will track MCP implementations of ACP and UCP automatically.
97
+
135
98
  ### Test Your Integration
136
99
 
137
100
  - Open your project's settings page
@@ -140,7 +103,7 @@ setInterval(() => {
140
103
 
141
104
  If your website is correctly connected, you should see visits from a test agent in the realtime timeline within a few seconds.
142
105
 
143
- ## How To Set Up Automatic Robots.txt ([Full Docs](https://knownagents.com/docs/robots-txt))
106
+ ## Set Up Automatic Robots.txt ([Automatic Robots.txt](https://knownagents.com/products/automatic-robots-txt))
144
107
 
145
108
  Serve a robots.txt that continuously updates with rules for new [crawlers, scrapers, AI agents, and other bots](https://knownagents.com/agents) as they're discovered.
146
109
 
@@ -159,7 +122,7 @@ const robotsTxt = await knownAgents.generateRobotsTXT([
159
122
 
160
123
  The return value is a plain text robots.txt string. Generate a `robotsTxt` periodically (e.g. once per day, using a cron job). Then, serve it from your website's `/robots.txt` endpoint.
161
124
 
162
- ## How To Use the Agent Identification API ([Full Docs](https://knownagents.com/docs/identification))
125
+ ## Use the Agent Identification API ([Agent Identification API](https://knownagents.com/products/agent-identification-api))
163
126
 
164
127
  Identify and verify [AI agents and other bots](https://knownagents.com/agents) in your own products, from incoming network requests. Authentication uses Web Bot Auth (HTTP message signatures), IP matching, or other available methods. This API can be useful for implementing access policies based on verified agent identity or enriching your own datasets.
165
128
 
@@ -175,28 +138,40 @@ Call `identifyAgents` to identify multiple requests at once:
175
138
  const identifications = await knownAgents.identifyAgents([
176
139
  {
177
140
  id: "request-1",
178
- request_headers: request1.headers
141
+ request_headers: request1.headers,
142
+ request_path: request1.url
179
143
  },
180
144
  {
181
145
  id: "request-2",
182
- request_headers: request2.headers
146
+ request_headers: request2.headers,
147
+ request_path: request2.url
183
148
  }
184
149
  ])
185
150
  ```
186
151
 
187
152
  These methods return an object (or array of objects) with the following fields:
188
153
 
189
- - `id`: The identifier from the request (if provided)
154
+ - `id`: The identifier from the request (if provided).
190
155
  - `result`: The identification result:
191
- - `"verified"`: The agent is identified and verified
192
- - `"verification_failed"`: The agent was identified but could not be verified
193
- - `"unknown_agent"`: The agent is not in our database
194
- - `"not_verifiable"`: The agent cannot be verified (no verification method available)
195
- - `agent_id`: The unique ID of the agent (if identified)
196
- - `agent_token`: The name of the agent (e.g. `"Googlebot"`) (if identified)
197
- - `agent_url`: The documentation URL of the agent (if identified)
198
- - `agent_type_name`: The type of agent (e.g. `"AI Agent"`) (if identified)
199
- - `operator_name`: The company behind the agent (e.g. `"Google"`) (if identified)
156
+ - `"verified"`: A known agent was identified and verified
157
+ - `"verification_failed"`: A known agent was identified, but failed verification
158
+ - `"not_verifiable"`: A known agent was identified, but no verification method was available
159
+ - `"not_identified"`: No known agent was identified
160
+ - `agent_id`: The unique ID of the identified agent.
161
+ - `agent_token`: The name of the agent (e.g. `"Claude-User"`) (if identified).
162
+ - `agent_url`: The documentation URL of the agent (if identified).
163
+ - `agent_type_name`: The type of agent (e.g. `"AI Assistant"`) (if identified).
164
+ - `operator_name`: The company operating the agent (e.g. `"Anthropic"`) (if identified).
165
+ - `is_disallowed_by_robots_txt`: Whether the identified agent is disallowed by robots.txt from accessing the `request_path`.
166
+ - `asn`: The autonomous system number associated with the request's IPv4 address.
167
+ - `asn_operator`: The operator of the recognized autonomous system associated with the request's IPv4 address.
168
+ - `asn_type`: The type of the recognized autonomous system associated with the request's IPv4 address:
169
+ - `"isp"`
170
+ - `"hosting"`
171
+ - `"business"`
172
+ - `"education"`
173
+ - `"government"`
174
+ - `automation_score`: An integer from `0` to `99` indicating the strength of heuristic evidence that the request was made by an automated client. Higher scores indicate stronger detected automation signals. A score of `0` means that no automation signals were detected, not that the client is certainly human.
200
175
 
201
176
  ## Requirements
202
177
 
@@ -1,21 +1,27 @@
1
1
  import type { IncomingMessage, ServerResponse } from "http";
2
2
  import type { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
- import type { AgentType, IdentificationRequest, IdentificationResult, TrackPageviewOrRESTCallOptions, VisitRequest } from "./types";
3
+ import type { AgentType, IdentificationRequest, IdentificationResult, KnownAgentsOptions, TrackPageviewOrRESTCallOptions, VisitRequest } from "./types";
4
4
  /**
5
5
  * Server-side client for Known Agents analytics, robots.txt generation, and agent identification.
6
6
  */
7
7
  export declare class KnownAgents {
8
8
  private readonly accessToken;
9
+ private readonly flushIntervalInMilliseconds;
10
+ private readonly flushQueueSize;
11
+ private readonly visitQueue;
12
+ private flushTimeout;
13
+ private pendingFlushPromise;
9
14
  /**
10
15
  * Creates a new instance of the Known Agents client.
11
16
  *
12
17
  * @param accessToken - Your project's access token.
18
+ * @param options - Optional batching configuration.
13
19
  */
14
- constructor(accessToken: string);
20
+ constructor(accessToken: string, options?: KnownAgentsOptions);
15
21
  /**
16
22
  * Tracks a single non-MCP pageview or REST API call for analytics.
17
- * The visit is sent after the response finishes so status, headers, and duration are included.
18
- * This method is non-blocking and handles errors internally.
23
+ * The visit is added to the visit event queue after the response finishes so status, headers, and duration are included.
24
+ * This method is non-blocking and handles upload errors internally.
19
25
  *
20
26
  * @param request - The incoming Node.js HTTP request.
21
27
  * @param response - The outgoing Node.js HTTP response.
@@ -26,7 +32,7 @@ export declare class KnownAgents {
26
32
  * Tracks a single MCP call handled by `StreamableHTTPServerTransport`.
27
33
  * Call this after connecting the transport and before the transport processes the request so request and response metadata can be captured.
28
34
  * Do not use this with transport instances shared across concurrent requests; use `trackVisits()` instead.
29
- * This method is non-blocking and handles errors internally.
35
+ * This method is non-blocking and handles upload errors internally.
30
36
  *
31
37
  * @param request - The incoming Node.js HTTP request.
32
38
  * @param response - The outgoing Node.js HTTP response.
@@ -34,13 +40,20 @@ export declare class KnownAgents {
34
40
  */
35
41
  trackMCPCall(request: IncomingMessage, response: ServerResponse, transport: StreamableHTTPServerTransport): void;
36
42
  /**
37
- * Tracks multiple visits in a batch for analytics.
38
- * This method is non-blocking and handles errors internally.
39
- * Recommended for high-traffic websites - batch visits and send periodically (e.g. every 30 seconds).
43
+ * Adds multiple visits to the visit event queue for analytics.
44
+ * This method is non-blocking and handles upload errors internally.
45
+ * All visit events are uploaded together when the queue size reaches the configured threshold or the flush interval elapses.
40
46
  *
41
47
  * @param requests - An array of visits.
42
48
  */
43
49
  trackVisits(requests: VisitRequest[]): void;
50
+ /**
51
+ * Uploads all events currently in the visit event queue and waits for pending uploads to finish.
52
+ * This method handles upload errors internally.
53
+ *
54
+ * @returns A promise that resolves after all pending upload attempts finish.
55
+ */
56
+ flush(): Promise<void>;
44
57
  /**
45
58
  * Generates a robots.txt file that disallows the specified agent types.
46
59
  * The generated file stays up-to-date with all current and future agents in the specified categories.
@@ -70,5 +83,7 @@ export declare class KnownAgents {
70
83
  * @throws {Error} If the API call fails or returns a non-200 status.
71
84
  */
72
85
  identifyAgents(requests: IdentificationRequest[]): Promise<IdentificationResult[]>;
86
+ private scheduleFlush;
87
+ private sendVisits;
73
88
  }
74
89
  //# sourceMappingURL=KnownAgents.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"KnownAgents.d.ts","sourceRoot":"","sources":["../src/KnownAgents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAA;AAC3D,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAA;AAGvG,OAAO,KAAK,EACR,SAAS,EACT,qBAAqB,EACrB,oBAAoB,EACpB,8BAA8B,EAC9B,YAAY,EACf,MAAM,SAAS,CAAA;AAEhB;;GAEG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IAEpC;;;;OAIG;gBACS,WAAW,EAAE,MAAM;IAI/B;;;;;;;;OAQG;IACH,uBAAuB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,8BAA8B,GAAG,IAAI;IAoC3H;;;;;;;;;OASG;IACH,YAAY,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,6BAA6B,GAAG,IAAI;IAqEhH;;;;;;OAMG;IACH,WAAW,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI;IA6B3C;;;;;;;;;OASG;IACG,iBAAiB,CAAC,UAAU,EAAE,SAAS,EAAE,EAAE,QAAQ,GAAE,MAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBzF;;;;;;;OAOG;IACG,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAmB5E;;;;;;;OAOG;IACG,cAAc,CAAC,QAAQ,EAAE,qBAAqB,EAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;CAsB3F"}
1
+ {"version":3,"file":"KnownAgents.d.ts","sourceRoot":"","sources":["../src/KnownAgents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAA;AAC3D,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAA;AAGvG,OAAO,KAAK,EACR,SAAS,EACT,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,8BAA8B,EAC9B,YAAY,EACf,MAAM,SAAS,CAAA;AAKhB;;GAEG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,2BAA2B,CAAQ;IACpD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,mBAAmB,CAAmC;IAE9D;;;;;OAKG;gBACS,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB;IAMjE;;;;;;;;OAQG;IACH,uBAAuB,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,8BAA8B,GAAG,IAAI;IAoC3H;;;;;;;;;OASG;IACH,YAAY,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,6BAA6B,GAAG,IAAI;IAoEhH;;;;;;OAMG;IACH,WAAW,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,IAAI;IA2B3C;;;;;OAKG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBtB;;;;;;;;;OASG;IACG,iBAAiB,CAAC,UAAU,EAAE,SAAS,EAAE,EAAE,QAAQ,GAAE,MAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBzF;;;;;;;OAOG;IACG,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAoB5E;;;;;;;OAOG;IACG,cAAc,CAAC,QAAQ,EAAE,qBAAqB,EAAE,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAsBxF,OAAO,CAAC,aAAa;YAaP,UAAU;CAmB3B"}
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KnownAgents = void 0;
4
4
  const headers_1 = require("./headers");
5
5
  const paths_1 = require("./paths");
6
+ const DEFAULT_FLUSH_INTERVAL_IN_MILLISECONDS = 10000;
7
+ const DEFAULT_FLUSH_QUEUE_SIZE = 1;
6
8
  /**
7
9
  * Server-side client for Known Agents analytics, robots.txt generation, and agent identification.
8
10
  */
@@ -11,14 +13,19 @@ class KnownAgents {
11
13
  * Creates a new instance of the Known Agents client.
12
14
  *
13
15
  * @param accessToken - Your project's access token.
16
+ * @param options - Optional batching configuration.
14
17
  */
15
- constructor(accessToken) {
18
+ constructor(accessToken, options = {}) {
19
+ this.visitQueue = [];
20
+ this.pendingFlushPromise = Promise.resolve();
16
21
  this.accessToken = accessToken;
22
+ this.flushIntervalInMilliseconds = options.flushIntervalInMilliseconds ?? DEFAULT_FLUSH_INTERVAL_IN_MILLISECONDS;
23
+ this.flushQueueSize = options.flushQueueSize ?? DEFAULT_FLUSH_QUEUE_SIZE;
17
24
  }
18
25
  /**
19
26
  * Tracks a single non-MCP pageview or REST API call for analytics.
20
- * The visit is sent after the response finishes so status, headers, and duration are included.
21
- * This method is non-blocking and handles errors internally.
27
+ * The visit is added to the visit event queue after the response finishes so status, headers, and duration are included.
28
+ * This method is non-blocking and handles upload errors internally.
22
29
  *
23
30
  * @param request - The incoming Node.js HTTP request.
24
31
  * @param response - The outgoing Node.js HTTP response.
@@ -58,7 +65,7 @@ class KnownAgents {
58
65
  * Tracks a single MCP call handled by `StreamableHTTPServerTransport`.
59
66
  * Call this after connecting the transport and before the transport processes the request so request and response metadata can be captured.
60
67
  * Do not use this with transport instances shared across concurrent requests; use `trackVisits()` instead.
61
- * This method is non-blocking and handles errors internally.
68
+ * This method is non-blocking and handles upload errors internally.
62
69
  *
63
70
  * @param request - The incoming Node.js HTTP request.
64
71
  * @param response - The outgoing Node.js HTTP response.
@@ -93,7 +100,6 @@ class KnownAgents {
93
100
  return;
94
101
  }
95
102
  const responseDurationInMilliseconds = Date.now() - created.getTime();
96
- const mcpResponseResult = mcpResponseBody.result?.structuredContent ?? mcpResponseBody.result;
97
103
  const visitRequest = {
98
104
  request_path: path,
99
105
  request_method: method,
@@ -108,8 +114,8 @@ class KnownAgents {
108
114
  mcp_response_result_is_error: mcpResponseBody.result?.isError,
109
115
  mcp_response_error_code: mcpResponseBody.error?.code,
110
116
  mcp_response_error_message: mcpResponseBody.error?.message,
111
- acp_response_body: (0, headers_1.getIsACPCall)(request.headers) ? mcpResponseResult : undefined,
112
- ucp_response_body: (0, headers_1.getIsUCPCall)(request.headers) ? mcpResponseResult : undefined,
117
+ acp_response_body: (0, headers_1.getIsACPCall)(request.headers) ? mcpResponseBody.result?.structuredContent ?? mcpResponseBody.result : undefined,
118
+ ucp_response_body: (0, headers_1.getIsUCPCall)(request.headers) ? mcpResponseBody.result?.structuredContent ?? mcpResponseBody.result : undefined,
113
119
  created: created.toISOString()
114
120
  };
115
121
  this.trackVisits([visitRequest]);
@@ -122,9 +128,9 @@ class KnownAgents {
122
128
  }
123
129
  }
124
130
  /**
125
- * Tracks multiple visits in a batch for analytics.
126
- * This method is non-blocking and handles errors internally.
127
- * Recommended for high-traffic websites - batch visits and send periodically (e.g. every 30 seconds).
131
+ * Adds multiple visits to the visit event queue for analytics.
132
+ * This method is non-blocking and handles upload errors internally.
133
+ * All visit events are uploaded together when the queue size reaches the configured threshold or the flush interval elapses.
128
134
  *
129
135
  * @param requests - An array of visits.
130
136
  */
@@ -143,16 +149,32 @@ class KnownAgents {
143
149
  if (filteredRequests.length === 0) {
144
150
  return;
145
151
  }
146
- fetch("https://api.knownagents.com/visits", {
147
- method: "POST",
148
- headers: {
149
- "Authorization": `Bearer ${this.accessToken}`,
150
- "Content-Type": "application/json"
151
- },
152
- body: JSON.stringify(filteredRequests)
153
- }).catch(error => {
154
- console.error(`Known Agents failed to track visits: ${error.message}`);
152
+ this.visitQueue.push(...filteredRequests);
153
+ if (this.visitQueue.length >= this.flushQueueSize) {
154
+ void this.flush();
155
+ return;
156
+ }
157
+ this.scheduleFlush();
158
+ }
159
+ /**
160
+ * Uploads all events currently in the visit event queue and waits for pending uploads to finish.
161
+ * This method handles upload errors internally.
162
+ *
163
+ * @returns A promise that resolves after all pending upload attempts finish.
164
+ */
165
+ flush() {
166
+ if (this.flushTimeout) {
167
+ clearTimeout(this.flushTimeout);
168
+ this.flushTimeout = undefined;
169
+ }
170
+ const requests = this.visitQueue.splice(0);
171
+ if (requests.length === 0) {
172
+ return this.pendingFlushPromise;
173
+ }
174
+ this.pendingFlushPromise = this.pendingFlushPromise.then(() => {
175
+ return this.sendVisits(requests);
155
176
  });
177
+ return this.pendingFlushPromise;
156
178
  }
157
179
  /**
158
180
  * Generates a robots.txt file that disallows the specified agent types.
@@ -199,7 +221,8 @@ class KnownAgents {
199
221
  "Content-Type": "application/json"
200
222
  },
201
223
  body: JSON.stringify({
202
- request_headers: (0, headers_1.getFilteredHeaders)(request.headers)
224
+ request_headers: (0, headers_1.getFilteredHeaders)(request.headers),
225
+ request_path: request.url
203
226
  })
204
227
  });
205
228
  if (response.ok) {
@@ -224,7 +247,7 @@ class KnownAgents {
224
247
  "Authorization": `Bearer ${this.accessToken}`,
225
248
  "Content-Type": "application/json"
226
249
  },
227
- body: JSON.stringify(requests.map(request => {
250
+ body: JSON.stringify(requests.map((request) => {
228
251
  return {
229
252
  ...request,
230
253
  request_headers: (0, headers_1.getFilteredHeaders)(request.request_headers),
@@ -238,5 +261,34 @@ class KnownAgents {
238
261
  throw new Error(`Known Agents failed to identify agents: ${response.status} ${response.statusText}`);
239
262
  }
240
263
  }
264
+ scheduleFlush() {
265
+ if (this.flushTimeout) {
266
+ return;
267
+ }
268
+ this.flushTimeout = setTimeout(() => {
269
+ this.flushTimeout = undefined;
270
+ void this.flush();
271
+ }, this.flushIntervalInMilliseconds);
272
+ this.flushTimeout.unref();
273
+ }
274
+ async sendVisits(requests) {
275
+ try {
276
+ const response = await fetch("https://api.knownagents.com/visits", {
277
+ method: "POST",
278
+ headers: {
279
+ "Authorization": `Bearer ${this.accessToken}`,
280
+ "Content-Type": "application/json"
281
+ },
282
+ body: JSON.stringify(requests)
283
+ });
284
+ if (!response.ok) {
285
+ throw new Error(`${response.status} ${response.statusText}`);
286
+ }
287
+ }
288
+ catch (error) {
289
+ const message = error instanceof Error ? error.message : String(error);
290
+ console.error(`Known Agents failed to track visits: ${message}`);
291
+ }
292
+ }
241
293
  }
242
294
  exports.KnownAgents = KnownAgents;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { KnownAgents } from "./KnownAgents";
2
- export { AgentType, type IdentificationRequest, type IdentificationResult, type TrackPageviewOrRESTCallOptions, type VisitRequest } from "./types";
2
+ export { AgentType, type IdentificationRequest, type IdentificationResult, type KnownAgentsOptions, type TrackPageviewOrRESTCallOptions, type VisitRequest } from "./types";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EACH,SAAS,EACT,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,8BAA8B,EACnC,KAAK,YAAY,EACpB,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EACH,SAAS,EACT,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,8BAA8B,EACnC,KAAK,YAAY,EACpB,MAAM,SAAS,CAAA"}
package/dist/types.d.ts CHANGED
@@ -1,4 +1,13 @@
1
1
  import type { IncomingHttpHeaders, OutgoingHttpHeaders } from "http";
2
+ /**
3
+ * Batching configuration for the Known Agents client.
4
+ */
5
+ export interface KnownAgentsOptions {
6
+ /** The maximum time a partial visit event queue waits before upload. Defaults to 10,000 milliseconds. */
7
+ flushIntervalInMilliseconds?: number;
8
+ /** The number of visit events in the queue that triggers an upload. Defaults to 1. */
9
+ flushQueueSize?: number;
10
+ }
2
11
  /**
3
12
  * Additional metadata to include when tracking a pageview or REST API call.
4
13
  */
@@ -10,7 +19,7 @@ export interface TrackPageviewOrRESTCallOptions {
10
19
  }
11
20
  /**
12
21
  * A normalized visit payload for the Known Agents API.
13
- * Use this with `trackVisits()` when batching requests yourself.
22
+ * Use this with `trackVisits()` to manually add visit events to the queue.
14
23
  */
15
24
  export interface VisitRequest {
16
25
  /** The URL path and query string of the request (e.g. "/about?foo=bar") */
@@ -55,31 +64,50 @@ export interface IdentificationRequest {
55
64
  id?: string;
56
65
  /** The HTTP request headers. Sensitive headers are removed before sending. */
57
66
  request_headers: IncomingHttpHeaders;
67
+ /** The path of the incoming request (e.g. `"/products/123"`). */
68
+ request_path?: string;
58
69
  }
59
70
  /**
60
71
  * The result of an agent identification request.
61
72
  */
62
73
  export interface IdentificationResult {
63
- /** The identifier from the request (if provided) */
74
+ /** The identifier from the request (if provided). */
64
75
  id?: string;
65
76
  /**
66
77
  * The identification result:
67
- * - "verified": The agent was identified and verified
68
- * - "verification_failed": The agent was identified but failed verification (spoofed)
69
- * - "unknown_agent": The request is from an unrecognized agent or human
70
- * - "not_verifiable": The agent was identified but cannot be verified
78
+ * - "verified": A known agent was identified and verified
79
+ * - "verification_failed": A known agent was identified, but failed verification
80
+ * - "not_verifiable": A known agent was identified, but no verification method was available
81
+ * - "not_identified": No known agent was identified
71
82
  */
72
- result: "verified" | "verification_failed" | "unknown_agent" | "not_verifiable";
73
- /** The unique ID of the identified agent */
83
+ result: "verified" | "verification_failed" | "not_verifiable" | "not_identified";
84
+ /** The unique ID of the identified agent. */
74
85
  agent_id?: string;
75
- /** The name of the identified agent (e.g. "Googlebot") */
86
+ /** The name of the agent (e.g. `"Claude-User"`) (if identified). */
76
87
  agent_token?: string;
77
- /** The documentation URL for the identified agent */
88
+ /** The documentation URL of the agent (if identified). */
78
89
  agent_url?: string;
79
- /** The type of the identified agent (e.g. "AI Agent", "Search Engine Crawler") */
90
+ /** The type of agent (e.g. `"AI Assistant"`) (if identified). */
80
91
  agent_type_name?: string;
81
- /** The company behind the identified agent (e.g. "Google") */
92
+ /** The company operating the agent (e.g. `"Anthropic"`) (if identified). */
82
93
  operator_name?: string;
94
+ /** Whether the identified agent is disallowed by robots.txt from accessing the `request_path`. */
95
+ is_disallowed_by_robots_txt?: boolean;
96
+ /** The autonomous system number associated with the request's IPv4 address. */
97
+ asn?: number;
98
+ /** The operator of the recognized autonomous system associated with the request's IPv4 address. */
99
+ asn_operator?: string;
100
+ /**
101
+ * The type of the recognized autonomous system associated with the request's IPv4 address:
102
+ * - "isp"
103
+ * - "hosting"
104
+ * - "business"
105
+ * - "education"
106
+ * - "government"
107
+ */
108
+ asn_type?: "isp" | "hosting" | "business" | "education" | "government";
109
+ /** An integer from `0` to `99` indicating the strength of heuristic evidence that the request was made by an automated client. Higher scores indicate stronger detected automation signals. A score of `0` means that no automation signals were detected, not that the client is certainly human. */
110
+ automation_score?: number;
83
111
  }
84
112
  /**
85
113
  * Agent types that can be controlled with robots.txt rules.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAA;AAEpE;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC3C,6BAA6B;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACzC,6BAA6B;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC5C;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IACzB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAA;IACpB,2CAA2C;IAC3C,cAAc,EAAE,MAAM,CAAA;IACtB,8EAA8E;IAC9E,eAAe,EAAE,mBAAmB,CAAA;IACpC,oDAAoD;IACpD,oBAAoB,CAAC,EAAE,MAAM,CAAA;IAC7B,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,mBAAmB,CAAA;IACtC,4CAA4C;IAC5C,iCAAiC,CAAC,EAAE,MAAM,CAAA;IAC1C,sCAAsC;IACtC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,gCAAgC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,kCAAkC;IAClC,4BAA4B,CAAC,EAAE,MAAM,CAAA;IACrC,qCAAqC;IACrC,+BAA+B,CAAC,EAAE,MAAM,CAAA;IACxC,kDAAkD;IAClD,4BAA4B,CAAC,EAAE,OAAO,CAAA;IACtC,kCAAkC;IAClC,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,qCAAqC;IACrC,0BAA0B,CAAC,EAAE,MAAM,CAAA;IACnC,4BAA4B;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC3C,4BAA4B;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC3C,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IAClC,4FAA4F;IAC5F,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,8EAA8E;IAC9E,eAAe,EAAE,mBAAmB,CAAA;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACjC,oDAAoD;IACpD,EAAE,CAAC,EAAE,MAAM,CAAA;IACX;;;;;;OAMG;IACH,MAAM,EAAE,UAAU,GAAG,qBAAqB,GAAG,eAAe,GAAG,gBAAgB,CAAA;IAC/E,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;GAEG;AACH,oBAAY,SAAS;IACjB,OAAO,aAAa;IACpB,WAAW,iBAAiB;IAC5B,aAAa,oBAAoB;IACjC,cAAc,qBAAqB;IACnC,aAAa,oBAAoB;IACjC,eAAe,sBAAsB;IACrC,QAAQ,aAAa;IACrB,cAAc,oBAAoB;IAClC,eAAe,qBAAqB;IACpC,OAAO,YAAY;IACnB,oBAAoB,0BAA0B;IAC9C,OAAO,YAAY;IACnB,UAAU,gBAAgB;IAC1B,mBAAmB,0BAA0B;IAC7C,eAAe,qBAAqB;IACpC,mBAAmB,0BAA0B;CAChD"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@knownagents/sdk",
3
- "version": "2.1.0",
4
- "description": "The official Node.js SDK for Known Agents: AI agent analytics (bot traffic), LLM referral tracking, automatic robots.txt, agent verification",
3
+ "version": "2.3.0",
4
+ "description": "The official Node.js SDK for Known Agents: AI agent analytics (bot traffic), LLM referral tracking, automatic robots.txt, agent identification",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,7 +32,7 @@
32
32
  "web scraping",
33
33
  "ai blocking",
34
34
  "bot blocking",
35
- "agent verification",
35
+ "agent identification",
36
36
  "bot verification",
37
37
  "verify ai agents",
38
38
  "web bot auth",
@@ -6,29 +6,41 @@ import type {
6
6
  AgentType,
7
7
  IdentificationRequest,
8
8
  IdentificationResult,
9
+ KnownAgentsOptions,
9
10
  TrackPageviewOrRESTCallOptions,
10
11
  VisitRequest
11
12
  } from "./types"
12
13
 
14
+ const DEFAULT_FLUSH_INTERVAL_IN_MILLISECONDS = 10000
15
+ const DEFAULT_FLUSH_QUEUE_SIZE = 1
16
+
13
17
  /**
14
18
  * Server-side client for Known Agents analytics, robots.txt generation, and agent identification.
15
19
  */
16
20
  export class KnownAgents {
17
21
  private readonly accessToken: string
22
+ private readonly flushIntervalInMilliseconds: number
23
+ private readonly flushQueueSize: number
24
+ private readonly visitQueue: VisitRequest[] = []
25
+ private flushTimeout: NodeJS.Timeout | undefined
26
+ private pendingFlushPromise: Promise<void> = Promise.resolve()
18
27
 
19
28
  /**
20
29
  * Creates a new instance of the Known Agents client.
21
- *
30
+ *
22
31
  * @param accessToken - Your project's access token.
32
+ * @param options - Optional batching configuration.
23
33
  */
24
- constructor(accessToken: string) {
34
+ constructor(accessToken: string, options: KnownAgentsOptions = {}) {
25
35
  this.accessToken = accessToken
36
+ this.flushIntervalInMilliseconds = options.flushIntervalInMilliseconds ?? DEFAULT_FLUSH_INTERVAL_IN_MILLISECONDS
37
+ this.flushQueueSize = options.flushQueueSize ?? DEFAULT_FLUSH_QUEUE_SIZE
26
38
  }
27
39
 
28
40
  /**
29
41
  * Tracks a single non-MCP pageview or REST API call for analytics.
30
- * The visit is sent after the response finishes so status, headers, and duration are included.
31
- * This method is non-blocking and handles errors internally.
42
+ * The visit is added to the visit event queue after the response finishes so status, headers, and duration are included.
43
+ * This method is non-blocking and handles upload errors internally.
32
44
  *
33
45
  * @param request - The incoming Node.js HTTP request.
34
46
  * @param response - The outgoing Node.js HTTP response.
@@ -74,7 +86,7 @@ export class KnownAgents {
74
86
  * Tracks a single MCP call handled by `StreamableHTTPServerTransport`.
75
87
  * Call this after connecting the transport and before the transport processes the request so request and response metadata can be captured.
76
88
  * Do not use this with transport instances shared across concurrent requests; use `trackVisits()` instead.
77
- * This method is non-blocking and handles errors internally.
89
+ * This method is non-blocking and handles upload errors internally.
78
90
  *
79
91
  * @param request - The incoming Node.js HTTP request.
80
92
  * @param response - The outgoing Node.js HTTP response.
@@ -98,7 +110,7 @@ export class KnownAgents {
98
110
  mcpRequestBody = message
99
111
  originalTransportOnMessage?.(message, extra)
100
112
  }
101
-
113
+
102
114
  const originalTransportSend = transport.send
103
115
  transport.send = async (message, options): Promise<void> => {
104
116
  mcpResponseBody = message
@@ -118,7 +130,6 @@ export class KnownAgents {
118
130
  }
119
131
 
120
132
  const responseDurationInMilliseconds = Date.now() - created.getTime()
121
- const mcpResponseResult = mcpResponseBody.result?.structuredContent ?? mcpResponseBody.result
122
133
 
123
134
  const visitRequest: VisitRequest = {
124
135
  request_path: path,
@@ -134,8 +145,8 @@ export class KnownAgents {
134
145
  mcp_response_result_is_error: mcpResponseBody.result?.isError,
135
146
  mcp_response_error_code: mcpResponseBody.error?.code,
136
147
  mcp_response_error_message: mcpResponseBody.error?.message,
137
- acp_response_body: getIsACPCall(request.headers) ? mcpResponseResult : undefined,
138
- ucp_response_body: getIsUCPCall(request.headers) ? mcpResponseResult : undefined,
148
+ acp_response_body: getIsACPCall(request.headers) ? mcpResponseBody.result?.structuredContent ?? mcpResponseBody.result : undefined,
149
+ ucp_response_body: getIsUCPCall(request.headers) ? mcpResponseBody.result?.structuredContent ?? mcpResponseBody.result : undefined,
139
150
  created: created.toISOString()
140
151
  }
141
152
 
@@ -148,11 +159,11 @@ export class KnownAgents {
148
159
  response.once("finish", sendVisit)
149
160
  }
150
161
  }
151
-
162
+
152
163
  /**
153
- * Tracks multiple visits in a batch for analytics.
154
- * This method is non-blocking and handles errors internally.
155
- * Recommended for high-traffic websites - batch visits and send periodically (e.g. every 30 seconds).
164
+ * Adds multiple visits to the visit event queue for analytics.
165
+ * This method is non-blocking and handles upload errors internally.
166
+ * All visit events are uploaded together when the queue size reaches the configured threshold or the flush interval elapses.
156
167
  *
157
168
  * @param requests - An array of visits.
158
169
  */
@@ -173,18 +184,41 @@ export class KnownAgents {
173
184
  return
174
185
  }
175
186
 
176
- fetch("https://api.knownagents.com/visits", {
177
- method: "POST",
178
- headers: {
179
- "Authorization": `Bearer ${this.accessToken}`,
180
- "Content-Type": "application/json"
181
- },
182
- body: JSON.stringify(filteredRequests)
183
- }).catch(error => {
184
- console.error(`Known Agents failed to track visits: ${error.message}`)
187
+ this.visitQueue.push(...filteredRequests)
188
+
189
+ if (this.visitQueue.length >= this.flushQueueSize) {
190
+ void this.flush()
191
+ return
192
+ }
193
+
194
+ this.scheduleFlush()
195
+ }
196
+
197
+ /**
198
+ * Uploads all events currently in the visit event queue and waits for pending uploads to finish.
199
+ * This method handles upload errors internally.
200
+ *
201
+ * @returns A promise that resolves after all pending upload attempts finish.
202
+ */
203
+ flush(): Promise<void> {
204
+ if (this.flushTimeout) {
205
+ clearTimeout(this.flushTimeout)
206
+ this.flushTimeout = undefined
207
+ }
208
+
209
+ const requests = this.visitQueue.splice(0)
210
+
211
+ if (requests.length === 0) {
212
+ return this.pendingFlushPromise
213
+ }
214
+
215
+ this.pendingFlushPromise = this.pendingFlushPromise.then(() => {
216
+ return this.sendVisits(requests)
185
217
  })
218
+
219
+ return this.pendingFlushPromise
186
220
  }
187
-
221
+
188
222
  /**
189
223
  * Generates a robots.txt file that disallows the specified agent types.
190
224
  * The generated file stays up-to-date with all current and future agents in the specified categories.
@@ -214,7 +248,7 @@ export class KnownAgents {
214
248
  throw new Error(`Known Agents failed to generate robots.txt: ${response.status} ${response.statusText}`)
215
249
  }
216
250
  }
217
-
251
+
218
252
  /**
219
253
  * Identifies and verifies a single agent from an HTTP request.
220
254
  * Uses Web Bot Auth (HTTP message signatures), IP matching, or other available methods.
@@ -231,7 +265,8 @@ export class KnownAgents {
231
265
  "Content-Type": "application/json"
232
266
  },
233
267
  body: JSON.stringify({
234
- request_headers: getFilteredHeaders(request.headers)
268
+ request_headers: getFilteredHeaders(request.headers),
269
+ request_path: request.url
235
270
  })
236
271
  })
237
272
 
@@ -257,7 +292,7 @@ export class KnownAgents {
257
292
  "Authorization": `Bearer ${this.accessToken}`,
258
293
  "Content-Type": "application/json"
259
294
  },
260
- body: JSON.stringify(requests.map(request => {
295
+ body: JSON.stringify(requests.map((request) => {
261
296
  return {
262
297
  ...request,
263
298
  request_headers: getFilteredHeaders(request.request_headers),
@@ -272,4 +307,36 @@ export class KnownAgents {
272
307
  }
273
308
  }
274
309
 
310
+ private scheduleFlush(): void {
311
+ if (this.flushTimeout) {
312
+ return
313
+ }
314
+
315
+ this.flushTimeout = setTimeout(() => {
316
+ this.flushTimeout = undefined
317
+ void this.flush()
318
+ }, this.flushIntervalInMilliseconds)
319
+
320
+ this.flushTimeout.unref()
321
+ }
322
+
323
+ private async sendVisits(requests: VisitRequest[]): Promise<void> {
324
+ try {
325
+ const response = await fetch("https://api.knownagents.com/visits", {
326
+ method: "POST",
327
+ headers: {
328
+ "Authorization": `Bearer ${this.accessToken}`,
329
+ "Content-Type": "application/json"
330
+ },
331
+ body: JSON.stringify(requests)
332
+ })
333
+
334
+ if (!response.ok) {
335
+ throw new Error(`${response.status} ${response.statusText}`)
336
+ }
337
+ } catch (error) {
338
+ const message = error instanceof Error ? error.message : String(error)
339
+ console.error(`Known Agents failed to track visits: ${message}`)
340
+ }
341
+ }
275
342
  }
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export {
3
3
  AgentType,
4
4
  type IdentificationRequest,
5
5
  type IdentificationResult,
6
+ type KnownAgentsOptions,
6
7
  type TrackPageviewOrRESTCallOptions,
7
8
  type VisitRequest
8
9
  } from "./types"
package/src/types.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import type { IncomingHttpHeaders, OutgoingHttpHeaders } from "http"
2
2
 
3
+ /**
4
+ * Batching configuration for the Known Agents client.
5
+ */
6
+ export interface KnownAgentsOptions {
7
+ /** The maximum time a partial visit event queue waits before upload. Defaults to 10,000 milliseconds. */
8
+ flushIntervalInMilliseconds?: number
9
+ /** The number of visit events in the queue that triggers an upload. Defaults to 1. */
10
+ flushQueueSize?: number
11
+ }
12
+
3
13
  /**
4
14
  * Additional metadata to include when tracking a pageview or REST API call.
5
15
  */
@@ -12,7 +22,7 @@ export interface TrackPageviewOrRESTCallOptions {
12
22
 
13
23
  /**
14
24
  * A normalized visit payload for the Known Agents API.
15
- * Use this with `trackVisits()` when batching requests yourself.
25
+ * Use this with `trackVisits()` to manually add visit events to the queue.
16
26
  */
17
27
  export interface VisitRequest {
18
28
  /** The URL path and query string of the request (e.g. "/about?foo=bar") */
@@ -58,32 +68,51 @@ export interface IdentificationRequest {
58
68
  id?: string
59
69
  /** The HTTP request headers. Sensitive headers are removed before sending. */
60
70
  request_headers: IncomingHttpHeaders
71
+ /** The path of the incoming request (e.g. `"/products/123"`). */
72
+ request_path?: string
61
73
  }
62
74
 
63
75
  /**
64
76
  * The result of an agent identification request.
65
77
  */
66
78
  export interface IdentificationResult {
67
- /** The identifier from the request (if provided) */
79
+ /** The identifier from the request (if provided). */
68
80
  id?: string
69
81
  /**
70
82
  * The identification result:
71
- * - "verified": The agent was identified and verified
72
- * - "verification_failed": The agent was identified but failed verification (spoofed)
73
- * - "unknown_agent": The request is from an unrecognized agent or human
74
- * - "not_verifiable": The agent was identified but cannot be verified
83
+ * - "verified": A known agent was identified and verified
84
+ * - "verification_failed": A known agent was identified, but failed verification
85
+ * - "not_verifiable": A known agent was identified, but no verification method was available
86
+ * - "not_identified": No known agent was identified
75
87
  */
76
- result: "verified" | "verification_failed" | "unknown_agent" | "not_verifiable"
77
- /** The unique ID of the identified agent */
88
+ result: "verified" | "verification_failed" | "not_verifiable" | "not_identified"
89
+ /** The unique ID of the identified agent. */
78
90
  agent_id?: string
79
- /** The name of the identified agent (e.g. "Googlebot") */
91
+ /** The name of the agent (e.g. `"Claude-User"`) (if identified). */
80
92
  agent_token?: string
81
- /** The documentation URL for the identified agent */
93
+ /** The documentation URL of the agent (if identified). */
82
94
  agent_url?: string
83
- /** The type of the identified agent (e.g. "AI Agent", "Search Engine Crawler") */
95
+ /** The type of agent (e.g. `"AI Assistant"`) (if identified). */
84
96
  agent_type_name?: string
85
- /** The company behind the identified agent (e.g. "Google") */
97
+ /** The company operating the agent (e.g. `"Anthropic"`) (if identified). */
86
98
  operator_name?: string
99
+ /** Whether the identified agent is disallowed by robots.txt from accessing the `request_path`. */
100
+ is_disallowed_by_robots_txt?: boolean
101
+ /** The autonomous system number associated with the request's IPv4 address. */
102
+ asn?: number
103
+ /** The operator of the recognized autonomous system associated with the request's IPv4 address. */
104
+ asn_operator?: string
105
+ /**
106
+ * The type of the recognized autonomous system associated with the request's IPv4 address:
107
+ * - "isp"
108
+ * - "hosting"
109
+ * - "business"
110
+ * - "education"
111
+ * - "government"
112
+ */
113
+ asn_type?: "isp" | "hosting" | "business" | "education" | "government"
114
+ /** An integer from `0` to `99` indicating the strength of heuristic evidence that the request was made by an automated client. Higher scores indicate stronger detected automation signals. A score of `0` means that no automation signals were detected, not that the client is certainly human. */
115
+ automation_score?: number
87
116
  }
88
117
 
89
118
  /**
@@ -1,28 +0,0 @@
1
- import type { IncomingMessage } from "http";
2
- import type { RESTACPResponseBody, RESTUCPResponseBody } from "./types";
3
- interface MCPCommerceResponseBody {
4
- result?: {
5
- currency?: string;
6
- totals?: {
7
- type?: string;
8
- amount?: number;
9
- }[];
10
- structuredContent?: {
11
- currency?: string;
12
- totals?: {
13
- type?: string;
14
- amount?: number;
15
- }[];
16
- };
17
- };
18
- }
19
- export declare function getRESTACPCurrencyIfPossible(restACPResponseBody: RESTACPResponseBody): string | undefined;
20
- export declare function getRESTACPTotalAmountIfPossible(restACPResponseBody: RESTACPResponseBody): number | undefined;
21
- export declare function getMCPACPCurrencyIfPossible(request: IncomingMessage, mcpResponseBody: MCPCommerceResponseBody): string | undefined;
22
- export declare function getMCPACPTotalAmountIfPossible(request: IncomingMessage, mcpResponseBody: MCPCommerceResponseBody): number | undefined;
23
- export declare function getRESTUCPCurrencyIfPossible(restUCPResponseBody: RESTUCPResponseBody): string | undefined;
24
- export declare function getRESTUCPTotalAmountIfPossible(restUCPResponseBody: RESTUCPResponseBody): number | undefined;
25
- export declare function getMCPUCPCurrencyIfPossible(request: IncomingMessage, mcpResponseBody: MCPCommerceResponseBody): string | undefined;
26
- export declare function getMCPUCPTotalAmountIfPossible(request: IncomingMessage, mcpResponseBody: MCPCommerceResponseBody): number | undefined;
27
- export {};
28
- //# sourceMappingURL=commerce.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"commerce.d.ts","sourceRoot":"","sources":["../src/commerce.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAE3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAEvE,UAAU,uBAAuB;IAC7B,MAAM,CAAC,EAAE;QACL,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,MAAM,CAAC,EAAE;YACL,IAAI,CAAC,EAAE,MAAM,CAAA;YACb,MAAM,CAAC,EAAE,MAAM,CAAA;SAClB,EAAE,CAAA;QACH,iBAAiB,CAAC,EAAE;YAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;YACjB,MAAM,CAAC,EAAE;gBACL,IAAI,CAAC,EAAE,MAAM,CAAA;gBACb,MAAM,CAAC,EAAE,MAAM,CAAA;aAClB,EAAE,CAAA;SACN,CAAA;KACJ,CAAA;CACJ;AAED,wBAAgB,4BAA4B,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAEzG;AAED,wBAAgB,+BAA+B,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAI5G;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAGlI;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAOrI;AAED,wBAAgB,4BAA4B,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAEzG;AAED,wBAAgB,+BAA+B,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,MAAM,GAAG,SAAS,CAI5G;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAGlI;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAOrI"}
package/dist/commerce.js DELETED
@@ -1,51 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getRESTACPCurrencyIfPossible = getRESTACPCurrencyIfPossible;
4
- exports.getRESTACPTotalAmountIfPossible = getRESTACPTotalAmountIfPossible;
5
- exports.getMCPACPCurrencyIfPossible = getMCPACPCurrencyIfPossible;
6
- exports.getMCPACPTotalAmountIfPossible = getMCPACPTotalAmountIfPossible;
7
- exports.getRESTUCPCurrencyIfPossible = getRESTUCPCurrencyIfPossible;
8
- exports.getRESTUCPTotalAmountIfPossible = getRESTUCPTotalAmountIfPossible;
9
- exports.getMCPUCPCurrencyIfPossible = getMCPUCPCurrencyIfPossible;
10
- exports.getMCPUCPTotalAmountIfPossible = getMCPUCPTotalAmountIfPossible;
11
- const headers_1 = require("./headers");
12
- function getRESTACPCurrencyIfPossible(restACPResponseBody) {
13
- return restACPResponseBody?.currency;
14
- }
15
- function getRESTACPTotalAmountIfPossible(restACPResponseBody) {
16
- return restACPResponseBody?.totals?.find((total) => {
17
- return total.type == "total";
18
- })?.amount;
19
- }
20
- function getMCPACPCurrencyIfPossible(request, mcpResponseBody) {
21
- const isProbablyACP = !(0, headers_1.getIsUCPCall)(request.headers) && (0, headers_1.getIsProbablyACPCall)(request.headers);
22
- return isProbablyACP ? mcpResponseBody.result?.currency : undefined;
23
- }
24
- function getMCPACPTotalAmountIfPossible(request, mcpResponseBody) {
25
- const isProbablyACP = !(0, headers_1.getIsUCPCall)(request.headers) && (0, headers_1.getIsProbablyACPCall)(request.headers);
26
- return isProbablyACP
27
- ? mcpResponseBody.result?.totals?.find((total) => {
28
- return total.type == "total";
29
- })?.amount
30
- : undefined;
31
- }
32
- function getRESTUCPCurrencyIfPossible(restUCPResponseBody) {
33
- return restUCPResponseBody?.currency;
34
- }
35
- function getRESTUCPTotalAmountIfPossible(restUCPResponseBody) {
36
- return restUCPResponseBody?.totals?.find((total) => {
37
- return total.type == "total";
38
- })?.amount;
39
- }
40
- function getMCPUCPCurrencyIfPossible(request, mcpResponseBody) {
41
- const isUCP = (0, headers_1.getIsUCPCall)(request.headers);
42
- return isUCP ? mcpResponseBody.result?.structuredContent?.currency : undefined;
43
- }
44
- function getMCPUCPTotalAmountIfPossible(request, mcpResponseBody) {
45
- const isUCP = (0, headers_1.getIsUCPCall)(request.headers);
46
- return isUCP
47
- ? mcpResponseBody.result?.structuredContent?.totals?.find((total) => {
48
- return total.type == "total";
49
- })?.amount
50
- : undefined;
51
- }