Skip to content

Commit

Permalink
callback, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
21e8 committed Dec 7, 2024
1 parent c1002f5 commit 951defd
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 43 deletions.
19 changes: 14 additions & 5 deletions src/__tests__/processors/telegram.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});
Expand All @@ -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)
);

Expand All @@ -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'
);
});

Expand Down Expand Up @@ -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)
);

Expand All @@ -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'
);
});

Expand Down Expand Up @@ -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');
});
});
45 changes: 23 additions & 22 deletions src/__tests__/utils/errorClassifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand All @@ -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"'
);
Expand All @@ -56,16 +56,16 @@ 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);
expect(isAggregated2).toBe(i === 4);
}
});

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'
);
Expand All @@ -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,
Expand All @@ -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'
);
Expand All @@ -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"'
);
Expand All @@ -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');
Expand Down Expand Up @@ -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
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/batcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
45 changes: 36 additions & 9 deletions src/processors/telegram.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import type { Message, MessageProcessor, TelegramConfig } from '../types';
import type {
Message,
MessageProcessor,
NotificationLevel,
TelegramConfig,
} from '../types';
import {
classifyError,
clearErrorTracking,
formatClassifiedError,
} from '../utils/errorClassifier';
// import fetch from 'node-fetch';

const EMOJIS: Record<NotificationLevel, string> = {
error: '🚨',
warning: '⚠️',
info: 'ℹ️',
};

type TelegramApiError = {
ok: boolean;
error_code: number;
description: string;
};

export function createTelegramProcessor(
config: TelegramConfig
): MessageProcessor {
Expand All @@ -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;
Expand All @@ -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();
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface TelegramConfig {
}

export interface MessageProcessor {
processBatch(messages: Message[]): Promise<void>;
processBatch(messages: Message[]): void | Promise<void>;
processBatchSync?(messages: Message[]): void;
}

Expand All @@ -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<void>;
processBatch(chatId: string): void;
flush(): Promise<void>;
flushSync(): void;
destroy(): void;
Expand Down
17 changes: 14 additions & 3 deletions src/utils/errorClassifier.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Define tuple types for better type safety
type ErrorPattern = readonly [
RegExp, // pattern
RegExp | ((message: string) => boolean) | Promise<boolean>, // pattern can be RegExp, function, or Promise
string, // category
'low' | 'medium' | 'high', // severity
[number, number]? // [windowMs, countThreshold] for aggregation
Expand Down Expand Up @@ -55,7 +55,9 @@ type ClassifiedError = readonly [
string? // timeWindow
];

export function classifyError(error: Error | string): ClassifiedError {
export async function classifyError(
error: Error | string
): Promise<ClassifiedError> {
const message = error instanceof Error ? error.message : error;
const now = Date.now();

Expand All @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"rootDir": "./src",
"baseUrl": "."
},
"include": ["src/**/*"],
"ninclude": ["src/**/*"],
"exclude": [
"node_modules",
"dist",
Expand Down

0 comments on commit 951defd

Please sign in to comment.