@bernierllc/email-tracking
v1.0.10
Published
Email delivery tracking service that monitors email lifecycle from send to delivery/bounce/open/click
Downloads
296
Readme
@bernierllc/email-tracking
Email delivery tracking service that monitors email lifecycle from send to delivery/bounce/open/click. Links sent emails to webhook events from email-webhook-events package.
Installation
npm install @bernierllc/email-trackingFeatures
- Send Tracking - Record all email sends with tracking IDs
- Event Linking - Link webhook events to sent emails by message ID
- Status Management - Track delivery status with priority-based updates
- Timeline Queries - Get complete event timeline for each email
- Recipient History - Aggregate statistics per email address
- Campaign Analytics - Calculate metrics for bulk email campaigns
- NeverHub Integration - Event bus and service discovery support
- Reputation Scoring - Track recipient engagement and reputation
Usage
Basic Setup
import { EmailTracking } from '@bernierllc/email-tracking';
const tracking = new EmailTracking({
enableOpenTracking: true,
enableClickTracking: true,
eventRetentionDays: 90,
aggregateStats: true
});
await tracking.initialize();Record Email Send
const result = await tracking.recordEmailSent({
messageId: 'msg_abc123',
provider: 'sendgrid',
from: '[email protected]',
to: ['[email protected]'],
subject: 'Welcome to our service',
campaignId: 'welcome_campaign',
metadata: { source: 'signup_flow' }
});
console.log('Tracking ID:', result.data?.id);
console.log('Status:', result.data?.currentStatus); // 'sent'Process Webhook Events
// Typically called automatically via NeverHub subscription
const result = await tracking.processWebhookEvent({
messageId: 'msg_abc123',
eventType: 'delivered',
timestamp: new Date().toISOString(),
metadata: { smtpResponse: '250 OK' }
});
// Status automatically updated to 'delivered'Get Email Timeline
const timeline = await tracking.getTimeline('track_xyz789');
if (timeline.success) {
console.log('Current Status:', timeline.data.currentStatus);
console.log('Events:');
timeline.data.events.forEach(event => {
console.log(` ${event.eventType} at ${event.timestamp}`);
});
}
// Output:
// Current Status: opened
// Events:
// sent at 2025-01-01T10:00:00Z
// delivered at 2025-01-01T10:01:00Z
// opened at 2025-01-01T10:15:00ZGet Recipient History
const history = await tracking.getRecipientHistory('[email protected]');
if (history.success) {
console.log('Total Sent:', history.data.totalSent);
console.log('Total Delivered:', history.data.totalDelivered);
console.log('Total Opened:', history.data.totalOpened);
console.log('Total Clicked:', history.data.totalClicked);
console.log('Reputation Score:', history.data.reputationScore);
}Get Campaign Statistics
const stats = await tracking.getCampaignStats('welcome_campaign');
if (stats.success) {
console.log('Campaign:', stats.data.campaignId);
console.log('Sent:', stats.data.totalSent);
console.log('Delivery Rate:', stats.data.deliveryRate + '%');
console.log('Open Rate:', stats.data.openRate + '%');
console.log('Click Rate:', stats.data.clickRate + '%');
}API Reference
EmailTracking
Main tracking service class.
Constructor
constructor(config?: TrackingConfig)TrackingConfig Options:
enableOpenTracking(boolean) - Enable open tracking (default: true)enableClickTracking(boolean) - Enable click tracking (default: true)eventRetentionDays(number) - How long to keep events (default: 90)aggregateStats(boolean) - Enable recipient history tracking (default: true)
Methods
initialize(): Promise<void>
Initialize the tracking service, set up NeverHub integration, and prepare database.
recordEmailSent(data: RecordEmailSentData): Promise<EmailTrackingResult<EmailSendRecord>>
Record an email send event with tracking metadata.
Parameters:
messageId(string) - Email provider message IDprovider(string) - Email provider (sendgrid, mailgun, ses, postmark)from(string) - Sender email addressto(string[]) - Recipient email addressescc?(string[]) - CC recipients (optional)bcc?(string[]) - BCC recipients (optional)subject(string) - Email subjectcampaignId?(string) - Campaign identifier (optional)metadata?(Record<string, unknown>) - Custom tracking metadata (optional)
Returns: EmailTrackingResult<EmailSendRecord> with tracking ID and status
processWebhookEvent(event: WebhookEventData): Promise<EmailTrackingResult>
Process a normalized webhook event from email-webhook-events package.
Parameters:
messageId(string) - Email provider message IDeventType(string) - Event type (delivered, bounce, open, click, spamreport, unsubscribe)timestamp(string | Date) - Event timestampmetadata?(Record<string, unknown>) - Event metadata (optional)
Returns: EmailTrackingResult indicating success/failure
getTimeline(trackingId: string): Promise<EmailTrackingResult<EmailTimeline>>
Get complete event timeline for an email.
Returns: EmailTimeline with send record, all events, current status, and last event timestamp
getRecipientHistory(emailAddress: string): Promise<EmailTrackingResult<RecipientHistory>>
Get aggregate statistics for a recipient email address.
Returns: RecipientHistory with totals, last event timestamps, and reputation score
getCampaignStats(campaignId: string): Promise<EmailTrackingResult<CampaignStatistics>>
Get aggregate statistics for a campaign.
Returns: CampaignStatistics with totals, rates, and percentages
Types
EmailDeliveryStatus
enum EmailDeliveryStatus {
SENT = 'sent',
DELIVERED = 'delivered',
BOUNCED = 'bounced',
OPENED = 'opened',
CLICKED = 'clicked',
COMPLAINED = 'complained',
UNSUBSCRIBED = 'unsubscribed',
FAILED = 'failed'
}Status Priority (highest to lowest):
- COMPLAINED (spam report)
- UNSUBSCRIBED
- BOUNCED
- FAILED
- CLICKED
- OPENED
- DELIVERED
- SENT
Status can only move UP the priority chain, never down.
EmailSendRecord
interface EmailSendRecord {
id: string;
messageId: string;
provider: EmailProvider;
from: string;
to: string[];
cc?: string[];
bcc?: string[];
subject: string;
campaignId?: string;
metadata?: Record<string, unknown>;
sentAt: Date;
currentStatus: EmailDeliveryStatus;
updatedAt: Date;
}EmailTimeline
interface EmailTimeline {
sendRecord: EmailSendRecord;
events: EmailEvent[];
currentStatus: EmailDeliveryStatus;
lastEventAt: Date;
}RecipientHistory
interface RecipientHistory {
emailAddress: string;
totalSent: number;
totalDelivered: number;
totalBounced: number;
totalOpened: number;
totalClicked: number;
totalComplained: number;
totalUnsubscribed: number;
lastSentAt?: Date;
lastDeliveredAt?: Date;
lastBouncedAt?: Date;
lastOpenedAt?: Date;
lastClickedAt?: Date;
reputationScore: number; // 0-100
}Reputation Scoring:
- Default: 100
- Bounce: -10
- Spam complaint: -25
- Unsubscribe: -5
CampaignStatistics
interface CampaignStatistics {
campaignId: string;
totalSent: number;
totalDelivered: number;
totalBounced: number;
totalOpened: number;
totalClicked: number;
totalComplained: number;
totalUnsubscribed: number;
deliveryRate: number; // percentage
openRate: number; // percentage
clickRate: number; // percentage
}Configuration
Environment Variables
# Enable/disable tracking features
EMAIL_TRACKING_ENABLE_OPEN_TRACKING=true
EMAIL_TRACKING_ENABLE_CLICK_TRACKING=true
# Event retention policy
EMAIL_TRACKING_EVENT_RETENTION_DAYS=90
# Recipient history tracking
EMAIL_TRACKING_AGGREGATE_STATS=trueConfiguration Precedence
- Constructor options (highest priority)
- Environment variables
- Default values (lowest priority)
Integration Status
- Logger: integrated - Structured logging for tracking operations
- Docs-Suite: ready - Complete TypeDoc API documentation
- NeverHub: required - Event subscriptions and service discovery
NeverHub Integration
The service automatically detects and integrates with NeverHub when available:
Events Published:
email.status_changed- When email delivery status changes
Events Subscribed:
email.sent- From email-sender or email-serviceemail.webhook.*- From email-webhook-events
Capabilities:
- Type:
email - Name:
delivery-tracking - Features: send-tracking, event-linking, timeline, recipient-history
Graceful Degradation
The service works without NeverHub but with reduced functionality:
- No automatic event subscription
- No status change events published
- Manual
recordEmailSent()andprocessWebhookEvent()calls required
Dependencies
- @bernierllc/email-webhook-events - Normalized webhook events
- @bernierllc/logger - Structured logging
- @bernierllc/neverhub-adapter - Service discovery and event bus
Use Cases
Email Service Integration
import { EmailTracking } from '@bernierllc/email-tracking';
import { EmailSender } from '@bernierllc/email-sender';
const tracking = new EmailTracking();
const sender = new EmailSender();
await tracking.initialize();
await sender.initialize();
// Send email
const sendResult = await sender.send({
to: '[email protected]',
subject: 'Welcome',
html: '<h1>Welcome!</h1>'
});
// Record tracking
if (sendResult.success) {
await tracking.recordEmailSent({
messageId: sendResult.data.messageId,
provider: 'sendgrid',
from: '[email protected]',
to: ['[email protected]'],
subject: 'Welcome',
campaignId: 'onboarding'
});
}Webhook Processing
import express from 'express';
import { EmailTracking } from '@bernierllc/email-tracking';
import { normalizeWebhookEvent } from '@bernierllc/email-webhook-events';
const app = express();
const tracking = new EmailTracking();
await tracking.initialize();
app.post('/webhooks/sendgrid', async (req, res) => {
const events = req.body;
for (const event of events) {
// Normalize webhook event
const normalized = normalizeWebhookEvent('sendgrid', event);
// Process with tracking
await tracking.processWebhookEvent(normalized);
}
res.sendStatus(200);
});Campaign Analytics Dashboard
async function getCampaignDashboard(campaignId: string) {
const stats = await tracking.getCampaignStats(campaignId);
if (!stats.success) {
throw new Error(stats.error);
}
return {
campaign: stats.data.campaignId,
sent: stats.data.totalSent,
delivered: stats.data.totalDelivered,
bounced: stats.data.totalBounced,
opened: stats.data.totalOpened,
clicked: stats.data.totalClicked,
metrics: {
deliveryRate: stats.data.deliveryRate + '%',
openRate: stats.data.openRate + '%',
clickRate: stats.data.clickRate + '%'
}
};
}Database Schema
The package uses an in-memory database for development. In production, implement the Database class with PostgreSQL or similar.
Tables:
email_sends- Send records with tracking IDsemail_events- Timeline events for each emailrecipient_history- Aggregate statistics per recipient
See plan file for complete schema and indexes.
Testing
# Run tests in watch mode
npm test
# Run tests once
npm run test:run
# Run with coverage
npm run test:coverageTest Coverage: 85%+ (service package requirement)
License
Copyright (c) 2025 Bernier LLC
This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
See Also
- @bernierllc/email-webhook-events - Normalized webhook events (dependency)
- @bernierllc/email-sender - Email sending abstraction
- @bernierllc/email-service - Complete email orchestration (uses this package)
- @bernierllc/email-suite - Complete email solution with tracking analytics
