Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/reporters/site-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { gunzipSync } from "node:zlib";
import { test, expect, describe, afterEach, vi } from "vitest";
import {
classifyIngestFailure,
Expand Down Expand Up @@ -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", () => {
Expand All @@ -300,7 +311,8 @@ describe("postToSiteApi payload baseBranch", () => {
}

function bodyFrom(fetchMock: ReturnType<typeof vi.fn>): Record<string, unknown> {
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 () => {
Expand Down
28 changes: 21 additions & 7 deletions src/reporters/site-api.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
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";
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;
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand Down
Loading