mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
fix(render): add end-to-end observability (#1248)
This commit is contained in:
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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" }));
|
||||
|
||||
@@ -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]",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user