mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
Merge branch 'main' of github.com:heygen-com/hyperframes
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user