Skip to content
Open
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
257 changes: 256 additions & 1 deletion apps/worker/src/log-processing.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { randomUUID } from "node:crypto";

import { afterAll, beforeEach, describe, expect, test } from "vitest";
import {
afterAll,
afterEach,
beforeEach,
describe,
expect,
test,
vi,
} from "vitest";

import {
eq,
Expand All @@ -16,6 +24,9 @@ import {
import { batchProcessLogs } from "./worker.js";

describe("Log Processing", () => {
const previousProviderErrorDiscordUrl =
process.env.PROVIDER_ERROR_DISCORD_URL;

interface TestIds {
apiKeyId: string;
email: string;
Expand Down Expand Up @@ -112,6 +123,12 @@ describe("Log Processing", () => {
await cleanupLogProcessingTestData(currentTestIds);
});

afterEach(() => {
process.env.PROVIDER_ERROR_DISCORD_URL = previousProviderErrorDiscordUrl;
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

describe("batchProcessLogs", () => {
test("should process logs and set processedAt timestamp", async () => {
// Insert unprocessed log directly
Expand Down Expand Up @@ -571,5 +588,243 @@ describe("Log Processing", () => {

expect(Number(updatedOrg!.credits)).toBe(initialCredits);
});

test.each([
{
statusCode: 429,
statusText: "Too Many Requests",
unifiedFinishReason: "upstream_error",
},
{
statusCode: 401,
statusText: "Unauthorized",
unifiedFinishReason: "gateway_error",
},
])(
"should report $unifiedFinishReason logs to Discord even for non-5xx statuses",
async ({ unifiedFinishReason, statusCode, statusText }) => {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(null, { status: 204 }));
vi.stubGlobal("fetch", fetchMock);
process.env.PROVIDER_ERROR_DISCORD_URL =
"https://discord.example/provider-errors";
const requestId = `test-request-${unifiedFinishReason}`;
const traceId = `trace-${unifiedFinishReason}`;

const insertedLogs = await db
.insert(log)
.values({
requestId,
organizationId: testOrg.id,
projectId: testProject.id,
apiKeyId: testApiKey.id,
cost: 0,
cached: false,
usedMode: "credits",
duration: 2450,
requestedModel: "openai/gpt-4o-mini",
requestedProvider: "openai",
usedModel: "gpt-4o-mini",
usedModelMapping: "gpt-4o-mini",
usedProvider: "openai",
responseSize: 150,
mode: "credits",
hasError: true,
errorDetails: {
statusCode,
statusText,
responseText:
"provider timed out for support@example.com " +
`request ${requestId} in project ${testProject.id}`,
cause: `trace ${traceId}`,
},
unifiedFinishReason,
traceId,
})
.returning({
id: log.id,
});
const insertedLogId = insertedLogs[0]?.id ?? "";

await batchProcessLogs();

expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith(
"https://discord.example/provider-errors",
expect.objectContaining({
method: "POST",
headers: {
"Content-Type": "application/json",
},
}),
);

const payloadText = String(fetchMock.mock.calls[0]?.[1]?.body ?? "{}");
const payload = JSON.parse(payloadText) as {
embeds?: Array<{
description?: string;
fields?: Array<{ name: string; value: string }>;
title?: string;
}>;
};

expect(payload.embeds?.[0]?.title).toContain(
unifiedFinishReason.replaceAll("_", " "),
);
expect(payload.embeds?.[0]?.description).toContain(
"provider timed out",
);
expect(payload.embeds?.[0]?.description).toContain("<redacted-email>");
expect(payloadText).not.toContain(requestId);
expect(payloadText).not.toContain(traceId);
expect(payloadText).not.toContain(testProject.id);
expect(payloadText).not.toContain(testOrg.id);
expect(insertedLogId).not.toBe("");
expect(payloadText).not.toContain(insertedLogId);
expect(payload.embeds?.[0]?.fields).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "Trace ID",
value: expect.stringMatching(/^redacted:/),
}),
expect.objectContaining({
name: "Request ID",
value: expect.stringMatching(/^redacted:/),
}),
expect.objectContaining({
name: "Project",
value: expect.stringMatching(/^redacted:/),
}),
expect.objectContaining({
name: "Organization",
value: expect.stringMatching(/^redacted:/),
}),
expect.objectContaining({
name: "Log ID",
value: expect.stringMatching(/^redacted:/),
}),
]),
);
},
);

test("should not report to Discord when the webhook URL is blank", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
process.env.PROVIDER_ERROR_DISCORD_URL = " ";

await db.insert(log).values({
requestId: "test-request-blank-webhook-url",
organizationId: testOrg.id,
projectId: testProject.id,
apiKeyId: testApiKey.id,
cost: 0,
cached: false,
usedMode: "credits",
duration: 1200,
requestedModel: "openai/gpt-4o-mini",
requestedProvider: "openai",
usedModel: "gpt-4o-mini",
usedProvider: "openai",
responseSize: 150,
mode: "credits",
hasError: true,
errorDetails: {
statusCode: 502,
statusText: "Bad Gateway",
responseText: "upstream unavailable",
},
unifiedFinishReason: "upstream_error",
});

await batchProcessLogs();

expect(fetchMock).not.toHaveBeenCalled();
});

test("should not report client errors to Discord", async () => {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(null, { status: 204 }));
vi.stubGlobal("fetch", fetchMock);
process.env.PROVIDER_ERROR_DISCORD_URL =
"https://discord.example/provider-errors";

await db.insert(log).values({
requestId: "test-request-client-error",
organizationId: testOrg.id,
projectId: testProject.id,
apiKeyId: testApiKey.id,
cost: 0,
cached: false,
usedMode: "credits",
duration: 900,
requestedModel: "openai/gpt-4o-mini",
requestedProvider: "openai",
usedModel: "gpt-4o-mini",
usedProvider: "openai",
responseSize: 150,
mode: "credits",
hasError: true,
errorDetails: {
statusCode: 400,
statusText: "Bad Request",
responseText: "invalid input",
},
unifiedFinishReason: "client_error",
});

await batchProcessLogs();

expect(fetchMock).not.toHaveBeenCalled();
});

test("should report provider errors without a finish reason", async () => {
const fetchMock = vi
.fn()
.mockResolvedValue(new Response(null, { status: 204 }));
vi.stubGlobal("fetch", fetchMock);
process.env.PROVIDER_ERROR_DISCORD_URL =
"https://discord.example/provider-errors";

await db.insert(log).values({
requestId: "test-request-missing-finish-reason",
organizationId: testOrg.id,
projectId: testProject.id,
apiKeyId: testApiKey.id,
cost: 0,
cached: false,
usedMode: "credits",
duration: 1200,
requestedModel: "openai/gpt-4o-mini",
requestedProvider: "openai",
usedModel: "gpt-4o-mini",
usedProvider: "openai",
responseSize: 150,
mode: "credits",
hasError: true,
errorDetails: {
statusCode: 502,
statusText: "Bad Gateway",
responseText: "upstream unavailable",
},
unifiedFinishReason: null,
});

await batchProcessLogs();

expect(fetchMock).toHaveBeenCalledTimes(1);

const payload = JSON.parse(
String(fetchMock.mock.calls[0]?.[1]?.body ?? "{}"),
) as {
embeds?: Array<{
title?: string;
}>;
};

expect(payload.embeds?.[0]?.title).toBe("Provider error");
});
});
});
Loading
Loading