@astralibx/email-analytics 2.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/README.md +133 -0
- package/dist/index.d.mts +267 -0
- package/dist/index.d.ts +267 -0
- package/dist/index.js +946 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +927 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @astralibx/email-analytics
|
|
2
|
+
|
|
3
|
+
Event-level email tracking with automatic daily aggregation and pre-built query APIs. Record individual send/deliver/bounce/open/click events, aggregate them into daily stats by account, rule, or template, and query time-series data through REST endpoints or the programmatic API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @astralibx/email-analytics
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer dependencies** (install separately):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install mongoose express
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import mongoose from 'mongoose';
|
|
21
|
+
import express from 'express';
|
|
22
|
+
import { createEmailAnalytics } from '@astralibx/email-analytics';
|
|
23
|
+
|
|
24
|
+
const conn = mongoose.createConnection('mongodb://localhost:27017/myapp');
|
|
25
|
+
|
|
26
|
+
const analytics = createEmailAnalytics({
|
|
27
|
+
db: { connection: conn, collectionPrefix: 'myapp_' },
|
|
28
|
+
options: {
|
|
29
|
+
eventTTLDays: 90,
|
|
30
|
+
timezone: 'Asia/Kolkata',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Mount REST routes
|
|
35
|
+
const app = express();
|
|
36
|
+
app.use('/api/analytics', analytics.routes);
|
|
37
|
+
|
|
38
|
+
// Record an event programmatically
|
|
39
|
+
await analytics.events.record({
|
|
40
|
+
type: 'sent',
|
|
41
|
+
accountId: '507f1f77bcf86cd799439011',
|
|
42
|
+
recipientEmail: 'user@example.com',
|
|
43
|
+
ruleId: '507f1f77bcf86cd799439012',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Run daily aggregation
|
|
47
|
+
await analytics.aggregator.aggregateDaily();
|
|
48
|
+
|
|
49
|
+
// Query overview stats
|
|
50
|
+
const overview = await analytics.query.getOverview(
|
|
51
|
+
new Date('2025-01-01'),
|
|
52
|
+
new Date('2025-01-31'),
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
- **Event Recording** -- single and batch insert with automatic timestamps ([docs](docs/event-recording.md))
|
|
59
|
+
- **Daily Aggregation** -- rolls up raw events into per-day stats by account, rule, template, and overall ([docs](docs/aggregation.md))
|
|
60
|
+
- **Range Aggregation** -- backfill or re-aggregate any date range ([docs](docs/aggregation.md))
|
|
61
|
+
- **Time-Series Queries** -- overview, timeline (daily/weekly/monthly), per-account, per-rule, per-template ([docs](docs/querying.md))
|
|
62
|
+
- **REST API** -- six endpoints with date-range query params ([docs](docs/api-routes.md))
|
|
63
|
+
- **TTL Cleanup** -- MongoDB TTL index auto-expires old events; manual purge also available ([docs](docs/event-recording.md#ttl-and-cleanup))
|
|
64
|
+
- **Programmatic API** -- use services directly without HTTP ([docs](docs/programmatic-api.md))
|
|
65
|
+
- **Error Handling** -- typed error classes for validation, date range, and aggregation failures ([docs](docs/error-handling.md))
|
|
66
|
+
|
|
67
|
+
## Integration with Other Packages
|
|
68
|
+
|
|
69
|
+
Wire hooks from `@astralibx/email-account-manager` and `@astralibx/email-rule-engine` to automatically record analytics events. See the [integration guide](docs/integration.md) for full examples.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { createEmailAccountManager } from '@astralibx/email-account-manager';
|
|
73
|
+
import { createEmailRuleEngine } from '@astralibx/email-rule-engine';
|
|
74
|
+
import { createEmailAnalytics } from '@astralibx/email-analytics';
|
|
75
|
+
|
|
76
|
+
const analytics = createEmailAnalytics({ db: { connection: conn } });
|
|
77
|
+
const accountManager = createEmailAccountManager({ db: { connection: conn } });
|
|
78
|
+
const ruleEngine = createEmailRuleEngine({ db: { connection: conn } });
|
|
79
|
+
|
|
80
|
+
// Record events from rule engine execution
|
|
81
|
+
ruleEngine.on('emailSent', async (detail) => {
|
|
82
|
+
await analytics.events.record({
|
|
83
|
+
type: 'sent',
|
|
84
|
+
accountId: detail.accountId,
|
|
85
|
+
ruleId: detail.ruleId,
|
|
86
|
+
templateId: detail.templateId,
|
|
87
|
+
recipientEmail: detail.recipientEmail,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
ruleEngine.on('emailFailed', async (detail) => {
|
|
92
|
+
await analytics.events.record({
|
|
93
|
+
type: 'failed',
|
|
94
|
+
accountId: detail.accountId,
|
|
95
|
+
ruleId: detail.ruleId,
|
|
96
|
+
recipientEmail: detail.recipientEmail,
|
|
97
|
+
metadata: { error: detail.error },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Documentation
|
|
103
|
+
|
|
104
|
+
| Guide | Description |
|
|
105
|
+
|-------|-------------|
|
|
106
|
+
| [Configuration](docs/configuration.md) | Full config reference |
|
|
107
|
+
| [Event Recording](docs/event-recording.md) | Recording events, batch insert, TTL |
|
|
108
|
+
| [Aggregation](docs/aggregation.md) | Daily aggregation, range backfill |
|
|
109
|
+
| [Querying](docs/querying.md) | Overview, timeline, grouped stats |
|
|
110
|
+
| [API Routes](docs/api-routes.md) | REST endpoints reference |
|
|
111
|
+
| [Programmatic API](docs/programmatic-api.md) | Using services directly |
|
|
112
|
+
| [Integration](docs/integration.md) | Wiring with account-manager and rule-engine |
|
|
113
|
+
| [Error Handling](docs/error-handling.md) | Error classes and codes |
|
|
114
|
+
|
|
115
|
+
## Exported Types
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Core types
|
|
119
|
+
export type { EmailAnalyticsConfig } from './types/config.types';
|
|
120
|
+
export type { EmailEvent, CreateEventInput } from './types/event.types';
|
|
121
|
+
export type { DailyStats, AccountStats, RuleStats, TemplateStats, OverviewStats, TimelineEntry } from './types/stats.types';
|
|
122
|
+
|
|
123
|
+
// Constants
|
|
124
|
+
export { EVENT_TYPE, AGGREGATION_INTERVAL, STATS_GROUP_BY } from './constants';
|
|
125
|
+
export type { EventType, AggregationInterval, StatsGroupBy } from './constants';
|
|
126
|
+
|
|
127
|
+
// Errors
|
|
128
|
+
export { AlxAnalyticsError, ConfigValidationError, InvalidDateRangeError, AggregationError } from './errors';
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Request, Response, Router } from 'express';
|
|
2
|
+
import * as mongoose from 'mongoose';
|
|
3
|
+
import { Connection, Model, Types, HydratedDocument, Schema } from 'mongoose';
|
|
4
|
+
import { LogAdapter, AlxError } from '@astralibx/core';
|
|
5
|
+
export { ConfigValidationError } from '@astralibx/core';
|
|
6
|
+
|
|
7
|
+
declare const EVENT_TYPE: {
|
|
8
|
+
readonly Sent: "sent";
|
|
9
|
+
readonly Delivered: "delivered";
|
|
10
|
+
readonly Bounced: "bounced";
|
|
11
|
+
readonly Complained: "complained";
|
|
12
|
+
readonly Opened: "opened";
|
|
13
|
+
readonly Clicked: "clicked";
|
|
14
|
+
readonly Unsubscribed: "unsubscribed";
|
|
15
|
+
readonly Failed: "failed";
|
|
16
|
+
};
|
|
17
|
+
type EventType = (typeof EVENT_TYPE)[keyof typeof EVENT_TYPE];
|
|
18
|
+
declare const AGGREGATION_INTERVAL: {
|
|
19
|
+
readonly Daily: "daily";
|
|
20
|
+
readonly Weekly: "weekly";
|
|
21
|
+
readonly Monthly: "monthly";
|
|
22
|
+
};
|
|
23
|
+
type AggregationInterval = (typeof AGGREGATION_INTERVAL)[keyof typeof AGGREGATION_INTERVAL];
|
|
24
|
+
declare const STATS_GROUP_BY: {
|
|
25
|
+
readonly Account: "account";
|
|
26
|
+
readonly Rule: "rule";
|
|
27
|
+
readonly Template: "template";
|
|
28
|
+
};
|
|
29
|
+
type StatsGroupBy = (typeof STATS_GROUP_BY)[keyof typeof STATS_GROUP_BY];
|
|
30
|
+
|
|
31
|
+
interface EmailAnalyticsConfig {
|
|
32
|
+
db: {
|
|
33
|
+
connection: Connection;
|
|
34
|
+
collectionPrefix?: string;
|
|
35
|
+
};
|
|
36
|
+
logger?: LogAdapter;
|
|
37
|
+
options?: {
|
|
38
|
+
eventTTLDays?: number;
|
|
39
|
+
timezone?: string;
|
|
40
|
+
aggregationSchedule?: AggregationInterval[];
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface IEmailEvent {
|
|
45
|
+
type: EventType;
|
|
46
|
+
accountId: Types.ObjectId;
|
|
47
|
+
ruleId?: Types.ObjectId;
|
|
48
|
+
templateId?: Types.ObjectId;
|
|
49
|
+
recipientEmail: string;
|
|
50
|
+
identifierId?: Types.ObjectId;
|
|
51
|
+
metadata?: Record<string, unknown>;
|
|
52
|
+
timestamp: Date;
|
|
53
|
+
createdAt: Date;
|
|
54
|
+
updatedAt: Date;
|
|
55
|
+
}
|
|
56
|
+
type EmailEventDocument = HydratedDocument<IEmailEvent>;
|
|
57
|
+
interface EmailEventStatics {
|
|
58
|
+
record(event: {
|
|
59
|
+
type: EventType;
|
|
60
|
+
accountId: string;
|
|
61
|
+
ruleId?: string;
|
|
62
|
+
templateId?: string;
|
|
63
|
+
recipientEmail: string;
|
|
64
|
+
identifierId?: string;
|
|
65
|
+
metadata?: Record<string, unknown>;
|
|
66
|
+
timestamp?: Date;
|
|
67
|
+
}): Promise<EmailEventDocument>;
|
|
68
|
+
findByDateRange(start: Date, end: Date, filters?: {
|
|
69
|
+
type?: EventType;
|
|
70
|
+
accountId?: string;
|
|
71
|
+
ruleId?: string;
|
|
72
|
+
templateId?: string;
|
|
73
|
+
}): Promise<EmailEventDocument[]>;
|
|
74
|
+
}
|
|
75
|
+
type EmailEventModel = Model<IEmailEvent> & EmailEventStatics;
|
|
76
|
+
interface CreateEmailEventSchemaOptions {
|
|
77
|
+
collectionName?: string;
|
|
78
|
+
eventTTLDays?: number;
|
|
79
|
+
}
|
|
80
|
+
declare function createEmailEventSchema(options?: CreateEmailEventSchemaOptions): Schema<IEmailEvent, Model<IEmailEvent, any, any, any, mongoose.Document<unknown, any, IEmailEvent, any, {}> & IEmailEvent & {
|
|
81
|
+
_id: Types.ObjectId;
|
|
82
|
+
} & {
|
|
83
|
+
__v: number;
|
|
84
|
+
}, any>, {}, {}, {}, {}, mongoose.DefaultSchemaOptions, IEmailEvent, mongoose.Document<unknown, {}, mongoose.FlatRecord<IEmailEvent>, {}, mongoose.DefaultSchemaOptions> & mongoose.FlatRecord<IEmailEvent> & {
|
|
85
|
+
_id: Types.ObjectId;
|
|
86
|
+
} & {
|
|
87
|
+
__v: number;
|
|
88
|
+
}>;
|
|
89
|
+
|
|
90
|
+
interface IAnalyticsStats {
|
|
91
|
+
date: string;
|
|
92
|
+
interval: AggregationInterval;
|
|
93
|
+
accountId: Types.ObjectId | null;
|
|
94
|
+
ruleId: Types.ObjectId | null;
|
|
95
|
+
templateId: Types.ObjectId | null;
|
|
96
|
+
sent: number;
|
|
97
|
+
delivered: number;
|
|
98
|
+
bounced: number;
|
|
99
|
+
complained: number;
|
|
100
|
+
opened: number;
|
|
101
|
+
clicked: number;
|
|
102
|
+
unsubscribed: number;
|
|
103
|
+
failed: number;
|
|
104
|
+
createdAt: Date;
|
|
105
|
+
updatedAt: Date;
|
|
106
|
+
}
|
|
107
|
+
type AnalyticsStatsDocument = HydratedDocument<IAnalyticsStats>;
|
|
108
|
+
interface AnalyticsStatsStatics {
|
|
109
|
+
upsertStats(date: string, interval: AggregationInterval, dimensions: {
|
|
110
|
+
accountId?: string;
|
|
111
|
+
ruleId?: string;
|
|
112
|
+
templateId?: string;
|
|
113
|
+
}, increments: Partial<Record<'sent' | 'delivered' | 'bounced' | 'complained' | 'opened' | 'clicked' | 'unsubscribed' | 'failed', number>>): Promise<AnalyticsStatsDocument>;
|
|
114
|
+
}
|
|
115
|
+
type AnalyticsStatsModel = Model<IAnalyticsStats> & AnalyticsStatsStatics;
|
|
116
|
+
interface CreateAnalyticsStatsSchemaOptions {
|
|
117
|
+
collectionName?: string;
|
|
118
|
+
}
|
|
119
|
+
declare function createAnalyticsStatsSchema(options?: CreateAnalyticsStatsSchemaOptions): Schema<IAnalyticsStats, Model<IAnalyticsStats, any, any, any, mongoose.Document<unknown, any, IAnalyticsStats, any, {}> & IAnalyticsStats & {
|
|
120
|
+
_id: Types.ObjectId;
|
|
121
|
+
} & {
|
|
122
|
+
__v: number;
|
|
123
|
+
}, any>, {}, {}, {}, {}, mongoose.DefaultSchemaOptions, IAnalyticsStats, mongoose.Document<unknown, {}, mongoose.FlatRecord<IAnalyticsStats>, {}, mongoose.DefaultSchemaOptions> & mongoose.FlatRecord<IAnalyticsStats> & {
|
|
124
|
+
_id: Types.ObjectId;
|
|
125
|
+
} & {
|
|
126
|
+
__v: number;
|
|
127
|
+
}>;
|
|
128
|
+
|
|
129
|
+
type EmailEvent = IEmailEvent;
|
|
130
|
+
interface CreateEventInput {
|
|
131
|
+
type: EventType;
|
|
132
|
+
accountId: string;
|
|
133
|
+
ruleId?: string;
|
|
134
|
+
templateId?: string;
|
|
135
|
+
recipientEmail: string;
|
|
136
|
+
identifierId?: string;
|
|
137
|
+
metadata?: Record<string, unknown>;
|
|
138
|
+
timestamp?: Date;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface EventQueryFilters {
|
|
142
|
+
dateFrom: Date;
|
|
143
|
+
dateTo: Date;
|
|
144
|
+
type?: string;
|
|
145
|
+
accountId?: string;
|
|
146
|
+
ruleId?: string;
|
|
147
|
+
recipientEmail?: string;
|
|
148
|
+
}
|
|
149
|
+
interface PaginatedEvents {
|
|
150
|
+
events: EmailEvent[];
|
|
151
|
+
total: number;
|
|
152
|
+
}
|
|
153
|
+
declare class EventRecorderService {
|
|
154
|
+
private EmailEvent;
|
|
155
|
+
private logger;
|
|
156
|
+
constructor(EmailEvent: EmailEventModel, logger: LogAdapter);
|
|
157
|
+
record(event: CreateEventInput): Promise<void>;
|
|
158
|
+
recordBatch(events: CreateEventInput[]): Promise<void>;
|
|
159
|
+
getEvents(filters: EventQueryFilters, page?: number, limit?: number): Promise<PaginatedEvents>;
|
|
160
|
+
purgeOldEvents(olderThanDays: number): Promise<number>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
declare class AggregatorService {
|
|
164
|
+
private EmailEvent;
|
|
165
|
+
private AnalyticsStats;
|
|
166
|
+
private timezone;
|
|
167
|
+
private logger;
|
|
168
|
+
constructor(EmailEvent: EmailEventModel, AnalyticsStats: AnalyticsStatsModel, timezone: string, logger: LogAdapter);
|
|
169
|
+
aggregateDaily(date?: Date): Promise<void>;
|
|
170
|
+
aggregateRange(from: Date, to: Date): Promise<void>;
|
|
171
|
+
private aggregateByAccount;
|
|
172
|
+
private aggregateByRule;
|
|
173
|
+
private aggregateByTemplate;
|
|
174
|
+
private aggregateOverall;
|
|
175
|
+
private getDayStart;
|
|
176
|
+
private getDayEnd;
|
|
177
|
+
private getTimezoneOffsetMs;
|
|
178
|
+
private toDateKey;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface BaseMetrics {
|
|
182
|
+
sent: number;
|
|
183
|
+
delivered: number;
|
|
184
|
+
bounced: number;
|
|
185
|
+
complained: number;
|
|
186
|
+
opened: number;
|
|
187
|
+
clicked: number;
|
|
188
|
+
unsubscribed: number;
|
|
189
|
+
failed: number;
|
|
190
|
+
}
|
|
191
|
+
interface DailyStats extends BaseMetrics {
|
|
192
|
+
date: string;
|
|
193
|
+
}
|
|
194
|
+
interface AccountStats extends BaseMetrics {
|
|
195
|
+
accountId: string;
|
|
196
|
+
}
|
|
197
|
+
interface RuleStats extends BaseMetrics {
|
|
198
|
+
ruleId: string;
|
|
199
|
+
}
|
|
200
|
+
interface TemplateStats extends BaseMetrics {
|
|
201
|
+
templateId: string;
|
|
202
|
+
}
|
|
203
|
+
interface OverviewStats extends BaseMetrics {
|
|
204
|
+
startDate: string;
|
|
205
|
+
endDate: string;
|
|
206
|
+
}
|
|
207
|
+
interface TimelineEntry extends BaseMetrics {
|
|
208
|
+
date: string;
|
|
209
|
+
interval: AggregationInterval;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
declare class QueryService {
|
|
213
|
+
private AnalyticsStats;
|
|
214
|
+
private logger;
|
|
215
|
+
constructor(AnalyticsStats: AnalyticsStatsModel, logger: LogAdapter);
|
|
216
|
+
getOverview(dateFrom: Date, dateTo: Date): Promise<OverviewStats>;
|
|
217
|
+
getTimeline(dateFrom: Date, dateTo: Date, interval?: AggregationInterval): Promise<TimelineEntry[]>;
|
|
218
|
+
getAccountStats(dateFrom: Date, dateTo: Date): Promise<AccountStats[]>;
|
|
219
|
+
getRuleStats(dateFrom: Date, dateTo: Date): Promise<RuleStats[]>;
|
|
220
|
+
getTemplateStats(dateFrom: Date, dateTo: Date): Promise<TemplateStats[]>;
|
|
221
|
+
private buildTimeGroupId;
|
|
222
|
+
private toDateKey;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
declare class AlxAnalyticsError extends AlxError {
|
|
226
|
+
readonly code: string;
|
|
227
|
+
constructor(message: string, code: string);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
declare class InvalidDateRangeError extends AlxAnalyticsError {
|
|
231
|
+
readonly startDate: string;
|
|
232
|
+
readonly endDate: string;
|
|
233
|
+
constructor(startDate: string, endDate: string);
|
|
234
|
+
}
|
|
235
|
+
declare class AggregationError extends AlxAnalyticsError {
|
|
236
|
+
readonly pipeline: string;
|
|
237
|
+
readonly originalError: Error;
|
|
238
|
+
constructor(pipeline: string, originalError: Error);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
declare function validateConfig(raw: unknown): void;
|
|
242
|
+
|
|
243
|
+
declare function createAnalyticsController(eventRecorder: EventRecorderService, aggregator: AggregatorService, queryService: QueryService): {
|
|
244
|
+
getOverview(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
245
|
+
getTimeline(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
246
|
+
getAccountStats(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
247
|
+
getRuleStats(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
248
|
+
getTemplateStats(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
249
|
+
triggerAggregation(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
type AnalyticsController = ReturnType<typeof createAnalyticsController>;
|
|
253
|
+
declare function createAnalyticsRoutes(controller: AnalyticsController): Router;
|
|
254
|
+
|
|
255
|
+
interface EmailAnalytics {
|
|
256
|
+
routes: Router;
|
|
257
|
+
events: EventRecorderService;
|
|
258
|
+
aggregator: AggregatorService;
|
|
259
|
+
query: QueryService;
|
|
260
|
+
models: {
|
|
261
|
+
EmailEvent: EmailEventModel;
|
|
262
|
+
AnalyticsStats: AnalyticsStatsModel;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
declare function createEmailAnalytics(config: EmailAnalyticsConfig): EmailAnalytics;
|
|
266
|
+
|
|
267
|
+
export { AGGREGATION_INTERVAL, type AccountStats, AggregationError, type AggregationInterval, AggregatorService, AlxAnalyticsError, type AnalyticsStatsDocument, type AnalyticsStatsModel, type AnalyticsStatsStatics, type BaseMetrics, type CreateAnalyticsStatsSchemaOptions, type CreateEmailEventSchemaOptions, type CreateEventInput, type DailyStats, EVENT_TYPE, type EmailAnalytics, type EmailAnalyticsConfig, type EmailEvent, type EmailEventDocument, type EmailEventModel, type EmailEventStatics, type EventQueryFilters, EventRecorderService, type EventType, type IAnalyticsStats, type IEmailEvent, InvalidDateRangeError, type OverviewStats, type PaginatedEvents, QueryService, type RuleStats, STATS_GROUP_BY, type StatsGroupBy, type TemplateStats, type TimelineEntry, createAnalyticsController, createAnalyticsRoutes, createAnalyticsStatsSchema, createEmailAnalytics, createEmailEventSchema, validateConfig };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Request, Response, Router } from 'express';
|
|
2
|
+
import * as mongoose from 'mongoose';
|
|
3
|
+
import { Connection, Model, Types, HydratedDocument, Schema } from 'mongoose';
|
|
4
|
+
import { LogAdapter, AlxError } from '@astralibx/core';
|
|
5
|
+
export { ConfigValidationError } from '@astralibx/core';
|
|
6
|
+
|
|
7
|
+
declare const EVENT_TYPE: {
|
|
8
|
+
readonly Sent: "sent";
|
|
9
|
+
readonly Delivered: "delivered";
|
|
10
|
+
readonly Bounced: "bounced";
|
|
11
|
+
readonly Complained: "complained";
|
|
12
|
+
readonly Opened: "opened";
|
|
13
|
+
readonly Clicked: "clicked";
|
|
14
|
+
readonly Unsubscribed: "unsubscribed";
|
|
15
|
+
readonly Failed: "failed";
|
|
16
|
+
};
|
|
17
|
+
type EventType = (typeof EVENT_TYPE)[keyof typeof EVENT_TYPE];
|
|
18
|
+
declare const AGGREGATION_INTERVAL: {
|
|
19
|
+
readonly Daily: "daily";
|
|
20
|
+
readonly Weekly: "weekly";
|
|
21
|
+
readonly Monthly: "monthly";
|
|
22
|
+
};
|
|
23
|
+
type AggregationInterval = (typeof AGGREGATION_INTERVAL)[keyof typeof AGGREGATION_INTERVAL];
|
|
24
|
+
declare const STATS_GROUP_BY: {
|
|
25
|
+
readonly Account: "account";
|
|
26
|
+
readonly Rule: "rule";
|
|
27
|
+
readonly Template: "template";
|
|
28
|
+
};
|
|
29
|
+
type StatsGroupBy = (typeof STATS_GROUP_BY)[keyof typeof STATS_GROUP_BY];
|
|
30
|
+
|
|
31
|
+
interface EmailAnalyticsConfig {
|
|
32
|
+
db: {
|
|
33
|
+
connection: Connection;
|
|
34
|
+
collectionPrefix?: string;
|
|
35
|
+
};
|
|
36
|
+
logger?: LogAdapter;
|
|
37
|
+
options?: {
|
|
38
|
+
eventTTLDays?: number;
|
|
39
|
+
timezone?: string;
|
|
40
|
+
aggregationSchedule?: AggregationInterval[];
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface IEmailEvent {
|
|
45
|
+
type: EventType;
|
|
46
|
+
accountId: Types.ObjectId;
|
|
47
|
+
ruleId?: Types.ObjectId;
|
|
48
|
+
templateId?: Types.ObjectId;
|
|
49
|
+
recipientEmail: string;
|
|
50
|
+
identifierId?: Types.ObjectId;
|
|
51
|
+
metadata?: Record<string, unknown>;
|
|
52
|
+
timestamp: Date;
|
|
53
|
+
createdAt: Date;
|
|
54
|
+
updatedAt: Date;
|
|
55
|
+
}
|
|
56
|
+
type EmailEventDocument = HydratedDocument<IEmailEvent>;
|
|
57
|
+
interface EmailEventStatics {
|
|
58
|
+
record(event: {
|
|
59
|
+
type: EventType;
|
|
60
|
+
accountId: string;
|
|
61
|
+
ruleId?: string;
|
|
62
|
+
templateId?: string;
|
|
63
|
+
recipientEmail: string;
|
|
64
|
+
identifierId?: string;
|
|
65
|
+
metadata?: Record<string, unknown>;
|
|
66
|
+
timestamp?: Date;
|
|
67
|
+
}): Promise<EmailEventDocument>;
|
|
68
|
+
findByDateRange(start: Date, end: Date, filters?: {
|
|
69
|
+
type?: EventType;
|
|
70
|
+
accountId?: string;
|
|
71
|
+
ruleId?: string;
|
|
72
|
+
templateId?: string;
|
|
73
|
+
}): Promise<EmailEventDocument[]>;
|
|
74
|
+
}
|
|
75
|
+
type EmailEventModel = Model<IEmailEvent> & EmailEventStatics;
|
|
76
|
+
interface CreateEmailEventSchemaOptions {
|
|
77
|
+
collectionName?: string;
|
|
78
|
+
eventTTLDays?: number;
|
|
79
|
+
}
|
|
80
|
+
declare function createEmailEventSchema(options?: CreateEmailEventSchemaOptions): Schema<IEmailEvent, Model<IEmailEvent, any, any, any, mongoose.Document<unknown, any, IEmailEvent, any, {}> & IEmailEvent & {
|
|
81
|
+
_id: Types.ObjectId;
|
|
82
|
+
} & {
|
|
83
|
+
__v: number;
|
|
84
|
+
}, any>, {}, {}, {}, {}, mongoose.DefaultSchemaOptions, IEmailEvent, mongoose.Document<unknown, {}, mongoose.FlatRecord<IEmailEvent>, {}, mongoose.DefaultSchemaOptions> & mongoose.FlatRecord<IEmailEvent> & {
|
|
85
|
+
_id: Types.ObjectId;
|
|
86
|
+
} & {
|
|
87
|
+
__v: number;
|
|
88
|
+
}>;
|
|
89
|
+
|
|
90
|
+
interface IAnalyticsStats {
|
|
91
|
+
date: string;
|
|
92
|
+
interval: AggregationInterval;
|
|
93
|
+
accountId: Types.ObjectId | null;
|
|
94
|
+
ruleId: Types.ObjectId | null;
|
|
95
|
+
templateId: Types.ObjectId | null;
|
|
96
|
+
sent: number;
|
|
97
|
+
delivered: number;
|
|
98
|
+
bounced: number;
|
|
99
|
+
complained: number;
|
|
100
|
+
opened: number;
|
|
101
|
+
clicked: number;
|
|
102
|
+
unsubscribed: number;
|
|
103
|
+
failed: number;
|
|
104
|
+
createdAt: Date;
|
|
105
|
+
updatedAt: Date;
|
|
106
|
+
}
|
|
107
|
+
type AnalyticsStatsDocument = HydratedDocument<IAnalyticsStats>;
|
|
108
|
+
interface AnalyticsStatsStatics {
|
|
109
|
+
upsertStats(date: string, interval: AggregationInterval, dimensions: {
|
|
110
|
+
accountId?: string;
|
|
111
|
+
ruleId?: string;
|
|
112
|
+
templateId?: string;
|
|
113
|
+
}, increments: Partial<Record<'sent' | 'delivered' | 'bounced' | 'complained' | 'opened' | 'clicked' | 'unsubscribed' | 'failed', number>>): Promise<AnalyticsStatsDocument>;
|
|
114
|
+
}
|
|
115
|
+
type AnalyticsStatsModel = Model<IAnalyticsStats> & AnalyticsStatsStatics;
|
|
116
|
+
interface CreateAnalyticsStatsSchemaOptions {
|
|
117
|
+
collectionName?: string;
|
|
118
|
+
}
|
|
119
|
+
declare function createAnalyticsStatsSchema(options?: CreateAnalyticsStatsSchemaOptions): Schema<IAnalyticsStats, Model<IAnalyticsStats, any, any, any, mongoose.Document<unknown, any, IAnalyticsStats, any, {}> & IAnalyticsStats & {
|
|
120
|
+
_id: Types.ObjectId;
|
|
121
|
+
} & {
|
|
122
|
+
__v: number;
|
|
123
|
+
}, any>, {}, {}, {}, {}, mongoose.DefaultSchemaOptions, IAnalyticsStats, mongoose.Document<unknown, {}, mongoose.FlatRecord<IAnalyticsStats>, {}, mongoose.DefaultSchemaOptions> & mongoose.FlatRecord<IAnalyticsStats> & {
|
|
124
|
+
_id: Types.ObjectId;
|
|
125
|
+
} & {
|
|
126
|
+
__v: number;
|
|
127
|
+
}>;
|
|
128
|
+
|
|
129
|
+
type EmailEvent = IEmailEvent;
|
|
130
|
+
interface CreateEventInput {
|
|
131
|
+
type: EventType;
|
|
132
|
+
accountId: string;
|
|
133
|
+
ruleId?: string;
|
|
134
|
+
templateId?: string;
|
|
135
|
+
recipientEmail: string;
|
|
136
|
+
identifierId?: string;
|
|
137
|
+
metadata?: Record<string, unknown>;
|
|
138
|
+
timestamp?: Date;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface EventQueryFilters {
|
|
142
|
+
dateFrom: Date;
|
|
143
|
+
dateTo: Date;
|
|
144
|
+
type?: string;
|
|
145
|
+
accountId?: string;
|
|
146
|
+
ruleId?: string;
|
|
147
|
+
recipientEmail?: string;
|
|
148
|
+
}
|
|
149
|
+
interface PaginatedEvents {
|
|
150
|
+
events: EmailEvent[];
|
|
151
|
+
total: number;
|
|
152
|
+
}
|
|
153
|
+
declare class EventRecorderService {
|
|
154
|
+
private EmailEvent;
|
|
155
|
+
private logger;
|
|
156
|
+
constructor(EmailEvent: EmailEventModel, logger: LogAdapter);
|
|
157
|
+
record(event: CreateEventInput): Promise<void>;
|
|
158
|
+
recordBatch(events: CreateEventInput[]): Promise<void>;
|
|
159
|
+
getEvents(filters: EventQueryFilters, page?: number, limit?: number): Promise<PaginatedEvents>;
|
|
160
|
+
purgeOldEvents(olderThanDays: number): Promise<number>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
declare class AggregatorService {
|
|
164
|
+
private EmailEvent;
|
|
165
|
+
private AnalyticsStats;
|
|
166
|
+
private timezone;
|
|
167
|
+
private logger;
|
|
168
|
+
constructor(EmailEvent: EmailEventModel, AnalyticsStats: AnalyticsStatsModel, timezone: string, logger: LogAdapter);
|
|
169
|
+
aggregateDaily(date?: Date): Promise<void>;
|
|
170
|
+
aggregateRange(from: Date, to: Date): Promise<void>;
|
|
171
|
+
private aggregateByAccount;
|
|
172
|
+
private aggregateByRule;
|
|
173
|
+
private aggregateByTemplate;
|
|
174
|
+
private aggregateOverall;
|
|
175
|
+
private getDayStart;
|
|
176
|
+
private getDayEnd;
|
|
177
|
+
private getTimezoneOffsetMs;
|
|
178
|
+
private toDateKey;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface BaseMetrics {
|
|
182
|
+
sent: number;
|
|
183
|
+
delivered: number;
|
|
184
|
+
bounced: number;
|
|
185
|
+
complained: number;
|
|
186
|
+
opened: number;
|
|
187
|
+
clicked: number;
|
|
188
|
+
unsubscribed: number;
|
|
189
|
+
failed: number;
|
|
190
|
+
}
|
|
191
|
+
interface DailyStats extends BaseMetrics {
|
|
192
|
+
date: string;
|
|
193
|
+
}
|
|
194
|
+
interface AccountStats extends BaseMetrics {
|
|
195
|
+
accountId: string;
|
|
196
|
+
}
|
|
197
|
+
interface RuleStats extends BaseMetrics {
|
|
198
|
+
ruleId: string;
|
|
199
|
+
}
|
|
200
|
+
interface TemplateStats extends BaseMetrics {
|
|
201
|
+
templateId: string;
|
|
202
|
+
}
|
|
203
|
+
interface OverviewStats extends BaseMetrics {
|
|
204
|
+
startDate: string;
|
|
205
|
+
endDate: string;
|
|
206
|
+
}
|
|
207
|
+
interface TimelineEntry extends BaseMetrics {
|
|
208
|
+
date: string;
|
|
209
|
+
interval: AggregationInterval;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
declare class QueryService {
|
|
213
|
+
private AnalyticsStats;
|
|
214
|
+
private logger;
|
|
215
|
+
constructor(AnalyticsStats: AnalyticsStatsModel, logger: LogAdapter);
|
|
216
|
+
getOverview(dateFrom: Date, dateTo: Date): Promise<OverviewStats>;
|
|
217
|
+
getTimeline(dateFrom: Date, dateTo: Date, interval?: AggregationInterval): Promise<TimelineEntry[]>;
|
|
218
|
+
getAccountStats(dateFrom: Date, dateTo: Date): Promise<AccountStats[]>;
|
|
219
|
+
getRuleStats(dateFrom: Date, dateTo: Date): Promise<RuleStats[]>;
|
|
220
|
+
getTemplateStats(dateFrom: Date, dateTo: Date): Promise<TemplateStats[]>;
|
|
221
|
+
private buildTimeGroupId;
|
|
222
|
+
private toDateKey;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
declare class AlxAnalyticsError extends AlxError {
|
|
226
|
+
readonly code: string;
|
|
227
|
+
constructor(message: string, code: string);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
declare class InvalidDateRangeError extends AlxAnalyticsError {
|
|
231
|
+
readonly startDate: string;
|
|
232
|
+
readonly endDate: string;
|
|
233
|
+
constructor(startDate: string, endDate: string);
|
|
234
|
+
}
|
|
235
|
+
declare class AggregationError extends AlxAnalyticsError {
|
|
236
|
+
readonly pipeline: string;
|
|
237
|
+
readonly originalError: Error;
|
|
238
|
+
constructor(pipeline: string, originalError: Error);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
declare function validateConfig(raw: unknown): void;
|
|
242
|
+
|
|
243
|
+
declare function createAnalyticsController(eventRecorder: EventRecorderService, aggregator: AggregatorService, queryService: QueryService): {
|
|
244
|
+
getOverview(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
245
|
+
getTimeline(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
246
|
+
getAccountStats(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
247
|
+
getRuleStats(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
248
|
+
getTemplateStats(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
249
|
+
triggerAggregation(req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
type AnalyticsController = ReturnType<typeof createAnalyticsController>;
|
|
253
|
+
declare function createAnalyticsRoutes(controller: AnalyticsController): Router;
|
|
254
|
+
|
|
255
|
+
interface EmailAnalytics {
|
|
256
|
+
routes: Router;
|
|
257
|
+
events: EventRecorderService;
|
|
258
|
+
aggregator: AggregatorService;
|
|
259
|
+
query: QueryService;
|
|
260
|
+
models: {
|
|
261
|
+
EmailEvent: EmailEventModel;
|
|
262
|
+
AnalyticsStats: AnalyticsStatsModel;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
declare function createEmailAnalytics(config: EmailAnalyticsConfig): EmailAnalytics;
|
|
266
|
+
|
|
267
|
+
export { AGGREGATION_INTERVAL, type AccountStats, AggregationError, type AggregationInterval, AggregatorService, AlxAnalyticsError, type AnalyticsStatsDocument, type AnalyticsStatsModel, type AnalyticsStatsStatics, type BaseMetrics, type CreateAnalyticsStatsSchemaOptions, type CreateEmailEventSchemaOptions, type CreateEventInput, type DailyStats, EVENT_TYPE, type EmailAnalytics, type EmailAnalyticsConfig, type EmailEvent, type EmailEventDocument, type EmailEventModel, type EmailEventStatics, type EventQueryFilters, EventRecorderService, type EventType, type IAnalyticsStats, type IEmailEvent, InvalidDateRangeError, type OverviewStats, type PaginatedEvents, QueryService, type RuleStats, STATS_GROUP_BY, type StatsGroupBy, type TemplateStats, type TimelineEntry, createAnalyticsController, createAnalyticsRoutes, createAnalyticsStatsSchema, createEmailAnalytics, createEmailEventSchema, validateConfig };
|