mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +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();
|
expect(await checkRenderResolutionPreflight(portraitHtml, "portrait", noModes)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a suggestion when a landscape preset is used on a portrait composition", async () => {
|
it("returns a suggestion + aspect-mismatch kind when a landscape preset is used on a portrait composition", async () => {
|
||||||
const message = await checkRenderResolutionPreflight(portraitHtml, "landscape", noModes);
|
const result = await checkRenderResolutionPreflight(portraitHtml, "landscape", noModes);
|
||||||
expect(message).toBeDefined();
|
expect(result?.message).toContain("--resolution portrait");
|
||||||
expect(message).toContain("--resolution portrait");
|
expect(result?.kind).toBe("aspect-mismatch");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("suggests landscape for a landscape composition rendered with a portrait preset", async () => {
|
it("suggests landscape for a landscape composition rendered with a portrait preset", async () => {
|
||||||
const message = await checkRenderResolutionPreflight(landscapeHtml, "portrait", noModes);
|
const result = await checkRenderResolutionPreflight(landscapeHtml, "portrait", noModes);
|
||||||
expect(message).toContain("--resolution landscape");
|
expect(result?.message).toContain("--resolution landscape");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves the 4K tier when suggesting a matching preset (square comp + landscape-4k → square-4k)", async () => {
|
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
|
// 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.
|
// preset that only surfaces via a same-tier swap, so guard it explicitly.
|
||||||
const message = await checkRenderResolutionPreflight(comp(2160, 2160), "landscape-4k", noModes);
|
const result = await checkRenderResolutionPreflight(comp(2160, 2160), "landscape-4k", noModes);
|
||||||
expect(message).toContain("--resolution square-4k");
|
expect(result?.message).toContain("--resolution square-4k");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not false-abort a landscape registry-block composition (data-width/height, no data-resolution)", async () => {
|
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 () => {
|
it("flags alpha output combined with outputResolution", async () => {
|
||||||
const message = await checkRenderResolutionPreflight(landscapeHtml, "landscape-4k", {
|
const result = await checkRenderResolutionPreflight(landscapeHtml, "landscape-4k", {
|
||||||
alphaRequested: true,
|
alphaRequested: true,
|
||||||
hdrRequested: false,
|
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 () => {
|
it("returns undefined when composition dimensions can't be determined (defers to the pipeline)", async () => {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import {
|
|||||||
trackRenderComplete,
|
trackRenderComplete,
|
||||||
trackRenderError,
|
trackRenderError,
|
||||||
trackRenderObservation,
|
trackRenderObservation,
|
||||||
|
trackRenderPreflightRejected,
|
||||||
} from "../telemetry/events.js";
|
} from "../telemetry/events.js";
|
||||||
import { maybePromptRenderFeedback } from "../telemetry/feedback.js";
|
import { maybePromptRenderFeedback } from "../telemetry/feedback.js";
|
||||||
import { renderJobObservabilityTelemetryPayload } from "../telemetry/renderObservability.js";
|
import { renderJobObservabilityTelemetryPayload } from "../telemetry/renderObservability.js";
|
||||||
@@ -86,6 +87,7 @@ import {
|
|||||||
fpsToNumber,
|
fpsToNumber,
|
||||||
fpsToFfmpegArg,
|
fpsToFfmpegArg,
|
||||||
type CanvasResolution,
|
type CanvasResolution,
|
||||||
|
type OutputResolutionIssueKind,
|
||||||
type Fps,
|
type Fps,
|
||||||
type FpsParseResult,
|
type FpsParseResult,
|
||||||
} from "@hyperframes/core";
|
} from "@hyperframes/core";
|
||||||
@@ -796,7 +798,7 @@ export default defineCommand({
|
|||||||
// own defense-in-depth check rather than blocking a render we can't reason
|
// own defense-in-depth check rather than blocking a render we can't reason
|
||||||
// about. See render-reliability workstream P1-3.
|
// about. See render-reliability workstream P1-3.
|
||||||
if (outputResolution) {
|
if (outputResolution) {
|
||||||
let resolutionIssue: string | undefined;
|
let resolutionIssue: { message: string; kind: OutputResolutionIssueKind } | undefined;
|
||||||
try {
|
try {
|
||||||
const renderTarget = entryFile ? resolve(project.dir, entryFile) : project.indexPath;
|
const renderTarget = entryFile ? resolve(project.dir, entryFile) : project.indexPath;
|
||||||
resolutionIssue = await checkRenderResolutionPreflight(
|
resolutionIssue = await checkRenderResolutionPreflight(
|
||||||
@@ -812,7 +814,11 @@ export default defineCommand({
|
|||||||
// the real problem with full context.
|
// the real problem with full context.
|
||||||
}
|
}
|
||||||
if (resolutionIssue) {
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1073,7 +1079,7 @@ export async function checkRenderResolutionPreflight(
|
|||||||
compositionHtml: string,
|
compositionHtml: string,
|
||||||
outputResolution: CanvasResolution | undefined,
|
outputResolution: CanvasResolution | undefined,
|
||||||
modes: { alphaRequested: boolean; hdrRequested: boolean },
|
modes: { alphaRequested: boolean; hdrRequested: boolean },
|
||||||
): Promise<string | undefined> {
|
): Promise<{ message: string; kind: OutputResolutionIssueKind } | undefined> {
|
||||||
if (!outputResolution) return undefined;
|
if (!outputResolution) return undefined;
|
||||||
const dims = await readCompositionDimensions(compositionHtml);
|
const dims = await readCompositionDimensions(compositionHtml);
|
||||||
// Couldn't determine the composition's actual dimensions — defer to the
|
// Couldn't determine the composition's actual dimensions — defer to the
|
||||||
@@ -1086,7 +1092,9 @@ export async function checkRenderResolutionPreflight(
|
|||||||
alphaRequested: modes.alphaRequested,
|
alphaRequested: modes.alphaRequested,
|
||||||
hdrRequested: modes.hdrRequested,
|
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";
|
const DOCKER_IMAGE_PREFIX = "hyperframes-renderer";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const {
|
|||||||
trackCommandFailure,
|
trackCommandFailure,
|
||||||
trackCliError,
|
trackCliError,
|
||||||
trackRenderFeedback,
|
trackRenderFeedback,
|
||||||
|
trackRenderPreflightRejected,
|
||||||
} = await import("./events.js");
|
} = await import("./events.js");
|
||||||
|
|
||||||
describe("render telemetry events", () => {
|
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", () => {
|
it("forwards distinctId to trackEvent so studio renders attribute to the browser user", () => {
|
||||||
trackRenderError({
|
trackRenderError({
|
||||||
fps: 30,
|
fps: 30,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { redactTelemetryString } from "@hyperframes/core";
|
import { redactTelemetryString, type OutputResolutionIssueKind } from "@hyperframes/core";
|
||||||
import { trackEvent } from "./client.js";
|
import { trackEvent } from "./client.js";
|
||||||
|
|
||||||
export interface RenderObservabilityTelemetryPayload {
|
export interface RenderObservabilityTelemetryPayload {
|
||||||
@@ -308,6 +308,15 @@ export function trackBrowserInstall(): void {
|
|||||||
trackEvent("browser_install", {});
|
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: {
|
export function trackCliError(props: {
|
||||||
error_name: string;
|
error_name: string;
|
||||||
error_message: string;
|
error_message: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user