From 951defd95a4df63a939ca43e036648bdd71bc9ed Mon Sep 17 00:00:00 2001 From: 21e8 Date: Sun, 8 Dec 2024 00:22:07 +0100 Subject: [PATCH] callback, tests --- src/__tests__/processors/telegram.test.ts | 19 ++++++--- src/__tests__/utils/errorClassifier.test.ts | 45 +++++++++++---------- src/batcher.ts | 2 +- src/processors/telegram.ts | 45 ++++++++++++++++----- src/types.ts | 4 +- src/utils/errorClassifier.ts | 17 ++++++-- tsconfig.json | 2 +- 7 files changed, 91 insertions(+), 43 deletions(-) diff --git a/src/__tests__/processors/telegram.test.ts b/src/__tests__/processors/telegram.test.ts index d445d75..77a0761 100644 --- a/src/__tests__/processors/telegram.test.ts +++ b/src/__tests__/processors/telegram.test.ts @@ -44,7 +44,7 @@ describe('TelegramProcessor', () => { const body = JSON.parse(options.body); expect(body).toEqual({ chat_id: defaultConfig.chatId, - text: '[INFO] info message\n[WARNING] warning message\n[ERROR] error message', + text: 'ℹ️ [INFO] info message\n⚠️ [WARNING] warning message\n🚨 [ERROR] error message', parse_mode: 'HTML', }); }); @@ -70,7 +70,11 @@ describe('TelegramProcessor', () => { ok: false, status: 400, statusText: 'Bad Request', - json: () => Promise.resolve({}), + json: () => Promise.resolve({ + ok: false, + error_code: 400, + description: 'Bad Request: message text is empty' + }), } as Response) ); @@ -80,7 +84,7 @@ describe('TelegramProcessor', () => { ]; await expect(processor.processBatch(messages)).rejects.toThrow( - 'Telegram API error: Bad Request' + 'Telegram API error: Bad Request - Bad Request: message text is empty' ); }); @@ -117,6 +121,11 @@ describe('TelegramProcessor', () => { Promise.resolve({ ok: false, status: 400, + json: () => Promise.resolve({ + ok: false, + error_code: 400, + description: 'Unknown Error' + }), } as Response) ); @@ -126,7 +135,7 @@ describe('TelegramProcessor', () => { ] as Message[]; await expect(processor.processBatch(messages)).rejects.toThrow( - 'Telegram API error: Unknown Error' + 'Telegram API error: Unknown Error - Unknown Error' ); }); @@ -157,7 +166,7 @@ describe('TelegramProcessor', () => { expect(global.fetch).toHaveBeenCalledTimes(1); const [, options] = (global.fetch as jest.Mock).mock.calls[0]; const body = JSON.parse(options.body); - expect(body.text).toContain('[ERROR] Error occurred'); + expect(body.text).toContain('🚨 [ERROR] Error occurred'); expect(body.text).toContain('Test error'); }); }); diff --git a/src/__tests__/utils/errorClassifier.test.ts b/src/__tests__/utils/errorClassifier.test.ts index f945eba..ea9a15e 100644 --- a/src/__tests__/utils/errorClassifier.test.ts +++ b/src/__tests__/utils/errorClassifier.test.ts @@ -18,14 +18,14 @@ describe('ErrorClassifier', () => { }); describe('error aggregation', () => { - it('should aggregate similar database errors after threshold', () => { + it('should aggregate similar database errors after threshold', async () => { const dbError = new Error( 'duplicate key value violates unique constraint "users_pkey"' ); // First 4 errors should not be aggregated for (let i = 0; i < 4; i++) { - const [, category, , , isAggregated] = classifyError(dbError); + const [, category, , , isAggregated] = await classifyError(dbError); expect(isAggregated).toBe(false); expect(category).toBe('DATABASE_CONSTRAINT_VIOLATION'); } @@ -39,14 +39,14 @@ describe('ErrorClassifier', () => { isAggregated, occurrences, timeWindow, - ] = classifyError(dbError); + ] = await classifyError(dbError); expect(isAggregated).toBe(true); expect(occurrences).toBe(5); expect(timeWindow).toBeDefined(); expect(details).toEqual(['constraint', 'users_pkey']); }); - it('should track different constraint violations separately', () => { + it('should track different constraint violations separately', async () => { const error1 = new Error( 'duplicate key value violates unique constraint "users_pkey"' ); @@ -56,8 +56,8 @@ describe('ErrorClassifier', () => { // Add 5 of each error for (let i = 0; i < 5; i++) { - const [, , , , isAggregated1] = classifyError(error1); - const [, , , , isAggregated2] = classifyError(error2); + const [, , , , isAggregated1] = await classifyError(error1); + const [, , , , isAggregated2] = await classifyError(error2); // Both should aggregate independently expect(isAggregated1).toBe(i === 4); @@ -65,7 +65,7 @@ describe('ErrorClassifier', () => { } }); - it('should clean old errors from aggregation window', () => { + it('should clean old errors from aggregation window', async () => { const dbError = new Error( 'duplicate key value violates unique constraint' ); @@ -74,18 +74,18 @@ describe('ErrorClassifier', () => { // Add 3 errors for (let i = 0; i < 3; i++) { - classifyError(dbError); + await classifyError(dbError); } // Move time forward past window jest.spyOn(Date, 'now').mockImplementation(() => now + 65000); // 65 seconds // Should not be aggregated as old errors are cleaned - const [, , , , isAggregated] = classifyError(dbError); + const [, , , , isAggregated] = await classifyError(dbError); expect(isAggregated).toBe(false); }); - it('should handle custom error patterns with aggregation', () => { + it('should handle custom error patterns with aggregation', async () => { addErrorPatterns([ [ /custom error/i, @@ -99,20 +99,20 @@ describe('ErrorClassifier', () => { // First 2 errors should not be aggregated for (let i = 0; i < 2; i++) { - const [, category, , , isAggregated] = classifyError(customError); + const [, category, , , isAggregated] = await classifyError(customError); expect(isAggregated).toBe(false); expect(category).toBe('CUSTOM_ERROR'); } // 3rd error should trigger aggregation const [, , , , isAggregated, occurrences, timeWindow] = - classifyError(customError); + await classifyError(customError); expect(isAggregated).toBe(true); expect(occurrences).toBe(3); expect(timeWindow).toBeDefined(); }); - it('should show correct time window in aggregated errors', () => { + it('should show correct time window in aggregated errors', async () => { const dbError = new Error( 'duplicate key value violates unique constraint' ); @@ -128,12 +128,12 @@ describe('ErrorClassifier', () => { jest.spyOn(Date, 'now').mockImplementation(() => now + 30000); // +30s classifyError(dbError); - const [, , , , isAggregated, , timeWindow] = classifyError(dbError); + const [, , , , isAggregated, , timeWindow] = await classifyError(dbError); expect(isAggregated).toBe(true); expect(timeWindow).toBe('30s'); }); - it('should aggregate messages sent within milliseconds', () => { + it('should aggregate messages sent within milliseconds', async () => { const dbError = new Error( 'duplicate key value violates unique constraint "users_pkey"' ); @@ -149,13 +149,14 @@ describe('ErrorClassifier', () => { // First 4 should not be aggregated for (let i = 0; i < 4; i++) { - const [, category, , , isAggregated] = results[i]; + const [, category, , , isAggregated] = await results[i]; expect(isAggregated).toBe(false); expect(category).toBe('DATABASE_CONSTRAINT_VIOLATION'); } // 5th message should show aggregation - const [, , , details, isAggregated, occurrences, timeWindow] = results[4]; + const [, , , details, isAggregated, occurrences, timeWindow] = + await results[4]; expect(isAggregated).toBe(true); expect(occurrences).toBe(5); expect(timeWindow).toBe('0s'); @@ -255,24 +256,24 @@ describe('ErrorClassifier', () => { expect(batch2.text).not.toContain('[AGGREGATED]'); }); - it('should handle custom patterns with precedence', () => { + it('should handle custom patterns with precedence', async () => { addErrorPatterns([ [/custom error/i, 'CUSTOM_ERROR', 'high'] ]); const error = new Error('custom error occurred'); - const [, category, severity] = classifyError(error); + const [, category, severity] = await classifyError(error); expect(category).toBe('CUSTOM_ERROR'); expect(severity).toBe('high'); }); - it('should clear error tracking completely', () => { + it('should clear error tracking completely', async () => { const error = new Error('test error'); - classifyError(error); // Add some tracking data + await classifyError(error); // Add some tracking data clearErrorTracking(); - const result = classifyError(error); + const result = await classifyError(error); expect(result[4]).toBe(false); // isAggregated should be false }); }); diff --git a/src/batcher.ts b/src/batcher.ts index 3bfa403..49458a1 100644 --- a/src/batcher.ts +++ b/src/batcher.ts @@ -102,7 +102,7 @@ export function createMessageBatcher( try { // Handle both sync and async calls const result = processor.processBatch([item]); - if (result instanceof Promise) { + if (result && typeof result.then === 'function') { result.catch((error) => { console.error(`Processor failed:`, error); }); diff --git a/src/processors/telegram.ts b/src/processors/telegram.ts index 2a64c7f..3a54c06 100644 --- a/src/processors/telegram.ts +++ b/src/processors/telegram.ts @@ -1,4 +1,9 @@ -import type { Message, MessageProcessor, TelegramConfig } from '../types'; +import type { + Message, + MessageProcessor, + NotificationLevel, + TelegramConfig, +} from '../types'; import { classifyError, clearErrorTracking, @@ -6,6 +11,18 @@ import { } from '../utils/errorClassifier'; // import fetch from 'node-fetch'; +const EMOJIS: Record = { + error: '🚨', + warning: '⚠️', + info: 'ℹ️', +}; + +type TelegramApiError = { + ok: boolean; + error_code: number; + description: string; +}; + export function createTelegramProcessor( config: TelegramConfig ): MessageProcessor { @@ -23,22 +40,22 @@ export function createTelegramProcessor( } try { - const formattedMessages = messages - .map((msg) => { + const texts = await Promise.all( + messages.map(async (msg) => { if (!msg.text.trim()) return null; const prefix = msg.level.toUpperCase(); - let text = `[${prefix}] ${msg.text}`; + let text = `${EMOJIS[msg.level]} [${prefix}] ${msg.text}`; if (msg.level === 'error' && msg.error) { - const classified = classifyError(msg.error); + const classified = await classifyError(msg.error); text += '\n' + formatClassifiedError(classified); } return text; }) - .filter(Boolean) - .join('\n'); + ); + const formattedMessages = texts.filter(Boolean).join('\n'); if (!formattedMessages) { console.log('[Telegram] No messages to send'); return; @@ -55,10 +72,20 @@ export function createTelegramProcessor( }); if (!response.ok) { - throw new Error(`Telegram API error: ${response.statusText || 'Unknown Error'}`); + const data = await response.json(); + console.error('[Telegram] API Response:', data); + const errorData = data as TelegramApiError; + throw new Error( + `Telegram API error: ${response.statusText || 'Unknown Error'} - ${errorData.description || JSON.stringify(data)}` + ); + } + } catch (error) { + console.error('[Telegram] Error sending message:', error); + if (error instanceof Error) { + throw error; } + throw new Error(`Telegram API error: ${JSON.stringify(error)}`); } finally { - // Clear error tracking after processing batch clearErrorTracking(); } } diff --git a/src/types.ts b/src/types.ts index 6125da4..fae826e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,7 +20,7 @@ export interface TelegramConfig { } export interface MessageProcessor { - processBatch(messages: Message[]): Promise; + processBatch(messages: Message[]): void | Promise; processBatchSync?(messages: Message[]): void; } @@ -29,7 +29,7 @@ export interface MessageBatcher { warning(message: string): void; error(message: string, error?: Error | string): void; queueMessage(message: string, level: NotificationLevel): void; - processBatch(chatId: string): Promise; + processBatch(chatId: string): void; flush(): Promise; flushSync(): void; destroy(): void; diff --git a/src/utils/errorClassifier.ts b/src/utils/errorClassifier.ts index 6fd2d93..4868844 100644 --- a/src/utils/errorClassifier.ts +++ b/src/utils/errorClassifier.ts @@ -1,6 +1,6 @@ // Define tuple types for better type safety type ErrorPattern = readonly [ - RegExp, // pattern + RegExp | ((message: string) => boolean) | Promise, // pattern can be RegExp, function, or Promise string, // category 'low' | 'medium' | 'high', // severity [number, number]? // [windowMs, countThreshold] for aggregation @@ -55,7 +55,9 @@ type ClassifiedError = readonly [ string? // timeWindow ]; -export function classifyError(error: Error | string): ClassifiedError { +export async function classifyError( + error: Error | string +): Promise { const message = error instanceof Error ? error.message : error; const now = Date.now(); @@ -64,7 +66,16 @@ export function classifyError(error: Error | string): ClassifiedError { console.log('Message:', message); for (const [pattern, category, severity, aggregation] of patterns) { - if (pattern.test(message)) { + let matches = false; + if (pattern instanceof RegExp) { + matches = pattern.test(message); + } else if (pattern instanceof Promise) { + matches = await pattern; + } else { + matches = pattern(message); + } + + if (matches) { const details: string[] = []; let trackerKey = category; diff --git a/tsconfig.json b/tsconfig.json index ad2f9b7..7c600eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "rootDir": "./src", "baseUrl": "." }, - "include": ["src/**/*"], + "ninclude": ["src/**/*"], "exclude": [ "node_modules", "dist",