@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
JavaScript
;
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