This commit is contained in:
Miguel Ángel
2026-08-30 02:16:15 -07:00
committed by GitHub
3 changed files with 103 additions and 7 deletions
+7 -6
View File
@@ -10,7 +10,7 @@ import { shouldTrack, flush } from "../telemetry/client.js";
import { getDoctorSummary } from "../telemetry/feedback.js";
import { readConfig, type RecentRenderRecord } from "../telemetry/config.js";
import { publishProjectArchive } from "../utils/publishProject.js";
import { submitFeedback } from "../utils/submitFeedback.js";
import { submitCatalogSearchMiss, 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";
@@ -205,13 +205,14 @@ export default defineCommand({
console.log(c.dim("Telemetry is disabled. Nothing sent."));
return;
}
trackCatalogSearchMiss({
query: searchMiss,
wanted: normalizeComment(args.wanted),
tier: normalizeComment(args.tier),
});
const wanted = normalizeComment(args.wanted);
const tier = normalizeComment(args.tier);
trackCatalogSearchMiss({ query: searchMiss, wanted, tier });
await flush();
// Ack before the forward, which is best-effort and bounded, so the
// reporter is never left waiting on it.
console.log(c.dim("Logged the gap. Thanks — that is how the catalog grows."));
await submitCatalogSearchMiss({ query: searchMiss, wanted, tier, cliVersion: VERSION });
return;
}
+57 -1
View File
@@ -6,7 +6,7 @@ vi.mock("./publishProject.js", () => ({
getPublishApiBaseUrl: getPublishApiBaseUrlMock,
}));
import { submitFeedback } from "./submitFeedback.js";
import { submitCatalogSearchMiss, submitFeedback } from "./submitFeedback.js";
describe("submitFeedback", () => {
beforeEach(() => {
@@ -98,3 +98,59 @@ describe("submitFeedback", () => {
await expect(submitFeedback({ rating: 3, cliVersion: "1.2.3" })).resolves.toBeUndefined();
});
});
describe("submitCatalogSearchMiss", () => {
beforeEach(() => {
getPublishApiBaseUrlMock.mockReturnValue("https://api.example.com");
});
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});
it("posts the gap to the catalog endpoint", async () => {
const fetchMock = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
vi.stubGlobal("fetch", fetchMock);
await submitCatalogSearchMiss({
query: "typewriter that deletes",
wanted: "text that types then backspaces",
tier: "on-device",
cliVersion: "0.7.106",
});
expect(fetchMock).toHaveBeenCalledOnce();
const [url, init] = fetchMock.mock.calls[0]!;
expect(url).toBe("https://api.example.com/v1/hyperframes/catalog_search_miss");
expect(JSON.parse(String(init?.body))).toEqual({
query: "typewriter that deletes",
wanted: "text that types then backspaces",
tier: "on-device",
cli_version: "0.7.106",
});
});
it("truncates a field the backend would reject outright", async () => {
const fetchMock = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
vi.stubGlobal("fetch", fetchMock);
await submitCatalogSearchMiss({ query: "q".repeat(900), cliVersion: "0.7.106" });
const [, init] = fetchMock.mock.calls[0]!;
expect(JSON.parse(String(init?.body)).query).toHaveLength(500);
});
it("never throws when the forward fails", async () => {
vi.stubGlobal(
"fetch",
vi.fn<typeof fetch>(async () => {
throw new Error("offline");
}),
);
await expect(
submitCatalogSearchMiss({ query: "a query", cliVersion: "0.7.106" }),
).resolves.toBeUndefined();
});
});
+39
View File
@@ -39,3 +39,42 @@ export async function submitFeedback(input: {
// Best-effort only.
}
}
const MAX_QUERY = 500;
const MAX_WANTED = 500;
const MAX_TIER = 50;
/**
* Forward a reported catalog gap to the same place a rating goes.
*
* A miss is the one search report that leaves the machine, and it is worth
* more to a person reading a channel than to a chart: it names a move the
* catalog does not have yet. Best-effort and bounded, exactly like
* `submitFeedback` — a gap report must never fail the command that sent it.
*/
export async function submitCatalogSearchMiss(input: {
query: string;
wanted?: string;
tier?: string;
cliVersion: string;
}): Promise<void> {
try {
const apiBaseUrl = getPublishApiBaseUrl();
await fetch(`${apiBaseUrl}/v1/hyperframes/catalog_search_miss`, {
method: "POST",
body: JSON.stringify({
query: cap(input.query, MAX_QUERY),
wanted: cap(input.wanted, MAX_WANTED),
tier: cap(input.tier, MAX_TIER),
cli_version: cap(input.cliVersion, MAX_CLI_VERSION),
}),
headers: {
"content-type": "application/json",
heygen_route: "canary",
},
signal: AbortSignal.timeout(5000),
});
} catch {
// Best-effort only.
}
}