feat(telemetry): attribute renders to the authoring workflow skill#1695
Conversation
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.
miga-heygen
left a comment
There was a problem hiding this comment.
Review: feat(telemetry): attribute renders to the authoring workflow skill
Clean, well-scoped change. The slug-gated --skill flag is a smart approach — keeps the anonymous telemetry pipeline safe from PII / high-cardinality strings while giving you per-workflow breakdowns.
What I checked
-
All three render paths instrumented —
renderOptionsBase(chunked/streaming),renderDocker, andrenderLocalall threadauthoringSkillthrough. Error path viahandleRenderErrorand success path viatrackRenderMetricsboth emitauthoring_skill. No missed path. -
Slug validation —
/^[a-z0-9][a-z0-9-]{0,63}$/is tight: lowercase + digits + hyphens, max 64 chars, can't start with a hyphen. Invalid values degrade toundefined(field omitted from the event), which is the right silent-fallback for telemetry. No crash, no empty string leaking in. -
Skills instrumented — All 9 render-producing skills updated: embedded-captions, faceless-explainer, graphic-overlays, motion-graphics, music-to-video, pr-to-video, product-launch-video, remotion-to-hyperframes, website-to-video. The two exclusions (general-video: freeform, slideshow: interactive deck) are documented in the PR body — makes sense.
-
Telemetry event mapping — Both
trackRenderCompleteandtrackRenderErrormapauthoringSkill → authoring_skillon the emitted event object. Consistent snake_case in the payload, camelCase in TS — matches the existing convention. -
CI — All 30 checks green.
Minor nit (non-blocking)
The shell script in embedded-captions/scripts/render-and-composite.sh uses the space-separated flag format (--skill embedded-captions) while all SKILL.md files use equals-sign format (--skill=faceless-explainer). Both parse identically with citty, so purely cosmetic — but picking one convention for grep-ability would be nice.
LGTM — ship it. 🚢
— Review by Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Verified at HEAD 08d25918.
Layering on Miga's review (posted ~5 min ago) — he already covered the three render paths, slug regex tightness, 9 skills + 2 documented exclusions, and the snake_case/camelCase mapping. All confirmed independently. Adding a few cross-cutting concerns the field-level read missed.
✅ What I confirmed
authoringSkillplumbed throughrenderOptionsBase(render.ts:774),renderDocker(render.ts:832, 1195),renderLocal(render.ts:858),handleRenderError(render.ts:1435), andtrackRenderMetrics(render.ts:1481). Both success and failure paths emitauthoring_skill— important sincerender_erroris exactly the cohort we'd want per-skill breakdowns on.- Regex
^[a-z0-9][a-z0-9-]{0,63}$correctly accepts all 9 instrumented slugs and rejects empty / whitespace / uppercase / underscore / unicode / leading-dash / overlong. Trailing-dash IS accepted (trailing-→ true) — minor and not exploitable through anonymous telemetry, just noting. - The 9 callers all pass the slug matching their directory name. Verified each.
🟡 Concerns (non-blocking)
-
Studio renders silently emit
authoring_skill: null.packages/cli/src/server/studioRenderTelemetry.ts:106,125(emitStudioRenderError/emitStudioRenderComplete) doesn't accept or forward an authoring skill — by design, studio renders aren't skill-driven. But this means downstream PostHog cohorts onauthoring_skill IS NULLwill mix two populations: (a) CLI renders where the caller didn't pass--skill(hyperframes-cliSKILL.md,hyperframes-coreSKILL.md,hyperframes-registry/references/contributing.mdall show un-flaggednpx hyperframes renderinvocations to LLMs), and (b) all studio renders. Thesource: "cli" | "studio"tag disambiguates if the dashboard remembers to filter on it. Worth a one-line callout in the PR body so the consumer dashboard author knows the partition. -
SKILL_SLUGregex duplicated betweenevents.ts:23andrender.ts:394. Same pattern, copy-pasted. Two divergences already:events.tsdoesargs.skill.trim()before the regex,render.tsdoes not — so--skill=" pr-to-video "is accepted byhyperframes eventsbut rejected byhyperframes render. Shell callers won't hit this in practice, but it's a real semantic drift. Cheap fix: extractSKILL_SLUG+normalizeSkillSlug()into a shared util intelemetry/(or whereverredactTelemetryStringlives). -
Cross-repo verification.
authoring_skillis a new top-level property onrender_complete/render_error. PostHog ingest is permissive (no schema validation), so the field will land — but is there a Datadog dashboard / PostHog insight / warehouse query already referencing this property that needs to be updated, or is this the first consumer wire-up? If new, a follow-up to land the cohort/dashboard would close the loop. (Not blocking — the field is safe to ship and accumulate before a consumer reads it.)
🟡 Nits
- PR body template placeholders. The What / Why / How / Test plan sections are template defaults ("Brief description of the change.", "Why is this change needed?", etc.). The top-paragraph summary you wrote is great and stands on its own — either drop the template scaffold or fill in even a one-liner per section, so future readers don't bounce off boilerplate.
- embedded-captions space-vs-equals form — already flagged by Miga. Agree, low-priority.
--skillnot in the curated examples table athelp.ts:96-49. Citty will still surface it underhyperframes render --help, so end users can discover it. Skill-side callers wire it themselves so no functional impact. Optional.
What I didn't verify
- Whether any in-flight PostHog dashboard / cohort already filters on
authoring_skill(would need to check PostHog directly, out-of-repo). - Whether
general-videoandslideshowhave render-invoking dev scripts I didn't enumerate that should opt-in later.
Nice scope and clean instrumentation. Approving in spirit — leaving as comment-state since I want the studio-vs-cli null partition called out in either the PR body or a follow-up.
— Review by Rames D Jusso
miguel-heygen
left a comment
There was a problem hiding this comment.
Verified current head 08d25918 after Miga and Rames D's passes.
Specific checks I repeated:
packages/cli/src/commands/render.tsslug-gates--skillbefore it enters telemetry, so malformed/high-cardinality values are omitted instead of emitted.- The value is threaded through batch, Docker, and local render paths, and both success + error telemetry paths carry it.
packages/cli/src/telemetry/events.tsemitsauthoring_skillonrender_completeandrender_errorwithout changing the existing opt-out/anonymous tracking gates.- I grepped remaining
hyperframes renderreferences; the untagged ones are generic docs/test-corpus examples or the documented non-video/general exclusions, not canonical render-producing workflow entrypoints.
Agree with the existing follow-ups: shared slug normalizer, dashboard/source partitioning for authoring_skill IS NULL, and cleanup of the PR template scaffold would all be useful. None changes the merge safety of this diff. CI is green.
Verdict: APPROVE
Reasoning: The telemetry field is tightly slug-gated, wired through all render execution outcomes, and every canonical render-producing skill command is attributed without changing render behavior or telemetry opt-out semantics.
— Magi
…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).
There was a problem hiding this comment.
Re-review after cbd5a3df
All prior feedback addressed — this is tighter than v1. Here's what changed and what I verified:
Fixes applied
-
Shared slug utility (
telemetry/skill.ts) — extractedSKILL_SLUGregex andnormalizeSkillSlug()into a shared module.events.tsnow importsSKILL_SLUGinstead of defining its own copy, andrender.tscallsnormalizeSkillSlug()instead of inline regex. Single source of truth — no more drift risk between the two commands. -
Stderr warning on invalid slugs — If someone passes
--skill=MotionGraphics(camelCase typo), the render still proceeds but writes a diagnostic to stderr: "ignoring --skill=... — not a valid slug ... this render will be unattributed." Surfaces the mistake instead of silently losing attribution. Good DX. -
Shell script flag format —
embedded-captions/scripts/render-and-composite.shswitched from--skill embedded-captions(space-separated) to--skill=embedded-captions(equals-sign). Consistent with all the SKILL.md files now. -
Test coverage —
skill.test.tscovers: valid slugs (including boundary 64-char), whitespace trimming, all invalid value classes (empty, uppercase, underscore, leading hyphen, 65-char, unicode, non-string types). Clean.
Verification
normalizeSkillSlugtrims before validating — handles shell quoting artifacts. Returnsundefinedfor non-string input, sonormalizeSkillSlug(undefined)when--skillis omitted just drops the field. Correct.- The stderr warning only fires when the raw input is a non-empty string AND
normalizeSkillSlugreturnsundefined— won't spam when--skillis simply not passed. Correct. - All three render paths + error path still thread
authoringSkillthrough. Unchanged from v1. - CI: most checks green, a few still pending (Windows tests, CLI smoke, global install). No failures.
Still LGTM. No new concerns.
— Review by Miga
There was a problem hiding this comment.
R2 verification at cbd5a3df
Layering on Miga's R2 (posted ~5 min ago) — they covered the three fixes (shared util, stderr warning, equals-form script, test coverage) and verified all render paths still thread authoringSkill. Confirmed independently. Per-R1-finding ledger below.
Per-R1-finding verdict
- 🟡 #1 Studio renders mixed with un-flagged CLI in
authoring_skill IS NULLcohort — ✅ resolved-already, my R1 miss. Re-grepping at HEAD:render.ts:1363emitssource: "cli",studioRenderTelemetry.ts:110,131emitsource: "studio". The partition I worried about is already enforced everywhere — any dashboard that breaks downauthoring_skill IS NULLcan additionally filter onsourceto disambiguate the two populations. No code change needed here; my R1 read missed the existing tag. - 🟡 #2
SKILL_SLUGregex duplicated + trim divergence — ✅ resolved.packages/cli/src/telemetry/skill.tsis the new single source of truth;events.ts:3andrender.ts:67both import from it.render.ts:395now callsnormalizeSkillSlug(args.skill)which trims internally.events.ts:47still trims locally before callingSKILL_SLUG.test()— semantically identical tonormalizeSkillSlugso no observable drift, just one redundant call site. Non-blocking polish:events.tscould also callnormalizeSkillSlugfor one fewer line, but the regex divergence risk is eliminated either way. - 🟡 #3 Cross-repo PostHog dashboard / Datadog insight wired up? —
⚠️ open, non-blocking. No follow-up note in PR body, commit message, or thread. Was a question in R1 not a blocker; if there's already a consumer dashboard waiting on this property, fine — if not, would be worth a JIRA/Linear handoff before this lands. - Nit a — PR body template placeholders —
⚠️ partial. Top-paragraph summary still excellent. What/Why/How/Test plan sections still carry boilerplate ("Brief description of the change.","Why is this change needed?", etc.). Future-reader nit, not a blocker. - Nit b — embedded-captions equals-form — ✅ resolved.
skills/embedded-captions/scripts/render-and-composite.sh:259,261now--skill=embedded-captions. Convention consistent with all SKILL.md files. - Nit c —
--skillnot inrender.tscuratedexamples— ❌ untouched. Theexamplesarray atpackages/cli/src/commands/render.ts:15still has no--skillexample. Optional; citty exposes the flag under--helpregardless.
🆕 R2-new observations
- Stderr warning for invalid slugs (
render.ts:396-403) is a nice DX upgrade — a typo like--skill=MotionGraphicsnow printsignoring --skill="..." — not a valid slug ... this render will be unattributedto stderr instead of silently losing attribution. The guard correctly fires only when raw input is a non-empty string and normalization dropped it, so no spam when--skillis simply omitted. skill.test.tscovers valid slugs, boundary 64-char, trim, and the right invalid classes (empty, whitespace-only, uppercase, underscore, leading hyphen, 65-char, unicode, non-string). Clean.- Verified
--skill=" pr-to-video "no longer diverges betweenhyperframes render(now trims vianormalizeSkillSlug) andhyperframes events(still trims locally before the same shared regex). Both accept it now.
CI
Most checks green at cbd5a3df. A few still in-progress (Windows tests, CLI smoke, global install, CodeQL JS) — no failures visible.
Posture
LGTM at cbd5a3df. Three of three 🟡 closed (one was my misread, one resolved cleanly, one open but explicitly non-blocking). Magi's prior approval at 08d25918 may or may not survive the push depending on branch protection — deferring stamp to Magi/Miga for a clean re-approve if needed.
— Review by Rames D Jusso
miguel-heygen
left a comment
There was a problem hiding this comment.
R2 verified at current head cbd5a3df, after the review-fix commit.
Strengths / checks:
packages/cli/src/telemetry/skill.tscentralizesSKILL_SLUGandnormalizeSkillSlug, soeventsandrenderno longer carry duplicated regexes or trim drift.packages/cli/src/commands/render.tswarns on a non-empty invalid--skillwhile still allowing the render to continue unattributed, preserving telemetry-never-breaks-render behavior.packages/cli/src/telemetry/skill.test.tspins valid, trimmed, invalid, boundary-length, unicode, and non-string cases.skills/embedded-captions/scripts/render-and-composite.shnow uses the same--skill=embedded-captionsform as the SKILL.md call sites.
Layering on Miga/Rames D/wanbi: their R2 findings are addressed or correctly classified as follow-ups. The remaining dashboard consumer and help/example cleanup are non-blocking. gh pr checks is green at this head, including Windows render/tests.
Verdict: APPROVE
Reasoning: The attribution path is now slug-gated through one shared helper, every render success/error path still emits the safe authoring_skill value, invalid inputs are visible to callers without failing renders, and CI is green.
— Magi
Add an optional
--skillflag tohyperframes renderand tag therender_complete/render_errorevents withauthoring_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.
What
Brief description of the change.
Why
Why is this change needed?
How
How was this implemented? Any notable design decisions?
Test plan
How was this tested?