Files
hyperframes/packages/cli/src/telemetry/events.ts
T

672 lines
27 KiB
TypeScript

import { redactTelemetryString, type OutputResolutionIssueKind } from "@hyperframes/core";
import type { SubTimelineWaitOutcome } from "@hyperframes/engine";
import { flush, trackEvent } from "./client.js";
import { readConfig } from "./config.js";
// run_id is attached only when the orchestrator set HYPERFRAMES_RUN_ID — an
// absent property, never null/"" (PostHog treats those as real values).
function runIdField(runId: string | undefined): { run_id?: string } {
return runId !== undefined ? { run_id: runId } : {};
}
export interface RenderObservabilityTelemetryPayload {
/** Worst sub-composition timeline wait outcome across sessions. */
subTimelineWait?: SubTimelineWaitOutcome;
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;
captureTransientRetries?: number;
captureMemoryExhaustionDetected?: boolean;
// Mirror of the DE inversion/router state on `RenderCaptureObservability` —
// sourced from the live-mutated capture object rather than `perfSummary`,
// so a hard failure (crash, OOM, timeout) that never reaches perfSummary
// construction still reports which DE experiment cohort it was in. Mapped
// to the SAME `de_*` event keys `trackRenderComplete` sets explicitly from
// `perfSummary.drawElement`; the caller must spread this payload FIRST so
// the more authoritative perfSummary value wins when both are present.
captureDeWorkerInversion?: string;
captureDePreInversionWorkers?: number;
captureDeParallelRouter?: string;
captureDePreRouterWorkers?: number;
captureDeSelfVerifyFallback?: boolean;
captureDeFallbackReason?: string;
/** Non-DE parallel-streaming router outcome ("screenshot" | "beginframe" —
* routed; "eligible_off" — would route but the kill switch is off). */
captureParallelStream?: string;
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 {
sub_timeline_wait: props.subTimelineWait,
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,
capture_transient_retries: props.captureTransientRetries,
capture_memory_exhaustion_detected: props.captureMemoryExhaustionDetected,
de_worker_inversion: props.captureDeWorkerInversion,
de_pre_inversion_workers: props.captureDePreInversionWorkers,
de_parallel_router: props.captureDeParallelRouter,
de_pre_router_workers: props.captureDePreRouterWorkers,
de_self_verify_fallback: props.captureDeSelfVerifyFallback,
de_fallback_reason: props.captureDeFallbackReason,
capture_parallel_stream: props.captureParallelStream,
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, runId?: string): void {
trackEvent("cli_command", {
command,
...runIdField(runId),
});
}
export function trackRenderComplete(
props: {
durationMs: number;
fps: number;
quality: string;
/** Authoring workflow skill that drove this render (e.g. "product-launch-video"). */
authoringSkill?: string;
workers?: number;
docker: boolean;
gpu: boolean;
// Static-frame dedup outcome (opt-out HF_STATIC_DEDUP=false). Undefined on
// render paths with no capture session.
staticDedupEnabled?: boolean;
staticDedupArmed?: boolean;
staticDedupSkipReason?: string;
staticDedupPredictedFrames?: number;
staticDedupReusedFrames?: number;
// BeginFrame no-damage reuse outcome (Linux/Docker lastFrameCache — the BF
// counterpart of static dedup). Undefined outside beginframe capture mode.
beginFrameNoDamageFrames?: number;
beginFrameHasDamageFrames?: number;
// drawElement fast-capture outcome (default-on release visibility).
// Undefined on render paths with no capture session.
deCaptureMode?: string;
deCompileGate?: string;
deClampReason?: string;
deWorkerInversion?: string;
dePreInversionWorkers?: number;
deParallelRouter?: string;
dePreRouterWorkers?: number;
deGateReason?: string;
deWorkerEncode?: boolean;
deVerifyArmed?: number;
deVerifyChecked?: number;
deVerifyMinDb?: number;
deVerifyInitMs?: number;
deSelfVerifyFallback?: boolean;
deFallbackReason?: string;
deBlankSuspects?: number;
deBlankDeterministicAccepts?: number;
deBlankRecaptures?: number;
deBoundaryFrames?: number;
deNcprFallbacks?: number;
// "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;
/** Warmup-robust per-frame capture median (basis for speedup estimates). */
captureP50Ms?: number;
/** <video> element count (speedup segmentation: injection comps read lower). */
videoCount?: 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;
stageCaptureSetupMs?: number;
stageCaptureFrameMs?: 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;
// Attribute this event to a specific user (e.g. the browser user who
// triggered a studio render); defaults to the install anonymousId.
distinctId?: string;
} & RenderObservabilityTelemetryPayload,
): void {
trackEvent(
"render_complete",
{
// Spread first: explicit de_* keys below (sourced from the more
// authoritative perfSummary.drawElement, always present on this
// success path) must win over the observability-capture fallback
// this shares with trackRenderError's failure path.
...renderObservabilityEventProperties(props),
duration_ms: props.durationMs,
fps: props.fps,
quality: props.quality,
authoring_skill: props.authoringSkill,
workers: props.workers,
docker: props.docker,
gpu: props.gpu,
static_dedup_enabled: props.staticDedupEnabled,
static_dedup_armed: props.staticDedupArmed,
static_dedup_skip_reason: props.staticDedupSkipReason,
static_dedup_predicted_frames: props.staticDedupPredictedFrames,
static_dedup_reused_frames: props.staticDedupReusedFrames,
begin_frame_no_damage_frames: props.beginFrameNoDamageFrames,
begin_frame_has_damage_frames: props.beginFrameHasDamageFrames,
de_capture_mode: props.deCaptureMode,
de_compile_gate: props.deCompileGate,
de_clamp_reason: props.deClampReason,
de_worker_inversion: props.deWorkerInversion,
de_pre_inversion_workers: props.dePreInversionWorkers,
de_parallel_router: props.deParallelRouter,
de_pre_router_workers: props.dePreRouterWorkers,
de_gate_reason: props.deGateReason,
de_worker_encode: props.deWorkerEncode,
de_verify_armed: props.deVerifyArmed,
de_verify_checked: props.deVerifyChecked,
de_verify_min_db: props.deVerifyMinDb,
de_verify_init_ms: props.deVerifyInitMs,
de_self_verify_fallback: props.deSelfVerifyFallback,
de_fallback_reason: props.deFallbackReason,
de_blank_suspects: props.deBlankSuspects,
de_blank_deterministic_accepts: props.deBlankDeterministicAccepts,
de_blank_recaptures: props.deBlankRecaptures,
de_boundary_frames: props.deBoundaryFrames,
de_ncpr_fallbacks: props.deNcprFallbacks,
source: props.source ?? "cli",
composition_duration_ms: props.compositionDurationMs,
composition_width: props.compositionWidth,
composition_height: props.compositionHeight,
total_frames: props.totalFrames,
speed_ratio: props.speedRatio,
capture_avg_ms: props.captureAvgMs,
capture_p50_ms: props.captureP50Ms,
video_count: props.videoCount,
capture_peak_ms: props.capturePeakMs,
peak_memory_mb: props.peakMemoryMb,
memory_free_mb: props.memoryFreeMb,
tmp_peak_bytes: props.tmpPeakBytes,
stage_compile_ms: props.stageCompileMs,
stage_video_extract_ms: props.stageVideoExtractMs,
stage_audio_process_ms: props.stageAudioProcessMs,
stage_capture_ms: props.stageCaptureMs,
stage_capture_setup_ms: props.stageCaptureSetupMs,
stage_capture_frame_ms: props.stageCaptureFrameMs,
stage_encode_ms: props.stageEncodeMs,
stage_assemble_ms: props.stageAssembleMs,
extract_resolve_ms: props.extractResolveMs,
extract_hdr_probe_ms: props.extractHdrProbeMs,
extract_hdr_preflight_ms: props.extractHdrPreflightMs,
extract_hdr_preflight_count: props.extractHdrPreflightCount,
extract_vfr_probe_ms: props.extractVfrProbeMs,
extract_vfr_preflight_ms: props.extractVfrPreflightMs,
extract_vfr_preflight_count: props.extractVfrPreflightCount,
extract_phase3_ms: props.extractPhase3Ms,
extract_cache_hits: props.extractCacheHits,
extract_cache_misses: props.extractCacheMisses,
},
props.distinctId,
);
// Send immediately instead of waiting for the exit-time flush. The render
// command's normal teardown (agent-pipe EPIPE → process.exit(0), or an
// explicit process.exit) kills the lazy beforeExit flush mid-flight, which
// is why only ~10-15% of successful renders ever produced a render_complete
// — and the survivors skewed toward users with low RTT to PostHog. The
// process is alive and idle here; if it still dies mid-request, the queue
// keeps the event for the exit-time flushSync() fallback.
void flush();
}
export function trackRenderError(
props: {
fps: number;
quality: string;
/** Authoring workflow skill that drove this render (e.g. "product-launch-video"). */
authoringSkill?: string;
docker: boolean;
workers?: number;
gpu?: boolean;
source?: "cli" | "studio";
failedStage?: string;
errorMessage?: string;
elapsedMs?: number;
peakMemoryMb?: number;
memoryFreeMb?: number;
// Attribute this event to a specific user (e.g. the browser user who
// triggered a studio render); defaults to the install anonymousId.
distinctId?: string;
} & RenderObservabilityTelemetryPayload,
): void {
trackEvent(
"render_error",
{
fps: props.fps,
quality: props.quality,
authoring_skill: props.authoringSkill,
docker: props.docker,
workers: props.workers,
gpu: props.gpu,
source: props.source ?? "cli",
failed_stage: props.failedStage,
error_message: props.errorMessage ? redactTelemetryMessage(props.errorMessage) : undefined,
elapsed_ms: props.elapsedMs,
peak_memory_mb: props.peakMemoryMb,
memory_free_mb: props.memoryFreeMb,
...renderObservabilityEventProperties(props),
},
props.distinctId,
);
// Same rationale as trackRenderComplete: error paths process.exit(1) before
// the lazy flush can win its race — send now, exit-time fallback covers the rest.
void flush();
}
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;
captureOperation?: string;
framesCompleted?: number;
totalFrames?: number;
heartbeatIndex?: number;
stageElapsedMs?: number;
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,
capture_operation: props.captureOperation,
frames_completed: props.framesCompleted,
total_frames: props.totalFrames,
heartbeat_index: props.heartbeatIndex,
stage_elapsed_ms: props.stageElapsedMs,
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,
});
}
export function trackInitTemplate(templateId: string, props?: { tailwind?: boolean }): void {
trackEvent("init_template", { template: templateId, tailwind: props?.tailwind });
}
export function trackBrowserInstall(): void {
trackEvent("browser_install", {});
}
// Sign-in lifecycle. The CLI tracks command and render lifecycles but never
// authentication, so `auth login` outcomes are invisible on the observability
// dashboards — a completed sign-in, a browser flow the user abandoned, and a
// rejected key all look identical (i.e. absent). These three events close that
// gap so the sign-in funnel is measurable like the render funnel already is.
// `method` is "oauth" (the default browser PKCE flow) or "api_key". No token,
// key, identity, email, or free text is ever attached — only the method and a
// low-cardinality outcome/reason.
//
// The three trackers accept an optional `distinctId`, forwarded to trackEvent
// exactly like trackRenderComplete/trackRenderError already do. It is unused
// today (events attribute to the install's anonymousId), but pre-plumbing it
// makes attributing a completed sign-in to a resolved identity later a one-line
// change at the callsite rather than a signature sweep.
export type AuthLoginMethod = "oauth" | "api_key";
export type AuthLoginFailureReason =
| "flow_error" // OAuth authorization/exchange threw a real error
| "flow_timeout" // OAuth callback wait elapsed (user closed the tab / walked away)
| "no_credential" // flow reported success but nothing was persisted
| "rejected" // backend rejected the supplied API key (401)
| "invalid_input" // key was empty, header-unsafe, or too short
| "aborted"; // prompt cancelled, or no key arrived on stdin before timeout
export function trackAuthLoginStarted(method: AuthLoginMethod, distinctId?: string): void {
trackEvent("auth_login_started", { method }, distinctId);
}
export function trackAuthLoginCompleted(method: AuthLoginMethod, distinctId?: string): void {
trackEvent("auth_login_completed", { method }, distinctId);
}
export function trackAuthLoginFailed(
method: AuthLoginMethod,
reason: AuthLoginFailureReason,
distinctId?: string,
): void {
trackEvent("auth_login_failed", { method, reason }, distinctId);
}
// Associate this install with the signed-in HeyGen account after a completed
// sign-in. Emits a PostHog `$identify` alias whose `$anon_distinct_id` is the
// install's anonymousId, so events recorded before sign-in stitch to the same
// person instead of stranding as a separate anonymous profile. Routed through
// trackEvent so it shares the opt-out gate and flush path — a no-op when
// telemetry is disabled. `distinctId` is the account email (else username);
// see the privacy notice in showTelemetryNotice and docs/packages/cli.mdx.
export function identifyUser(distinctId: string): void {
if (!distinctId) return;
trackEvent("$identify", { $anon_distinct_id: readConfig().anonymousId }, distinctId);
}
// A render was rejected by the output-resolution/alpha/HDR pre-flight (P1-3)
// before any browser/ffmpeg work. Counts the "caught early" saves on dashboard
// 1783183, distinct from deep render failures. `kind` is the low-cardinality
// `OutputResolutionIssueKind` (aspect-mismatch / alpha-incompatible / etc.),
// typed to the union so the metric can never carry free text.
export function trackRenderPreflightRejected(props: { kind: OutputResolutionIssueKind }): void {
trackEvent("render_preflight_rejected", { kind: props.kind });
}
export function trackCliError(props: {
error_name: string;
error_message: string;
stack_trace?: string;
command?: string;
kind: "uncaught_exception" | "unhandled_rejection" | "command_error";
}): void {
trackEvent("cli_error", {
error_name: props.error_name,
// Redact before truncating — CLI messages and stack traces carry absolute
// install paths (/Users/...), cache dirs, and user-supplied args. Same
// redaction the render_* events already apply.
error_message: redactTelemetryMessage(props.error_message).slice(0, 1000),
stack_trace: props.stack_trace
? redactTelemetryMessage(props.stack_trace).slice(0, 2000)
: undefined,
command: props.command,
kind: props.kind,
});
}
/**
* One figma import outcome (asset/tokens/component). Carries capability mix,
* dedup effectiveness, and fidelity-degradation counts — never fileKeys,
* node ids, names, or descriptions.
*/
export function trackFigmaImport(props: {
phase: "asset" | "tokens" | "component";
durationMs: number;
reused?: boolean;
tokensMode?: "variables" | "styles";
entryCount?: number;
unresolvedBindings?: number;
rasterizedNodes?: number;
rasterizeFailures?: number;
}): void {
trackEvent("figma_import", {
phase: props.phase,
duration_ms: props.durationMs,
...(props.reused !== undefined ? { reused: props.reused } : {}),
...(props.tokensMode !== undefined ? { tokens_mode: props.tokensMode } : {}),
...(props.entryCount !== undefined ? { entry_count: props.entryCount } : {}),
...(props.unresolvedBindings !== undefined
? { unresolved_bindings: props.unresolvedBindings }
: {}),
...(props.rasterizedNodes !== undefined ? { rasterized_nodes: props.rasterizedNodes } : {}),
...(props.rasterizeFailures !== undefined
? { rasterize_failures: props.rasterizeFailures }
: {}),
});
}
// Report why a command failed before it exits non-zero. cli_command_result
// records the failure but not the reason; this fills that gap via cli_error so
// command failures are diagnosable. Enqueues synchronously — the process `exit`
// handler flushes it. Drop this into any command's failure path.
export function trackCommandFailure(command: string, err: unknown): void {
const error = err instanceof Error ? err : new Error(String(err));
trackCliError({
error_name: error.name,
error_message: error.message,
stack_trace: error.stack,
command,
kind: "command_error",
});
}
// Whisper being absent/uninstallable is an environment prerequisite gap, not a
// command crash — track it on its own low-severity metric instead of cli_error
// so the command-failure budget reflects real bugs. `optional` records whether
// the caller (init / skill pipeline) treated captions as skippable.
export function trackTranscribeUnavailable(props: { optional: boolean }): void {
trackEvent("transcribe_unavailable", { optional: props.optional });
}
// grade-compare / compare stand up headless Chrome and render up to 16 cells.
// Cell count, truncation-cap hits, and whether the render-ready timeout fired
// are the signals needed before safely lifting the cap. Low-cardinality only.
export function trackCompareSheet(props: {
command: "grade-compare" | "compare";
cells: number;
truncated: boolean;
total: number;
renderReadyTimedOut: boolean;
}): void {
trackEvent("media_use_compare", {
command: props.command,
cells: props.cells,
truncated: props.truncated,
total: props.total,
render_ready_timed_out: props.renderReadyTimedOut,
});
}
// A skills install was skipped because a required prerequisite binary is
// absent from PATH (e.g. git on a fresh Windows box). Best-effort callers
// (init) skip cleanly rather than crash, so the skip is otherwise invisible;
// this surfaces the rare environments that hit it. `reason` is a low-cardinality
// binary tag (e.g. "git_missing"), never a path or free text.
export function trackSkillsInstallSkipped(props: { reason: string }): void {
trackEvent("cli skill install skipped", { reason: props.reason });
}
export function trackRenderFeedback(props: {
rating: number;
renderDurationMs?: number;
comment?: string;
doctorSummary?: string;
}): void {
trackEvent("survey sent", {
$survey_id: "render_satisfaction",
$survey_response: props.rating,
...(props.comment ? { $survey_response_2: props.comment } : {}),
...(props.renderDurationMs !== undefined ? { render_duration_ms: props.renderDurationMs } : {}),
...(props.doctorSummary ? { doctor_summary: props.doctorSummary } : {}),
});
}
export function trackCommandResult(props: {
command: string;
success: boolean;
exitCode: number;
durationMs: number;
runId?: string;
}): void {
trackEvent("cli_command_result", {
command: props.command,
success: props.success,
exit_code: props.exitCode,
duration_ms: props.durationMs,
...runIdField(props.runId),
});
}
export function trackCheckReport(props: {
contrastGate: boolean;
motionGate: boolean;
captionZoneGate: boolean;
frameCheckGate: boolean;
snapshotsGate: boolean;
lintErrors: number;
lintWarnings: number;
runtimeErrors: number;
runtimeWarnings: number;
layoutErrors: number;
layoutWarnings: number;
motionErrors: number;
motionWarnings: number;
contrastErrors: number;
contrastWarnings: number;
launchSettleMs: number;
seekLoopMs: number;
contrastMs: number;
gridPoints: number;
contrastPoints: number;
ok: boolean;
exitCode: number;
runId?: string;
}): void {
trackEvent("check_report", {
gate_contrast: props.contrastGate,
gate_motion: props.motionGate,
gate_caption_zone: props.captionZoneGate,
gate_frame_check: props.frameCheckGate,
gate_snapshots: props.snapshotsGate,
lint_errors: props.lintErrors,
lint_warnings: props.lintWarnings,
runtime_errors: props.runtimeErrors,
runtime_warnings: props.runtimeWarnings,
layout_errors: props.layoutErrors,
layout_warnings: props.layoutWarnings,
motion_errors: props.motionErrors,
motion_warnings: props.motionWarnings,
contrast_errors: props.contrastErrors,
contrast_warnings: props.contrastWarnings,
launch_settle_ms: props.launchSettleMs,
seek_loop_ms: props.seekLoopMs,
contrast_ms: props.contrastMs,
grid_points: props.gridPoints,
contrast_points: props.contrastPoints,
ok: props.ok,
exit_code: props.exitCode,
...runIdField(props.runId),
});
}