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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# @copilotkit/pathfinder

## 1.15.4

### Patch Changes

- **Re-ship of v1.15.3 with the broken `getBlockedQueries` SQL fixed and defensive client-side panel isolation.** v1.15.3 was reverted via PR #117 within minutes of deploy because the `/api/analytics/blocked-queries` route 500'd against real Postgres and the page's `Promise.all` then nuked the entire `/analytics` dashboard. v1.15.4 re-ships all three v1.15.3 changes (blocked filter on empty-results, new Blocked Queries panel, Pacific timestamps) with the SQL and resilience defects fixed.
- **`getBlockedQueries` SQL — rewrote as a CTE-backed correlated subquery.** The original v1.15.3 SQL grouped the outer query on `COALESCE(block_reason, '<unknown>')` but the correlated subquery referenced raw `query_log.block_reason`, which Postgres rejects as an ungrouped column reference (`subquery uses ungrouped column "query_log.block_reason" from outer query`). The fix pre-projects the COALESCEd expression into a CTE (`blocked_rows.block_reason_key`) so the outer GROUP BY and the inner correlated reference operate on the same column. Verified against the actual prod Postgres schema before push.
- **Analytics page — `Promise.all` replaced with `Promise.allSettled`.** A single panel's fetch failure used to throw out of `Promise.all` and blank the entire `/analytics` page (this is what made the v1.15.3 SQL bug catastrophic instead of merely cosmetic). Each section now renders independently: the summary endpoint failing still surfaces via the top-level error banner (the page can't render statcards without it), but per-panel failures (empty queries / blocked queries / tool counts) degrade to an empty state with a `console.error` and leave the rest of the dashboard intact. Auth (401) errors still bubble to the outer catch since a bad token fails every endpoint identically.

## 1.15.3 — REVERTED

This release was reverted via PR #117 immediately after deploy. Its `getBlockedQueries` SQL was incompatible with real Postgres (ungrouped column reference inside a correlated subquery) and its client-side `Promise.all` rendering blanked the entire `/analytics` page on a single panel's failure. See v1.15.4 for the fixed re-ship.

## 1.15.2

### Patch Changes
Expand Down
373 changes: 311 additions & 62 deletions docs/analytics.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@copilotkit/pathfinder",
"version": "1.15.2",
"version": "1.15.4",
"description": "The knowledge server for AI agents — index docs, code, Notion, Slack, and Discord into searchable, agent-accessible knowledge via MCP. Supports OpenAI, Ollama, and local transformers.js embeddings.",
"type": "module",
"repository": {
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/analytics-auth-length-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ vi.mock("../db/analytics.js", () => ({
getAnalyticsSummary: vi.fn(),
getTopQueries: vi.fn(),
getEmptyQueries: vi.fn(),
getBlockedQueries: vi.fn(),
getToolCounts: vi.fn(),
}));

Expand Down Expand Up @@ -79,6 +80,7 @@ describe("analyticsAuth length-check early return (R4-18)", () => {
getAnalyticsSummary: async () => ({ total: 0 }) as never,
getTopQueries: async () => [],
getEmptyQueries: async () => [],
getBlockedQueries: async () => [],
getToolCounts: async () => [],
});
server = http.createServer(app);
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/analytics-error-log-ip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ vi.mock("../db/analytics.js", () => ({
getAnalyticsSummary: vi.fn(),
getTopQueries: vi.fn(),
getEmptyQueries: vi.fn(),
getBlockedQueries: vi.fn(),
getToolCounts: vi.fn(),
}));

Expand Down Expand Up @@ -47,6 +48,9 @@ function buildApp() {
getEmptyQueries: async () => {
throw new Error("db boom");
},
getBlockedQueries: async () => {
throw new Error("db boom");
},
getToolCounts: async () => {
throw new Error("db boom");
},
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/analytics-sendfile-correlation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ vi.mock("../db/analytics.js", () => ({
getAnalyticsSummary: vi.fn(),
getTopQueries: vi.fn(),
getEmptyQueries: vi.fn(),
getBlockedQueries: vi.fn(),
getToolCounts: vi.fn(),
}));

Expand Down
84 changes: 84 additions & 0 deletions src/__tests__/analytics-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import path from "node:path";
const mockGetAnalyticsSummary = vi.fn();
const mockGetTopQueries = vi.fn();
const mockGetEmptyQueries = vi.fn();
const mockGetBlockedQueries = vi.fn();
const mockGetToolCounts = vi.fn();

vi.mock("../db/analytics.js", () => ({
getAnalyticsSummary: (...args: unknown[]) => mockGetAnalyticsSummary(...args),
getTopQueries: (...args: unknown[]) => mockGetTopQueries(...args),
getEmptyQueries: (...args: unknown[]) => mockGetEmptyQueries(...args),
getBlockedQueries: (...args: unknown[]) => mockGetBlockedQueries(...args),
getToolCounts: (...args: unknown[]) => mockGetToolCounts(...args),
}));

Expand Down Expand Up @@ -82,6 +84,8 @@ function buildTestApp() {
mockGetTopQueries(...args),
getEmptyQueries: (...args: Parameters<typeof mockGetEmptyQueries>) =>
mockGetEmptyQueries(...args),
getBlockedQueries: (...args: Parameters<typeof mockGetBlockedQueries>) =>
mockGetBlockedQueries(...args),
getToolCounts: (...args: Parameters<typeof mockGetToolCounts>) =>
mockGetToolCounts(...args),
analyticsHtmlPath,
Expand Down Expand Up @@ -366,6 +370,86 @@ describe("Analytics server routes (HTTP-level)", () => {
});
});

// ---- /api/analytics/blocked-queries (v1.15.3) ----------------------------

describe("GET /api/analytics/blocked-queries (v1.15.3)", () => {
it("returns blocked-row groups with a valid token", async () => {
mockGetAnalyticsConfigFn.mockReturnValue({
enabled: true,
log_queries: true,
retention_days: 90,
token: "secret",
});
mockGetBlockedQueries.mockResolvedValue([
{
block_reason: "box-office",
hits: 142,
last_seen: "2026-06-17T16:40:32.000Z",
sample_queries: ["box office weekend"],
},
]);

await startApp();
const res = await request(
server,
"GET",
"/api/analytics/blocked-queries?days=7",
{ Authorization: "Bearer secret" },
);

expect(res.status).toBe(200);
const body = JSON.parse(res.body);
expect(Array.isArray(body)).toBe(true);
expect(body[0].block_reason).toBe("box-office");
expect(body[0].hits).toBe(142);
// The route forwards the parsed days value to the DB call.
expect(mockGetBlockedQueries).toHaveBeenCalledWith(7);
});

it("returns 401 without a token", async () => {
mockGetAnalyticsConfigFn.mockReturnValue({
enabled: true,
log_queries: true,
retention_days: 90,
token: "secret",
});

await startApp();
const res = await request(
server,
"GET",
"/api/analytics/blocked-queries",
);
expect(res.status).toBe(401);
});

it("returns 500 with a clean error envelope when the DB call rejects", async () => {
mockGetAnalyticsConfigFn.mockReturnValue({
enabled: true,
log_queries: true,
retention_days: 90,
token: "secret",
});
mockGetBlockedQueries.mockRejectedValue(new Error("boom"));
const consoleErrSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});

await startApp();
const res = await request(
server,
"GET",
"/api/analytics/blocked-queries",
{ Authorization: "Bearer secret" },
);

expect(res.status).toBe(500);
const body = JSON.parse(res.body);
expect(body.error).toBe("Failed to fetch blocked queries");
consoleErrSpy.mockRestore();
});
});

// ---- Auto-generated token via HTTP ---------------------------------------

describe("auto-generated token (HTTP-level)", () => {
Expand Down
Loading