From 6562c087d25b3c39f153bd89c4a11d3ee76ed7c6 Mon Sep 17 00:00:00 2001 From: Wraith Protocol Dev Date: Fri, 28 Aug 2026 19:34:41 +0100 Subject: [PATCH] fix(listener): improve Discord delivery failure logging - Replace duplicate success log with a single, richer delivery log - Add classifyHttpStatus() to map status codes to readable categories (rate_limited, auth_error, not_found, client_error, server_error) - Add safeReadResponseBody() to capture error bodies with 300-char cap - Surface retryAfter header on 429 responses for actionable diagnostics - Pass logContext through sendWebhook() so timeout logs carry requestId and eventId for end-to-end traceability - Fix sendTestMessage() to log error detail on non-ok responses instead of a bare ok:true/false field - Webhook URL and tokens never appear in any log path; only webhookId is logged - Update e2e test assertions to match renamed log messages and new field names (httpStatus, httpCategory) in both delivery lifecycle and multi-channel test suites --- .../multi-channel-delivery.e2e.test.ts | 8 +- ...otification-delivery-lifecycle.e2e.test.ts | 2 +- listener/src/services/discord-notification.ts | 83 +++++++++++++------ 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/listener/src/__tests__/multi-channel-delivery.e2e.test.ts b/listener/src/__tests__/multi-channel-delivery.e2e.test.ts index 767a1164..22900a86 100644 --- a/listener/src/__tests__/multi-channel-delivery.e2e.test.ts +++ b/listener/src/__tests__/multi-channel-delivery.e2e.test.ts @@ -242,11 +242,11 @@ describe('Multi-channel notification delivery (e2e)', () => { // The service logs the underlying webhook failure with status detail... expect(logger.error).toHaveBeenCalledWith( - 'Discord webhook failed', + 'Discord webhook delivery failed', expect.objectContaining({ webhookId: 'ops', - status: 429, - statusText: 'Too Many Requests', + httpStatus: 429, + httpCategory: 'rate_limited', requestId: 'req-logged', eventId: 'event-logged', }) @@ -267,7 +267,7 @@ describe('Multi-channel notification delivery (e2e)', () => { expect(results.audit).toBe(false); expect(logger.error).toHaveBeenCalledWith( - 'Error sending Discord notification', + 'Discord webhook request error', expect.objectContaining({ webhookId: 'audit', eventId: 'event-network' }) ); }); diff --git a/listener/src/__tests__/notification-delivery-lifecycle.e2e.test.ts b/listener/src/__tests__/notification-delivery-lifecycle.e2e.test.ts index 8c7255ab..2194288e 100644 --- a/listener/src/__tests__/notification-delivery-lifecycle.e2e.test.ts +++ b/listener/src/__tests__/notification-delivery-lifecycle.e2e.test.ts @@ -295,7 +295,7 @@ describe('Notification delivery lifecycle (e2e)', () => { expect(result).toBe(false); expect(logger.error).toHaveBeenCalledWith( - 'Error sending Discord notification', + 'Discord webhook request error', expect.objectContaining({ eventId: 'evt-neterr', webhookId: 'test-webhook', diff --git a/listener/src/services/discord-notification.ts b/listener/src/services/discord-notification.ts index 55540be8..5bfc3511 100644 --- a/listener/src/services/discord-notification.ts +++ b/listener/src/services/discord-notification.ts @@ -26,6 +26,7 @@ export function createDiscordService(config: DiscordConfig): DiscordNotification } // --------------------------------------------------------------------------- +<<<<<<< HEAD // Discord content safety // --------------------------------------------------------------------------- @@ -52,6 +53,35 @@ export function sanitizeForDiscord(text: string): string { return text .replace(MENTION_PATTERN, '[mention removed]') .replace(MARKDOWN_CHARS, '\\$1'); +======= +// Internal helpers +// --------------------------------------------------------------------------- + +/** + * Classify an HTTP status code into a readable diagnostic category. + * This keeps log fields actionable without leaking raw status text verbatim. + */ +function classifyHttpStatus(status: number): string { + if (status === 429) return 'rate_limited'; + if (status === 401 || status === 403) return 'auth_error'; + if (status === 404) return 'not_found'; + if (status >= 400 && status < 500) return 'client_error'; + if (status >= 500) return 'server_error'; + return 'unexpected'; +} + +/** + * Read the response body safely, truncating to avoid bloated logs. + * Returns null on read failure so callers always get a loggable value. + */ +async function safeReadResponseBody(response: Response, maxLength = 300): Promise { + try { + const text = await response.text(); + return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text; + } catch { + return null; + } +>>>>>>> 5bc550e (fix(listener): improve Discord delivery failure logging) } export class DiscordNotificationService { @@ -111,15 +141,11 @@ export class DiscordNotificationService { while (attempt <= maxRetries) { const attemptStart = Date.now(); try { - const response = await this.sendWebhook(message); + const response = await this.sendWebhook(message, logContext); const durationMs = Date.now() - attemptStart; if (response.ok) { this.deduplicator.markSent(fingerprint); - logger.info('Discord notification sent successfully', { - eventId: event.id, - contractAddress: contractConfig.address, - }); logger.info('Discord notification delivered', { ...logContext, durationMs, @@ -128,7 +154,8 @@ export class DiscordNotificationService { return true; } - const errorText = await response.text(); + const responseCategory = classifyHttpStatus(response.status); + const errorBody = await safeReadResponseBody(response); this.analytics?.record({ notificationType: NotificationType.DISCORD, contractAddress: contractConfig.address, @@ -137,17 +164,18 @@ export class DiscordNotificationService { errorReason: `HTTP ${response.status}`, timestamp: Date.now(), }); - logger.error('Discord webhook failed', { + logger.error('Discord webhook delivery failed', { ...logContext, - status: response.status, - statusText: response.statusText, - error: errorText, + httpStatus: response.status, + httpCategory: responseCategory, + ...(responseCategory === 'rate_limited' && { retryAfter: response.headers?.get('retry-after') }), + errorSummary: errorBody, durationMs, attempt, }); } catch (error) { const durationMs = Date.now() - attemptStart; - logger.error('Error sending Discord notification', { + logger.error('Discord webhook request error', { ...logContext, error, durationMs, @@ -201,28 +229,32 @@ export class DiscordNotificationService { ], }; - logger.info('Sending Discord test message', { - requestId, - webhookId: this.config.webhookId, - }); + const logContext = { requestId, webhookId: this.config.webhookId }; + logger.info('Sending Discord test message', logContext); const startTime = Date.now(); try { - const response = await this.sendWebhook(message); + const response = await this.sendWebhook(message, logContext); const durationMs = Date.now() - startTime; - logger.info('Discord test message delivered', { - requestId, - webhookId: this.config.webhookId, - ok: response.ok, + if (response.ok) { + logger.info('Discord test message delivered', { ...logContext, durationMs }); + return true; + } + + const errorBody = await safeReadResponseBody(response); + logger.error('Discord test message failed', { + ...logContext, + httpStatus: response.status, + httpCategory: classifyHttpStatus(response.status), + errorSummary: errorBody, durationMs, }); - - return response.ok; + return false; } catch (error) { - logger.error('Error sending test message', { - requestId, + logger.error('Discord test message request error', { + ...logContext, error, durationMs: Date.now() - startTime, }); @@ -234,7 +266,7 @@ export class DiscordNotificationService { return new Promise((resolve) => setTimeout(resolve, ms)); } - private async sendWebhook(message: DiscordMessage): Promise { + private async sendWebhook(message: DiscordMessage, logContext?: Record): Promise { try { const response = await sendWebhook(this.config.webhookUrl, message, { timeoutMs: this.config.timeoutMs, @@ -244,6 +276,7 @@ export class DiscordNotificationService { if (error && error.name === 'AbortError') { this.timeoutCount++; logger.error('Discord webhook request timed out', { + ...logContext, webhookId: this.config.webhookId, timeoutMs: this.config.timeoutMs ?? 5000, });