Files
Hubert Gancarczyk 8483fe23f9 feat: add telemetry (#274)
## Summary

Adds anonymous, opt-out telemetry for Argent installer, update/uninstall
flows, CLI telemetry controls, tool-server lifecycle, and tool
invocation health signals.
## What Changed
- Added a new private `@argent/telemetry` package
- Introduced typed telemetry events with runtime property
allowlisting/sanitization.
- Added lazy anonymous identity creation in `~/.argent/telemetry-id`.
- Added consent handling via: `argent telemetry enable`, `argent
telemetry disable`, `argent telemetry status`
- Added local debug logging with `ARGENT_TELEMETRY_DEBUG=1`.
- Tracks installer funnel events for init, global install/update
decisions, editor selection, allowlist decisions, skill installation,
rules/agents copy, update, and uninstall.
- Tracks tool-server start/stop and tool invocation/complete/fail
events.
- Adds per-tool invocation IDs so request metadata can be correlated
without relying on ordering.
- Records only coarse platform metadata (`ios` / `android`) for device
tools
- Ensures short-lived commands drain telemetry with bounded shutdown
before exit.
- Wires telemetry package into bundling and injects full Argent CLI
version metadata at build time.
- Added central `FAILURE_CODES` and `FailureSignal` helpers in
`@argent/registry`.
- Attached structured, sanitized failure metadata across registry
errors, tool-server tools/services, HTTP validation/dependency gates,
installer flows, and `argent run`.
- Added telemetry allowlisting for safe failure fields only: static
error codes/stages, coarse area/kind/command, bounded exit codes,
allowlisted signals/spawn codes, and coarse network failure classes.
- Split CLI-local `argent run` failures into `cli:run_fail` so they are
not double-counted as tool failures.
- Added installer telemetry for failed package actions, update/uninstall
failures, rules/agents copy results, and skill refresh results.
- Made telemetry consent writes atomic and kept telemetry status
side-effect free via `peekAnonId()`.
- Adds a coarse, anonymous signal to tool telemetry: which AI coding
tool is driving the MCP server (Claude Code, Codex, Cursor, Gemini, VS
Code, Windsurf, Zed, opencode, Copilot, or other).

## Tests Added / Updated

- Telemetry unit coverage for consent precedence, identity creation,
base props, sanitization, PostHog config, debug logging, shutdown,
opt-out, and forget behavior.
- Registry/tool-server tests for invocation IDs, platform metadata
boundaries, startup failure flushing, and tool lifecycle telemetry.
- Installer tests for final telemetry drain, update action
classification, and uninstall identity cleanup ordering.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 18:22:34 +02:00

62 lines
2.0 KiB
TypeScript

import vendors from "ci-info/vendors.json";
type VendorEnv =
| string
| { env: string; includes: string }
| { any: string[] }
| Record<string, string>;
interface VendorDefinition {
env: VendorEnv | VendorEnv[];
}
// Copied verbatim from ci-info 4.4.0's `isCI` detector: the same generic env
// vars and the `CI === "false"` bypass in isCi() below. ci-info is the de-facto
// ecosystem standard (npm, Jest, etc.), so we inherit exactly its false-positive
// surface — narrowing this list would diverge from it and risk false negatives
// (missing real Jenkins/TeamCity/TaskCluster CI). We re-implement rather than
// import ci-info's `isCI` because that value is computed once at import time;
// this wrapper takes `env` lazily so tests can inject it.
const GENERIC_CI_ENV_VARS = [
"BUILD_ID",
"BUILD_NUMBER",
"CI",
"CI_APP_ID",
"CI_BUILD_ID",
"CI_BUILD_NUMBER",
"CI_NAME",
"CONTINUOUS_INTEGRATION",
"RUN_ID",
] as const;
function checkEnv(env: NodeJS.ProcessEnv, def: VendorEnv): boolean {
if (typeof def === "string") return Boolean(env[def]);
if ("env" in def) {
const value = env[def.env];
return Boolean(value && value.includes(def.includes));
}
if ("any" in def && Array.isArray(def.any)) {
return def.any.some((key: string) => Boolean(env[key]));
}
return Object.entries(def).every(([key, value]) => env[key] === value);
}
function isKnownVendorCi(env: NodeJS.ProcessEnv): boolean {
return (vendors as VendorDefinition[]).some((vendor) => {
const defs = Array.isArray(vendor.env) ? vendor.env : [vendor.env];
return defs.every((def) => checkEnv(env, def));
});
}
export function isCi(env: NodeJS.ProcessEnv = process.env): boolean {
if (env.CI === "false") return false;
if (GENERIC_CI_ENV_VARS.some((name) => Boolean(env[name]))) return true;
return isKnownVendorCi(env);
}
/** Exposed for vitest coverage assertions; do not import outside tests. */
export const _CI_VENDOR_COUNT_FOR_TEST: number = (vendors as VendorDefinition[]).length;