mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
fix(ingestion): log the real error and elapsed time when a scan submit gets no response
A submit that never gets a response is the one case where the error's identity is
the whole diagnosis, and it was the one thing the log did not carry:
JSON.stringify(new Error()) is `{}` because Error has no enumerable own properties,
so every no-response failure recorded `error: {}`. Pass it through safeError, which
the repo already uses for exactly this, and add the wall time across all attempts —
the attempt count alone cannot tell three 15s aborts from an instant rejection.
The test pins the serialization rather than the call: reverting to the raw error
fails with `expected '{}' not to be '{}'`.
The logging mock in the covering suite was hand-listed and silently dropped
safeError; spread the original instead, since that module is pure apart from
logToAxiom.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type * as FliptClient from '~/server/flipt/client';
|
||||
import type * as LoggingClient from '~/server/logging/client';
|
||||
|
||||
const {
|
||||
mockDbWrite,
|
||||
@@ -37,7 +38,12 @@ vi.mock('~/server/services/orchestrator/client', () => ({
|
||||
internalOrchestratorClient: {},
|
||||
}));
|
||||
|
||||
vi.mock('~/server/logging/client', () => ({ logToAxiom: mockLogToAxiom }));
|
||||
// Spread the original: `safeError` is real, pure serialization logic the submit-failure
|
||||
// log depends on, and a hand-listed factory silently dropped it.
|
||||
vi.mock('~/server/logging/client', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof LoggingClient>()),
|
||||
logToAxiom: mockLogToAxiom,
|
||||
}));
|
||||
|
||||
vi.mock('~/shared/utils/air', () => ({ stringifyAIR: mockStringifyAIR }));
|
||||
|
||||
@@ -692,6 +698,30 @@ describe('createImageIngestionRequest', () => {
|
||||
expect(mockLogToAxiom).toHaveBeenCalledWith(expect.objectContaining({ name, imageId: 1 }));
|
||||
});
|
||||
|
||||
// A no-response submit is the one case where the error's identity IS the diagnosis, and
|
||||
// `JSON.stringify(new Error())` is `{}` — so logging the error object recorded nothing.
|
||||
it('serializes an Error from a failed submit instead of logging an empty object', async () => {
|
||||
vi.useRealTimers();
|
||||
mockSubmitWorkflow.mockResolvedValue({
|
||||
data: undefined,
|
||||
error: new Error('The operation was aborted due to timeout'),
|
||||
response: undefined,
|
||||
});
|
||||
|
||||
await createImageIngestionRequest({ imageId: 1, url: 'image-key' });
|
||||
|
||||
const logged = mockLogToAxiom.mock.calls.at(-1)?.[0];
|
||||
expect(JSON.stringify(logged?.error)).not.toBe('{}');
|
||||
expect(logged?.error).toEqual(
|
||||
expect.objectContaining({
|
||||
name: 'Error',
|
||||
message: 'The operation was aborted due to timeout',
|
||||
})
|
||||
);
|
||||
// Tells three 15s aborts apart from an instant rejection; the attempt count cannot.
|
||||
expect(typeof logged?.elapsedMs).toBe('number');
|
||||
});
|
||||
|
||||
it('submits one imageScanning step when the flag is on', async () => {
|
||||
mockIsFlipt.mockResolvedValue(true);
|
||||
await createImageIngestionRequest({ imageId: 1, url: 'image-key' });
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getEdgeUrl } from '~/client-utils/edge-url';
|
||||
import { dbRead, dbWrite } from '~/server/db/client';
|
||||
import { env } from '~/env/server';
|
||||
import { isProd } from '~/env/other';
|
||||
import { logToAxiom } from '~/server/logging/client';
|
||||
import { logToAxiom, safeError } from '~/server/logging/client';
|
||||
import { internalOrchestratorClient } from '~/server/services/orchestrator/client';
|
||||
import { submitWorkflowWithRetry } from '~/server/services/orchestrator/workflows';
|
||||
import { hashContent } from '~/server/services/entity-moderation.service';
|
||||
@@ -191,6 +191,7 @@ export async function createImageIngestionRequest({
|
||||
|
||||
// Re-submit transient infra failures (5xx / no-response), reusing the same
|
||||
// `externalId` so a 500 that actually created the workflow isn't duplicated.
|
||||
const submitStartedAt = Date.now();
|
||||
const result = await submitWorkflowWithRetry(
|
||||
{
|
||||
client: internalOrchestratorClient,
|
||||
@@ -222,7 +223,14 @@ export async function createImageIngestionRequest({
|
||||
attempts,
|
||||
responseStatus: response?.status,
|
||||
serverTiming,
|
||||
error,
|
||||
// JSON.stringify(new Error()) is `{}` — Error carries no enumerable own
|
||||
// properties — so logging the error directly recorded nothing at all, and
|
||||
// a no-response submit is exactly the case where its name is the whole
|
||||
// diagnosis (an abort is not a connection reset).
|
||||
error: safeError(error),
|
||||
// Wall time across every attempt. Distinguishes three per-attempt aborts
|
||||
// (~45s) from a fast rejection, which the attempt count alone does not.
|
||||
elapsedMs: Date.now() - submitStartedAt,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user