From f78d066434c8906318ca76e3a44f8fdec136292e Mon Sep 17 00:00:00 2001 From: Richard R Date: Wed, 27 May 2026 05:58:48 -0600 Subject: [PATCH] refactor(api): enhance tts segment error handling and logging patterns Update TTS segment ensure route to distinguish aborted and failed segment generation, providing more granular logging and context. Replace ambiguous message variables with clearer detail fields, and improve log event naming for abort scenarios. Adjust ESLint logger call selectors for maintainability and consistency, consolidating selector logic and enforcing stricter server logger usage patterns. These changes improve observability and error traceability in TTS segment operations. --- eslint.config.mjs | 24 +++++---- src/app/api/tts/segments/ensure/route.ts | 67 ++++++++++++++---------- 2 files changed, 52 insertions(+), 39 deletions(-) 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; } }