@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.
- package/.eslintrc.cjs +24 -0
- package/README.md +478 -0
- package/__mocks__/neverhub-adapter.ts +29 -0
- package/__tests__/EmailTracking.test.ts +334 -0
- package/__tests__/campaign-stats.test.ts +224 -0
- package/__tests__/config.test.ts +108 -0
- package/__tests__/integration.test.ts +244 -0
- package/__tests__/recipient-history.test.ts +210 -0
- package/__tests__/status-calculator.test.ts +141 -0
- package/coverage/clover.xml +187 -0
- package/coverage/coverage-final.json +6 -0
- package/coverage/lcov-report/EmailTracking.ts.html +973 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/config.ts.html +163 -0
- package/coverage/lcov-report/database.ts.html +622 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +176 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/status-calculator.ts.html +214 -0
- package/coverage/lcov-report/types.ts.html +430 -0
- package/coverage/lcov.info +356 -0
- package/dist/EmailTracking.d.ts +22 -0
- package/dist/EmailTracking.d.ts.map +1 -0
- package/dist/EmailTracking.js +253 -0
- package/dist/EmailTracking.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -0
- package/dist/database.d.ts +22 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +156 -0
- package/dist/database.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/status-calculator.d.ts +4 -0
- package/dist/status-calculator.d.ts.map +1 -0
- package/dist/status-calculator.js +40 -0
- package/dist/status-calculator.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/jest.config.cjs +32 -0
- package/package.json +54 -0
- package/src/EmailTracking.ts +296 -0
- package/src/config.ts +26 -0
- package/src/database.ts +179 -0
- package/src/index.ts +12 -0
- package/src/status-calculator.ts +43 -0
- package/src/types.ts +115 -0
- 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
|
+
}
|