Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
60 changes: 60 additions & 0 deletions packages/producer/src/services/htmlCompiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2267,3 +2267,63 @@
expect(compiled.html).not.toMatch(/window\.__hfVariablesByComp\s*=\s*Object\.assign/);
});
});

// STUDIO-5433: an ffprobe failure on a `<video>` source is fail-fast (unlike
// audio's graceful-degrade), so the exception surfaces to Datadog. The runFfprobe
// telemetry sanitizer strips the local filePath, leaving errors like `moov atom
// not found\n[input]: Invalid data found when processing input` with no attribution
// — ops needed a Temporal history dump to identify the failing source. These tests
// assert the compile-phase wrapper adds the redacted remote `src` to the thrown
// message so the next occurrence is diagnosable directly.
describe("STUDIO-5433 — ffprobe failure includes src URL for attribution", () => {
function writeCorruptVideoProject(videoSrc: string, assetBytes: Buffer): string {
const projectDir = mkdtempSync(join(tmpdir(), "hf-studio-5433-"));
mkdirSync(join(projectDir, "assets"), { recursive: true });
writeFileSync(join(projectDir, "assets", "clip.mp4"), assetBytes);
writeFileSync(
join(projectDir, "index.html"),
`<!DOCTYPE html>
<html>
<body>
<div id="root" data-composition-id="root" data-start="0" data-duration="4" data-width="640" data-height="360">
<video
id="clip"
src="${videoSrc}"
data-start="0"
data-duration="4"
data-width="640"
data-height="360"
></video>
</div>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["root"] = { duration: () => 4 };
</script>
</body>
</html>`,
);
return projectDir;
}

it("wraps the ffprobe error with [src=<relative-path>] when the local video is corrupt", async () => {
// 0-byte mp4 — ffprobe reports "Invalid data found when processing input",
// the same class as the STUDIO-5433 moov failure. Fail-fast semantics remain
// (video branch throws, unlike audio's graceful-degrade to duration=0).
const projectDir = writeCorruptVideoProject("assets/clip.mp4", Buffer.alloc(0));

let thrown: unknown;
try {
await compileForRender(projectDir, join(projectDir, "index.html"), projectDir);
} catch (error) {
thrown = error;
}

expect(thrown).toBeInstanceOf(Error);
const message = (thrown as Error).message;
expect(message).toContain("[src=assets/clip.mp4]");

Check failure on line 2323 in packages/producer/src/services/htmlCompiler.test.ts

View workflow job for this annotation

GitHub Actions / Producer: unit tests

error: expect(received).toContain(expected)

Expected to contain: "[src=assets/clip.mp4]" Received: "[FFmpeg] ffprobe not found. Please install FFmpeg. [src=[path]]" at <anonymous> (/home/runner/work/hyperframes/hyperframes/packages/producer/src/services/htmlCompiler.test.ts:2323:21)
// Original ffprobe diagnostic must still be present so failure classifiers
// downstream (e.g. hyperframes_render_metrics.py) continue to match.
expect(message).toMatch(/ffprobe|Invalid data|No video stream/i);
});

});
27 changes: 25 additions & 2 deletions packages/producer/src/services/htmlCompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
shouldClampResolvedMediaDuration,
CSS_URL_RE,
isNonRelativeUrl,
redactTelemetryString,
type ResolvedDuration,
type UnresolvedElement,
} from "@hyperframes/core";
Expand Down Expand Up @@ -433,6 +434,24 @@ async function resolveMediaDuration(
return { duration: 0, resolvedPath: filePath };
}

// STUDIO-5433: attach the remote `src` to any ffprobe failure surfaced from
// this branch. `extractMediaMetadata` → `runFfprobe` intentionally redacts
// its local `filePath` out of the error message (see
// engine/utils/ffprobe.ts::redactFfprobeInput), so a bare `moov atom not
// found` in Datadog carries no attribution and requires a Temporal history
// dump to identify the offending source. Re-throwing with the `src`
// (query-string redacted via `redactTelemetryString` so pre-signed URL
// signatures never reach telemetry) makes the next occurrence diagnosable
// directly from the render error. Fail-fast semantics for the video branch
// are preserved — only the message is enriched.
const withSrcContext = (error: unknown): Error => {
const originalMessage = error instanceof Error ? error.message : String(error);
const safeSrc = redactTelemetryString(src);
const wrapped = new Error(`${originalMessage} [src=${safeSrc}]`);
if (error instanceof Error && error.stack) wrapped.stack = error.stack;
return wrapped;
};

return withMediaProbeSlot(async () => {
let profile: MediaProbeProfile;
try {
Expand All @@ -442,13 +461,17 @@ async function resolveMediaDuration(
// probe failure, while invalid/unreadable audio sources resolve to zero
// duration and are excluded by the compiler.
if (tagName !== "video") return { duration: 0, resolvedPath: filePath };
throw error;
throw withSrcContext(error);
}
assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);

let metadata: { durationSeconds: number };
if (tagName === "video") {
metadata = await extractMediaMetadata(filePath);
try {
metadata = await extractMediaMetadata(filePath);
} catch (error) {
throw withSrcContext(error);
}
} else {
try {
metadata = await extractAudioMetadata(filePath);
Expand Down
Loading