mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:49:49 +00:00
feat(cli): emit render_preflight_rejected telemetry for P1-3 pre-flight saves (#1856)
The P1-3 aspect/alpha/HDR pre-flight (#1843) aborts an incompatible render before any browser/ffmpeg work, but that "save" was invisible on dashboard 1783183 — indistinguishable from a deep failure or a user giving up. checkRenderResolutionPreflight now returns { message, kind } (kind = the existing low-cardinality OutputResolutionIssueKind), and the render command emits render_preflight_rejected { kind } before exiting. No parsers change — the helper already carried kind. trackRenderPreflightRejected is typed to the union so the metric can't carry free text. Tests: preflight tests assert kind for all five kinds; an events test locks the emit. Further follow-up (still log-only): encoder-frame-0-exit counter and a P1-4 doctor cli_env_check event. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
438474c968
commit
9b41891f3a
@@ -439,22 +439,22 @@ describe("checkRenderResolutionPreflight", () => {
|
||||
expect(await checkRenderResolutionPreflight(portraitHtml, "portrait", noModes)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns a suggestion when a landscape preset is used on a portrait composition", async () => {
|
||||
const message = await checkRenderResolutionPreflight(portraitHtml, "landscape", noModes);
|
||||
expect(message).toBeDefined();
|
||||
expect(message).toContain("--resolution portrait");
|
||||
it("returns a suggestion + aspect-mismatch kind when a landscape preset is used on a portrait composition", async () => {
|
||||
const result = await checkRenderResolutionPreflight(portraitHtml, "landscape", noModes);
|
||||
expect(result?.message).toContain("--resolution portrait");
|
||||
expect(result?.kind).toBe("aspect-mismatch");
|
||||
});
|
||||
|
||||
it("suggests landscape for a landscape composition rendered with a portrait preset", async () => {
|
||||
const message = await checkRenderResolutionPreflight(landscapeHtml, "portrait", noModes);
|
||||
expect(message).toContain("--resolution landscape");
|
||||
const result = await checkRenderResolutionPreflight(landscapeHtml, "portrait", noModes);
|
||||
expect(result?.message).toContain("--resolution landscape");
|
||||
});
|
||||
|
||||
it("preserves the 4K tier when suggesting a matching preset (square comp + landscape-4k → square-4k)", async () => {
|
||||
// Tier-aware suggestion is the load-bearing new behavior; square-4k is the
|
||||
// preset that only surfaces via a same-tier swap, so guard it explicitly.
|
||||
const message = await checkRenderResolutionPreflight(comp(2160, 2160), "landscape-4k", noModes);
|
||||
expect(message).toContain("--resolution square-4k");
|
||||
const result = await checkRenderResolutionPreflight(comp(2160, 2160), "landscape-4k", noModes);
|
||||
expect(result?.message).toContain("--resolution square-4k");
|
||||
});
|
||||
|
||||
it("does not false-abort a landscape registry-block composition (data-width/height, no data-resolution)", async () => {
|
||||
@@ -467,11 +467,34 @@ describe("checkRenderResolutionPreflight", () => {
|
||||
});
|
||||
|
||||
it("flags alpha output combined with outputResolution", async () => {
|
||||
const message = await checkRenderResolutionPreflight(landscapeHtml, "landscape-4k", {
|
||||
const result = await checkRenderResolutionPreflight(landscapeHtml, "landscape-4k", {
|
||||
alphaRequested: true,
|
||||
hdrRequested: false,
|
||||
});
|
||||
expect(message).toContain("alpha output");
|
||||
expect(result?.message).toContain("alpha output");
|
||||
expect(result?.kind).toBe("alpha-incompatible");
|
||||
});
|
||||
|
||||
// The three remaining kinds share the same rejection sink (→ one emit each);
|
||||
// guard their classification so the telemetry dimension stays accurate.
|
||||
it("classifies an HDR + outputResolution combination as hdr-incompatible", async () => {
|
||||
const result = await checkRenderResolutionPreflight(landscapeHtml, "landscape", {
|
||||
alphaRequested: false,
|
||||
hdrRequested: true,
|
||||
});
|
||||
expect(result?.kind).toBe("hdr-incompatible");
|
||||
});
|
||||
|
||||
it("classifies a preset smaller than the composition as downsampling", async () => {
|
||||
// 3840×2160 comp + landscape (1920×1080): same 16:9 aspect, target smaller.
|
||||
const result = await checkRenderResolutionPreflight(comp(3840, 2160), "landscape", noModes);
|
||||
expect(result?.kind).toBe("downsampling");
|
||||
});
|
||||
|
||||
it("classifies a non-integer upscale as non-integer-scale", async () => {
|
||||
// 1280×720 comp + landscape (1920×1080): same 16:9 aspect, 1.5× scale.
|
||||
const result = await checkRenderResolutionPreflight(comp(1280, 720), "landscape", noModes);
|
||||
expect(result?.kind).toBe("non-integer-scale");
|
||||
});
|
||||
|
||||
it("returns undefined when composition dimensions can't be determined (defers to the pipeline)", async () => {
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
trackRenderComplete,
|
||||
trackRenderError,
|
||||
trackRenderObservation,
|
||||
trackRenderPreflightRejected,
|
||||
} from "../telemetry/events.js";
|
||||
import { maybePromptRenderFeedback } from "../telemetry/feedback.js";
|
||||
import { renderJobObservabilityTelemetryPayload } from "../telemetry/renderObservability.js";
|
||||
@@ -86,6 +87,7 @@ import {
|
||||
fpsToNumber,
|
||||
fpsToFfmpegArg,
|
||||
type CanvasResolution,
|
||||
type OutputResolutionIssueKind,
|
||||
type Fps,
|
||||
type FpsParseResult,
|
||||
} from "@hyperframes/core";
|
||||
@@ -796,7 +798,7 @@ export default defineCommand({
|
||||
// own defense-in-depth check rather than blocking a render we can't reason
|
||||
// about. See render-reliability workstream P1-3.
|
||||
if (outputResolution) {
|
||||
let resolutionIssue: string | undefined;
|
||||
let resolutionIssue: { message: string; kind: OutputResolutionIssueKind } | undefined;
|
||||
try {
|
||||
const renderTarget = entryFile ? resolve(project.dir, entryFile) : project.indexPath;
|
||||
resolutionIssue = await checkRenderResolutionPreflight(
|
||||
@@ -812,7 +814,11 @@ export default defineCommand({
|
||||
// the real problem with full context.
|
||||
}
|
||||
if (resolutionIssue) {
|
||||
errorBox("Output resolution incompatible", resolutionIssue);
|
||||
// Count the pre-flight save so dashboard 1783183 can distinguish
|
||||
// "caught early by pre-flight" from a deep render failure or a user who
|
||||
// gave up — i.e. measure whether the P1-3 fix is doing its job.
|
||||
trackRenderPreflightRejected({ kind: resolutionIssue.kind });
|
||||
errorBox("Output resolution incompatible", resolutionIssue.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1073,7 +1079,7 @@ export async function checkRenderResolutionPreflight(
|
||||
compositionHtml: string,
|
||||
outputResolution: CanvasResolution | undefined,
|
||||
modes: { alphaRequested: boolean; hdrRequested: boolean },
|
||||
): Promise<string | undefined> {
|
||||
): Promise<{ message: string; kind: OutputResolutionIssueKind } | undefined> {
|
||||
if (!outputResolution) return undefined;
|
||||
const dims = await readCompositionDimensions(compositionHtml);
|
||||
// Couldn't determine the composition's actual dimensions — defer to the
|
||||
@@ -1086,7 +1092,9 @@ export async function checkRenderResolutionPreflight(
|
||||
alphaRequested: modes.alphaRequested,
|
||||
hdrRequested: modes.hdrRequested,
|
||||
});
|
||||
return compat.ok ? undefined : compat.message;
|
||||
// Narrow to the incompatible case; `message`/`kind` are always set there.
|
||||
if (compat.ok || !compat.message || !compat.kind) return undefined;
|
||||
return { message: compat.message, kind: compat.kind };
|
||||
}
|
||||
|
||||
const DOCKER_IMAGE_PREFIX = "hyperframes-renderer";
|
||||
|
||||
@@ -12,6 +12,7 @@ const {
|
||||
trackCommandFailure,
|
||||
trackCliError,
|
||||
trackRenderFeedback,
|
||||
trackRenderPreflightRejected,
|
||||
} = await import("./events.js");
|
||||
|
||||
describe("render telemetry events", () => {
|
||||
@@ -39,6 +40,13 @@ describe("render telemetry events", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("emits render_preflight_rejected with the low-cardinality issue kind", () => {
|
||||
trackRenderPreflightRejected({ kind: "aspect-mismatch" });
|
||||
expect(trackEvent).toHaveBeenCalledWith("render_preflight_rejected", {
|
||||
kind: "aspect-mismatch",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards distinctId to trackEvent so studio renders attribute to the browser user", () => {
|
||||
trackRenderError({
|
||||
fps: 30,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { redactTelemetryString } from "@hyperframes/core";
|
||||
import { redactTelemetryString, type OutputResolutionIssueKind } from "@hyperframes/core";
|
||||
import { trackEvent } from "./client.js";
|
||||
|
||||
export interface RenderObservabilityTelemetryPayload {
|
||||
@@ -308,6 +308,15 @@ export function trackBrowserInstall(): void {
|
||||
trackEvent("browser_install", {});
|
||||
}
|
||||
|
||||
// A render was rejected by the output-resolution/alpha/HDR pre-flight (P1-3)
|
||||
// before any browser/ffmpeg work. Counts the "caught early" saves on dashboard
|
||||
// 1783183, distinct from deep render failures. `kind` is the low-cardinality
|
||||
// `OutputResolutionIssueKind` (aspect-mismatch / alpha-incompatible / etc.),
|
||||
// typed to the union so the metric can never carry free text.
|
||||
export function trackRenderPreflightRejected(props: { kind: OutputResolutionIssueKind }): void {
|
||||
trackEvent("render_preflight_rejected", { kind: props.kind });
|
||||
}
|
||||
|
||||
export function trackCliError(props: {
|
||||
error_name: string;
|
||||
error_message: string;
|
||||
|
||||
Reference in New Issue
Block a user