UNPKG

@454creative/easy-email

Version:

A framework-agnostic email service library for Node.js with observability and monitoring

431 lines 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ObservabilityService = void 0; const email_options_interface_1 = require("../interfaces/email-options.interface"); // Simple UUID generator (in production, use a proper UUID library) function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } class ObservabilityService { constructor(config = { enabled: true }) { this.events = []; this.auditLogs = []; this.config = config; this.metrics = this.initializeMetrics(); } static getInstance(config) { if (!ObservabilityService.instance) { ObservabilityService.instance = new ObservabilityService(config); } return ObservabilityService.instance; } initializeMetrics() { return { totalSent: 0, totalFailed: 0, successRate: 0, averageSendTime: 0, providerBreakdown: { smtp: { sent: 0, failed: 0, avgTime: 0 }, sendgrid: { sent: 0, failed: 0, avgTime: 0 }, ses: { sent: 0, failed: 0, avgTime: 0 }, }, last24Hours: { sent: 0, failed: 0, avgTime: 0, }, sesMetrics: { quotaUsage: 0, bounceRate: 0, complaintRate: 0, deliveryRate: 0, lastQuotaCheck: undefined, }, }; } /** * Track email sending attempt */ trackEmailAttempt(options, provider, _startTime) { if (!this.config.enabled) return ''; const eventId = generateUUID(); const event = this.createEmailEvent(eventId, 'email_sent', provider, options); this.events.push(event); // Don't log here - wait for completion to log with complete information return eventId; } /** * Track email success */ trackEmailSuccess(eventId, response, duration, provider) { if (!this.config.enabled) return; const event = this.events.find(e => e.id === eventId); if (event) { event.messageId = response.messageId; event.duration = duration; event.type = 'email_sent'; this.updateMetrics(provider, true, duration); this.createAuditLog('email_sent', response, provider); this.logEvent(event); // Log the completed event with duration information } } /** * Track email failure */ trackEmailFailure(eventId, error, duration, provider) { if (!this.config.enabled) return; const event = this.events.find(e => e.id === eventId); if (event) { event.type = 'email_failed'; event.duration = duration; event.error = { message: error.message || 'Unknown error', code: error.code, details: error.details, }; this.updateMetrics(provider, false, duration); this.createAuditLog('email_failed', { success: false, error }, provider); this.logEvent(event); // Log the completed event with duration and error information } } /** * Track connection verification */ trackConnectionVerification(provider, success, duration) { if (!this.config.enabled) return; const event = this.createConnectionEvent(provider, success, duration); this.events.push(event); this.logEvent(event); } /** * Track SES quota usage */ trackSesQuotaUsage(quotaData) { if (!this.config.enabled || !this.metrics.sesMetrics) return; const usagePercent = (quotaData.sentLast24Hours / quotaData.max24HourSend) * 100; this.metrics.sesMetrics.quotaUsage = usagePercent; this.metrics.sesMetrics.lastQuotaCheck = new Date().toISOString(); // Log quota usage event const event = { id: generateUUID(), timestamp: new Date().toISOString(), type: 'connection_verified', provider: 'ses', metadata: { quotaUsage: usagePercent, max24HourSend: quotaData.max24HourSend, sentLast24Hours: quotaData.sentLast24Hours, maxSendRate: quotaData.maxSendRate, }, }; this.events.push(event); this.logEvent(event); } /** * Track SES sending statistics */ trackSesStatistics(statsData) { if (!this.config.enabled || !this.metrics.sesMetrics) return; const totalAttempts = statsData.deliveryAttempts; const successfulDeliveries = totalAttempts - statsData.bounces - statsData.complaints - statsData.rejects; this.metrics.sesMetrics.deliveryRate = totalAttempts > 0 ? (successfulDeliveries / totalAttempts) * 100 : 0; this.metrics.sesMetrics.bounceRate = totalAttempts > 0 ? (statsData.bounces / totalAttempts) * 100 : 0; this.metrics.sesMetrics.complaintRate = totalAttempts > 0 ? (statsData.complaints / totalAttempts) * 100 : 0; // Log statistics event const event = { id: generateUUID(), timestamp: new Date().toISOString(), type: 'connection_verified', provider: 'ses', metadata: { deliveryRate: this.metrics.sesMetrics.deliveryRate, bounceRate: this.metrics.sesMetrics.bounceRate, complaintRate: this.metrics.sesMetrics.complaintRate, deliveryAttempts: statsData.deliveryAttempts, bounces: statsData.bounces, complaints: statsData.complaints, rejects: statsData.rejects, }, }; this.events.push(event); this.logEvent(event); } /** * Track SES error with specific error type */ trackSesError(error, errorType, duration) { if (!this.config.enabled) return; const event = { id: generateUUID(), timestamp: new Date().toISOString(), type: 'email_failed', provider: 'ses', duration, error: { message: error.message || String(error), code: errorType, details: { sesErrorType: errorType, originalError: error, }, }, metadata: { errorType, duration, }, }; this.events.push(event); this.updateMetrics(email_options_interface_1.EmailProviderType.SES, false, duration); this.logEvent(event); } /** * Get current metrics */ getMetrics() { return Object.assign({}, this.metrics); } /** * Get SES-specific metrics */ getSesMetrics() { return this.metrics.sesMetrics; } /** * Get metrics for a specific provider */ getProviderMetrics(provider) { let providerKey; switch (provider) { case email_options_interface_1.EmailProviderType.SMTP: providerKey = 'smtp'; break; case email_options_interface_1.EmailProviderType.SENDGRID: providerKey = 'sendgrid'; break; case email_options_interface_1.EmailProviderType.SES: providerKey = 'ses'; break; default: providerKey = 'smtp'; } return this.metrics.providerBreakdown[providerKey]; } /** * Get events within time range */ getEvents(startTime, endTime) { let filteredEvents = [...this.events]; if (startTime) { filteredEvents = filteredEvents.filter(e => new Date(e.timestamp) >= startTime); } if (endTime) { filteredEvents = filteredEvents.filter(e => new Date(e.timestamp) <= endTime); } return filteredEvents; } /** * Get audit logs */ getAuditLogs(startTime, endTime) { let filteredLogs = [...this.auditLogs]; if (startTime) { filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) >= startTime); } if (endTime) { filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) <= endTime); } return filteredLogs; } /** * Clear all data (useful for testing) */ clearData() { this.events = []; this.auditLogs = []; this.metrics = this.initializeMetrics(); } sanitizeRecipients(recipients) { if (!this.config.includeSensitiveData) { return '[REDACTED]'; } if (Array.isArray(recipients)) { return recipients.map(r => r.email).join(', '); } return recipients.email; } updateMetrics(provider, success, duration) { let providerKey; switch (provider) { case email_options_interface_1.EmailProviderType.SMTP: providerKey = 'smtp'; break; case email_options_interface_1.EmailProviderType.SENDGRID: providerKey = 'sendgrid'; break; case email_options_interface_1.EmailProviderType.SES: providerKey = 'ses'; break; default: providerKey = 'smtp'; // fallback } this.metrics.totalSent++; if (success) { this.metrics.providerBreakdown[providerKey].sent++; } else { this.metrics.totalFailed++; this.metrics.providerBreakdown[providerKey].failed++; } // Update average send time const currentAvg = this.metrics.providerBreakdown[providerKey].avgTime; const currentCount = this.metrics.providerBreakdown[providerKey].sent + this.metrics.providerBreakdown[providerKey].failed; this.metrics.providerBreakdown[providerKey].avgTime = (currentAvg * (currentCount - 1) + duration) / currentCount; // Update overall success rate this.metrics.successRate = (this.metrics.totalSent - this.metrics.totalFailed) / this.metrics.totalSent; // Update 24-hour metrics (simplified - in production you'd want more sophisticated time tracking) const now = new Date(); const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); const recentEvents = this.events.filter(e => new Date(e.timestamp) >= oneDayAgo); this.metrics.last24Hours.sent = recentEvents.filter(e => e.type === 'email_sent').length; this.metrics.last24Hours.failed = recentEvents.filter(e => e.type === 'email_failed').length; if (recentEvents.length > 0) { const recentDurations = recentEvents.filter(e => e.duration).map(e => e.duration); this.metrics.last24Hours.avgTime = recentDurations.reduce((a, b) => a + b, 0) / recentDurations.length; } // Call custom metrics collector if provided if (this.config.metricsCollector) { this.config.metricsCollector(this.metrics); } } createAuditLog(action, response, provider) { let providerKey; switch (provider) { case email_options_interface_1.EmailProviderType.SMTP: providerKey = 'smtp'; break; case email_options_interface_1.EmailProviderType.SENDGRID: providerKey = 'sendgrid'; break; case email_options_interface_1.EmailProviderType.SES: providerKey = 'ses'; break; default: providerKey = 'smtp'; // fallback } const auditLog = { timestamp: new Date().toISOString(), action, emailDetails: { to: [], // Would be populated from original options subject: '', // Would be populated from original options provider: providerKey, messageId: response.messageId, }, metadata: { success: response.success, error: response.error, }, }; this.auditLogs.push(auditLog); } logEvent(event) { if (this.config.customLogger) { this.config.customLogger(event); } else { console.log(`[EMAIL-EVENT] ${event.type.toUpperCase()}: ${event.provider}`, { recipient: event.recipient, subject: event.subject, duration: event.duration, error: event.error, }); } } /** * Create email event with common properties * @private */ createEmailEvent(eventId, type, provider, options) { var _a; let providerKey; switch (provider) { case email_options_interface_1.EmailProviderType.SMTP: providerKey = 'smtp'; break; case email_options_interface_1.EmailProviderType.SENDGRID: providerKey = 'sendgrid'; break; case email_options_interface_1.EmailProviderType.SES: providerKey = 'ses'; break; default: providerKey = 'smtp'; // fallback } return { id: eventId, timestamp: new Date().toISOString(), type, provider: providerKey, recipient: this.sanitizeRecipients(options.to), subject: options.subject, metadata: { hasAttachments: !!((_a = options.attachments) === null || _a === void 0 ? void 0 : _a.length), hasHtml: !!options.html, hasText: !!options.text, ccCount: Array.isArray(options.cc) ? options.cc.length : options.cc ? 1 : 0, bccCount: Array.isArray(options.bcc) ? options.bcc.length : options.bcc ? 1 : 0, }, }; } /** * Create connection verification event * @private */ createConnectionEvent(provider, success, duration) { let providerKey; switch (provider) { case email_options_interface_1.EmailProviderType.SMTP: providerKey = 'smtp'; break; case email_options_interface_1.EmailProviderType.SENDGRID: providerKey = 'sendgrid'; break; case email_options_interface_1.EmailProviderType.SES: providerKey = 'ses'; break; default: providerKey = 'smtp'; // fallback } return { id: generateUUID(), timestamp: new Date().toISOString(), type: success ? 'connection_verified' : 'connection_failed', provider: providerKey, duration, error: success ? undefined : { message: 'Connection verification failed', code: 'CONNECTION_FAILED', }, }; } } exports.ObservabilityService = ObservabilityService; //# sourceMappingURL=observability.service.js.map