Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions packages/cli/src/commands/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,22 +443,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 () => {
Expand All @@ -471,11 +471,12 @@ 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");
});

it("returns undefined when composition dimensions can't be determined (defers to the pipeline)", async () => {
Expand Down
16 changes: 12 additions & 4 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -86,6 +87,7 @@ import {
fpsToNumber,
fpsToFfmpegArg,
type CanvasResolution,
type OutputResolutionIssueKind,
type Fps,
type FpsParseResult,
} from "@hyperframes/core";
Expand Down Expand Up @@ -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(
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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";
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/telemetry/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
trackCommandFailure,
trackCliError,
trackRenderFeedback,
trackRenderPreflightRejected,
} = await import("./events.js");

describe("render telemetry events", () => {
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/telemetry/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.),
// never free text.
export function trackRenderPreflightRejected(props: { kind: string }): void {
trackEvent("render_preflight_rejected", { kind: props.kind });
}

export function trackCliError(props: {
error_name: string;
error_message: string;
Expand Down
Loading