Skip to content

Commit b0e54b2

Browse files
authored
[wrangler] Add AI agent detection to analytics events (#11820)
* [wrangler] Add AI agent detection to analytics events * fix: skip process ancestry checks to avoid wmic errors on Windows The am-i-vibing library's detectAgenticEnvironment function uses process-ancestry to check for running processes. When processAncestry is undefined, it calls getProcessAncestry() which uses execSync('ps ...') to walk up the entire process tree. For each provider with processChecks (6 providers), it would call getProcessAncestry() separately, causing: - Massive slowdowns (tests took 78+ seconds instead of 3 seconds) - Timeouts in CI environments (especially on Linux) - Race conditions where output wasn't captured properly By passing an empty array [], we skip process tree traversal entirely. Environment variable detection is sufficient for identifying most agentic environments (Claude Code, Cursor, Windsurf, etc.) and is instantaneous.
1 parent 2203af4 commit b0e54b2

File tree

7 files changed

+125
-4
lines changed

7 files changed

+125
-4
lines changed

.changeset/agentic-analytics.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Add AI agent detection to analytics events
6+
7+
Wrangler now detects when commands are executed by AI coding agents (such as Claude Code, Cursor, GitHub Copilot, etc.) using the `am-i-vibing` library. This information is included as an `agent` property in all analytics events, helping Cloudflare understand how developers interact with Wrangler through AI assistants.
8+
9+
The `agent` property will contain the agent ID (e.g., `"claude-code"`, `"cursor-agent"`) when detected, or `null` when running outside an agentic environment.

packages/wrangler/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"@types/yargs": "^17.0.22",
113113
"@vitest/ui": "catalog:default",
114114
"@webcontainer/env": "^1.1.0",
115+
"am-i-vibing": "^0.1.0",
115116
"capnweb": "^0.1.0",
116117
"chalk": "^5.2.0",
117118
"chokidar": "^4.0.1",

packages/wrangler/src/__tests__/metrics.test.ts

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from "node:fs";
22
import { writeWranglerConfig } from "@cloudflare/workers-utils/test-helpers";
3+
import { detectAgenticEnvironment } from "am-i-vibing";
34
import { http, HttpResponse } from "msw";
45
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
56
import { CI } from "../is-ci";
@@ -26,6 +27,7 @@ import { runInTempDir } from "./helpers/run-in-tmp";
2627
import { runWrangler } from "./helpers/run-wrangler";
2728
import type { MockInstance } from "vitest";
2829

30+
vi.mock("am-i-vibing");
2931
vi.mock("../metrics/helpers");
3032
vi.mock("../metrics/send-event");
3133
vi.mock("../package-manager");
@@ -82,6 +84,16 @@ describe("metrics", () => {
8284
});
8385

8486
describe("sendAdhocEvent()", () => {
87+
beforeEach(() => {
88+
// Default: no agent detected
89+
vi.mocked(detectAgenticEnvironment).mockReturnValue({
90+
isAgentic: false,
91+
id: null,
92+
name: null,
93+
type: null,
94+
});
95+
});
96+
8597
it("should send a request to the default URL", async () => {
8698
const requests = mockMetricRequest();
8799

@@ -92,7 +104,7 @@ describe("metrics", () => {
92104
await Promise.all(dispatcher.requests);
93105
expect(requests.count).toBe(1);
94106
expect(std.debug).toMatchInlineSnapshot(
95-
`"Metrics dispatcher: Posting data {\\"deviceId\\":\\"f82b1f46-eb7b-4154-aa9f-ce95f23b2288\\",\\"event\\":\\"some-event\\",\\"timestamp\\":1733961600000,\\"properties\\":{\\"category\\":\\"Workers\\",\\"wranglerVersion\\":\\"1.2.3\\",\\"wranglerMajorVersion\\":1,\\"wranglerMinorVersion\\":2,\\"wranglerPatchVersion\\":3,\\"os\\":\\"foo:bar\\",\\"a\\":1,\\"b\\":2}}"`
107+
`"Metrics dispatcher: Posting data {\\"deviceId\\":\\"f82b1f46-eb7b-4154-aa9f-ce95f23b2288\\",\\"event\\":\\"some-event\\",\\"timestamp\\":1733961600000,\\"properties\\":{\\"category\\":\\"Workers\\",\\"wranglerVersion\\":\\"1.2.3\\",\\"wranglerMajorVersion\\":1,\\"wranglerMinorVersion\\":2,\\"wranglerPatchVersion\\":3,\\"os\\":\\"foo:bar\\",\\"agent\\":null,\\"a\\":1,\\"b\\":2}}"`
96108
);
97109
expect(std.out).toMatchInlineSnapshot(`""`);
98110
expect(std.warn).toMatchInlineSnapshot(`""`);
@@ -123,7 +135,7 @@ describe("metrics", () => {
123135

124136
expect(requests.count).toBe(0);
125137
expect(std.debug).toMatchInlineSnapshot(
126-
`"Metrics dispatcher: Dispatching disabled - would have sent {\\"deviceId\\":\\"f82b1f46-eb7b-4154-aa9f-ce95f23b2288\\",\\"event\\":\\"some-event\\",\\"timestamp\\":1733961600000,\\"properties\\":{\\"category\\":\\"Workers\\",\\"wranglerVersion\\":\\"1.2.3\\",\\"wranglerMajorVersion\\":1,\\"wranglerMinorVersion\\":2,\\"wranglerPatchVersion\\":3,\\"os\\":\\"foo:bar\\",\\"a\\":1,\\"b\\":2}}."`
138+
`"Metrics dispatcher: Dispatching disabled - would have sent {\\"deviceId\\":\\"f82b1f46-eb7b-4154-aa9f-ce95f23b2288\\",\\"event\\":\\"some-event\\",\\"timestamp\\":1733961600000,\\"properties\\":{\\"category\\":\\"Workers\\",\\"wranglerVersion\\":\\"1.2.3\\",\\"wranglerMajorVersion\\":1,\\"wranglerMinorVersion\\":2,\\"wranglerPatchVersion\\":3,\\"os\\":\\"foo:bar\\",\\"agent\\":null,\\"a\\":1,\\"b\\":2}}."`
127139
);
128140
expect(std.out).toMatchInlineSnapshot(`""`);
129141
expect(std.warn).toMatchInlineSnapshot(`""`);
@@ -143,7 +155,7 @@ describe("metrics", () => {
143155
await Promise.all(dispatcher.requests);
144156

145157
expect(std.debug).toMatchInlineSnapshot(`
146-
"Metrics dispatcher: Posting data {\\"deviceId\\":\\"f82b1f46-eb7b-4154-aa9f-ce95f23b2288\\",\\"event\\":\\"some-event\\",\\"timestamp\\":1733961600000,\\"properties\\":{\\"category\\":\\"Workers\\",\\"wranglerVersion\\":\\"1.2.3\\",\\"wranglerMajorVersion\\":1,\\"wranglerMinorVersion\\":2,\\"wranglerPatchVersion\\":3,\\"os\\":\\"foo:bar\\",\\"a\\":1,\\"b\\":2}}
158+
"Metrics dispatcher: Posting data {\\"deviceId\\":\\"f82b1f46-eb7b-4154-aa9f-ce95f23b2288\\",\\"event\\":\\"some-event\\",\\"timestamp\\":1733961600000,\\"properties\\":{\\"category\\":\\"Workers\\",\\"wranglerVersion\\":\\"1.2.3\\",\\"wranglerMajorVersion\\":1,\\"wranglerMinorVersion\\":2,\\"wranglerPatchVersion\\":3,\\"os\\":\\"foo:bar\\",\\"agent\\":null,\\"a\\":1,\\"b\\":2}}
147159
Metrics dispatcher: Failed to send request: Failed to fetch"
148160
`);
149161
expect(std.out).toMatchInlineSnapshot(`""`);
@@ -162,12 +174,47 @@ describe("metrics", () => {
162174

163175
expect(requests.count).toBe(0);
164176
expect(std.debug).toMatchInlineSnapshot(
165-
`"Metrics dispatcher: Source Key not provided. Be sure to initialize before sending events {\\"deviceId\\":\\"f82b1f46-eb7b-4154-aa9f-ce95f23b2288\\",\\"event\\":\\"some-event\\",\\"timestamp\\":1733961600000,\\"properties\\":{\\"category\\":\\"Workers\\",\\"wranglerVersion\\":\\"1.2.3\\",\\"wranglerMajorVersion\\":1,\\"wranglerMinorVersion\\":2,\\"wranglerPatchVersion\\":3,\\"os\\":\\"foo:bar\\",\\"a\\":1,\\"b\\":2}}"`
177+
`"Metrics dispatcher: Source Key not provided. Be sure to initialize before sending events {\\"deviceId\\":\\"f82b1f46-eb7b-4154-aa9f-ce95f23b2288\\",\\"event\\":\\"some-event\\",\\"timestamp\\":1733961600000,\\"properties\\":{\\"category\\":\\"Workers\\",\\"wranglerVersion\\":\\"1.2.3\\",\\"wranglerMajorVersion\\":1,\\"wranglerMinorVersion\\":2,\\"wranglerPatchVersion\\":3,\\"os\\":\\"foo:bar\\",\\"agent\\":null,\\"a\\":1,\\"b\\":2}}"`
166178
);
167179
expect(std.out).toMatchInlineSnapshot(`""`);
168180
expect(std.warn).toMatchInlineSnapshot(`""`);
169181
expect(std.err).toMatchInlineSnapshot(`""`);
170182
});
183+
184+
it("should include agent ID when detected", async () => {
185+
vi.mocked(detectAgenticEnvironment).mockReturnValue({
186+
isAgentic: true,
187+
id: "claude-code",
188+
name: "Claude Code",
189+
type: "agent",
190+
});
191+
192+
const requests = mockMetricRequest();
193+
const dispatcher = getMetricsDispatcher({
194+
sendMetrics: true,
195+
});
196+
dispatcher.sendAdhocEvent("some-event", { a: 1 });
197+
await Promise.all(dispatcher.requests);
198+
199+
expect(requests.count).toBe(1);
200+
expect(std.debug).toContain('"agent":"claude-code"');
201+
});
202+
203+
it("should set agent to null if detection throws", async () => {
204+
vi.mocked(detectAgenticEnvironment).mockImplementation(() => {
205+
throw new Error("Detection failed");
206+
});
207+
208+
const requests = mockMetricRequest();
209+
const dispatcher = getMetricsDispatcher({
210+
sendMetrics: true,
211+
});
212+
dispatcher.sendAdhocEvent("some-event", { a: 1 });
213+
await Promise.all(dispatcher.requests);
214+
215+
expect(requests.count).toBe(1);
216+
expect(std.debug).toContain('"agent":null');
217+
});
171218
});
172219

173220
it("should keep track of all requests made", async () => {
@@ -207,10 +254,18 @@ describe("metrics", () => {
207254
hasAssets: false,
208255
argsUsed: [],
209256
argsCombination: "",
257+
agent: null,
210258
command: "wrangler docs",
211259
args: {},
212260
};
213261
beforeEach(() => {
262+
// Default: no agent detected
263+
vi.mocked(detectAgenticEnvironment).mockReturnValue({
264+
isAgentic: false,
265+
id: null,
266+
name: null,
267+
type: null,
268+
});
214269
globalThis.ALGOLIA_APP_ID = "FAKE-ID";
215270
globalThis.ALGOLIA_PUBLIC_KEY = "FAKE-KEY";
216271
msw.use(
@@ -488,6 +543,23 @@ describe("metrics", () => {
488543
expect(std.debug).toContain('"errorMessage":"yargs validation error"');
489544
});
490545

546+
it("should include agent ID in command events when detected", async () => {
547+
vi.mocked(detectAgenticEnvironment).mockReturnValue({
548+
isAgentic: true,
549+
id: "cursor-agent",
550+
name: "Cursor Agent",
551+
type: "agent",
552+
});
553+
554+
writeWranglerConfig();
555+
const requests = mockMetricRequest();
556+
557+
await runWrangler("docs arg");
558+
559+
expect(requests.count).toBe(2);
560+
expect(std.debug).toContain('"agent":"cursor-agent"');
561+
});
562+
491563
describe("banner", () => {
492564
beforeEach(() => {
493565
vi.mocked(getWranglerVersion).mockReturnValue("1.2.3");

packages/wrangler/src/metrics/metrics-dispatcher.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { configFormat } from "@cloudflare/workers-utils";
2+
import { detectAgenticEnvironment } from "am-i-vibing";
23
import chalk from "chalk";
34
import { fetch } from "undici";
45
import isInteractive from "../is-interactive";
@@ -64,6 +65,19 @@ export function getMetricsDispatcher(options: MetricsConfigOptions) {
6465
const amplitude_session_id = Date.now();
6566
let amplitude_event_id = 0;
6667

68+
// Detect agent environment once when dispatcher is created
69+
// Pass empty array for processAncestry to skip process tree checks entirely.
70+
// Process tree traversal uses execSync('ps ...') which is slow and can cause
71+
// timeouts, especially in CI environments. Environment variable detection
72+
// is sufficient for identifying most agentic environments.
73+
let agent: string | null = null;
74+
try {
75+
const agentDetection = detectAgenticEnvironment(process.env, []);
76+
agent = agentDetection.id;
77+
} catch {
78+
// Silent failure - agent remains null
79+
}
80+
6781
return {
6882
/**
6983
* This doesn't have a session id and is not tied to the command events.
@@ -82,6 +96,7 @@ export function getMetricsDispatcher(options: MetricsConfigOptions) {
8296
wranglerMinorVersion,
8397
wranglerPatchVersion,
8498
os: getOS(),
99+
agent,
85100
...properties,
86101
},
87102
});
@@ -140,6 +155,7 @@ export function getMetricsDispatcher(options: MetricsConfigOptions) {
140155
hasAssets: options.hasAssets ?? false,
141156
argsUsed: sanitizedArgsKeys,
142157
argsCombination: sanitizedArgsKeys.join(", "),
158+
agent,
143159
};
144160

145161
// get the args where we don't want to redact their values

packages/wrangler/src/metrics/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export type CommonEventProperties = {
6565
* Same as argsUsed except concatenated for convenience in Amplitude
6666
*/
6767
argsCombination: string;
68+
/**
69+
* The detected AI agent environment ID, if any (e.g., "claude-code", "cursor-agent").
70+
* Null if not running in an agentic environment.
71+
*/
72+
agent: string | null;
6873
};
6974

7075
/** We send a metrics event at the start and end of a command run */

packages/wrangler/telemetry.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Telemetry in Wrangler allows us to better identify bugs and gain visibility on u
2626
- The format of the Wrangler configuration file (e.g. `toml`, `jsonc`)
2727
- Total session duration of the command run (e.g. 3 seconds, etc.)
2828
- Whether the Wrangler client is running in CI or in an interactive instance
29+
- Whether the command was executed by an AI coding agent (e.g. Claude Code, Cursor, GitHub Copilot), and if so, which agent
2930
- Error _type_ (e.g. `APIError` or `UserError`), and sanitised error messages that will not include user information like filepaths or stack traces (e.g. `Asset too large`).
3031
- General machine information such as OS and OS Version
3132

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)