mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
* feat(telemetry): attribute renders to the authoring workflow skill Add an optional `--skill` flag to `hyperframes render` and tag the `render_complete` / `render_error` events with `authoring_skill`, so render usage can be broken down per authoring workflow. The value is slug-gated (a malformed value is ignored) and the existing anonymous / opt-out telemetry pipeline is otherwise unchanged. Each end-user workflow that renders now passes `--skill=<name>` on its render command: embedded-captions, faceless-explainer, graphic-overlays, motion-graphics, music-to-video, pr-to-video, product-launch-video, remotion-to-hyperframes, website-to-video. Not instrumented, by design: general-video renders freeform with no canonical render command to attach to, and slideshow produces an interactive deck rather than a rendered video. Both can follow up if per-skill numbers are wanted. * fix(telemetry): address review — shared slug util, equals-form flag, invalid-value warning - Extract the SKILL_SLUG regex + a normalizeSkillSlug() helper into telemetry/skill.ts, shared by the `events` and `render` commands (the regex was duplicated). `render` adopts normalizeSkillSlug (so it now trims the value, matching `events`); `events` references the shared SKILL_SLUG. + unit test. - `render` warns on a non-empty but invalid --skill value (e.g. a camelCase typo) so attribution isn't silently lost — stderr only, never fails the render. - embedded-captions render script: `--skill embedded-captions` -> `--skill=embedded-captions`. On an older CLI that does not declare --skill, the space form leaks the value as a positional and clobbers the project dir (resolveProject fails); the equals form is parsed as a self-delimiting flag and safely ignored. Verified via Node parseArgs(strict:false). Addresses review feedback on the PR (shared util + .trim drift, version-skew safety, invalid-value visibility). --------- Co-authored-by: kiritowoo <295860553+kiritowoo@users.noreply.github.com>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
import { defineCommand } from "citty";
|
|
import { trackEvent, flush } from "../telemetry/client.js";
|
|
import { SKILL_SLUG } from "../telemetry/skill.js";
|
|
|
|
// Skill-usage telemetry endpoint. A skill reports its own invocation/outcome —
|
|
// ideally from its own bundled script, so it fires deterministically rather
|
|
// than relying on the agent to remember:
|
|
//
|
|
// npx hyperframes events --skill=product-launch-video
|
|
// npx hyperframes events --skill=product-launch-video --event=skill_completed --outcome=success
|
|
//
|
|
// Rides the SAME anonymous PostHog pipeline + consent gates as every other CLI
|
|
// event (DO_NOT_TRACK / telemetry opt-out, anonymous install UUID, IP stripped).
|
|
//
|
|
// Telemetry must NEVER break the calling skill: every arg is optional (a missing
|
|
// or malformed value is a silent no-op, not a non-zero exit), the body is
|
|
// guarded, and flush() carries its own hard timeout. This command always exits 0.
|
|
|
|
const ALLOWED_EVENTS = ["skill_invoked", "skill_completed"];
|
|
const ALLOWED_OUTCOMES = ["success", "error", "abort"];
|
|
|
|
export default defineCommand({
|
|
meta: {
|
|
name: "events",
|
|
description:
|
|
"Emit an anonymous skill-usage telemetry event (skills report their own invocation/outcome). Honors DO_NOT_TRACK / telemetry opt-out.",
|
|
},
|
|
args: {
|
|
skill: {
|
|
type: "string",
|
|
description: "Authoring skill slug, e.g. product-launch-video",
|
|
},
|
|
event: {
|
|
type: "string",
|
|
description: "Event name: skill_invoked | skill_completed (default: skill_invoked)",
|
|
default: "skill_invoked",
|
|
},
|
|
outcome: {
|
|
type: "string",
|
|
description: "Optional outcome for completion events: success | error | abort",
|
|
},
|
|
},
|
|
async run({ args }) {
|
|
// Best-effort: nothing here may fail the skill that called us. Missing or
|
|
// malformed input is a silent no-op rather than a non-zero exit.
|
|
try {
|
|
const skill = typeof args.skill === "string" ? args.skill.trim() : "";
|
|
if (!SKILL_SLUG.test(skill)) return; // missing / non-slug → no-op
|
|
|
|
const event = ALLOWED_EVENTS.includes(args.event) ? args.event : "skill_invoked";
|
|
const props: Record<string, string> = { authoring_skill: skill };
|
|
if (args.outcome && ALLOWED_OUTCOMES.includes(args.outcome)) {
|
|
props["outcome"] = args.outcome;
|
|
}
|
|
trackEvent(event, props);
|
|
await flush();
|
|
} catch {
|
|
// swallow — telemetry must never surface a non-zero exit to the caller
|
|
}
|
|
},
|
|
});
|