UNPKG

@ahmedhegazee/nestjs-telescope

Version:

Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling

522 lines 23.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const testing_1 = require("@nestjs/testing"); const exception_watcher_service_1 = require("./exception-watcher.service"); const telescope_service_1 = require("../../core/services/telescope.service"); const exception_watcher_filter_1 = require("./exception-watcher.filter"); const exception_watcher_config_1 = require("./exception-watcher.config"); describe('ExceptionWatcherService', () => { let service; let telescopeService; beforeEach(async () => { const mockTelescopeService = { record: jest.fn(), }; const module = await testing_1.Test.createTestingModule({ providers: [ exception_watcher_service_1.ExceptionWatcherService, { provide: telescope_service_1.TelescopeService, useValue: mockTelescopeService, }, { provide: 'EXCEPTION_WATCHER_CONFIG', useValue: exception_watcher_config_1.defaultExceptionWatcherConfig, }, ], }).compile(); service = module.get(exception_watcher_service_1.ExceptionWatcherService); telescopeService = module.get(telescope_service_1.TelescopeService); }); afterEach(() => { jest.clearAllMocks(); process.removeAllListeners('unhandledRejection'); process.removeAllListeners('uncaughtException'); }); describe('initialization', () => { it('should be defined', () => { expect(service).toBeDefined(); }); it('should initialize with default metrics', () => { const metrics = service.getMetrics(); expect(metrics.totalExceptions).toBe(0); expect(metrics.uniqueExceptions).toBe(0); expect(metrics.errorRate).toBe(0); expect(metrics.criticalErrors).toBe(0); }); it('should setup global exception handlers on module init', async () => { const originalProcessOn = process.on; process.on = jest.fn(); await service.onModuleInit(); expect(process.on).toHaveBeenCalledWith('unhandledRejection', expect.any(Function)); expect(process.on).toHaveBeenCalledWith('uncaughtException', expect.any(Function)); process.on = originalProcessOn; }); it('should not setup handlers if config disabled', async () => { const disabledConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, captureUnhandledRejections: false, captureUncaughtExceptions: false, }; const disabledService = new exception_watcher_service_1.ExceptionWatcherService(telescopeService, disabledConfig); const originalProcessOn = process.on; process.on = jest.fn(); await disabledService.onModuleInit(); expect(process.on).not.toHaveBeenCalledWith('unhandledRejection', expect.any(Function)); expect(process.on).not.toHaveBeenCalledWith('uncaughtException', expect.any(Function)); process.on = originalProcessOn; }); }); describe('exception tracking', () => { it('should track exception with full context', () => { const context = { id: 'test-exception-1', timestamp: new Date(), error: new Error('Test error'), errorType: 'Error', errorMessage: 'Test error', statusCode: 500, classification: { type: 'system', category: exception_watcher_filter_1.ErrorCategory.SERVER_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.HIGH, fingerprint: 'test-fingerprint', groupId: 'test-group', }, traceId: 'trace-123', requestId: 'req-456', userId: 'user-789', }; service.trackException(context); expect(telescopeService.record).toHaveBeenCalledWith(expect.objectContaining({ type: 'exception', content: expect.objectContaining({ exception: expect.objectContaining({ id: 'test-exception-1', type: 'Error', message: 'Test error', }), }), })); const metrics = service.getMetrics(); expect(metrics.totalExceptions).toBe(1); expect(metrics.highSeverityErrors).toBe(1); }); it('should not track exception if disabled', () => { const disabledConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, enabled: false, }; const disabledService = new exception_watcher_service_1.ExceptionWatcherService(telescopeService, disabledConfig); const context = { id: 'test-exception-1', timestamp: new Date(), error: new Error('Test error'), errorType: 'Error', errorMessage: 'Test error', }; disabledService.trackException(context); expect(telescopeService.record).not.toHaveBeenCalled(); }); it('should handle tracking errors gracefully', () => { telescopeService.record.mockImplementation(() => { throw new Error('Telescope service error'); }); const context = { id: 'test-exception-1', timestamp: new Date(), error: new Error('Test error'), errorType: 'Error', errorMessage: 'Test error', }; expect(() => service.trackException(context)).not.toThrow(); }); }); describe('error grouping', () => { it('should group similar exceptions', () => { const context1 = { id: 'exception-1', timestamp: new Date(), error: new Error('Database connection failed'), errorType: 'DatabaseError', errorMessage: 'Database connection failed', classification: { type: 'database', category: exception_watcher_filter_1.ErrorCategory.DATABASE_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.HIGH, fingerprint: 'db-fingerprint', groupId: 'db-group-1', }, }; const context2 = { id: 'exception-2', timestamp: new Date(), error: new Error('Database connection failed'), errorType: 'DatabaseError', errorMessage: 'Database connection failed', classification: { type: 'database', category: exception_watcher_filter_1.ErrorCategory.DATABASE_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.HIGH, fingerprint: 'db-fingerprint', groupId: 'db-group-1', }, }; service.trackException(context1); service.trackException(context2); const groups = service.getExceptionGroups(); expect(groups).toHaveLength(1); expect(groups[0].count).toBe(2); expect(groups[0].groupId).toBe('db-group-1'); }); it('should track affected users and requests', () => { const context1 = { id: 'exception-1', timestamp: new Date(), error: new Error('Test error'), errorType: 'Error', errorMessage: 'Test error', userId: 'user-1', requestId: 'req-1', classification: { type: 'system', category: exception_watcher_filter_1.ErrorCategory.SERVER_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.MEDIUM, fingerprint: 'test-fingerprint', groupId: 'test-group', }, }; const context2 = { id: 'exception-2', timestamp: new Date(), error: new Error('Test error'), errorType: 'Error', errorMessage: 'Test error', userId: 'user-2', requestId: 'req-2', classification: { type: 'system', category: exception_watcher_filter_1.ErrorCategory.SERVER_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.MEDIUM, fingerprint: 'test-fingerprint', groupId: 'test-group', }, }; service.trackException(context1); service.trackException(context2); const group = service.getExceptionGroup('test-group'); expect(group).toBeDefined(); expect(group.affectedUsers.size).toBe(2); expect(group.affectedRequests).toHaveLength(2); }); }); describe('metrics calculation', () => { it('should calculate error rates correctly', () => { const contexts = Array.from({ length: 10 }, (_, i) => ({ id: `exception-${i}`, timestamp: new Date(), error: new Error(`Test error ${i}`), errorType: 'Error', errorMessage: `Test error ${i}`, classification: { type: 'system', category: exception_watcher_filter_1.ErrorCategory.SERVER_ERROR, severity: i < 5 ? exception_watcher_filter_1.ErrorSeverity.HIGH : exception_watcher_filter_1.ErrorSeverity.LOW, fingerprint: `fingerprint-${i}`, groupId: `group-${i}`, }, })); contexts.forEach(context => service.trackException(context)); const metrics = service.getMetrics(); expect(metrics.totalExceptions).toBe(10); expect(metrics.uniqueExceptions).toBe(10); expect(metrics.highSeverityErrors).toBe(5); expect(metrics.lowSeverityErrors).toBe(5); }); it('should track top errors correctly', () => { const createException = (id, groupId, count) => { for (let i = 0; i < count; i++) { const context = { id: `${id}-${i}`, timestamp: new Date(), error: new Error(`Error ${id}`), errorType: 'Error', errorMessage: `Error ${id}`, classification: { type: 'system', category: exception_watcher_filter_1.ErrorCategory.SERVER_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.MEDIUM, fingerprint: `fingerprint-${groupId}`, groupId, }, }; service.trackException(context); } }; createException('A', 'group-A', 5); createException('B', 'group-B', 3); createException('C', 'group-C', 7); const metrics = service.getMetrics(); expect(metrics.topErrors).toHaveLength(3); expect(metrics.topErrors[0].count).toBe(7); expect(metrics.topErrors[1].count).toBe(5); expect(metrics.topErrors[2].count).toBe(3); }); }); describe('alerting system', () => { it('should generate error rate alerts', (done) => { const alertConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, alertThresholds: { errorRate: 0.5, criticalErrors: 10, timeWindow: 60000, }, }; const alertService = new exception_watcher_service_1.ExceptionWatcherService(telescopeService, alertConfig); alertService.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('error_rate'); expect(alert.severity).toBe('high'); expect(alert.message).toContain('Error rate exceeded threshold'); done(); }); for (let i = 0; i < 10; i++) { const context = { id: `exception-${i}`, timestamp: new Date(), error: new Error(`Test error ${i}`), errorType: 'Error', errorMessage: `Test error ${i}`, }; alertService.trackException(context); } }); it('should generate critical error alerts', (done) => { const alertConfig = { ...exception_watcher_config_1.defaultExceptionWatcherConfig, alertThresholds: { errorRate: 100, criticalErrors: 2, timeWindow: 60000, }, }; const alertService = new exception_watcher_service_1.ExceptionWatcherService(telescopeService, alertConfig); alertService.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('critical_errors'); expect(alert.severity).toBe('critical'); expect(alert.message).toContain('Critical errors exceeded threshold'); done(); }); for (let i = 0; i < 3; i++) { const context = { id: `critical-exception-${i}`, timestamp: new Date(), error: new Error(`Critical error ${i}`), errorType: 'CriticalError', errorMessage: `Critical error ${i}`, classification: { type: 'system', category: exception_watcher_filter_1.ErrorCategory.SYSTEM_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.CRITICAL, fingerprint: `critical-fingerprint-${i}`, groupId: `critical-group-${i}`, }, }; alertService.trackException(context); } }); it('should generate new error alerts', (done) => { service.getAlertsStream().subscribe(alert => { expect(alert.type).toBe('new_error'); expect(alert.severity).toBe('medium'); expect(alert.message).toContain('New error type detected'); done(); }); const context = { id: 'new-exception', timestamp: new Date(), error: new Error('New error type'), errorType: 'NewErrorType', errorMessage: 'New error type', classification: { type: 'system', category: exception_watcher_filter_1.ErrorCategory.SYSTEM_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.MEDIUM, fingerprint: 'new-fingerprint', groupId: 'new-group', }, }; service.trackException(context); }); }); describe('correlation', () => { it('should correlate exceptions with requests', () => { const context = { id: 'exception-1', timestamp: new Date(), error: new Error('Test error'), errorType: 'Error', errorMessage: 'Test error', traceId: 'trace-123', requestId: 'req-456', }; service.trackException(context); expect(context.performance?.requestCorrelationId).toBe('trace-123'); }); it('should correlate exceptions with queries', () => { const context = { id: 'exception-1', timestamp: new Date(), error: new Error('Database error'), errorType: 'DatabaseError', errorMessage: 'Database error', traceId: 'trace-123', }; service.trackException(context); expect(context.performance?.queryCorrelationId).toBe('trace-123'); }); it('should correlate exceptions with sessions', () => { const context = { id: 'exception-1', timestamp: new Date(), error: new Error('Session error'), errorType: 'SessionError', errorMessage: 'Session error', sessionId: 'session-789', }; service.trackException(context); expect(context.performance?.sessionCorrelationId).toBe('session-789'); }); }); describe('global exception handlers', () => { it('should handle unhandled promise rejections', () => { const spy = jest.spyOn(service, 'trackException'); const error = new Error('Unhandled promise rejection'); service.handleUnhandledRejection(error, Promise.resolve()); expect(spy).toHaveBeenCalledWith(expect.objectContaining({ errorType: 'UnhandledPromiseRejection', errorMessage: 'Unhandled promise rejection', classification: expect.objectContaining({ severity: exception_watcher_filter_1.ErrorSeverity.HIGH, }), })); }); it('should handle uncaught exceptions', () => { const spy = jest.spyOn(service, 'trackException'); const error = new Error('Uncaught exception'); service.handleUncaughtException(error); expect(spy).toHaveBeenCalledWith(expect.objectContaining({ errorType: 'UncaughtException', errorMessage: 'Uncaught exception', classification: expect.objectContaining({ severity: exception_watcher_filter_1.ErrorSeverity.CRITICAL, }), })); }); }); describe('public API', () => { it('should provide metrics stream', (done) => { const metricsStream = service.getMetricsStream(); metricsStream.subscribe(metrics => { expect(metrics).toBeDefined(); expect(metrics.totalExceptions).toBeGreaterThanOrEqual(0); done(); }); const context = { id: 'test-exception', timestamp: new Date(), error: new Error('Test error'), errorType: 'Error', errorMessage: 'Test error', }; service.trackException(context); }); it('should resolve exception groups', () => { const context = { id: 'exception-1', timestamp: new Date(), error: new Error('Test error'), errorType: 'Error', errorMessage: 'Test error', classification: { type: 'system', category: exception_watcher_filter_1.ErrorCategory.SERVER_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.MEDIUM, fingerprint: 'test-fingerprint', groupId: 'test-group', }, }; service.trackException(context); const resolved = service.resolveExceptionGroup('test-group', 'john.doe', 'Fixed the issue'); expect(resolved).toBe(true); const group = service.getExceptionGroup('test-group'); expect(group?.resolved).toBe(true); expect(group?.assignedTo).toBe('john.doe'); expect(group?.notes).toBe('Fixed the issue'); }); it('should acknowledge alerts', () => { const alerts = []; service.getAlertsStream().subscribe(alert => alerts.push(alert)); const context = { id: 'new-exception', timestamp: new Date(), error: new Error('New error'), errorType: 'NewError', errorMessage: 'New error', classification: { type: 'system', category: exception_watcher_filter_1.ErrorCategory.SYSTEM_ERROR, severity: exception_watcher_filter_1.ErrorSeverity.MEDIUM, fingerprint: 'new-fingerprint', groupId: 'new-group', }, }; service.trackException(context); setTimeout(() => { expect(alerts.length).toBeGreaterThan(0); const alert = alerts[0]; const acknowledged = service.acknowledgeAlert(alert.id); expect(acknowledged).toBe(true); expect(alert.acknowledged).toBe(true); }, 100); }); it('should return recent exceptions', () => { const contexts = Array.from({ length: 5 }, (_, i) => ({ id: `exception-${i}`, timestamp: new Date(), error: new Error(`Test error ${i}`), errorType: 'Error', errorMessage: `Test error ${i}`, })); contexts.forEach(context => service.trackException(context)); const recent = service.getRecentExceptions(3); expect(recent).toHaveLength(3); expect(recent[0].id).toBe('exception-4'); }); }); describe('cleanup and resource management', () => { it('should cleanup on destroy', () => { const destroySpy = jest.spyOn(service.destroy$, 'next'); const completeSpy = jest.spyOn(service.destroy$, 'complete'); service.onDestroy(); expect(destroySpy).toHaveBeenCalled(); expect(completeSpy).toHaveBeenCalled(); }); it('should limit history size', () => { const maxSize = service.maxHistorySize; for (let i = 0; i < maxSize + 100; i++) { const context = { id: `exception-${i}`, timestamp: new Date(), error: new Error(`Test error ${i}`), errorType: 'Error', errorMessage: `Test error ${i}`, }; service.trackException(context); } const history = service.exceptionHistory; expect(history.length).toBeLessThanOrEqual(maxSize); }); }); }); //# sourceMappingURL=exception-watcher.service.spec.js.map