mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
The standalone `feedback` command runs separately from `render`, so it has no access to the prior render's elapsed time, yet it always passed renderDurationMs: 0 to the feedback analytics event. Since that path is the one used in practice (the auto-prompt returns early for agent and non-interactive runtimes), nearly every feedback event recorded a render duration of exactly 0, which is misleading rather than absent. Make renderDurationMs optional and only include render_duration_ms in the event when a real value is supplied. The standalone command no longer passes a duration; the auto-prompt path still forwards the real elapsed time.
182 lines
5.1 KiB
TypeScript
182 lines
5.1 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
const trackEvent = vi.fn();
|
|
vi.mock("./client.js", () => ({
|
|
trackEvent: (...args: unknown[]) => trackEvent(...args),
|
|
}));
|
|
|
|
const {
|
|
trackRenderComplete,
|
|
trackRenderError,
|
|
trackRenderObservation,
|
|
trackCommandFailure,
|
|
trackCliError,
|
|
trackRenderFeedback,
|
|
} = 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",
|
|
}),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it("forwards distinctId to trackEvent so studio renders attribute to the browser user", () => {
|
|
trackRenderError({
|
|
fps: 30,
|
|
quality: "standard",
|
|
docker: false,
|
|
source: "studio",
|
|
distinctId: "browser-user-123",
|
|
});
|
|
|
|
expect(trackEvent).toHaveBeenCalledWith(
|
|
"render_error",
|
|
expect.objectContaining({ source: "studio" }),
|
|
"browser-user-123",
|
|
);
|
|
});
|
|
|
|
it("sends split capture-stage timing fields on render_complete", () => {
|
|
trackRenderComplete({
|
|
durationMs: 6000,
|
|
fps: 30,
|
|
quality: "standard",
|
|
docker: false,
|
|
gpu: false,
|
|
stageCaptureMs: 5100,
|
|
stageCaptureSetupMs: 1860,
|
|
stageCaptureFrameMs: 3240,
|
|
captureAvgMs: 27,
|
|
});
|
|
|
|
expect(trackEvent).toHaveBeenCalledWith(
|
|
"render_complete",
|
|
expect.objectContaining({
|
|
stage_capture_ms: 5100,
|
|
stage_capture_setup_ms: 1860,
|
|
stage_capture_frame_ms: 3240,
|
|
capture_avg_ms: 27,
|
|
}),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
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]",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("trackRenderFeedback", () => {
|
|
beforeEach(() => {
|
|
trackEvent.mockClear();
|
|
});
|
|
|
|
it("omits render_duration_ms when no duration is known (standalone feedback)", () => {
|
|
trackRenderFeedback({ rating: 4, comment: "great" });
|
|
|
|
const [, props] = trackEvent.mock.calls[0] as [string, Record<string, unknown>];
|
|
expect(props).not.toHaveProperty("render_duration_ms");
|
|
expect(props.$survey_response).toBe(4);
|
|
});
|
|
|
|
it("includes render_duration_ms when a real duration is supplied", () => {
|
|
trackRenderFeedback({ rating: 5, renderDurationMs: 6000 });
|
|
|
|
expect(trackEvent).toHaveBeenCalledWith(
|
|
"survey sent",
|
|
expect.objectContaining({ render_duration_ms: 6000 }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("trackCliError", () => {
|
|
beforeEach(() => {
|
|
trackEvent.mockClear();
|
|
});
|
|
|
|
it("redacts install paths from error_message and stack_trace", () => {
|
|
trackCliError({
|
|
error_name: "Error",
|
|
error_message: "ENOENT: open '/Users/alice/project/index.html'",
|
|
stack_trace: "Error: boom\n at /Users/alice/.cache/hyperframes/chrome/headless",
|
|
command: "info",
|
|
kind: "command_error",
|
|
});
|
|
|
|
const [, props] = trackEvent.mock.calls[0] as [string, Record<string, string>];
|
|
expect(props.error_message).not.toContain("/Users/alice");
|
|
expect(props.error_message).toContain("[path]");
|
|
expect(props.stack_trace).not.toContain("/Users/alice");
|
|
});
|
|
});
|
|
|
|
describe("trackCommandFailure", () => {
|
|
beforeEach(() => {
|
|
trackEvent.mockClear();
|
|
});
|
|
|
|
it("reports an Error as a command_error with name/message/stack", () => {
|
|
const err = new Error("ffmpeg is required to extract audio");
|
|
trackCommandFailure("transcribe", err);
|
|
|
|
expect(trackEvent).toHaveBeenCalledWith(
|
|
"cli_error",
|
|
expect.objectContaining({
|
|
kind: "command_error",
|
|
command: "transcribe",
|
|
error_name: "Error",
|
|
error_message: "ffmpeg is required to extract audio",
|
|
// stack_trace is asserted (redacted) in the trackCliError suite; the
|
|
// raw err.stack no longer matches once paths are stripped.
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("coerces a non-Error reason (e.g. a string) into the message", () => {
|
|
trackCommandFailure("transcribe", "No words found in transcript.");
|
|
|
|
expect(trackEvent).toHaveBeenCalledWith(
|
|
"cli_error",
|
|
expect.objectContaining({
|
|
kind: "command_error",
|
|
command: "transcribe",
|
|
error_message: "No words found in transcript.",
|
|
}),
|
|
);
|
|
});
|
|
});
|