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
7 changes: 7 additions & 0 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2262,8 +2262,15 @@ telemetry disabled it prints
npx hyperframes telemetry enable
npx hyperframes telemetry disable
npx hyperframes telemetry status
npx hyperframes telemetry status --json
```

Use `telemetry status --json` to check telemetry from an agent or script. It
returns `enabled`, `source`, `configPath`, `statePath`, and `commandCount`.
`enabled` reflects the effective state after environment
and build overrides, and `source` names the setting or override that determines it.
`--json` is supported only by `status`.

Telemetry collects command names, render performance, render checkpoint and
error names, aggregate browser diagnostic counts, browser initialization
duration and tween count, aggregate video-extraction workload counts (extracted
Expand Down
41 changes: 38 additions & 3 deletions packages/cli/src/commands/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,25 @@ async function loadTelemetryCommand(options?: {
async function runSubcommand(
command: Awaited<ReturnType<typeof loadTelemetryCommand>>["command"],
subcommand: string,
json = false,
): Promise<void> {
await command.run?.({
args: { subcommand },
rawArgs: [subcommand],
args: { subcommand, json },
rawArgs: json ? [subcommand, "--json"] : [subcommand],
cmd: command,
} as never);
}

async function runWithCapturedOutput(
command: Awaited<ReturnType<typeof loadTelemetryCommand>>["command"],
subcommand: string,
json = false,
): Promise<string> {
const lines: string[] = [];
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
lines.push(args.map(String).join(" "));
});
await runSubcommand(command, subcommand);
await runSubcommand(command, subcommand, json);
return lines.join("\n");
}

Expand All @@ -91,6 +93,39 @@ describe("telemetry command", () => {
);
});

it.each([
{ configEnabled: true, enabled: true, source: "config" },
{ configEnabled: false, enabled: false, source: "config" },
{ env: "HYPERFRAMES_NO_TELEMETRY", enabled: false, source: "HYPERFRAMES_NO_TELEMETRY" },
{ env: "DO_NOT_TRACK", enabled: false, source: "DO_NOT_TRACK" },
{ devMode: true, enabled: false, source: "dev_mode" },
{ apiKey: "disabled", enabled: false, source: "telemetry_disabled_build" },
])("reports JSON status from $source with enabled=$enabled", async (options) => {
if (options.env) process.env[options.env] = "1";
const { command, writeConfigWithResult } = await loadTelemetryCommand(options);

const output = await runWithCapturedOutput(command, "status", true);

expect(JSON.parse(output)).toEqual({
enabled: options.enabled,
source: options.source,
configPath: "/test/.hyperframes/config.json",
statePath: "/test/.local/state/hyperframes/install-state.json",
commandCount: 7,
});
expect(writeConfigWithResult).not.toHaveBeenCalled();
});

it.each(["enable", "disable", ""])("rejects JSON for telemetry %s", async (subcommand) => {
const { command, writeConfigWithResult } = await loadTelemetryCommand();

await expect(runSubcommand(command, subcommand, true)).rejects.toMatchObject({
name: "CliUsageError",
message: "--json is only supported for telemetry status",
});
expect(writeConfigWithResult).not.toHaveBeenCalled();
});

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);
Expand Down
25 changes: 22 additions & 3 deletions packages/cli/src/commands/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "../telemetry/config.js";
import { effectiveTelemetryStatus, type TelemetryStatusSource } from "../telemetry/policy.js";
import { c } from "../ui/colors.js";
import { failCommand, failUsage } from "../utils/commandResult.js";
import { CliUsageError, failCommand, failUsage } from "../utils/commandResult.js";
import type { Example } from "./_examples.js";

export const examples: Example[] = [
Expand Down Expand Up @@ -53,9 +53,24 @@ function setTelemetryEnabled(enabled: boolean): void {
console.log();
}

function runStatus(): void {
function runStatus(json: boolean): void {
const config = readConfigFresh();
const effective = effectiveTelemetryStatus(config.telemetryEnabled);
if (json) {
console.log(
JSON.stringify(
{
...effective,
configPath: CONFIG_PATH,
statePath: STATE_PATH,
commandCount: config.commandCount,
},
null,
2,
),
);
return;
}
const status = effective.enabled ? c.success("enabled") : c.dim("disabled");
console.log();
console.log(` ${c.dim("Status:")} ${status}`);
Expand All @@ -76,6 +91,7 @@ function runStatus(): void {
export default defineCommand({
meta: { name: "telemetry", description: "Manage usage telemetry" },
args: {
json: { type: "boolean", description: "Output status as JSON", default: false },
subcommand: {
type: "positional",
description: "Subcommand: enable, disable, status",
Expand All @@ -84,6 +100,9 @@ export default defineCommand({
},
async run({ args }) {
const subcommand = args.subcommand;
if (args.json && subcommand !== "status") {
throw new CliUsageError("--json is only supported for telemetry status");
}

if (!subcommand || subcommand === "") {
console.log(`
Expand Down Expand Up @@ -119,7 +138,7 @@ ${c.dim("You can also set")} ${c.accent("HYPERFRAMES_NO_TELEMETRY=1")} ${c.dim("
case "disable":
return setTelemetryEnabled(false);
case "status":
return runStatus();
return runStatus(args.json);
default:
console.error(
`${c.error("Unknown subcommand:")} ${subcommand}\n\nRun ${c.accent("hyperframes telemetry --help")} for usage.`,
Expand Down
Loading