@454creative/easy-email
Version:
A framework-agnostic email service library for Node.js with observability and monitoring
431 lines • 15.7 kB
JavaScript
"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