fix(render): add end-to-end observability (#1248)

This commit is contained in:
Miguel Ángel
2026-06-06 23:55:58 -04:00
committed by GitHub
parent 9aebc8db70
commit 29d6f1eac9
28 changed files with 1928 additions and 274 deletions
+98 -2
View File
@@ -45,15 +45,20 @@ import { loadProducer } from "../utils/producer.js";
import { c } from "../ui/colors.js";
import { formatBytes, formatDuration, errorBox } from "../ui/format.js";
import { renderProgress } from "../ui/progress.js";
import { trackRenderComplete, trackRenderError } from "../telemetry/events.js";
import {
trackRenderComplete,
trackRenderError,
trackRenderObservation,
} from "../telemetry/events.js";
import { maybePromptRenderFeedback } from "../telemetry/feedback.js";
import { renderJobObservabilityTelemetryPayload } from "../telemetry/renderObservability.js";
import { bytesToMb } from "../telemetry/system.js";
import { VERSION } from "../version.js";
import { isDevMode } from "../utils/env.js";
import { buildDockerRunArgs, resolveDockerPlatform } from "../utils/dockerRunArgs.js";
import { normalizeErrorMessage } from "../utils/errorMessage.js";
import { findFFmpeg, getFFmpegInstallHint } from "../browser/ffmpeg.js";
import type { RenderJob } from "@hyperframes/producer";
import type { ProducerLogger, RenderJob } from "@hyperframes/producer";
import {
normalizeResolutionFlag,
parseFps,
@@ -932,6 +937,9 @@ export async function renderLocal(
}
const startTime = Date.now();
const logger = createRenderTelemetryLogger(
producer.createConsoleLogger?.("info") ?? createNoopProducerLogger(),
);
// Pass the resolved browser path to the producer via env var so
// resolveConfig() picks it up. This bridges the CLI's ensureBrowser()
@@ -947,6 +955,7 @@ export async function renderLocal(
format: options.format,
workers: options.workers,
useGpu: options.gpu,
logger,
producerConfig: producer.resolveConfig({
browserGpuMode: options.browserGpuMode ?? "software",
...(options.pageNavigationTimeoutMs != null
@@ -979,6 +988,7 @@ export async function renderLocal(
false,
"Try --docker for containerized rendering",
job.failedStage,
job,
);
}
@@ -1019,6 +1029,89 @@ function getMemorySnapshot() {
};
}
function metaString(meta: Record<string, unknown> | undefined, key: string): string | undefined {
const value = meta?.[key];
return typeof value === "string" ? value : undefined;
}
function metaNumber(meta: Record<string, unknown> | undefined, key: string): number | undefined {
const value = meta?.[key];
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function metaBoolean(meta: Record<string, unknown> | undefined, key: string): boolean | undefined {
const value = meta?.[key];
return typeof value === "boolean" ? value : undefined;
}
function trackRenderTraceFromLog(message: string, meta: Record<string, unknown> | undefined): void {
if (message !== "[Render:trace]") return;
const status = metaString(meta, "status");
if (status !== "checkpoint" && status !== "error") return;
trackRenderObservation({
source: "cli",
renderJobId: metaString(meta, "renderJobId"),
phase: metaString(meta, "phase"),
status,
compositionHash: metaString(meta, "compositionHash"),
elapsedMs: metaNumber(meta, "elapsedMs"),
durationMs: metaNumber(meta, "durationMs"),
message: metaString(meta, "message"),
workerCount: metaNumber(meta, "workerCount"),
forceScreenshot: metaBoolean(meta, "forceScreenshot"),
useStreamingEncode: metaBoolean(meta, "useStreamingEncode"),
useLayeredComposite: metaBoolean(meta, "useLayeredComposite"),
usePageSideCompositing: metaBoolean(meta, "usePageSideCompositing"),
hasHdrContent: metaBoolean(meta, "hasHdrContent"),
captureMode: metaString(meta, "captureMode"),
videoCount: metaNumber(meta, "videoCount"),
extractedVideoCount: metaNumber(meta, "extractedVideoCount"),
totalFramesExtracted: metaNumber(meta, "totalFramesExtracted"),
maxFramesPerVideo: metaNumber(meta, "maxFramesPerVideo"),
avgFramesPerExtractedVideo: metaNumber(meta, "avgFramesPerExtractedVideo"),
vfrPreflightCount: metaNumber(meta, "vfrPreflightCount"),
vfrPreflightMs: metaNumber(meta, "vfrPreflightMs"),
cacheHits: metaNumber(meta, "cacheHits"),
cacheMisses: metaNumber(meta, "cacheMisses"),
});
}
function createRenderTelemetryLogger(base: ProducerLogger): ProducerLogger {
return {
error(message, meta) {
base.error(message, meta);
trackRenderTraceFromLog(message, meta);
},
warn(message, meta) {
base.warn(message, meta);
trackRenderTraceFromLog(message, meta);
},
info(message, meta) {
base.info(message, meta);
trackRenderTraceFromLog(message, meta);
},
debug(message, meta) {
base.debug(message, meta);
trackRenderTraceFromLog(message, meta);
},
isLevelEnabled(level) {
return base.isLevelEnabled?.(level) ?? true;
},
};
}
function createNoopProducerLogger(): ProducerLogger {
return {
error() {},
warn() {},
info() {},
debug() {},
isLevelEnabled() {
return true;
},
};
}
function handleRenderError(
error: unknown,
options: RenderOptions,
@@ -1026,6 +1119,7 @@ function handleRenderError(
docker: boolean,
hint: string,
failedStage?: string,
job?: RenderJob,
): never {
const message = normalizeErrorMessage(error);
trackRenderError({
@@ -1037,6 +1131,7 @@ function handleRenderError(
elapsedMs: Date.now() - startTime,
errorMessage: message,
failedStage,
...renderJobObservabilityTelemetryPayload(job),
...getMemorySnapshot(),
});
errorBox("Render failed", message, hint);
@@ -1099,6 +1194,7 @@ function trackRenderMetrics(
extractPhase3Ms: extract?.extractMs,
extractCacheHits: extract?.cacheHits,
extractCacheMisses: extract?.cacheMisses,
...renderJobObservabilityTelemetryPayload(job),
...getMemorySnapshot(),
});
}
@@ -1,5 +1,5 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import type { RenderPerfSummary } from "@hyperframes/producer";
import type { RenderJob, RenderPerfSummary } from "@hyperframes/producer";
// Mock `../telemetry/events.js` so we can capture trackRenderComplete /
// trackRenderError calls and verify the payload mapping without firing
@@ -21,6 +21,60 @@ const opts = {
quality: "standard",
};
const fullObservability: NonNullable<RenderPerfSummary["observability"]> = {
renderJobId: "render-123",
compositionHash: "abc123",
eventCount: 8,
lastEvent: {
phase: "pipeline",
status: "checkpoint",
elapsedMs: 5000,
message: "completed",
},
failedPhase: undefined,
browserDiagnostics: {
total: 4,
errors: 1,
pageErrors: 0,
requestFailed: 1,
httpErrors: 1,
navigationStarts: 1,
navigationFailures: 0,
consoleErrors: 0,
consoleWarnings: 1,
},
capture: {
forceScreenshot: true,
captureMode: "screenshot",
workerCount: 1,
useStreamingEncode: false,
useLayeredComposite: true,
usePageSideCompositing: false,
hasHdrContent: true,
browserGpuMode: "auto",
protocolTimeoutMs: 300_000,
pageNavigationTimeoutMs: 60_000,
playerReadyTimeoutMs: 45_000,
},
extraction: {
videoCount: 6,
extractedVideoCount: 6,
totalFramesExtracted: 167_400,
maxFramesPerVideo: 27_900,
avgFramesPerExtractedVideo: 27_900,
vfrProbeMs: 120,
vfrPreflightMs: 2400,
vfrPreflightCount: 6,
cacheHits: 0,
cacheMisses: 6,
},
init: {
initDurationMs: 1234,
tweenCount: 42,
},
events: [],
};
const fullPerf: RenderPerfSummary = {
renderId: "r-1",
totalElapsedMs: 5000,
@@ -57,6 +111,7 @@ const fullPerf: RenderPerfSummary = {
tmpPeakBytes: 1024,
captureAvgMs: 13,
capturePeakMs: 25,
observability: fullObservability,
};
describe("studioRenderTelemetry", () => {
@@ -111,6 +166,33 @@ describe("studioRenderTelemetry", () => {
expect(p.extractPhase3Ms).toBe(60);
expect(p.extractCacheHits).toBe(3);
expect(p.extractCacheMisses).toBe(4);
// observability aggregate
expect(p.observabilityRenderJobId).toBe("render-123");
expect(p.observabilityCompositionHash).toBe("abc123");
expect(p.observabilityEventCount).toBe(8);
expect(p.observabilityLastPhase).toBe("pipeline");
expect(p.observabilityLastStatus).toBe("checkpoint");
expect(p.browserDiagnosticCount).toBe(4);
expect(p.browserDiagnosticErrors).toBe(1);
expect(p.browserDiagnosticRequestFailed).toBe(1);
expect(p.browserDiagnosticHttpErrors).toBe(1);
expect(p.browserDiagnosticNavigationStarts).toBe(1);
expect(p.browserDiagnosticConsoleWarnings).toBe(1);
expect(p.captureMode).toBe("screenshot");
expect(p.captureForceScreenshot).toBe(true);
expect(p.captureWorkerCount).toBe(1);
expect(p.captureUseLayeredComposite).toBe(true);
expect(p.captureHasHdrContent).toBe(true);
expect(p.capturePageNavigationTimeoutMs).toBe(60_000);
expect(p.observabilityExtractVideoCount).toBe(6);
expect(p.observabilityExtractedVideoCount).toBe(6);
expect(p.observabilityExtractTotalFrames).toBe(167_400);
expect(p.observabilityExtractMaxFramesPerVideo).toBe(27_900);
expect(p.observabilityExtractAvgFramesPerVideo).toBe(27_900);
expect(p.observabilityExtractVfrPreflightCount).toBe(6);
expect(p.observabilityExtractCacheMisses).toBe(6);
expect(p.observabilityInitDurationMs).toBe(1234);
expect(p.observabilityInitTweenCount).toBe(42);
});
it("omits all perf-derived fields when perfSummary is undefined", () => {
@@ -147,7 +229,7 @@ describe("studioRenderTelemetry", () => {
describe("emitStudioRenderError", () => {
it("tags with source: 'studio' and forwards failedStage + elapsedMs", () => {
emitStudioRenderError(opts, 1200, "encode", new Error("boom"));
emitStudioRenderError(opts, 1200, "encode", new Error("boom"), undefined);
expect(trackRenderError).toHaveBeenCalledOnce();
const p = trackRenderError.mock.calls[0]![0];
expect(p.source).toBe("studio");
@@ -160,16 +242,55 @@ describe("studioRenderTelemetry", () => {
});
it("stringifies non-Error throwables", () => {
emitStudioRenderError(opts, 100, undefined, "string error");
emitStudioRenderError(opts, 100, undefined, "string error", undefined);
expect(trackRenderError.mock.calls[0]![0].errorMessage).toBe("string error");
});
it("does not include a workers field on the error event payload", () => {
// Documented behavior: studio renders don't request a worker count,
// and the early-failure path doesn't have perfSummary to read it from.
emitStudioRenderError(opts, 100, undefined, new Error("x"));
emitStudioRenderError(opts, 100, undefined, new Error("x"), undefined);
const p = trackRenderError.mock.calls[0]![0];
expect(p.workers).toBeUndefined();
});
it("maps observability from a failed producer job", () => {
const job: RenderJob = {
id: "r-error",
config: { fps: opts.fps, quality: "standard" },
status: "failed",
progress: 25,
currentStage: "Starting frame capture",
createdAt: new Date(),
errorDetails: {
message: "Navigation timeout of 60000 ms exceeded",
elapsedMs: 60_001,
freeMemoryMB: 1024,
observability: {
...fullObservability,
failedPhase: "capture_hdr_layered",
lastEvent: {
phase: "capture_hdr_layered",
status: "error",
elapsedMs: 60_001,
durationMs: 60_000,
message: "Navigation timeout of 60000 ms exceeded",
},
},
},
};
emitStudioRenderError(opts, 60_001, "Starting frame capture", new Error("timeout"), job);
const p = trackRenderError.mock.calls[0]![0];
expect(p.observabilityFailedPhase).toBe("capture_hdr_layered");
expect(p.observabilityLastPhase).toBe("capture_hdr_layered");
expect(p.observabilityLastStatus).toBe("error");
expect(p.browserDiagnosticCount).toBe(4);
expect(p.captureMode).toBe("screenshot");
expect(p.capturePageNavigationTimeoutMs).toBe(60_000);
expect(p.observabilityExtractTotalFrames).toBe(167_400);
expect(p.observabilityExtractVfrPreflightCount).toBe(6);
});
});
});
@@ -9,8 +9,12 @@
import { freemem } from "node:os";
import type { Fps } from "@hyperframes/core";
import { fpsToNumber } from "@hyperframes/core";
import type { RenderPerfSummary } from "@hyperframes/producer";
import type { RenderJob, RenderPerfSummary } from "@hyperframes/producer";
import { trackRenderComplete, trackRenderError } from "../telemetry/events.js";
import {
renderJobObservabilityTelemetryPayload,
renderObservabilityTelemetryPayload,
} from "../telemetry/renderObservability.js";
import { bytesToMb } from "../telemetry/system.js";
export interface StudioRenderOpts {
@@ -78,6 +82,7 @@ function perfPayload(
tmpPeakBytes: perf.tmpPeakBytes,
...stagesPayload(perf.stages),
...extractPayload(perf.videoExtractBreakdown),
...renderObservabilityTelemetryPayload(perf.observability),
};
}
@@ -86,6 +91,7 @@ export function emitStudioRenderError(
elapsedMs: number,
failedStage: string | undefined,
err: unknown,
job: RenderJob | undefined,
): void {
// `workers` is intentionally omitted: studio renders don't accept a
// user-supplied worker count (the producer picks its default), so on early
@@ -99,6 +105,7 @@ export function emitStudioRenderError(
failedStage,
errorMessage: err instanceof Error ? err.message : String(err),
elapsedMs,
...renderJobObservabilityTelemetryPayload(job),
...memSnapshot(),
});
}
+5 -1
View File
@@ -28,6 +28,7 @@ import {
} from "@hyperframes/core/studio-api";
import { getElementScreenshotClip } from "@hyperframes/core/studio-api/screenshot-clip";
import type { ScreenshotClip } from "@hyperframes/core/studio-api/screenshot-clip";
import type { RenderJob } from "@hyperframes/producer";
const STUDIO_MANUAL_EDITS_PATH = ".hyperframes/studio-manual-edits.json";
@@ -278,6 +279,7 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
// Run render asynchronously, mutating the state object
const startTime = Date.now();
(async () => {
let renderJob: RenderJob | undefined;
try {
const { createRenderJob, executeRenderJob } = await import("@hyperframes/producer");
const { ensureBrowser } = await import("../browser/manager.js");
@@ -303,6 +305,7 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
...(manualEditsRenderScript ? { renderBodyScripts: [manualEditsRenderScript] } : {}),
...(opts.composition ? { entryFile: opts.composition } : {}),
});
renderJob = job;
const onProgress = (j: { progress: number; currentStage?: string }) => {
state.progress = j.progress;
if (j.currentStage) state.stage = j.currentStage;
@@ -319,7 +322,8 @@ export function createStudioServer(options: StudioServerOptions): StudioServer {
} catch (err) {
state.status = "failed";
state.error = err instanceof Error ? err.message : String(err);
emitStudioRenderError(opts, Date.now() - startTime, state.stage, err);
// fallow-ignore-next-line code-duplication
emitStudioRenderError(opts, Date.now() - startTime, state.stage, err, renderJob);
try {
const metaPath = opts.outputPath.replace(/\.(mp4|webm|mov)$/, ".meta.json");
writeFileSync(metaPath, JSON.stringify({ status: "failed" }));
+52
View File
@@ -0,0 +1,52 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
const trackEvent = vi.fn();
vi.mock("./client.js", () => ({
trackEvent: (...args: unknown[]) => trackEvent(...args),
}));
const { trackRenderError, trackRenderObservation } = await import("./events.js");
describe("render telemetry events", () => {
beforeEach(() => {
trackEvent.mockClear();
});
it("redacts paths and URL query strings from render error messages", () => {
trackRenderError({
fps: 30,
quality: "standard",
docker: false,
errorMessage:
"ENOENT: open '/home/ubuntu/project/media/video.mp4' https://example.com/video.mp4?token=secret",
observabilityCompositionHash: "abc123",
});
expect(trackEvent).toHaveBeenCalledWith(
"render_error",
expect.objectContaining({
error_message: "ENOENT: open '[path]' https://example.com/video.mp4?…",
observability_composition_hash: "abc123",
}),
);
});
it("redacts render_observation messages and includes renderJobId for correlation", () => {
trackRenderObservation({
renderJobId: "render-123",
phase: "capture_hdr_layered",
status: "error",
compositionHash: "abc123",
message: "Navigation failed for C:\\Users\\Alice\\project\\video.mov?not-a-query",
});
expect(trackEvent).toHaveBeenCalledWith(
"render_observation",
expect.objectContaining({
render_job_id: "render-123",
composition_hash: "abc123",
message: "Navigation failed for [path]",
}),
);
});
});
+205 -56
View File
@@ -1,51 +1,142 @@
import { redactTelemetryString } from "@hyperframes/core";
import { trackEvent } from "./client.js";
export interface RenderObservabilityTelemetryPayload {
observabilityRenderJobId?: string;
observabilityCompositionHash?: string;
observabilityEventCount?: number;
observabilityLastPhase?: string;
observabilityLastStatus?: string;
observabilityFailedPhase?: string;
browserDiagnosticCount?: number;
browserDiagnosticErrors?: number;
browserDiagnosticPageErrors?: number;
browserDiagnosticRequestFailed?: number;
browserDiagnosticHttpErrors?: number;
browserDiagnosticNavigationStarts?: number;
browserDiagnosticNavigationFailures?: number;
browserDiagnosticConsoleErrors?: number;
browserDiagnosticConsoleWarnings?: number;
captureMode?: string;
captureForceScreenshot?: boolean;
captureWorkerCount?: number;
captureUseStreamingEncode?: boolean;
captureUseLayeredComposite?: boolean;
captureUsePageSideCompositing?: boolean;
captureHasHdrContent?: boolean;
captureBrowserGpuMode?: string;
captureProtocolTimeoutMs?: number;
capturePageNavigationTimeoutMs?: number;
capturePlayerReadyTimeoutMs?: number;
observabilityExtractVideoCount?: number;
observabilityExtractedVideoCount?: number;
observabilityExtractTotalFrames?: number;
observabilityExtractMaxFramesPerVideo?: number;
observabilityExtractAvgFramesPerVideo?: number;
observabilityExtractVfrProbeMs?: number;
observabilityExtractVfrPreflightMs?: number;
observabilityExtractVfrPreflightCount?: number;
observabilityExtractCacheHits?: number;
observabilityExtractCacheMisses?: number;
observabilityInitDurationMs?: number;
observabilityInitTweenCount?: number;
}
function renderObservabilityEventProperties(props: RenderObservabilityTelemetryPayload) {
return {
observability_render_job_id: props.observabilityRenderJobId,
observability_composition_hash: props.observabilityCompositionHash,
observability_event_count: props.observabilityEventCount,
observability_last_phase: props.observabilityLastPhase,
observability_last_status: props.observabilityLastStatus,
observability_failed_phase: props.observabilityFailedPhase,
browser_diagnostic_count: props.browserDiagnosticCount,
browser_diagnostic_errors: props.browserDiagnosticErrors,
browser_diagnostic_page_errors: props.browserDiagnosticPageErrors,
browser_diagnostic_request_failed: props.browserDiagnosticRequestFailed,
browser_diagnostic_http_errors: props.browserDiagnosticHttpErrors,
browser_diagnostic_navigation_starts: props.browserDiagnosticNavigationStarts,
browser_diagnostic_navigation_failures: props.browserDiagnosticNavigationFailures,
browser_diagnostic_console_errors: props.browserDiagnosticConsoleErrors,
browser_diagnostic_console_warnings: props.browserDiagnosticConsoleWarnings,
capture_mode: props.captureMode,
capture_force_screenshot: props.captureForceScreenshot,
capture_worker_count: props.captureWorkerCount,
capture_use_streaming_encode: props.captureUseStreamingEncode,
capture_use_layered_composite: props.captureUseLayeredComposite,
capture_use_page_side_compositing: props.captureUsePageSideCompositing,
capture_has_hdr_content: props.captureHasHdrContent,
capture_browser_gpu_mode: props.captureBrowserGpuMode,
capture_protocol_timeout_ms: props.captureProtocolTimeoutMs,
capture_page_navigation_timeout_ms: props.capturePageNavigationTimeoutMs,
capture_player_ready_timeout_ms: props.capturePlayerReadyTimeoutMs,
observability_extract_video_count: props.observabilityExtractVideoCount,
observability_extracted_video_count: props.observabilityExtractedVideoCount,
observability_extract_total_frames: props.observabilityExtractTotalFrames,
observability_extract_max_frames_per_video: props.observabilityExtractMaxFramesPerVideo,
observability_extract_avg_frames_per_video: props.observabilityExtractAvgFramesPerVideo,
observability_extract_vfr_probe_ms: props.observabilityExtractVfrProbeMs,
observability_extract_vfr_preflight_ms: props.observabilityExtractVfrPreflightMs,
observability_extract_vfr_preflight_count: props.observabilityExtractVfrPreflightCount,
observability_extract_cache_hits: props.observabilityExtractCacheHits,
observability_extract_cache_misses: props.observabilityExtractCacheMisses,
observability_init_duration_ms: props.observabilityInitDurationMs,
observability_init_tween_count: props.observabilityInitTweenCount,
};
}
function redactTelemetryMessage(value: string): string {
return redactTelemetryString(value);
}
export function trackCommand(command: string): void {
trackEvent("cli_command", { command });
}
export function trackRenderComplete(props: {
durationMs: number;
fps: number;
quality: string;
workers?: number;
docker: boolean;
gpu: boolean;
// "cli" when triggered by `hyperframes render` (default), "studio" when
// triggered by a studio preview-server render (POST /api/projects/:id/render).
source?: "cli" | "studio";
// Composition metadata
compositionDurationMs?: number;
compositionWidth?: number;
compositionHeight?: number;
totalFrames?: number;
// Processing efficiency
speedRatio?: number;
captureAvgMs?: number;
capturePeakMs?: number;
// Resource usage
peakMemoryMb?: number;
memoryFreeMb?: number;
tmpPeakBytes?: number;
// Per-stage timings (subset of RenderPerfSummary.stages)
stageCompileMs?: number;
stageVideoExtractMs?: number;
stageAudioProcessMs?: number;
stageCaptureMs?: number;
stageEncodeMs?: number;
stageAssembleMs?: number;
// Video-extraction breakdown (from RenderPerfSummary.videoExtractBreakdown)
extractResolveMs?: number;
extractHdrProbeMs?: number;
extractHdrPreflightMs?: number;
extractHdrPreflightCount?: number;
extractVfrProbeMs?: number;
extractVfrPreflightMs?: number;
extractVfrPreflightCount?: number;
extractPhase3Ms?: number;
extractCacheHits?: number;
extractCacheMisses?: number;
}): void {
export function trackRenderComplete(
props: {
durationMs: number;
fps: number;
quality: string;
workers?: number;
docker: boolean;
gpu: boolean;
// "cli" when triggered by `hyperframes render` (default), "studio" when
// triggered by a studio preview-server render (POST /api/projects/:id/render).
source?: "cli" | "studio";
// Composition metadata
compositionDurationMs?: number;
compositionWidth?: number;
compositionHeight?: number;
totalFrames?: number;
// Processing efficiency
speedRatio?: number;
captureAvgMs?: number;
capturePeakMs?: number;
// Resource usage
peakMemoryMb?: number;
memoryFreeMb?: number;
tmpPeakBytes?: number;
// Per-stage timings (subset of RenderPerfSummary.stages)
stageCompileMs?: number;
stageVideoExtractMs?: number;
stageAudioProcessMs?: number;
stageCaptureMs?: number;
stageEncodeMs?: number;
stageAssembleMs?: number;
// Video-extraction breakdown (from RenderPerfSummary.videoExtractBreakdown)
extractResolveMs?: number;
extractHdrProbeMs?: number;
extractHdrPreflightMs?: number;
extractHdrPreflightCount?: number;
extractVfrProbeMs?: number;
extractVfrPreflightMs?: number;
extractVfrPreflightCount?: number;
extractPhase3Ms?: number;
extractCacheHits?: number;
extractCacheMisses?: number;
} & RenderObservabilityTelemetryPayload,
): void {
trackEvent("render_complete", {
duration_ms: props.durationMs,
fps: props.fps,
@@ -80,22 +171,25 @@ export function trackRenderComplete(props: {
extract_phase3_ms: props.extractPhase3Ms,
extract_cache_hits: props.extractCacheHits,
extract_cache_misses: props.extractCacheMisses,
...renderObservabilityEventProperties(props),
});
}
export function trackRenderError(props: {
fps: number;
quality: string;
docker: boolean;
workers?: number;
gpu?: boolean;
source?: "cli" | "studio";
failedStage?: string;
errorMessage?: string;
elapsedMs?: number;
peakMemoryMb?: number;
memoryFreeMb?: number;
}): void {
export function trackRenderError(
props: {
fps: number;
quality: string;
docker: boolean;
workers?: number;
gpu?: boolean;
source?: "cli" | "studio";
failedStage?: string;
errorMessage?: string;
elapsedMs?: number;
peakMemoryMb?: number;
memoryFreeMb?: number;
} & RenderObservabilityTelemetryPayload,
): void {
trackEvent("render_error", {
fps: props.fps,
quality: props.quality,
@@ -104,10 +198,65 @@ export function trackRenderError(props: {
gpu: props.gpu,
source: props.source ?? "cli",
failed_stage: props.failedStage,
error_message: props.errorMessage,
error_message: props.errorMessage ? redactTelemetryMessage(props.errorMessage) : undefined,
elapsed_ms: props.elapsedMs,
peak_memory_mb: props.peakMemoryMb,
memory_free_mb: props.memoryFreeMb,
...renderObservabilityEventProperties(props),
});
}
export function trackRenderObservation(props: {
source?: "cli" | "studio";
renderJobId?: string;
phase?: string;
status?: string;
compositionHash?: string;
elapsedMs?: number;
durationMs?: number;
message?: string;
workerCount?: number;
forceScreenshot?: boolean;
useStreamingEncode?: boolean;
useLayeredComposite?: boolean;
usePageSideCompositing?: boolean;
hasHdrContent?: boolean;
captureMode?: string;
videoCount?: number;
extractedVideoCount?: number;
totalFramesExtracted?: number;
maxFramesPerVideo?: number;
avgFramesPerExtractedVideo?: number;
vfrPreflightCount?: number;
vfrPreflightMs?: number;
cacheHits?: number;
cacheMisses?: number;
}): void {
trackEvent("render_observation", {
source: props.source ?? "cli",
render_job_id: props.renderJobId,
phase: props.phase,
status: props.status,
composition_hash: props.compositionHash,
elapsed_ms: props.elapsedMs,
duration_ms: props.durationMs,
message: props.message ? redactTelemetryMessage(props.message) : undefined,
worker_count: props.workerCount,
force_screenshot: props.forceScreenshot,
use_streaming_encode: props.useStreamingEncode,
use_layered_composite: props.useLayeredComposite,
use_page_side_compositing: props.usePageSideCompositing,
has_hdr_content: props.hasHdrContent,
capture_mode: props.captureMode,
video_count: props.videoCount,
extracted_video_count: props.extractedVideoCount,
total_frames_extracted: props.totalFramesExtracted,
max_frames_per_video: props.maxFramesPerVideo,
avg_frames_per_extracted_video: props.avgFramesPerExtractedVideo,
vfr_preflight_count: props.vfrPreflightCount,
vfr_preflight_ms: props.vfrPreflightMs,
extract_cache_hits: props.cacheHits,
extract_cache_misses: props.cacheMisses,
});
}
@@ -0,0 +1,62 @@
import type { RenderJob, RenderPerfSummary } from "@hyperframes/producer";
import type { RenderObservabilityTelemetryPayload } from "./events.js";
type RenderObservabilitySummary = NonNullable<RenderPerfSummary["observability"]>;
export function renderObservabilityTelemetryPayload(
observability: RenderObservabilitySummary | undefined,
): RenderObservabilityTelemetryPayload {
if (!observability) return {};
const diagnostics = observability.browserDiagnostics;
const capture = observability.capture;
const extraction = observability.extraction;
const init = observability.init;
return {
observabilityRenderJobId: observability.renderJobId,
observabilityCompositionHash: observability.compositionHash,
observabilityEventCount: observability.eventCount,
observabilityLastPhase: observability.lastEvent?.phase,
observabilityLastStatus: observability.lastEvent?.status,
observabilityFailedPhase: observability.failedPhase,
browserDiagnosticCount: diagnostics.total,
browserDiagnosticErrors: diagnostics.errors,
browserDiagnosticPageErrors: diagnostics.pageErrors,
browserDiagnosticRequestFailed: diagnostics.requestFailed,
browserDiagnosticHttpErrors: diagnostics.httpErrors,
browserDiagnosticNavigationStarts: diagnostics.navigationStarts,
browserDiagnosticNavigationFailures: diagnostics.navigationFailures,
browserDiagnosticConsoleErrors: diagnostics.consoleErrors,
browserDiagnosticConsoleWarnings: diagnostics.consoleWarnings,
captureMode: capture.captureMode,
captureForceScreenshot: capture.forceScreenshot,
captureWorkerCount: capture.workerCount,
captureUseStreamingEncode: capture.useStreamingEncode,
captureUseLayeredComposite: capture.useLayeredComposite,
captureUsePageSideCompositing: capture.usePageSideCompositing,
captureHasHdrContent: capture.hasHdrContent,
captureBrowserGpuMode: capture.browserGpuMode,
captureProtocolTimeoutMs: capture.protocolTimeoutMs,
capturePageNavigationTimeoutMs: capture.pageNavigationTimeoutMs,
capturePlayerReadyTimeoutMs: capture.playerReadyTimeoutMs,
observabilityExtractVideoCount: extraction?.videoCount,
observabilityExtractedVideoCount: extraction?.extractedVideoCount,
observabilityExtractTotalFrames: extraction?.totalFramesExtracted,
observabilityExtractMaxFramesPerVideo: extraction?.maxFramesPerVideo,
observabilityExtractAvgFramesPerVideo: extraction?.avgFramesPerExtractedVideo,
observabilityExtractVfrProbeMs: extraction?.vfrProbeMs,
observabilityExtractVfrPreflightMs: extraction?.vfrPreflightMs,
observabilityExtractVfrPreflightCount: extraction?.vfrPreflightCount,
observabilityExtractCacheHits: extraction?.cacheHits,
observabilityExtractCacheMisses: extraction?.cacheMisses,
observabilityInitDurationMs: init?.initDurationMs,
observabilityInitTweenCount: init?.tweenCount,
};
}
export function renderJobObservabilityTelemetryPayload(
job: RenderJob | undefined,
): RenderObservabilityTelemetryPayload {
return renderObservabilityTelemetryPayload(
job?.errorDetails?.observability ?? job?.perfSummary?.observability,
);
}