feat(VocabService): enhance logging and error handling in lesson assistant message flow
All checks were successful
Deploy to production / deploy (push) Successful in 2m18s

- Introduced detailed logging throughout the sendLessonAssistantMessage method to track request lifecycle, including start, abort conditions, and response handling.
- Improved error handling for various scenarios such as disabled assistant, unconfigured settings, empty messages, and fetch failures, providing clearer feedback to users.
- Added logging for response parsing and upstream errors to facilitate debugging and improve overall service reliability.
This commit is contained in:
Torsten Schulz (local)
2026-05-07 08:58:22 +02:00
parent cfab56f63d
commit 2c453a4a6b

View File

@@ -2808,17 +2808,33 @@ export default class VocabService {
}
async sendLessonAssistantMessage(hashedUserId, lessonId, payload = {}) {
const requestId = `assist-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
const log = (...args) => console.log(`[LLM ${requestId}]`, ...args);
const startedAt = Date.now();
const user = await this._getUserByHashedId(hashedUserId);
const lesson = await this.getLesson(hashedUserId, lessonId);
const config = await this._getUserLlmConfig(user.id);
log('start', {
userId: user.id,
lessonId,
enabled: config.enabled,
configured: config.configured,
hasKey: config.hasKey,
baseUrl: config.baseUrl || '(default openai)',
model: config.model
});
if (!config.enabled) {
log('aborted: assistant disabled in user settings');
const err = new Error('Der Sprachassistent ist in deinen Einstellungen derzeit deaktiviert.');
err.status = 400;
throw err;
}
if (!config.configured) {
log('aborted: assistant not configured');
const err = new Error('Der Sprachassistent ist noch nicht eingerichtet. Bitte hinterlege zuerst Modell und API-Zugang in den Einstellungen.');
err.status = 400;
throw err;
@@ -2826,6 +2842,7 @@ export default class VocabService {
const message = String(payload?.message || '').trim();
if (!message) {
log('aborted: empty message');
const err = new Error('Bitte gib eine Nachricht für den Sprachassistenten ein.');
err.status = 400;
throw err;
@@ -2848,7 +2865,10 @@ export default class VocabService {
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout >= 30000
? configuredTimeout
: 300000;
const timeout = setTimeout(() => controller.abort(), timeoutMs);
const timeout = setTimeout(() => {
log('timeout reached, aborting fetch', { timeoutMs });
controller.abort();
}, timeoutMs);
const temperatureByMode = {
explain: 0.4,
@@ -2857,7 +2877,17 @@ export default class VocabService {
};
const temperature = Number.isFinite(temperatureByMode[mode]) ? temperatureByMode[mode] : 0.5;
log('request', {
endpoint,
mode,
temperature,
timeoutMs,
historyMessages: history.length,
messagePreview: message.slice(0, 120)
});
let response;
let fetchStartedAt = Date.now();
try {
response = await fetch(endpoint, {
method: 'POST',
@@ -2879,7 +2909,17 @@ export default class VocabService {
]
})
});
log('response received', {
status: response.status,
latencyMs: Date.now() - fetchStartedAt
});
} catch (error) {
log('fetch failed', {
name: error?.name,
message: error?.message,
cause: error?.cause?.code || error?.cause?.errno || null,
latencyMs: Date.now() - fetchStartedAt
});
const err = new Error(
error?.name === 'AbortError'
? 'Der Sprachassistent hat das Antwort-Zeitlimit überschritten.'
@@ -2894,11 +2934,16 @@ export default class VocabService {
let responseData = null;
try {
responseData = await response.json();
} catch {
} catch (parseError) {
log('failed to parse response JSON', { message: parseError?.message });
responseData = null;
}
if (!response.ok) {
log('upstream returned non-ok', {
status: response.status,
body: responseData ? JSON.stringify(responseData).slice(0, 500) : null
});
const messageFromApi = responseData?.error?.message || responseData?.message || 'Der Sprachassistent hat die Anfrage abgelehnt.';
const err = new Error(messageFromApi);
err.status = response.status || 502;
@@ -2907,11 +2952,20 @@ export default class VocabService {
const reply = this._extractAssistantContent(responseData);
if (!reply) {
log('empty reply from upstream', {
responsePreview: responseData ? JSON.stringify(responseData).slice(0, 500) : null
});
const err = new Error('Der Sprachassistent hat keine verwertbare Antwort geliefert.');
err.status = 502;
throw err;
}
log('done', {
totalMs: Date.now() - startedAt,
model: responseData?.model || config.model,
replyLength: reply.length
});
return {
reply,
model: responseData?.model || config.model,