Skip to content
Merged
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
24 changes: 24 additions & 0 deletions packages/cli/src/commands/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1861,4 +1861,28 @@ describe("render command batch options", () => {
}, 60_000);
});

describe("normalizeStageCode", () => {
const { normalizeStageCode } = renderModule;

it("maps every known updateJobStatus stage string to its code", () => {
expect(normalizeStageCode("Queued")).toBe("queued");
expect(normalizeStageCode("Compiling composition")).toBe("compiling_composition");
expect(normalizeStageCode("Extracting video frames")).toBe("extracting_video_frames");
expect(normalizeStageCode("Processing audio tracks")).toBe("processing_audio_tracks");
expect(normalizeStageCode("Starting frame capture")).toBe("starting_frame_capture");
expect(normalizeStageCode("Render complete")).toBe("render_complete");
expect(normalizeStageCode("Render cancelled")).toBe("render_cancelled");
expect(normalizeStageCode("pipeline")).toBe("pipeline");
});

it("slugifies an unrecognized stage string instead of bucketing it as unknown", () => {
expect(normalizeStageCode("Some New Stage!")).toBe("some_new_stage");
});

it("falls back to unknown only when slugifying produces nothing usable", () => {
expect(normalizeStageCode("")).toBe("unknown");
expect(normalizeStageCode("!!!")).toBe("unknown");
});
});

// Variables-helper tests live in `../utils/variables.test.ts`.
42 changes: 42 additions & 0 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,37 @@ function reportDeParallelRouterBreakerTrip(quiet: boolean): void {
);
}

/**
* `job.currentStage`/`failedStage` are free-text progress labels
* (`updateJobStatus`'s callers each pass their own human sentence — "Compiling
* composition", "Extracting video frames", …), which makes an exact string
* property unbounded in a telemetry event. This maps the known set to a
* stable snake_case code, and slugifies anything unrecognized instead of
* bucketing it into a single opaque "unknown" — a future stage string still
* gets a distinct, readable code without needing this map updated first.
*/
const KNOWN_STAGE_CODES: Readonly<Record<string, string>> = {
Queued: "queued",
"Compiling composition": "compiling_composition",
"Extracting video frames": "extracting_video_frames",
"Processing audio tracks": "processing_audio_tracks",
"Starting frame capture": "starting_frame_capture",
"Render complete": "render_complete",
"Render cancelled": "render_cancelled",
pipeline: "pipeline",
};

export function normalizeStageCode(stage: string): string {
const known = KNOWN_STAGE_CODES[stage];
if (known) return known;
const slug = stage
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
return slug || "unknown";
}

function handleRenderError(
error: unknown,
options: RenderOptions,
Expand All @@ -1520,6 +1551,14 @@ function handleRenderError(
elapsedMs: Date.now() - startTime,
errorMessage: message,
failedStage,
// A bucketable failure taxonomy alongside the free-text error_message
// above: error.name is one of ~20 typed producer error classes
// (CaptureFailure, DrawElementCaptureError, SwiftShaderAssertionError, …);
// failed_stage_code is the same job.currentStage value normalized to a
// stable code. Error-conditional by nature — there is no equivalent on
// the render_complete success path, since nothing failed to name.
errorName: error instanceof Error ? error.name : "unknown",
failedStageCode: normalizeStageCode(failedStage || "pipeline"),
...renderJobObservabilityTelemetryPayload(job),
...getMemorySnapshot(),
});
Expand Down Expand Up @@ -1622,6 +1661,9 @@ function trackRenderMetrics(
dePreInversionWorkers: perf?.drawElement?.preInversionWorkers,
compositionElementCount: perf?.drawElement?.compositionElementCount,
compositionElementCountSource: perf?.drawElement?.compositionElementCountSource,
compositionElementTags: perf?.drawElement?.compositionElementTags,
arollVideoCount: perf?.drawElement?.arollVideoCount,
heygenVideoCount: perf?.drawElement?.heygenVideoCount,
deShortBand: perf?.drawElement?.shortBand,
deParallelRouter: perf?.drawElement?.parallelRouter,
dePreRouterWorkers: perf?.drawElement?.preRouterWorkers,
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/telemetry/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export interface RenderObservabilityTelemetryPayload {
captureDePreInversionWorkers?: number;
captureCompositionElementCount?: number;
captureCompositionElementCountSource?: string;
captureCompositionElementTags?: Readonly<Record<string, number>>;
captureArollVideoCount?: number;
captureHeygenVideoCount?: number;
captureDeShortBand?: string;
captureDeParallelRouter?: string;
captureDeGpuRenderer?: string;
Expand Down Expand Up @@ -137,6 +140,9 @@ function renderObservabilityEventProperties(props: RenderObservabilityTelemetryP
de_pre_inversion_workers: props.captureDePreInversionWorkers,
composition_element_count: props.captureCompositionElementCount,
composition_element_count_source: props.captureCompositionElementCountSource,
composition_element_tags: props.captureCompositionElementTags,
aroll_video_count: props.captureArollVideoCount,
heygen_video_count: props.captureHeygenVideoCount,
de_short_band: props.captureDeShortBand,
de_parallel_router: props.captureDeParallelRouter,
gpu_renderer: props.captureDeGpuRenderer,
Expand Down Expand Up @@ -276,6 +282,9 @@ export function trackRenderComplete(
dePreInversionWorkers?: number;
compositionElementCount?: number;
compositionElementCountSource?: string;
compositionElementTags?: Readonly<Record<string, number>>;
arollVideoCount?: number;
heygenVideoCount?: number;
deShortBand?: string;
deParallelRouter?: string;
dePreRouterWorkers?: number;
Expand Down Expand Up @@ -380,6 +389,9 @@ export function trackRenderComplete(
de_pre_inversion_workers: props.dePreInversionWorkers,
composition_element_count: props.compositionElementCount,
composition_element_count_source: props.compositionElementCountSource,
composition_element_tags: props.compositionElementTags,
aroll_video_count: props.arollVideoCount,
heygen_video_count: props.heygenVideoCount,
de_short_band: props.deShortBand,
de_parallel_router: props.deParallelRouter,
de_pre_router_workers: props.dePreRouterWorkers,
Expand Down Expand Up @@ -457,6 +469,10 @@ export function trackRenderError(
gpu?: boolean;
source?: "cli" | "studio";
failedStage?: string;
/** One of ~20 typed producer error classes (CaptureFailure, DrawElementCaptureError, …), or "unknown" for a non-Error throw. */
errorName?: string;
/** failedStage normalized to a stable snake_case code. */
failedStageCode?: string;
errorMessage?: string;
elapsedMs?: number;
peakMemoryMb?: number;
Expand All @@ -477,6 +493,8 @@ export function trackRenderError(
gpu: props.gpu,
source: props.source ?? "cli",
failed_stage: props.failedStage,
error_name: props.errorName,
failed_stage_code: props.failedStageCode,
error_message: props.errorMessage ? redactTelemetryMessage(props.errorMessage) : undefined,
elapsed_ms: props.elapsedMs,
peak_memory_mb: props.peakMemoryMb,
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/telemetry/renderObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export function renderObservabilityTelemetryPayload(
captureDePreInversionWorkers: capture.dePreInversionWorkers,
captureCompositionElementCount: capture.compositionElementCount,
captureCompositionElementCountSource: capture.compositionElementCountSource,
captureCompositionElementTags: capture.compositionElementTags,
captureArollVideoCount: capture.arollVideoCount,
captureHeygenVideoCount: capture.heygenVideoCount,
captureDeShortBand: capture.deShortBand,
captureDeParallelRouter: capture.deParallelRouter,
captureDeGpuRenderer: capture.deGpuRenderer,
Expand Down
26 changes: 25 additions & 1 deletion packages/producer/src/services/render/observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export interface RenderCaptureObservability {
* Element count for the short-comp band gate (`resolveCompositionElementCount`):
* the LIVE DOM size from the already-running probe session when one is
* initialized, falling back to a static scan of the compiled HTML
* (`countElementTags`) otherwise. Live is authoritative — a static scan
* (`scanElementTags`) otherwise. Live is authoritative — a static scan
* cannot see elements a composition's own script creates at runtime.
*
* Emitted on every render, not just inverted ones — this is the variable the
Expand All @@ -93,6 +93,30 @@ export interface RenderCaptureObservability {
* unlock for the band.
*/
compositionElementCountSource?: "live" | "static";
/**
* Per-tag breakdown of the same static scan behind `compositionElementCount`
* — one shared regex pass feeds both fields, so a fleet query summing this
* map's values always reconciles against the integer. Capped by
* `scanElementTags` (top tags by count + an `other` bucket) so a
* pathological composition's distinct tag count can't inflate the event
* payload. Only set when
* `compositionElementCountSource` is "static" — the live path measures a
* DOM node count directly and never runs this scan.
*/
compositionElementTags?: Readonly<Record<string, number>>;
/**
* `<video data-aroll="true">` elements from the same static scan as
* `compositionElementTags`. Only set when `compositionElementCountSource`
* is "static".
*/
arollVideoCount?: number;
/**
* `<video data-media-source="heygen">` elements from the same static scan
* as `compositionElementTags` — the media-use skill stamps this attribute
* only when the mounted video's ledger record traces to the "heygen.video"
* provider. Only set when `compositionElementCountSource` is "static".
*/
heygenVideoCount?: number;
/**
* Short-comp band decision, emitted only when the band is DECISIVE — every
* other inversion-eligibility condition passed and only the floor (250 vs
Expand Down
9 changes: 9 additions & 0 deletions packages/producer/src/services/render/perfSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ export interface DrawElementPerfInput {
compositionElementCount?: number;
/** Provenance of the element count: "live" (probe DOM, trusted to gate) | "static" (source scan, not). */
compositionElementCountSource?: "live" | "static";
/** Per-tag breakdown of the same static scan behind compositionElementCount; only set when the source above is "static". */
compositionElementTags?: Readonly<Record<string, number>>;
/** `<video data-aroll="true">` count from the same static scan; only set when compositionElementCountSource is "static". */
arollVideoCount?: number;
/** `<video data-media-source="heygen">` count from the same static scan; only set when compositionElementCountSource is "static". */
heygenVideoCount?: number;
/** Short-comp band decision when the band was DECISIVE: "applied" (inverts once HF_DE_SHORT_BAND_ROUTE is on; counterfactual in the baseline release) | "skipped_elements" (element ceiling was the only blocker); unset when the band could not have affected this render. */
shortBand?: "applied" | "skipped_elements" | "unmeasured";
parallelRouter?: "routed" | "reverted";
Expand Down Expand Up @@ -135,6 +141,9 @@ function aggregateDrawElement(
preInversionWorkers: de.preInversionWorkers,
compositionElementCount: de.compositionElementCount,
compositionElementCountSource: de.compositionElementCountSource,
compositionElementTags: de.compositionElementTags,
arollVideoCount: de.arollVideoCount,
heygenVideoCount: de.heygenVideoCount,
shortBand: de.shortBand,
parallelRouter: de.parallelRouter ?? "none",
preRouterWorkers: de.preRouterWorkers,
Expand Down
Loading
Loading