Merge branch 'main' of github.com:heygen-com/hyperframes

This commit is contained in:
Vance Ingalls
2026-07-06 17:48:21 -07:00
3 changed files with 132 additions and 0 deletions
+4
View File
@@ -7,6 +7,7 @@ import { trackRenderFeedback } from "../telemetry/events.js";
import { shouldTrack, flush } from "../telemetry/client.js";
import { getDoctorSummary } from "../telemetry/feedback.js";
import { publishProjectArchive } from "../utils/publishProject.js";
import { submitFeedback } from "../utils/submitFeedback.js";
import { buildIssueUrl, HYPERFRAMES_REPO_URL } from "../utils/feedbackIssue.js";
import { VERSION } from "../version.js";
import { c } from "../ui/colors.js";
@@ -164,7 +165,10 @@ export default defineCommand({
trackRenderFeedback({ rating, comment, doctorSummary });
await flush();
// Ack first so the user isn't kept waiting on the best-effort forward (which
// is bounded to a few seconds and never surfaces an error either way).
console.log(c.dim("Thanks for the feedback!"));
await submitFeedback({ rating, comment, cliVersion: VERSION, env: doctorSummary });
if (args["file-issue"] === true) {
await fileGithubIssue({
@@ -0,0 +1,88 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const getPublishApiBaseUrlMock = vi.hoisted(() => vi.fn(() => "https://api.example.com"));
vi.mock("./publishProject.js", () => ({
getPublishApiBaseUrl: getPublishApiBaseUrlMock,
}));
import { submitFeedback } from "./submitFeedback.js";
describe("submitFeedback", () => {
beforeEach(() => {
getPublishApiBaseUrlMock.mockReturnValue("https://api.example.com");
});
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});
it("posts feedback to the backend endpoint", async () => {
const fetchMock = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
vi.stubGlobal("fetch", fetchMock);
await submitFeedback({
rating: 4,
comment: "fast but font missing",
cliVersion: "1.2.3",
env: "os=darwin/arm64 node=v22.11.0",
});
expect(fetchMock).toHaveBeenCalledOnce();
expect(getPublishApiBaseUrlMock).toHaveBeenCalledOnce();
expect(fetchMock).toHaveBeenCalledWith(
"https://api.example.com/v1/hyperframes/feedback",
expect.objectContaining({
method: "POST",
headers: { "content-type": "application/json", heygen_route: "canary" },
body: JSON.stringify({
rating: 4,
comment: "fast but font missing",
cli_version: "1.2.3",
env: "os=darwin/arm64 node=v22.11.0",
}),
signal: expect.any(AbortSignal),
}),
);
});
it("truncates over-long fields to the backend caps", async () => {
const fetchMock = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
vi.stubGlobal("fetch", fetchMock);
await submitFeedback({
rating: 3,
comment: "x".repeat(2500),
cliVersion: "v".repeat(200),
env: "e".repeat(600),
});
const requestInit = fetchMock.mock.calls[0]?.[1];
expect(requestInit).toBeDefined();
const body = JSON.parse(requestInit?.body as string);
expect(body.comment).toHaveLength(2000);
expect(body.cli_version).toHaveLength(100);
expect(body.env).toHaveLength(500);
});
it("does not reject when fetch rejects", async () => {
const fetchMock = vi.fn().mockRejectedValueOnce(new TypeError("fetch failed"));
vi.stubGlobal("fetch", fetchMock);
await expect(
submitFeedback({ rating: 1, cliVersion: "1.2.3", env: "os=linux" }),
).resolves.toBeUndefined();
});
it("always resolves regardless of fetch outcome", async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(new Response(null, { status: 500 }))
.mockRejectedValueOnce(new Error("offline"));
vi.stubGlobal("fetch", fetchMock);
await expect(submitFeedback({ rating: 2, cliVersion: "1.2.3" })).resolves.toBeUndefined();
await expect(submitFeedback({ rating: 3, cliVersion: "1.2.3" })).resolves.toBeUndefined();
});
});
+40
View File
@@ -0,0 +1,40 @@
import { getPublishApiBaseUrl } from "./publishProject.js";
// Match the backend DTO caps (HyperframesFeedbackRequest). Truncate here so an
// over-long field (e.g. a pasted stack trace) is still forwarded truncated,
// rather than rejected by the backend with a 422 the best-effort path swallows.
const MAX_COMMENT = 2000;
const MAX_CLI_VERSION = 100;
const MAX_ENV = 500;
function cap(value: string | undefined, max: number): string | undefined {
if (value === undefined) return undefined;
return value.length > max ? value.slice(0, max) : value;
}
export async function submitFeedback(input: {
rating: number;
comment?: string;
cliVersion: string;
env?: string;
}): Promise<void> {
try {
const apiBaseUrl = getPublishApiBaseUrl();
await fetch(`${apiBaseUrl}/v1/hyperframes/feedback`, {
method: "POST",
body: JSON.stringify({
rating: input.rating,
comment: cap(input.comment, MAX_COMMENT),
cli_version: cap(input.cliVersion, MAX_CLI_VERSION),
env: cap(input.env, MAX_ENV),
}),
headers: {
"content-type": "application/json",
heygen_route: "canary",
},
signal: AbortSignal.timeout(5000),
});
} catch {
// Best-effort only.
}
}