mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
8483fe23f9
## 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>
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { run } from "../src/run.js";
|
|
|
|
const toolsClientMock = vi.hoisted(() => ({
|
|
fetchTool: vi.fn(),
|
|
callTool: vi.fn(),
|
|
}));
|
|
|
|
const telemetryMock = vi.hoisted(() => ({
|
|
init: vi.fn(),
|
|
shutdown: vi.fn(async () => undefined),
|
|
track: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@argent/tools-client", () => ({
|
|
createToolsClient: vi.fn(() => toolsClientMock),
|
|
}));
|
|
|
|
vi.mock("@argent/telemetry", () => telemetryMock);
|
|
|
|
const toolMeta = {
|
|
name: "sample-tool",
|
|
description: "Sample tool",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {},
|
|
},
|
|
};
|
|
|
|
describe("argent run telemetry", () => {
|
|
let exitSpy: ReturnType<typeof vi.spyOn>;
|
|
let errorSpy: ReturnType<typeof vi.spyOn>;
|
|
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
toolsClientMock.fetchTool.mockResolvedValue(toolMeta);
|
|
toolsClientMock.callTool.mockResolvedValue({ data: { ok: true } });
|
|
exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
|
|
throw new Error(`process.exit:${code}`);
|
|
}) as typeof process.exit);
|
|
errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
exitSpy.mockRestore();
|
|
errorSpy.mockRestore();
|
|
logSpy.mockRestore();
|
|
});
|
|
|
|
it("emits cli:run_fail, not tool:fail, when tool-server call fails", async () => {
|
|
toolsClientMock.callTool.mockRejectedValue(new Error("server already tracked this"));
|
|
|
|
await expect(run(["sample-tool"], { paths: {} as never })).rejects.toThrow("process.exit:1");
|
|
|
|
expect(console.error).toHaveBeenCalledWith("server already tracked this");
|
|
expect(telemetryMock.track).toHaveBeenCalledWith(
|
|
"cli:run_fail",
|
|
expect.objectContaining({
|
|
tool: "sample-tool",
|
|
error_code: "CLI_RUN_TOOL_CALL_FAILED",
|
|
failure_stage: "cli_run_call_tool",
|
|
failure_area: "cli",
|
|
error_kind: "unknown",
|
|
})
|
|
);
|
|
expect(telemetryMock.track).not.toHaveBeenCalledWith("tool:fail", expect.anything());
|
|
expect(telemetryMock.shutdown).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("emits cli:run_fail, not tool:fail, for local CLI argument parsing failures", async () => {
|
|
await expect(
|
|
run(["sample-tool", "--args", "not-json"], { paths: {} as never })
|
|
).rejects.toThrow("process.exit:2");
|
|
|
|
expect(toolsClientMock.callTool).not.toHaveBeenCalled();
|
|
expect(telemetryMock.track).toHaveBeenCalledWith(
|
|
"cli:run_fail",
|
|
expect.objectContaining({
|
|
tool: "sample-tool",
|
|
error_code: "CLI_RUN_ARGS_JSON_INVALID",
|
|
failure_stage: "cli_run_parse_raw_args",
|
|
failure_area: "cli",
|
|
error_kind: "validation",
|
|
})
|
|
);
|
|
expect(telemetryMock.track).not.toHaveBeenCalledWith("tool:fail", expect.anything());
|
|
expect(telemetryMock.shutdown).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|