@devtune/ai-traffic 0.12.0 → 0.13.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
@@ -1,6 +1,6 @@
1
1
  # @devtune/ai-traffic
2
2
 
3
- Measure which AI crawlers visit your site with a dependency-free sensor that filters at the edge and forwards only matched machine traffic to DevTune.
3
+ Measure which AI crawlers visit your site, and the visitors AI answers send you, with a dependency-free sensor that filters at the edge and forwards only matched requests to DevTune.
4
4
 
5
5
  ## Install
6
6
 
@@ -14,7 +14,7 @@ Create a project-scoped server-side ingest key and expose it only to your server
14
14
  DEVTUNE_AI_TRAFFIC_INGEST_KEY=dt_ingest_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
15
15
  ```
16
16
 
17
- On Vercel you can use a project ID instead of a key. See [Vercel without an ingest key](#vercel-without-an-ingest-key).
17
+ On Vercel you can use a project ID instead of a key. See [Vercel OIDC without an ingest key](#vercel-oidc-without-an-ingest-key).
18
18
 
19
19
  ## Quickstarts
20
20
 
@@ -76,9 +76,9 @@ createServer((request, response) => {
76
76
 
77
77
  The hook records the final `response.statusCode` without blocking or changing the response.
78
78
 
79
- ## Vercel without an ingest key
79
+ ## Vercel OIDC without an ingest key
80
80
 
81
- On Vercel, the sensor can authenticate with each deployment's Vercel OIDC token instead of an ingest key, so there is no long-lived secret to store. Vercel linking must be enabled for your DevTune account while it is in beta.
81
+ On Vercel, the sensor can authenticate with each deployment's Vercel OIDC token instead of an ingest key, so there is no long-lived secret to store.
82
82
 
83
83
  1. In DevTune, open **Settings › Integrations › Edge middleware** and select **Add Vercel project**. Enter the Vercel team ID, shown as **Team ID** in the team's **Settings › General**, and the Vercel project ID, shown as **Project ID** in the project's **Settings › General**.
84
84
  2. Copy the **DevTune project ID** from the same page and add it to the Vercel project's environment variables. It is not a secret.
@@ -100,13 +100,15 @@ On Vercel, the sensor can authenticate with each deployment's Vercel OIDC token
100
100
 
101
101
  The same `projectId` option works for the Express and Node HTTP adapters running on Vercel.
102
102
 
103
+ If you write your own adapter with `projectId` set, and it skips some requests or calls `trackRequest()` after the response, call the client's `captureVercelOidcToken(request)` on every request as it arrives, before any filtering. Vercel puts a current token on every request, so browser requests keep the token for pending sends current.
104
+
103
105
  When both options are set, the sensor sends with the Vercel token and falls back to the ingest key if Vercel or DevTune cannot verify it. With only `projectId`, nothing is sent from a host that is not Vercel, and the sensor logs one warning per process. If a link stays **Not seen yet** after a deploy that received crawler traffic, check both Vercel IDs.
104
106
 
105
107
  The token is read from each incoming request, never from the environment, and is sent only to Vercel to obtain a token for DevTune. That token is cached in memory until shortly before it expires and is sent only to the ingest endpoint.
106
108
 
107
109
  ## Privacy by Design
108
110
 
109
- The sensor filters requests where your application runs. It uses a cheap user-agent hint before consulting the AI bot registry, and only matched machine hits are eligible to be forwarded. It uses no cookies, no fingerprinting, and no full request logs.
111
+ The sensor filters requests where your application runs. It forwards two kinds of request and nothing else: a request whose user agent matches the AI bot registry, checked after a cheap user-agent hint, and a page load that arrived from an AI answer (see [AI Referral Visits](#ai-referral-visits)). It uses no cookies, no fingerprinting, and no full request logs.
110
112
 
111
113
  Requests that a page makes for itself are never forwarded. A crawler that renders pages runs their JavaScript, and its browser then fires prefetches, client navigations and other `fetch()` calls from inside each page. Counting them would report crawls of pages it only fetched in the background; on DevTune's own site, one rendering crawler made three such requests to `/` for every page it loaded. The sensor recognises them from the fetch metadata the browser attaches, so no configuration is needed:
112
114
 
@@ -138,8 +140,22 @@ For a matched crawler, DevTune receives only:
138
140
  - timestamp
139
141
  - the address the request arrived from, used to verify the crawler and then discarded
140
142
  - for a signed agent, its Web Bot Auth signature material, used to verify the agent and then discarded (see [Signed Agents](#signed-agents))
143
+ - whether the request came through a CDN (see [CDN Flag](#cdn-flag))
144
+
145
+ For an AI referral visit, DevTune receives only:
146
+
147
+ - `path`
148
+ - origin plus path, with query parameters removed
149
+ - user agent
150
+ - response status when the adapter can know it
151
+ - timestamp
152
+ - the referring site's origin, never the rest of the `Referer`
153
+ - the `utm_source` and `utm_medium` values, and whether an `oppref` parameter was present, never its value
154
+ - whether the request came through a CDN
155
+
156
+ The page path is sent as it is, so a path that carries a token or an identifier, such as `/invite/abc123`, is sent too. A referral visit never carries an address or signature material.
141
157
 
142
- Request bodies, cookies, unrelated headers, and query strings are never sent. Signature material is only forwarded for a request without a query, so a reset link, OAuth callback or search URL a signed agent follows never leaves the site. Only these request headers are ever forwarded with a signature: `accept`, `accept-encoding`, `accept-language`, `cache-control`, `content-length`, `content-type`, `date`, `from`, `host` and `user-agent`, exported as `WEB_BOT_AUTH_FORWARDED_HEADERS`. A request whose signature covers any other header, such as `cookie`, `authorization` or an API key, forwards no signature material at all, so credentials never leave the site. An address is sent only for a request whose user agent matches the AI bot registry, so ordinary browser traffic never reaches DevTune at all. Note the limit of that statement: the match is on the user agent, and anyone can send a crawler's user agent, so a person whose client does that would have their address sent. That is the same fact the verification exists to handle, and it is why the address is checked and discarded rather than stored. DevTune checks it against the claimed operator's published networks on receipt and keeps the verdict, not the address. Set `trustedProxy: "none"` to send no address at all; DevTune then records that traffic as unverified.
158
+ Request bodies, cookies, unrelated headers, and other query parameters are never sent. Signature material is only forwarded for a request without a query, so a reset link, OAuth callback or search URL a signed agent follows never leaves the site. Only these request headers are ever forwarded with a signature: `accept`, `accept-encoding`, `accept-language`, `cache-control`, `content-length`, `content-type`, `date`, `from`, `host` and `user-agent`, exported as `WEB_BOT_AUTH_FORWARDED_HEADERS`. A request whose signature covers any other header, such as `cookie`, `authorization` or an API key, forwards no signature material at all, so credentials never leave the site. An address is sent only for a request whose user agent matches the AI bot registry. Referral visits never carry one, and other browser traffic never reaches DevTune at all. Note the limit of that statement: the match is on the user agent, and anyone can send a crawler's user agent, so a person whose client does that would have their address sent. That is the same fact the verification exists to handle, and it is why the address is checked and discarded rather than stored. DevTune checks it against the claimed operator's published networks on receipt and keeps the verdict, not the address. Set `trustedProxy: "none"` to send no address at all; DevTune then records that traffic as unverified.
143
159
 
144
160
  ## Bot Verification
145
161
 
@@ -241,9 +257,54 @@ const match = detectAiCrawler(userAgent, {
241
257
  Ingest and registry requests include an `x-devtune-sdk-version` header with the package version, so
242
258
  DevTune can show an upgrade notice when a newer release is available. It carries no visitor data.
243
259
 
244
- ## Machine Traffic and Human Referrals
260
+ ## AI Referral Visits
261
+
262
+ From 0.13.0 the sensor also records people who click through to your site from an AI answer. Because it runs on your server, it counts visitors whose browser blocks DevTune's tracking snippet.
263
+
264
+ Set `captureReferrals: false` to record AI bot requests only, as before 0.13.0. Every adapter takes it with its other options:
265
+
266
+ ```ts
267
+ const trackAiTraffic = createDevTuneAiTrafficMiddleware({
268
+ ingestKey: process.env.DEVTUNE_AI_TRAFFIC_INGEST_KEY!,
269
+ captureReferrals: false,
270
+ });
271
+ ```
272
+
273
+ A request is a referral visit when all of these hold:
245
274
 
246
- The sensor deliberately measures machine traffic only. DevTune gets human visits from AI products through its GA4 integration, where sessions, engagement, and conversions provide a richer picture than middleware referrer matching. The referrer detector is available for local classification, but the sensor adapters never forward human referral visits.
275
+ - it is a `GET`;
276
+ - it came from another site: its `Sec-Fetch-Site` is not `same-origin` or `same-site`;
277
+ - it is a top-level page load: its `Sec-Fetch-Dest` is `document` or it has no fetch metadata, so a frame load is checked as a crawler only;
278
+ - it is not a request made from inside a page and not a framework data request, the same rules as for crawlers (see [Privacy by Design](#privacy-by-design));
279
+ - its `Referer` host, or failing that its `utm_source`, is one of the registry's AI referral sources, such as `chatgpt.com` or `utm_source=perplexity`.
280
+
281
+ A `Referer` host also matches its subdomains. A `utm_source` matches case-insensitively, either as a listed source or as a hostname on the hosts list. A request whose user agent matches the AI bot registry is always recorded as a bot request, whatever its `Referer`.
282
+
283
+ The referral sources arrive with the bot patterns in each registry refresh, so a new source needs no upgrade. The package bundles DevTune's lists, which it uses before the first refresh. A refresh whose referral sources are missing or unusable, as from a registry older than referral capture, keeps the last good lists.
284
+
285
+ DevTune classifies every event again on receipt. A paid click, one whose `utm_medium` is a paid medium such as `cpc` or `paid_ai`, or that carries OpenAI's `oppref` ad click ID, is not counted as an AI referral. Where the adapter knows the response status, a page load answered with a 301, 302, 303, 307 or 308 is not recorded, because the visit is counted where the redirect lands. Next.js middleware runs before the response and sends no status, so see the [limit on redirects](https://docs.devtune.ai/ai-traffic/edge-middleware#limits) there.
286
+
287
+ `trackRequest()` returns `{ kind: "referral", platform, event }` for a referral visit and `{ kind: "bot", entry, event }` for a crawler, or `null`:
288
+
289
+ ```ts
290
+ const match = aiTraffic.trackRequest(request, event);
291
+
292
+ if (match?.kind === "bot") {
293
+ console.log(match.entry.label);
294
+ }
295
+ ```
296
+
297
+ Referral visits wait in their own queue, bounded by `maxQueuedReferralEvents` (default 200), and have their own retry budget, so a burst of them never pushes out crawler events or spends their retries. Crawler events are sent first, and a batch never mixes the two (see [Rate-Limit Retries](#rate-limit-retries)).
298
+
299
+ If you write your own adapter, pass `method` and the full request `url`, query included. Without either the sensor records crawlers only. Fetch requests and the Express and Node adapters already provide both.
300
+
301
+ For when DevTune treats the sensor's referral count for a day as complete, and its limits, see [AI referral visits](https://docs.devtune.ai/ai-traffic/edge-middleware#ai-referral-visits).
302
+
303
+ ### CDN Flag
304
+
305
+ Every event carries `cdn: true` when its request came through a CDN that marks what it forwards: Cloudflare (`cf-ray`), CloudFront (`x-amz-cf-id`) or Fastly (any `fastly-*` header). A cache in front of your app can answer page loads the sensor never sees, so DevTune does not treat the sensor's referral count for that host and day as complete. A cache that adds none of these headers is not detected.
306
+
307
+ A custom adapter's `HeadersLike` can provide `keys()` so any `fastly-*` header is found. Without it, only `fastly-client-ip`, `fastly-ff` and `fastly-ssl` are checked.
247
308
 
248
309
  ## Production Notes
249
310
 
@@ -267,7 +328,11 @@ Registry refreshes use `waitUntil` when provided, with a five-second fetch timeo
267
328
 
268
329
  A `429` from the ingest endpoint is the one failure the server tells us is temporary, so the batch is redelivered rather than dropped. The client waits for the response's `Retry-After` — delay-seconds or an HTTP date — and falls back to exponential backoff from 250 ms when the header is absent or unusable. The batch being retried is held intact, so events that arrive during a backoff are sent separately rather than folded into it.
269
330
 
270
- Retries are bounded so a short-lived runtime cannot be held open by a throttled endpoint. The budget belongs to one flush, not to each batch: `maxRetryAttempts` defaults to 3 and `maxRetryDelayMs` caps each wait at 5000 ms, including a `Retry-After` longer than the cap, so a flush adds at most 15 s no matter how deep the queue is. When the budget is spent the current batch is dropped with the usual sampled warning and the drain stops; whatever is behind it stays queued for the next flush, which gets a fresh budget. `maxRetryAttempts: 0` disables waiting entirely: a 429 drops the batch on its first rejection, as it did before this behaviour existed. The drain still stops there rather than dropping every batch behind it, so the rest stays queued. Tune both:
331
+ Retries are bounded so a short-lived runtime cannot be held open by a throttled endpoint. The budget belongs to one flush, not to each batch, and crawler events and referral visits each get their own: `maxRetryAttempts` defaults to 3 and `maxRetryDelayMs` caps each wait at 5000 ms, including a `Retry-After` longer than the cap, so a flush adds at most 15 s per kind, 30 s in all, no matter how deep the queues are. When a budget is spent the current batch is dropped with the usual sampled warning. If it was the crawler budget the flush ends there, because referral visits would meet the same limit; if it was the referral budget, crawler events keep draining. A referral batch rate-limited while crawler events are waiting goes back to the front of the referral queue, and the crawler events go first.
332
+
333
+ What a spent budget leaves queued gets one follow-up flush with fresh budgets, crawler events first, after `maxRetryDelayMs` or `flushIntervalMs`, whichever is longer. If the follow-up is throttled too it schedules none, and the rest waits for the next tracked request. The follow-up runs only in a long-lived process: its timer does not keep a Node process open and is not tied to `waitUntil`, so it does not survive a Vercel Function, Edge or Workers response. A tracked request that fills a batch still flushes immediately.
334
+
335
+ `maxRetryAttempts: 0` disables waiting entirely: a 429 drops the batch on its first rejection, as it did before this behaviour existed. The drain still stops there rather than dropping every batch behind it, so the rest stays queued. Tune both:
271
336
 
272
337
  ```ts
273
338
  const trackAiTraffic = createDevTuneAiTrafficMiddleware({
@@ -277,7 +342,7 @@ const trackAiTraffic = createDevTuneAiTrafficMiddleware({
277
342
  });
278
343
  ```
279
344
 
280
- Because backing off lets the queue drain slower than traffic arrives, the queue itself is bounded. `maxQueuedEvents` defaults to 1000; past it the oldest events are shed with a sampled warning, so a sustained throttle cannot grow memory without limit.
345
+ Because backing off lets the queue drain slower than traffic arrives, the queue itself is bounded. `maxQueuedEvents` defaults to 1000; past it the oldest events are shed with a sampled warning, so a sustained throttle cannot grow memory without limit. Referral visits have their own bound, `maxQueuedReferralEvents`, which defaults to 200 and sheds only referral visits.
281
346
 
282
347
  Non-429 responses and network errors are unchanged: the batch is dropped after a single attempt, so a hard outage never keeps the queue alive. The one exception is a `503` to a batch sent with a Vercel token and no ingest key configured: DevTune could not reach Vercel to verify the token, so it is retried like a `429` within the same budget.
283
348
 
@@ -318,7 +383,7 @@ an AI client or its model provider.
318
383
 
319
384
  From 0.5.0, new platform names and traffic classes can arrive through the remote registry without a package upgrade. DevTune handles classification and identity verification on its servers. The SDK keeps validating match patterns before using them.
320
385
 
321
- Clients start with the bundled snapshot. The first request, then the first request after the one-hour cache expires, triggers a background refresh from `https://devtune.ai/api/v1/llm-traffic/registry`. This includes ordinary browser requests, but the registry GET contains no visitor data and only matching machine requests are sent to ingest. Node, Express and middleware use the same policy. Concurrent requests share one refresh, and `ETag` avoids downloading an unchanged registry.
386
+ Clients start with the bundled snapshot. The first request, then the first request after the one-hour cache expires, triggers a background refresh from `https://devtune.ai/api/v1/llm-traffic/registry`. This includes ordinary browser requests, but the registry GET contains no visitor data and only matched requests are sent to ingest. The refresh brings the AI referral sources along with the bot patterns. Node, Express and middleware use the same policy. Concurrent requests share one refresh, and `ETag` avoids downloading an unchanged registry.
322
387
 
323
388
  A failed refresh keeps the last working registry, or the bundled snapshot before the first successful refresh. Retries wait for the next cache interval. Requests use the currently cached rules while a refresh is in progress, so a newly added bot may be missed until that refresh completes. Create the client or middleware once per process or runtime instance to reuse its cache.
324
389
 
@@ -339,7 +404,7 @@ const trackAiTraffic = createDevTuneAiTrafficNodeHandler({
339
404
 
340
405
  ### Failure Behavior
341
406
 
342
- Registry refresh and ingest sends are fire-and-forget and guarded. Network failures may drop telemetry, but they do not block, throw into, or alter the customer response. The ingest endpoint and batched wire format are unchanged.
407
+ Registry refresh and ingest sends are fire-and-forget and guarded. Network failures may drop telemetry, but they do not block, throw into, or alter the customer response.
343
408
 
344
409
  ## Documentation
345
410