@bernierllc/email-tracking 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.eslintrc.cjs +24 -0
  2. package/README.md +478 -0
  3. package/__mocks__/neverhub-adapter.ts +29 -0
  4. package/__tests__/EmailTracking.test.ts +334 -0
  5. package/__tests__/campaign-stats.test.ts +224 -0
  6. package/__tests__/config.test.ts +108 -0
  7. package/__tests__/integration.test.ts +244 -0
  8. package/__tests__/recipient-history.test.ts +210 -0
  9. package/__tests__/status-calculator.test.ts +141 -0
  10. package/coverage/clover.xml +187 -0
  11. package/coverage/coverage-final.json +6 -0
  12. package/coverage/lcov-report/EmailTracking.ts.html +973 -0
  13. package/coverage/lcov-report/base.css +224 -0
  14. package/coverage/lcov-report/block-navigation.js +87 -0
  15. package/coverage/lcov-report/config.ts.html +163 -0
  16. package/coverage/lcov-report/database.ts.html +622 -0
  17. package/coverage/lcov-report/favicon.png +0 -0
  18. package/coverage/lcov-report/index.html +176 -0
  19. package/coverage/lcov-report/prettify.css +1 -0
  20. package/coverage/lcov-report/prettify.js +2 -0
  21. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  22. package/coverage/lcov-report/sorter.js +210 -0
  23. package/coverage/lcov-report/status-calculator.ts.html +214 -0
  24. package/coverage/lcov-report/types.ts.html +430 -0
  25. package/coverage/lcov.info +356 -0
  26. package/dist/EmailTracking.d.ts +22 -0
  27. package/dist/EmailTracking.d.ts.map +1 -0
  28. package/dist/EmailTracking.js +253 -0
  29. package/dist/EmailTracking.js.map +1 -0
  30. package/dist/config.d.ts +3 -0
  31. package/dist/config.d.ts.map +1 -0
  32. package/dist/config.js +27 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/database.d.ts +22 -0
  35. package/dist/database.d.ts.map +1 -0
  36. package/dist/database.js +156 -0
  37. package/dist/database.js.map +1 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +33 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/status-calculator.d.ts +4 -0
  43. package/dist/status-calculator.d.ts.map +1 -0
  44. package/dist/status-calculator.js +40 -0
  45. package/dist/status-calculator.js.map +1 -0
  46. package/dist/types.d.ts +98 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +22 -0
  49. package/dist/types.js.map +1 -0
  50. package/jest.config.cjs +32 -0
  51. package/package.json +54 -0
  52. package/src/EmailTracking.ts +296 -0
  53. package/src/config.ts +26 -0
  54. package/src/database.ts +179 -0
  55. package/src/index.ts +12 -0
  56. package/src/status-calculator.ts +43 -0
  57. package/src/types.ts +115 -0
  58. package/tsconfig.json +31 -0
package/.eslintrc.cjs ADDED
@@ -0,0 +1,24 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ module.exports = {
10
+ parser: '@typescript-eslint/parser',
11
+ extends: [
12
+ 'eslint:recommended',
13
+ 'plugin:@typescript-eslint/recommended'
14
+ ],
15
+ parserOptions: {
16
+ ecmaVersion: 2020,
17
+ sourceType: 'module'
18
+ },
19
+ rules: {
20
+ '@typescript-eslint/no-explicit-any': 'warn',
21
+ '@typescript-eslint/explicit-function-return-type': 'off',
22
+ '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }]
23
+ }
24
+ };
package/README.md ADDED
@@ -0,0 +1,478 @@
1
+ # @bernierllc/email-tracking
2
+
3
+ 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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bernierllc/email-tracking
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - **Send Tracking** - Record all email sends with tracking IDs
14
+ - **Event Linking** - Link webhook events to sent emails by message ID
15
+ - **Status Management** - Track delivery status with priority-based updates
16
+ - **Timeline Queries** - Get complete event timeline for each email
17
+ - **Recipient History** - Aggregate statistics per email address
18
+ - **Campaign Analytics** - Calculate metrics for bulk email campaigns
19
+ - **NeverHub Integration** - Event bus and service discovery support
20
+ - **Reputation Scoring** - Track recipient engagement and reputation
21
+
22
+ ## Usage
23
+
24
+ ### Basic Setup
25
+
26
+ ```typescript
27
+ import { EmailTracking } from '@bernierllc/email-tracking';
28
+
29
+ const tracking = new EmailTracking({
30
+ enableOpenTracking: true,
31
+ enableClickTracking: true,
32
+ eventRetentionDays: 90,
33
+ aggregateStats: true
34
+ });
35
+
36
+ await tracking.initialize();
37
+ ```
38
+
39
+ ### Record Email Send
40
+
41
+ ```typescript
42
+ const result = await tracking.recordEmailSent({
43
+ messageId: 'msg_abc123',
44
+ provider: 'sendgrid',
45
+ from: 'sender@example.com',
46
+ to: ['recipient@example.com'],
47
+ subject: 'Welcome to our service',
48
+ campaignId: 'welcome_campaign',
49
+ metadata: { source: 'signup_flow' }
50
+ });
51
+
52
+ console.log('Tracking ID:', result.data?.id);
53
+ console.log('Status:', result.data?.currentStatus); // 'sent'
54
+ ```
55
+
56
+ ### Process Webhook Events
57
+
58
+ ```typescript
59
+ // Typically called automatically via NeverHub subscription
60
+ const result = await tracking.processWebhookEvent({
61
+ messageId: 'msg_abc123',
62
+ eventType: 'delivered',
63
+ timestamp: new Date().toISOString(),
64
+ metadata: { smtpResponse: '250 OK' }
65
+ });
66
+
67
+ // Status automatically updated to 'delivered'
68
+ ```
69
+
70
+ ### Get Email Timeline
71
+
72
+ ```typescript
73
+ const timeline = await tracking.getTimeline('track_xyz789');
74
+
75
+ if (timeline.success) {
76
+ console.log('Current Status:', timeline.data.currentStatus);
77
+ console.log('Events:');
78
+ timeline.data.events.forEach(event => {
79
+ console.log(` ${event.eventType} at ${event.timestamp}`);
80
+ });
81
+ }
82
+
83
+ // Output:
84
+ // Current Status: opened
85
+ // Events:
86
+ // sent at 2025-01-01T10:00:00Z
87
+ // delivered at 2025-01-01T10:01:00Z
88
+ // opened at 2025-01-01T10:15:00Z
89
+ ```
90
+
91
+ ### Get Recipient History
92
+
93
+ ```typescript
94
+ const history = await tracking.getRecipientHistory('user@example.com');
95
+
96
+ if (history.success) {
97
+ console.log('Total Sent:', history.data.totalSent);
98
+ console.log('Total Delivered:', history.data.totalDelivered);
99
+ console.log('Total Opened:', history.data.totalOpened);
100
+ console.log('Total Clicked:', history.data.totalClicked);
101
+ console.log('Reputation Score:', history.data.reputationScore);
102
+ }
103
+ ```
104
+
105
+ ### Get Campaign Statistics
106
+
107
+ ```typescript
108
+ const stats = await tracking.getCampaignStats('welcome_campaign');
109
+
110
+ if (stats.success) {
111
+ console.log('Campaign:', stats.data.campaignId);
112
+ console.log('Sent:', stats.data.totalSent);
113
+ console.log('Delivery Rate:', stats.data.deliveryRate + '%');
114
+ console.log('Open Rate:', stats.data.openRate + '%');
115
+ console.log('Click Rate:', stats.data.clickRate + '%');
116
+ }
117
+ ```
118
+
119
+ ## API Reference
120
+
121
+ ### EmailTracking
122
+
123
+ Main tracking service class.
124
+
125
+ #### Constructor
126
+
127
+ ```typescript
128
+ constructor(config?: TrackingConfig)
129
+ ```
130
+
131
+ **TrackingConfig Options:**
132
+ - `enableOpenTracking` (boolean) - Enable open tracking (default: true)
133
+ - `enableClickTracking` (boolean) - Enable click tracking (default: true)
134
+ - `eventRetentionDays` (number) - How long to keep events (default: 90)
135
+ - `aggregateStats` (boolean) - Enable recipient history tracking (default: true)
136
+
137
+ #### Methods
138
+
139
+ ##### `initialize(): Promise<void>`
140
+
141
+ Initialize the tracking service, set up NeverHub integration, and prepare database.
142
+
143
+ ##### `recordEmailSent(data: RecordEmailSentData): Promise<EmailTrackingResult<EmailSendRecord>>`
144
+
145
+ Record an email send event with tracking metadata.
146
+
147
+ **Parameters:**
148
+ - `messageId` (string) - Email provider message ID
149
+ - `provider` (string) - Email provider (sendgrid, mailgun, ses, postmark)
150
+ - `from` (string) - Sender email address
151
+ - `to` (string[]) - Recipient email addresses
152
+ - `cc?` (string[]) - CC recipients (optional)
153
+ - `bcc?` (string[]) - BCC recipients (optional)
154
+ - `subject` (string) - Email subject
155
+ - `campaignId?` (string) - Campaign identifier (optional)
156
+ - `metadata?` (Record<string, unknown>) - Custom tracking metadata (optional)
157
+
158
+ **Returns:** `EmailTrackingResult<EmailSendRecord>` with tracking ID and status
159
+
160
+ ##### `processWebhookEvent(event: WebhookEventData): Promise<EmailTrackingResult>`
161
+
162
+ Process a normalized webhook event from email-webhook-events package.
163
+
164
+ **Parameters:**
165
+ - `messageId` (string) - Email provider message ID
166
+ - `eventType` (string) - Event type (delivered, bounce, open, click, spamreport, unsubscribe)
167
+ - `timestamp` (string | Date) - Event timestamp
168
+ - `metadata?` (Record<string, unknown>) - Event metadata (optional)
169
+
170
+ **Returns:** `EmailTrackingResult` indicating success/failure
171
+
172
+ ##### `getTimeline(trackingId: string): Promise<EmailTrackingResult<EmailTimeline>>`
173
+
174
+ Get complete event timeline for an email.
175
+
176
+ **Returns:** `EmailTimeline` with send record, all events, current status, and last event timestamp
177
+
178
+ ##### `getRecipientHistory(emailAddress: string): Promise<EmailTrackingResult<RecipientHistory>>`
179
+
180
+ Get aggregate statistics for a recipient email address.
181
+
182
+ **Returns:** `RecipientHistory` with totals, last event timestamps, and reputation score
183
+
184
+ ##### `getCampaignStats(campaignId: string): Promise<EmailTrackingResult<CampaignStatistics>>`
185
+
186
+ Get aggregate statistics for a campaign.
187
+
188
+ **Returns:** `CampaignStatistics` with totals, rates, and percentages
189
+
190
+ ### Types
191
+
192
+ #### EmailDeliveryStatus
193
+
194
+ ```typescript
195
+ enum EmailDeliveryStatus {
196
+ SENT = 'sent',
197
+ DELIVERED = 'delivered',
198
+ BOUNCED = 'bounced',
199
+ OPENED = 'opened',
200
+ CLICKED = 'clicked',
201
+ COMPLAINED = 'complained',
202
+ UNSUBSCRIBED = 'unsubscribed',
203
+ FAILED = 'failed'
204
+ }
205
+ ```
206
+
207
+ **Status Priority** (highest to lowest):
208
+ 1. COMPLAINED (spam report)
209
+ 2. UNSUBSCRIBED
210
+ 3. BOUNCED
211
+ 4. FAILED
212
+ 5. CLICKED
213
+ 6. OPENED
214
+ 7. DELIVERED
215
+ 8. SENT
216
+
217
+ Status can only move UP the priority chain, never down.
218
+
219
+ #### EmailSendRecord
220
+
221
+ ```typescript
222
+ interface EmailSendRecord {
223
+ id: string;
224
+ messageId: string;
225
+ provider: EmailProvider;
226
+ from: string;
227
+ to: string[];
228
+ cc?: string[];
229
+ bcc?: string[];
230
+ subject: string;
231
+ campaignId?: string;
232
+ metadata?: Record<string, unknown>;
233
+ sentAt: Date;
234
+ currentStatus: EmailDeliveryStatus;
235
+ updatedAt: Date;
236
+ }
237
+ ```
238
+
239
+ #### EmailTimeline
240
+
241
+ ```typescript
242
+ interface EmailTimeline {
243
+ sendRecord: EmailSendRecord;
244
+ events: EmailEvent[];
245
+ currentStatus: EmailDeliveryStatus;
246
+ lastEventAt: Date;
247
+ }
248
+ ```
249
+
250
+ #### RecipientHistory
251
+
252
+ ```typescript
253
+ interface RecipientHistory {
254
+ emailAddress: string;
255
+ totalSent: number;
256
+ totalDelivered: number;
257
+ totalBounced: number;
258
+ totalOpened: number;
259
+ totalClicked: number;
260
+ totalComplained: number;
261
+ totalUnsubscribed: number;
262
+ lastSentAt?: Date;
263
+ lastDeliveredAt?: Date;
264
+ lastBouncedAt?: Date;
265
+ lastOpenedAt?: Date;
266
+ lastClickedAt?: Date;
267
+ reputationScore: number; // 0-100
268
+ }
269
+ ```
270
+
271
+ **Reputation Scoring:**
272
+ - Default: 100
273
+ - Bounce: -10
274
+ - Spam complaint: -25
275
+ - Unsubscribe: -5
276
+
277
+ #### CampaignStatistics
278
+
279
+ ```typescript
280
+ interface CampaignStatistics {
281
+ campaignId: string;
282
+ totalSent: number;
283
+ totalDelivered: number;
284
+ totalBounced: number;
285
+ totalOpened: number;
286
+ totalClicked: number;
287
+ totalComplained: number;
288
+ totalUnsubscribed: number;
289
+ deliveryRate: number; // percentage
290
+ openRate: number; // percentage
291
+ clickRate: number; // percentage
292
+ }
293
+ ```
294
+
295
+ ## Configuration
296
+
297
+ ### Environment Variables
298
+
299
+ ```bash
300
+ # Enable/disable tracking features
301
+ EMAIL_TRACKING_ENABLE_OPEN_TRACKING=true
302
+ EMAIL_TRACKING_ENABLE_CLICK_TRACKING=true
303
+
304
+ # Event retention policy
305
+ EMAIL_TRACKING_EVENT_RETENTION_DAYS=90
306
+
307
+ # Recipient history tracking
308
+ EMAIL_TRACKING_AGGREGATE_STATS=true
309
+ ```
310
+
311
+ ### Configuration Precedence
312
+
313
+ 1. Constructor options (highest priority)
314
+ 2. Environment variables
315
+ 3. Default values (lowest priority)
316
+
317
+ ## Integration Status
318
+
319
+ - **Logger**: integrated - Structured logging for tracking operations
320
+ - **Docs-Suite**: ready - Complete TypeDoc API documentation
321
+ - **NeverHub**: required - Event subscriptions and service discovery
322
+
323
+ ### NeverHub Integration
324
+
325
+ The service automatically detects and integrates with NeverHub when available:
326
+
327
+ **Events Published:**
328
+ - `email.status_changed` - When email delivery status changes
329
+
330
+ **Events Subscribed:**
331
+ - `email.sent` - From email-sender or email-service
332
+ - `email.webhook.*` - From email-webhook-events
333
+
334
+ **Capabilities:**
335
+ - Type: `email`
336
+ - Name: `delivery-tracking`
337
+ - Features: send-tracking, event-linking, timeline, recipient-history
338
+
339
+ ### Graceful Degradation
340
+
341
+ The service works without NeverHub but with reduced functionality:
342
+ - No automatic event subscription
343
+ - No status change events published
344
+ - Manual `recordEmailSent()` and `processWebhookEvent()` calls required
345
+
346
+ ## Dependencies
347
+
348
+ - [@bernierllc/email-webhook-events](https://www.npmjs.com/package/@bernierllc/email-webhook-events) - Normalized webhook events
349
+ - [@bernierllc/logger](https://www.npmjs.com/package/@bernierllc/logger) - Structured logging
350
+ - [@bernierllc/neverhub-adapter](https://www.npmjs.com/package/@bernierllc/neverhub-adapter) - Service discovery and event bus
351
+
352
+ ## Use Cases
353
+
354
+ ### Email Service Integration
355
+
356
+ ```typescript
357
+ import { EmailTracking } from '@bernierllc/email-tracking';
358
+ import { EmailSender } from '@bernierllc/email-sender';
359
+
360
+ const tracking = new EmailTracking();
361
+ const sender = new EmailSender();
362
+
363
+ await tracking.initialize();
364
+ await sender.initialize();
365
+
366
+ // Send email
367
+ const sendResult = await sender.send({
368
+ to: 'user@example.com',
369
+ subject: 'Welcome',
370
+ html: '<h1>Welcome!</h1>'
371
+ });
372
+
373
+ // Record tracking
374
+ if (sendResult.success) {
375
+ await tracking.recordEmailSent({
376
+ messageId: sendResult.data.messageId,
377
+ provider: 'sendgrid',
378
+ from: 'noreply@example.com',
379
+ to: ['user@example.com'],
380
+ subject: 'Welcome',
381
+ campaignId: 'onboarding'
382
+ });
383
+ }
384
+ ```
385
+
386
+ ### Webhook Processing
387
+
388
+ ```typescript
389
+ import express from 'express';
390
+ import { EmailTracking } from '@bernierllc/email-tracking';
391
+ import { normalizeWebhookEvent } from '@bernierllc/email-webhook-events';
392
+
393
+ const app = express();
394
+ const tracking = new EmailTracking();
395
+
396
+ await tracking.initialize();
397
+
398
+ app.post('/webhooks/sendgrid', async (req, res) => {
399
+ const events = req.body;
400
+
401
+ for (const event of events) {
402
+ // Normalize webhook event
403
+ const normalized = normalizeWebhookEvent('sendgrid', event);
404
+
405
+ // Process with tracking
406
+ await tracking.processWebhookEvent(normalized);
407
+ }
408
+
409
+ res.sendStatus(200);
410
+ });
411
+ ```
412
+
413
+ ### Campaign Analytics Dashboard
414
+
415
+ ```typescript
416
+ async function getCampaignDashboard(campaignId: string) {
417
+ const stats = await tracking.getCampaignStats(campaignId);
418
+
419
+ if (!stats.success) {
420
+ throw new Error(stats.error);
421
+ }
422
+
423
+ return {
424
+ campaign: stats.data.campaignId,
425
+ sent: stats.data.totalSent,
426
+ delivered: stats.data.totalDelivered,
427
+ bounced: stats.data.totalBounced,
428
+ opened: stats.data.totalOpened,
429
+ clicked: stats.data.totalClicked,
430
+ metrics: {
431
+ deliveryRate: stats.data.deliveryRate + '%',
432
+ openRate: stats.data.openRate + '%',
433
+ clickRate: stats.data.clickRate + '%'
434
+ }
435
+ };
436
+ }
437
+ ```
438
+
439
+ ## Database Schema
440
+
441
+ The package uses an in-memory database for development. In production, implement the `Database` class with PostgreSQL or similar.
442
+
443
+ **Tables:**
444
+ - `email_sends` - Send records with tracking IDs
445
+ - `email_events` - Timeline events for each email
446
+ - `recipient_history` - Aggregate statistics per recipient
447
+
448
+ See plan file for complete schema and indexes.
449
+
450
+ ## Testing
451
+
452
+ ```bash
453
+ # Run tests in watch mode
454
+ npm test
455
+
456
+ # Run tests once
457
+ npm run test:run
458
+
459
+ # Run with coverage
460
+ npm run test:coverage
461
+ ```
462
+
463
+ **Test Coverage:** 85%+ (service package requirement)
464
+
465
+ ## License
466
+
467
+ Copyright (c) 2025 Bernier LLC
468
+
469
+ This file is licensed to the client under a limited-use license.
470
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
471
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
472
+
473
+ ## See Also
474
+
475
+ - [@bernierllc/email-webhook-events](https://www.npmjs.com/package/@bernierllc/email-webhook-events) - Normalized webhook events (dependency)
476
+ - [@bernierllc/email-sender](https://www.npmjs.com/package/@bernierllc/email-sender) - Email sending abstraction
477
+ - [@bernierllc/email-service](../email-service) - Complete email orchestration (uses this package)
478
+ - [@bernierllc/email-suite](../email-suite) - Complete email solution with tracking analytics
@@ -0,0 +1,29 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ export class NeverHubAdapter {
10
+ static async detect(): Promise<boolean> {
11
+ return false; // NeverHub not available in tests
12
+ }
13
+
14
+ async register(): Promise<void> {
15
+ // Mock implementation
16
+ }
17
+
18
+ async subscribe(): Promise<void> {
19
+ // Mock implementation
20
+ }
21
+
22
+ async publishEvent(): Promise<void> {
23
+ // Mock implementation
24
+ }
25
+
26
+ async getService(): Promise<Console> {
27
+ return console;
28
+ }
29
+ }