Files
hyperframes/packages/cli/src/telemetry/config.ts
T
Miguel Ángel d625dc8509 feat: post-render and Studio feedback collection via PostHog surveys (#1101)
* feat(cli): prompt for render satisfaction after successful renders

* feat: add text feedback, doctor context, and Studio render feedback UI

* feat(studio): replace render feedback with session-based Studio experience bar

Move the feedback prompt out of RenderQueueItem (where it triggered every
5th render) into a standalone StudioFeedbackBar mounted at the bottom of
the preview area. The new bar is session-gated (shows after the 5th studio
session), auto-dismisses after 20s, and respects a 30-day cooldown once
dismissed or submitted. Renames telemetry to trackStudioFeedback with a
"studio_experience" survey ID to reflect the broader scope.

* feat(studio): attach browser doctor summary to feedback events

* fix(studio): use recurring interval for feedback instead of one-time cooldown

* fix(cli): skip feedback prompt when an agent runtime is detected

* feat(cli): add hyperframes feedback command and agent render hint

- New `hyperframes feedback --rating <1-5> --comment "..."` command
  for submitting anonymous render satisfaction feedback via telemetry.
- When an AI agent runtime is detected after a render, print a dimmed
  hint to stdout so the agent can optionally call the command instead
  of silently skipping the readline prompt.
- Export getDoctorSummary from telemetry/feedback.ts to share the
  system-info collector between the interactive prompt and the CLI command.
- Register the command in cli.ts and help.ts under Settings.

* fix(studio): align feedback interval to every 15 sessions

* fix: show CLI feedback on first render, Studio every 10 sessions

* feat: add env flags to disable feedback prompts

* feat: env flags to configure feedback prompt frequency

* fix: address review — agent hint reachability, cadence gate, session debounce, deprecated API
2026-05-28 12:17:47 -04:00

136 lines
4.6 KiB
TypeScript

import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { randomUUID } from "node:crypto";
// ---------------------------------------------------------------------------
// Config directory: ~/.hyperframes/
// ---------------------------------------------------------------------------
const CONFIG_DIR = join(homedir(), ".hyperframes");
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
export interface HyperframesConfig {
/** Whether anonymous telemetry is enabled (default: true in production) */
telemetryEnabled: boolean;
/** Stable anonymous identifier — no PII, just a random UUID */
anonymousId: string;
/** Whether the first-run telemetry notice has been shown */
telemetryNoticeShown: boolean;
/** Total CLI command invocations (for engagement prompts) */
commandCount: number;
/** Total successful renders (for feedback prompt gating) */
renderSuccessCount: number;
/** The renderSuccessCount at which feedback was last shown */
lastFeedbackPromptAt: number;
/** ISO timestamp of the last npm registry version check */
lastUpdateCheck?: string;
/** Latest version found on npm */
latestVersion?: string;
/**
* Auto-update marker. Set when a background install is spawned so a
* subsequent run can skip re-triggering it. Cleared once
* `completedUpdate` captures the outcome.
*/
pendingUpdate?: {
/** Version being installed. */
version: string;
/** Install command being run, for debug logging. */
command: string;
/** ISO timestamp of when the background install was launched. */
startedAt: string;
};
/**
* Outcome of the last completed auto-update, written by the detached
* installer. Surfaced once in the next invocation and then cleared.
*/
completedUpdate?: {
version: string;
/** Whether the install succeeded. */
ok: boolean;
/** ISO timestamp of when the installer finished. */
finishedAt: string;
/** Non-empty when `ok === false` — the installer's stderr tail. */
error?: string;
/** True after the result has been surfaced once to the user. */
reported?: boolean;
};
}
const DEFAULT_CONFIG: HyperframesConfig = {
telemetryEnabled: true,
anonymousId: "",
telemetryNoticeShown: false,
commandCount: 0,
renderSuccessCount: 0,
lastFeedbackPromptAt: 0,
};
let cachedConfig: HyperframesConfig | null = null;
/**
* Read the config file, creating it with defaults if it doesn't exist.
* Returns a mutable copy — call `writeConfig()` to persist changes.
*/
export function readConfig(): HyperframesConfig {
if (cachedConfig) return { ...cachedConfig };
if (!existsSync(CONFIG_FILE)) {
const config = { ...DEFAULT_CONFIG, anonymousId: randomUUID() };
writeConfig(config);
return config;
}
try {
const raw = readFileSync(CONFIG_FILE, "utf-8");
const parsed = JSON.parse(raw) as Partial<HyperframesConfig>;
const config: HyperframesConfig = {
telemetryEnabled: parsed.telemetryEnabled ?? DEFAULT_CONFIG.telemetryEnabled,
anonymousId: parsed.anonymousId || randomUUID(),
telemetryNoticeShown: parsed.telemetryNoticeShown ?? DEFAULT_CONFIG.telemetryNoticeShown,
commandCount: parsed.commandCount ?? DEFAULT_CONFIG.commandCount,
renderSuccessCount: parsed.renderSuccessCount ?? DEFAULT_CONFIG.renderSuccessCount,
lastFeedbackPromptAt: parsed.lastFeedbackPromptAt ?? DEFAULT_CONFIG.lastFeedbackPromptAt,
lastUpdateCheck: parsed.lastUpdateCheck,
latestVersion: parsed.latestVersion,
pendingUpdate: parsed.pendingUpdate,
completedUpdate: parsed.completedUpdate,
};
cachedConfig = config;
return { ...config };
} catch {
// Corrupted config — reset
const config = { ...DEFAULT_CONFIG, anonymousId: randomUUID() };
writeConfig(config);
return config;
}
}
/**
* Persist config to disk. Updates the in-memory cache.
*/
export function writeConfig(config: HyperframesConfig): void {
try {
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
cachedConfig = { ...config };
} catch {
// Non-fatal — telemetry should never break the CLI
}
}
/**
* Increment the command counter and persist.
*/
export function incrementCommandCount(): number {
const config = readConfig();
config.commandCount++;
writeConfig(config);
return config.commandCount;
}
/** Expose the config directory path for the telemetry command output */
export const CONFIG_PATH = CONFIG_FILE;