diff --git a/src/reporters/site-api.test.ts b/src/reporters/site-api.test.ts index 19d0106..4776392 100644 --- a/src/reporters/site-api.test.ts +++ b/src/reporters/site-api.test.ts @@ -1,3 +1,4 @@ +import { gunzipSync } from "node:zlib"; import { test, expect, describe, afterEach, vi } from "vitest"; import { classifyIngestFailure, @@ -280,6 +281,16 @@ describe("postToSiteApi authentication", () => { expect(headersFrom(fetchMock).has("authorization")).toBe(false); }); + + test("gzips the request body and advertises Content-Encoding: gzip", async () => { + const fetchMock = stubOkFetch(); + + await postToSiteApi("https://api.querydoctor.com", [makeQuery("hash-a")]); + + expect(headersFrom(fetchMock).get("content-encoding")).toBe("gzip"); + const body = fetchMock.mock.calls[0]![1]!.body as Buffer; + expect(JSON.parse(gunzipSync(body).toString("utf8")).repo).toBeDefined(); + }); }); describe("postToSiteApi payload baseBranch", () => { @@ -300,7 +311,8 @@ describe("postToSiteApi payload baseBranch", () => { } function bodyFrom(fetchMock: ReturnType): Record { - return JSON.parse(fetchMock.mock.calls[0]![1]!.body as string); + const body = fetchMock.mock.calls[0]![1]!.body as Buffer; + return JSON.parse(gunzipSync(body).toString("utf8")); } test("forwards GITHUB_BASE_REF as baseBranch on a PR run", async () => { diff --git a/src/reporters/site-api.ts b/src/reporters/site-api.ts index 1ea86b4..c97a4b8 100644 --- a/src/reporters/site-api.ts +++ b/src/reporters/site-api.ts @@ -1,3 +1,5 @@ +import { gzip } from "node:zlib"; +import { promisify } from "node:util"; import * as github from "@actions/github"; import { isTestOriginQuery } from "@query-doctor/core"; import type { ComputedStats, FullSchema, IndexRecommendation, Nudge, SQLCommenterTag, StatisticsMode, TableReference } from "@query-doctor/core"; @@ -5,6 +7,8 @@ import { DEFAULT_CONFIG, type AnalyzerConfig } from "../config.ts"; import type { OptimizedQuery } from "../sql/recent-query.ts"; import type { Op } from "jsondiffpatch/formatters/jsonpatch"; +const gzipAsync = promisify(gzip); + interface CiRunPayload { repo: string; branch: string; @@ -452,16 +456,26 @@ export async function postToSiteApi( } const url = `${endpoint.replace(/\/$/, "")}/ci/runs`; - console.log(`Posting CI run to ${url} (${queries.length} queries)`); + + // Gzip the body: CI run payloads reach multiple MB (many queries + schema), + // and the Site API decompresses request bodies before enforcing its size + // limit. See Query-Doctor/Site raise-json-body-limit work. + const body = await gzipAsync(JSON.stringify(payload)); + + const sentKib = (body.byteLength / 1024).toFixed(1); + console.log( + `Posting CI run to ${url} (${queries.length} queries, ${sentKib} KiB gzipped)`, + ); try { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", + "Content-Encoding": "gzip", ...(token ? { Authorization: `Bearer ${token}` } : {}), }, - body: JSON.stringify(payload), + body, }); if (!response.ok) { const text = await response.text(); @@ -474,18 +488,18 @@ export async function postToSiteApi( }, }; } - const body = (await response.json()) as { + const responseBody = (await response.json()) as { id: string; url?: string | null; metadata?: CiRunMetadata | null; }; - console.log(`Site API ingestion successful: ${JSON.stringify(body)}`); + console.log(`Site API ingestion successful: ${JSON.stringify(responseBody)}`); return { ok: true, result: { - id: body.id, - url: body.url ?? null, - metadata: body.metadata ?? null, + id: responseBody.id, + url: responseBody.url ?? null, + metadata: responseBody.metadata ?? null, }, }; } catch (err) {