mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
feat(feedback): adopt 0–10 recommendation scale (#2438)
* feat(feedback): adopt 10-point recommendation scale * docs(feedback): keep OSS scale contract self-contained
This commit is contained in:
@@ -27,7 +27,7 @@ describe("buildIssueUrl", () => {
|
||||
const url = buildIssueUrl(base);
|
||||
expect(decoded(url, "title")).toBe("[feedback] GSAP timeline froze on seek");
|
||||
const body = decoded(url, "body");
|
||||
expect(body).toContain("2/5");
|
||||
expect(body).toContain("2/10");
|
||||
expect(body).toContain("https://hyperframes.dev/p/abc123");
|
||||
expect(body).toContain("os=darwin/arm64");
|
||||
expect(body).toContain("cli=1.2.3");
|
||||
@@ -35,7 +35,7 @@ describe("buildIssueUrl", () => {
|
||||
|
||||
it("falls back to a generic title when there is no comment", () => {
|
||||
const url = buildIssueUrl({ ...base, comment: undefined });
|
||||
expect(decoded(url, "title")).toBe("Render feedback (rating 2/5)");
|
||||
expect(decoded(url, "title")).toBe("Render feedback (rating 2/10)");
|
||||
});
|
||||
|
||||
it("truncates an overlong comment in the body", () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { FEEDBACK_RATING_SCALE } from "./feedbackRating.js";
|
||||
|
||||
// Reading package.json at runtime from the single-file bundled CLI is awkward,
|
||||
// so we keep the canonical repo as a constant. It must match the `repository.url`
|
||||
// in packages/cli/package.json.
|
||||
@@ -33,7 +35,7 @@ function truncate(value: string, max: number): string {
|
||||
|
||||
function buildIssueTitle(rating: number, comment?: string): string {
|
||||
const firstLine = comment?.split("\n")[0]?.trim();
|
||||
if (!firstLine) return `Render feedback (rating ${rating}/5)`;
|
||||
if (!firstLine) return `Render feedback (rating ${rating}/${FEEDBACK_RATING_SCALE})`;
|
||||
return `[feedback] ${truncate(firstLine, TITLE_MAX)}`;
|
||||
}
|
||||
|
||||
@@ -44,7 +46,7 @@ function buildIssueBody(input: IssueInput): string {
|
||||
: "_Publishing the repro failed, no public link available._";
|
||||
|
||||
return [
|
||||
`**Rating:** ${input.rating}/5`,
|
||||
`**Rating:** ${input.rating}/${FEEDBACK_RATING_SCALE}`,
|
||||
"",
|
||||
"## Comment",
|
||||
comment ? truncate(comment, COMMENT_MAX) : "_No comment provided._",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { FEEDBACK_RATING_SCALE, parseFeedbackRating } from "./feedbackRating.js";
|
||||
|
||||
describe("parseFeedbackRating", () => {
|
||||
it.each(["0", "10"])("accepts the NPS boundary %s", (raw) => {
|
||||
expect(parseFeedbackRating(raw)).toBe(Number(raw));
|
||||
});
|
||||
|
||||
it.each(["-1", "11", "4.5", "10x", ""])("rejects invalid rating %j", (raw) => {
|
||||
expect(parseFeedbackRating(raw)).toBeNull();
|
||||
});
|
||||
|
||||
it("declares the serialized rating scale", () => {
|
||||
expect(FEEDBACK_RATING_SCALE).toBe(10);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
export const FEEDBACK_RATING_SCALE = 10;
|
||||
|
||||
export function parseFeedbackRating(raw: string): number | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!/^\d+$/.test(trimmed)) return null;
|
||||
const rating = Number(trimmed);
|
||||
return Number.isInteger(rating) && rating >= 0 && rating <= FEEDBACK_RATING_SCALE ? rating : null;
|
||||
}
|
||||
@@ -38,6 +38,7 @@ describe("submitFeedback", () => {
|
||||
headers: { "content-type": "application/json", heygen_route: "canary" },
|
||||
body: JSON.stringify({
|
||||
rating: 4,
|
||||
rating_scale: 10,
|
||||
comment: "fast but font missing",
|
||||
cli_version: "1.2.3",
|
||||
env: "os=darwin/arm64 node=v22.11.0",
|
||||
@@ -47,6 +48,17 @@ describe("submitFeedback", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([0, 10])("serializes the NPS boundary %i without changing it", async (rating) => {
|
||||
const fetchMock = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await submitFeedback({ rating, cliVersion: "1.2.3" });
|
||||
|
||||
const requestInit = fetchMock.mock.calls[0]?.[1];
|
||||
const body = JSON.parse(requestInit?.body as string);
|
||||
expect(body).toMatchObject({ rating, rating_scale: 10 });
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getPublishApiBaseUrl } from "./publishProject.js";
|
||||
import { FEEDBACK_RATING_SCALE } from "./feedbackRating.js";
|
||||
|
||||
// Match the backend DTO caps (HyperframesFeedbackRequest). Truncate here so an
|
||||
// over-long field (e.g. a pasted stack trace) is still forwarded truncated,
|
||||
@@ -24,6 +25,7 @@ export async function submitFeedback(input: {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
rating: input.rating,
|
||||
rating_scale: FEEDBACK_RATING_SCALE,
|
||||
comment: cap(input.comment, MAX_COMMENT),
|
||||
cli_version: cap(input.cliVersion, MAX_CLI_VERSION),
|
||||
env: cap(input.env, MAX_ENV),
|
||||
|
||||
Reference in New Issue
Block a user