diff --git a/eslint.config.mjs b/eslint.config.mjs index faab7be..3d21537 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,6 +9,15 @@ const compat = new FlatCompat({ baseDirectory: __dirname, }); +const LOGGER_LEVEL_METHOD = "trace|debug|info|warn|error|fatal"; +const STATIC_LOGGER_CALL_SELECTOR = + `CallExpression[callee.type='MemberExpression'][callee.computed=false][callee.property.name=/^(${LOGGER_LEVEL_METHOD})$/]`; +const DYNAMIC_LOGGER_CALL_SELECTOR = + "CallExpression[callee.type='MemberExpression'][callee.computed=true]"; +const LOGGER_RECEIVER_SELECTOR = + ":matches([callee.object.name=/^(logger|serverLogger)$/],[callee.object.property.name='logger'])"; +const SERVER_LOGGER_CALL_SELECTOR = `:matches(${STATIC_LOGGER_CALL_SELECTOR}${LOGGER_RECEIVER_SELECTOR},${DYNAMIC_LOGGER_CALL_SELECTOR}${LOGGER_RECEIVER_SELECTOR})`; + const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), { @@ -39,32 +48,27 @@ const eslintConfig = [ "no-restricted-syntax": [ "error", { - selector: - ":matches(CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.name=/^(logger|serverLogger)$/],CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.property.name='logger'])[arguments.length<2]", + selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.length<2]`, message: "Server logger calls must pass context + message: logger.({ event, ...ctx }, 'message').", }, { - selector: - ":matches(CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.name=/^(logger|serverLogger)$/],CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.property.name='logger'])[arguments.0.type='Literal']", + selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type='Literal']`, message: "Server logger first argument must be an object with an event field, not a string literal.", }, { - selector: - ":matches(CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.name=/^(logger|serverLogger)$/],CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.property.name='logger'])[arguments.0.type='TemplateLiteral']", + selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type='TemplateLiteral']`, message: "Server logger first argument must be an object with an event field, not a template string.", }, { - selector: - ":matches(CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.name=/^(logger|serverLogger)$/],CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.property.name='logger'])[arguments.0.type='ObjectExpression']:not(:has(Property[key.name='event']))", + selector: `${SERVER_LOGGER_CALL_SELECTOR}[arguments.0.type='ObjectExpression']:not(:has(Property[key.name='event']))`, message: "Server logger context object must include an event field.", }, { - selector: - ":matches(CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.name=/^(logger|serverLogger)$/],CallExpression[callee.property.name=/^(trace|debug|info|warn|error|fatal)$/][callee.object.property.name='logger']) > ObjectExpression:first-child > Property[key.name='err']", + selector: `${SERVER_LOGGER_CALL_SELECTOR} > ObjectExpression:first-child > Property[key.name='err']`, message: "Use `error` (typically from errorToLog(...)) instead of `err` in server logs.", }, diff --git a/src/app/api/tts/segments/ensure/route.ts b/src/app/api/tts/segments/ensure/route.ts index c7b4a75..a5c079f 100644 --- a/src/app/api/tts/segments/ensure/route.ts +++ b/src/app/api/tts/segments/ensure/route.ts @@ -424,7 +424,7 @@ export async function POST(request: NextRequest) { segmentId: segment.segmentId, aborted, error: alignError instanceof Error ? alignError.message : String(alignError), - }); + }, 'Self-heal alignment unavailable'); alignment = null; } } @@ -666,7 +666,7 @@ export async function POST(request: NextRequest) { segmentId: segment.segmentId, aborted, error: alignError instanceof Error ? alignError.message : String(alignError), - }); + }, 'Alignment unavailable'); alignment = null; } } @@ -708,7 +708,7 @@ export async function POST(request: NextRequest) { status: 'completed', }); } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to generate segment'; + const detail = error instanceof Error ? error.message : 'Failed to generate segment'; const aborted = isAbortLikeError(error); const upstreamStatus = getUpstreamStatus(error); const retryAfterSeconds = upstreamStatus === 429 @@ -719,29 +719,46 @@ export async function POST(request: NextRequest) { : upstreamStatus && upstreamStatus >= 500 ? 'UPSTREAM_TTS_ERROR' : 'TTS_SEGMENT_GENERATION_FAILED'; - const level = aborted ? 'info' : 'error'; - logger[level]({ - event: 'tts.segments.ensure.segment_failed', - requestId, - documentId: parsed.documentId, - segmentId: segment.segmentId, - failedStage, - elapsedMs: Date.now() - segmentStartedAt, - stageTimings, - aborted, - upstreamStatus, - retryAfterSeconds, - errorCode, - message, - }); + if (aborted) { + logger.info({ + event: 'tts.segments.ensure.segment_aborted', + requestId, + documentId: parsed.documentId, + segmentId: segment.segmentId, + failedStage, + elapsedMs: Date.now() - segmentStartedAt, + stageTimings, + aborted: true, + errorCode, + detail, + completedSoFar: manifest.length, + totalRequested: normalized.length, + }, 'Stopping segment ensure after abort'); + } else { + logger.error({ + event: 'tts.segments.ensure.segment_failed', + requestId, + documentId: parsed.documentId, + segmentId: segment.segmentId, + failedStage, + elapsedMs: Date.now() - segmentStartedAt, + stageTimings, + aborted: false, + upstreamStatus, + retryAfterSeconds, + errorCode, + detail, + error: errorToLog(error), + }, 'TTS segment generation failed'); + } await db .update(ttsSegmentVariants) .set({ status: aborted ? 'pending' : 'error', error: aborted ? null : ( upstreamStatus - ? `${errorCode}${retryAfterSeconds ? ` (retry after ${retryAfterSeconds}s)` : ''}: ${message}` - : message + ? `${errorCode}${retryAfterSeconds ? ` (retry after ${retryAfterSeconds}s)` : ''}: ${detail}` + : detail ), updatedAt: Date.now(), }) @@ -764,21 +781,13 @@ export async function POST(request: NextRequest) { ? null : { code: errorCode, - detail: message, + detail, ...(typeof upstreamStatus === 'number' ? { upstreamStatus } : {}), ...(typeof retryAfterSeconds === 'number' ? { retryAfterSeconds } : {}), }, }); if (aborted || request.signal.aborted) { - logger.info({ - event: 'tts.segments.ensure.stop_after_abort', - requestId, - documentId: parsed.documentId, - segmentId: segment.segmentId, - completedSoFar: manifest.length, - totalRequested: normalized.length, - }, 'Stopping segment ensure after abort'); break; } }