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:
@@ -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