mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
* feat(media-use): usage visibility — shared telemetry identity, miss log, resolve --stats - U6: join the CLI/studio telemetry identity — read the shared install id from ~/.hyperframes/config.json (seed if absent) instead of a media-use-only ~/.media/anon-id, and $identify to the HeyGen account (email/username) once per run on sign-in. One PostHog person across surfaces; pseudonymous before sign-in, account-linked after. Event properties stay coarse (no intent/paths). - U1: one-time first-run disclosure to stderr + Privacy section in SKILL.md; honors DO_NOT_TRACK / HYPERFRAMES_NO_TELEMETRY. - U2: persist resolve misses to ~/.media/misses.jsonl (local → intent kept; the media_use_resolve_miss telemetry event stays intent-free). - U3: `resolve --stats` (+ --days) — local usage report over .media/ + ~/.media (volume by type, source/provider/via split, hit-rate, top missed intents, global-cache size/reuse); human + --json. - U4: reproducible PostHog dashboard definition (references/telemetry-dashboard.md). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv * fix(media-use): address #2113 review — shared notice state, legacy id migration, stats robustness - Notice-shown state now lives in the shared ~/.hyperframes/config.json (config.telemetryNoticeShown, the CLI's own field) instead of a media-use-only ~/.media marker — so shared-identity users see the first-run notice once per person, not once per tool. - Migrate a pre-existing ~/.media/anon-id into the shared config on upgrade, so media-use-only users keep their PostHog persona instead of resetting. - buildStats: --days only windows on a positive finite value (negative/NaN → all time, not an empty report); dropped the top-level catch that masked a real error as an all-zero "no usage" report (sub-reads are individually guarded). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H5k87mPZ4d6yiFwcWSb8Vv --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
2.5 KiB
JavaScript
79 lines
2.5 KiB
JavaScript
import { strict as assert } from "node:assert";
|
|
import { test } from "node:test";
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { readMisses, recordMiss } from "./misses.mjs";
|
|
|
|
function sandbox() {
|
|
const root = mkdtempSync(join(tmpdir(), "mu-misses-"));
|
|
const home = join(root, "home");
|
|
mkdirSync(home, { recursive: true });
|
|
process.env.HOME = home;
|
|
return { root, home };
|
|
}
|
|
|
|
function restoreEnv(saved) {
|
|
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
|
|
Object.assign(process.env, saved);
|
|
}
|
|
|
|
test("recordMiss appends a well-formed local miss", () => {
|
|
const savedEnv = { ...process.env };
|
|
const { root, home } = sandbox();
|
|
try {
|
|
recordMiss({ type: "bgm", intent: "moody synth pulse", provider_override: true });
|
|
const misses = readMisses();
|
|
assert.equal(misses.length, 1);
|
|
assert.equal(misses[0].type, "bgm");
|
|
assert.equal(misses[0].intent, "moody synth pulse");
|
|
assert.equal(misses[0].provider_override, true);
|
|
assert.equal(misses[0].local_only, false);
|
|
assert.ok(!Number.isNaN(Date.parse(misses[0].ts)));
|
|
|
|
const raw = readFileSync(join(home, ".media/misses.jsonl"), "utf8");
|
|
assert.match(raw, /moody synth pulse/);
|
|
} finally {
|
|
restoreEnv(savedEnv);
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("recordMiss swallows filesystem failures", () => {
|
|
const savedEnv = { ...process.env };
|
|
const { root, home } = sandbox();
|
|
try {
|
|
writeFileSync(join(home, ".media"), "not a directory");
|
|
assert.doesNotThrow(() =>
|
|
recordMiss({ type: "image", intent: "unwritable", local_only: true }),
|
|
);
|
|
assert.equal(existsSync(join(home, ".media/misses.jsonl")), false);
|
|
} finally {
|
|
restoreEnv(savedEnv);
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("readMisses skips corrupt lines", () => {
|
|
const savedEnv = { ...process.env };
|
|
const { root, home } = sandbox();
|
|
try {
|
|
mkdirSync(join(home, ".media"), { recursive: true });
|
|
writeFileSync(
|
|
join(home, ".media/misses.jsonl"),
|
|
[
|
|
JSON.stringify({ ts: "2026-07-09T00:00:00.000Z", type: "bgm", intent: "one" }),
|
|
"{not json",
|
|
JSON.stringify({ ts: "2026-07-09T00:00:01.000Z", type: "sfx", intent: "two" }),
|
|
].join("\n"),
|
|
);
|
|
assert.deepEqual(
|
|
readMisses().map((miss) => miss.intent),
|
|
["one", "two"],
|
|
);
|
|
} finally {
|
|
restoreEnv(savedEnv);
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|