fix(cli): make telemetry opt-out durable (#2852)

* fix(cli): make telemetry opt-out durable

* fix(cli): make telemetry status trustworthy
This commit is contained in:
Miguel Ángel
2026-07-28 20:35:54 +02:00
committed by GitHub
parent c691869e22
commit 880021411d
13 changed files with 491 additions and 52 deletions
+146
View File
@@ -0,0 +1,146 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const baseConfig = {
telemetryEnabled: true,
anonymousId: "test-install",
telemetryNoticeShown: true,
commandCount: 7,
renderSuccessCount: 0,
lastFeedbackPromptAt: 0,
};
async function loadTelemetryCommand(options?: {
writeSucceeds?: boolean;
configEnabled?: boolean;
devMode?: boolean;
apiKey?: string;
}) {
const config = {
...baseConfig,
telemetryEnabled: options?.configEnabled ?? true,
};
const writeConfigWithResult = vi.fn(() =>
options?.writeSucceeds === false
? { ok: false as const, error: "EACCES: permission denied" }
: { ok: true as const },
);
vi.resetModules();
vi.doMock("../telemetry/config.js", () => ({
CONFIG_PATH: "/test/.hyperframes/config.json",
readConfig: () => {
throw new Error("telemetry commands must bypass stale cached config");
},
readConfigFresh: () => ({ ...config }),
writeConfigWithResult,
}));
vi.doMock("../utils/env.js", () => ({
isDevMode: () => options?.devMode ?? false,
}));
vi.doMock("../telemetry/transport.js", () => ({
POSTHOG_API_KEY: options?.apiKey ?? "phc_test",
}));
const module = await import("./telemetry.js");
return { command: module.default, writeConfigWithResult };
}
async function runSubcommand(
command: Awaited<ReturnType<typeof loadTelemetryCommand>>["command"],
subcommand: string,
): Promise<void> {
await command.run?.({
args: { subcommand },
rawArgs: [subcommand],
cmd: command,
} as never);
}
async function runWithCapturedOutput(
command: Awaited<ReturnType<typeof loadTelemetryCommand>>["command"],
subcommand: string,
): Promise<string> {
const lines: string[] = [];
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
lines.push(args.map(String).join(" "));
});
await runSubcommand(command, subcommand);
return lines.join("\n");
}
describe("telemetry command", () => {
afterEach(() => {
vi.doUnmock("../telemetry/config.js");
vi.doUnmock("../utils/env.js");
vi.doUnmock("../telemetry/transport.js");
vi.restoreAllMocks();
vi.resetModules();
delete process.env["HYPERFRAMES_NO_TELEMETRY"];
delete process.env["DO_NOT_TRACK"];
});
it("persists disable from a fresh config snapshot", async () => {
const { command, writeConfigWithResult } = await loadTelemetryCommand();
vi.spyOn(console, "log").mockImplementation(() => undefined);
await runSubcommand(command, "disable");
expect(writeConfigWithResult).toHaveBeenCalledWith(
expect.objectContaining({ telemetryEnabled: false }),
);
});
it("fails instead of claiming success when the preference cannot be persisted", async () => {
const { command } = await loadTelemetryCommand({ writeSucceeds: false });
const stdout = vi.spyOn(console, "log").mockImplementation(() => undefined);
const stderr = vi.spyOn(console, "error").mockImplementation(() => undefined);
await expect(runSubcommand(command, "disable")).rejects.toMatchObject({
name: "CliRuntimeError",
});
expect(stderr).toHaveBeenCalledWith(expect.stringContaining("Could not persist"));
expect(stderr).toHaveBeenCalledWith(expect.stringContaining("EACCES: permission denied"));
expect(stdout).not.toHaveBeenCalledWith(expect.stringContaining("Telemetry disabled"));
});
it("reports the effective env-var opt-out instead of the stored preference", async () => {
process.env["HYPERFRAMES_NO_TELEMETRY"] = "1";
const { command } = await loadTelemetryCommand({ configEnabled: true });
const output = await runWithCapturedOutput(command, "status");
expect(output).toContain("disabled");
expect(output).toContain("HYPERFRAMES_NO_TELEMETRY");
expect(output).toContain("Tracked commands:");
});
it("reports DO_NOT_TRACK as the effective opt-out source", async () => {
process.env["DO_NOT_TRACK"] = "1";
const { command } = await loadTelemetryCommand({ configEnabled: true });
const output = await runWithCapturedOutput(command, "status");
expect(output).toContain("DO_NOT_TRACK");
});
it.each([
["enable", "Telemetry preference"],
["disable", "Telemetry disabled"],
])("explains the effective override after telemetry %s", async (subcommand, expectedSuccess) => {
process.env["HYPERFRAMES_NO_TELEMETRY"] = "true";
const { command } = await loadTelemetryCommand({ configEnabled: subcommand === "disable" });
const output = await runWithCapturedOutput(command, subcommand);
expect(output).toContain(expectedSuccess);
expect(output).toContain("remains disabled");
expect(output).toContain("HYPERFRAMES_NO_TELEMETRY");
});
it("reports dev mode as the effective source", async () => {
const { command } = await loadTelemetryCommand({ devMode: true });
const output = await runWithCapturedOutput(command, "status");
expect(output).toContain("disabled");
expect(output).toContain("dev_mode");
});
it("reports a telemetry-disabled build as the effective source", async () => {
const { command } = await loadTelemetryCommand({ apiKey: "disabled" });
const output = await runWithCapturedOutput(command, "status");
expect(output).toContain("disabled");
expect(output).toContain("telemetry_disabled_build");
});
});