Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/cli/src/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";

import {
analyticsUserId,
Expand All @@ -8,6 +8,10 @@ import {
telemetryCredential,
} from "./analytics.js";

afterEach(() => {
vi.unstubAllEnvs();
});

describe("analyticsUserId", () => {
it("reports a run nothing identified as anonymous, not as an empty identity", () => {
expect(analyticsUserId("")).toBe("anonymous");
Expand Down Expand Up @@ -124,10 +128,12 @@ describe("getErrorAnalyticsEventProperties", () => {
{
version: "2.33.2",
ci: true,
agent: "codex",
},
);

expect(properties).toMatchObject({
agent: "codex",
ci: true,
errCode: "API_ERROR",
message: "branch already exists",
Expand Down Expand Up @@ -168,4 +174,17 @@ describe("getAnalyticsEventProperties", () => {
}).flags.output,
).toBe("secret-value");
});

it("attributes commands run by a coding agent", () => {
vi.stubEnv("CODEX_CI", undefined);
vi.stubEnv("CODEX_THREAD_ID", undefined);
vi.stubEnv("CODEX_SESSION_ID", undefined);
vi.stubEnv("CLAUDE_CODE_CHILD_SESSION", "1");

expect(
getAnalyticsEventProperties({
_: ["branches", "list"],
}).agent,
).toBe("claude-code");
});
});
6 changes: 5 additions & 1 deletion packages/cli/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type AuthContext, getAuthContext } from "./auth_context.js";
import { credentialsPath } from "./config.js";
import { isCurrentBranchProbe } from "./context.js";
import { storeFor } from "./credential_io.js";
import { getGithubEnvVars, isCi } from "./env.js";
import { getCliAgent, getGithubEnvVars, isCi } from "./env.js";
import type { ErrorCode } from "./errors.js";
import { log } from "./log.js";
import pkg from "./pkg.js";
Expand Down Expand Up @@ -124,12 +124,14 @@ type AnalyticsEventProperties = {
output: string | undefined;
};
ci: boolean;
agent: ReturnType<typeof getCliAgent>;
githubEnvVars: ReturnType<typeof getGithubEnvVars>;
};

type ErrorEventContext = {
version: string;
ci: boolean;
agent: ReturnType<typeof getCliAgent>;
};

/**
Expand Down Expand Up @@ -331,6 +333,7 @@ const getErrorAnalyticsEventContext = (
): ErrorEventContext => ({
version: pkg.version,
ci: isCi(),
agent: getCliAgent(process.env),
});

export const getAnalyticsEventProperties = (
Expand All @@ -342,5 +345,6 @@ export const getAnalyticsEventProperties = (
output: args.output,
},
ci: isCi(),
agent: getCliAgent(process.env),
githubEnvVars: getGithubEnvVars(process.env),
});
71 changes: 70 additions & 1 deletion packages/cli/src/env.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,74 @@
import { describe, expect, it } from "vitest";
import { getGithubEnvVars } from "./env";
import { getCliAgent, getGithubEnvVars } from "./env";

describe("getCliAgent", () => {
it("attributes a Claude Code child session", () => {
expect(getCliAgent({ CLAUDE_CODE_CHILD_SESSION: "1" })).toBe(
"claude-code",
);
});

it("attributes a Codex thread", () => {
expect(
getCliAgent({
CODEX_CI: "1",
CODEX_THREAD_ID: "thread-123",
}),
).toBe("codex");
});

it("attributes a Codex session", () => {
expect(
getCliAgent({
CODEX_CI: "1",
CODEX_SESSION_ID: "session-123",
}),
).toBe("codex");
});

it("does not attribute a user-initiated Codex shell command", () => {
expect(
getCliAgent({
CODEX_THREAD_ID: "thread-123",
CODEX_SESSION_ID: "session-123",
}),
).toBeUndefined();
});

it("does not attribute disabled or empty direct markers", () => {
expect(
getCliAgent({
CLAUDE_CODE_CHILD_SESSION: "false",
CODEX_THREAD_ID: "",
CODEX_SESSION_ID: " ",
}),
).toBeUndefined();
});

it("does not attribute ambient or sandbox utility markers", () => {
expect(
getCliAgent({
CLAUDECODE: "1",
CLAUDE_CODE: "1",
CLAUDE_CLI: "1",
CODEX: "1",
CODEX_CI: "1",
CODEX_SANDBOX: "seatbelt",
CODEX_SANDBOX_NETWORK_DISABLED: "1",
}),
).toBeUndefined();
});

it("omits attribution when nested agent markers conflict", () => {
expect(
getCliAgent({
CLAUDE_CODE_CHILD_SESSION: "1",
CODEX_CI: "1",
CODEX_THREAD_ID: "thread-123",
}),
).toBeUndefined();
});
});

describe("getGithubEnvVars", () => {
it("success all keys", () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ export const isDebug = () => {
return Boolean(process.env.DEBUG);
};

export type CliAgent = "claude-code" | "codex";

export const getCliAgent = (env: NodeJS.Dict<string>): CliAgent | undefined => {
const claudeCode = env.CLAUDE_CODE_CHILD_SESSION === "1";
const codex =
env.CODEX_CI === "1" &&
(Boolean(env.CODEX_THREAD_ID?.trim()) ||
Boolean(env.CODEX_SESSION_ID?.trim()));

if (claudeCode === codex) return undefined;
return claudeCode ? "claude-code" : "codex";
};

export const getGithubEnvVars = (env: NodeJS.Dict<string>) => {
const vars = [
// github action info
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/init/agent_snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,8 @@ async function runInit(
});
}

const child = spawn(process.execPath, [CLI, "init", ...args], {
const cliArgs = [CLI, "--no-analytics", "init", ...args];
const child = spawn(process.execPath, cliArgs, {
cwd,
stdio: ["ignore", "pipe", "pipe"],
env: {
Expand All @@ -355,7 +356,6 @@ async function runInit(
NEON_CONFIG_DIR: configDir,
NEON_STUB_LOG: stubLog,
NEON_BOOTSTRAP_MANIFEST_URL: manifestUrl,
NEON_NO_ANALYTICS: "1",
CI: "true",
NO_COLOR: "1",
FORCE_COLOR: "0",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/init/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,15 +204,15 @@ describe("`neon init` failure output", { timeout: 20_000 }, () => {
writeFileSync(resolve(configDir, "credentials.json"), credentials, {
mode: 0o600,
});
const result = spawnSync(process.execPath, [CLI, "init", ...args], {
const cliArgs = [CLI, "--no-analytics", "init", ...args];
const result = spawnSync(process.execPath, cliArgs, {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
env: {
PATH: process.env.PATH ?? "",
HOME: process.env.HOME ?? "",
NEON_CONFIG_DIR: configDir,
NEON_NO_ANALYTICS: "1",
},
});
return { ...result, configDir };
Expand Down
Loading