2021-01-16 19:22:52 +03:00
|
|
|
const EventProcessingResult = require('./event-processing-result');
|
2021-12-02 15:17:38 +03:00
|
|
|
const debug = require('@tryghost/debug')('services:email-analytics');
|
2020-11-26 16:09:38 +03:00
|
|
|
|
2022-11-29 13:15:19 +03:00
|
|
|
/**
|
|
|
|
* @typedef {import('@tryghost/email-service').EmailEventProcessor} EmailEventProcessor
|
|
|
|
*/
|
|
|
|
|
2021-01-16 19:22:52 +03:00
|
|
|
module.exports = class EmailAnalyticsService {
|
2022-11-29 13:15:19 +03:00
|
|
|
config;
|
|
|
|
settings;
|
|
|
|
queries;
|
|
|
|
eventProcessor;
|
|
|
|
providers;
|
|
|
|
|
|
|
|
/**
|
2022-12-14 13:17:45 +03:00
|
|
|
* @param {object} dependencies
|
2022-11-29 13:15:19 +03:00
|
|
|
* @param {EmailEventProcessor} dependencies.eventProcessor
|
|
|
|
*/
|
|
|
|
constructor({config, settings, queries, eventProcessor, providers}) {
|
2020-11-26 16:09:38 +03:00
|
|
|
this.config = config;
|
|
|
|
this.settings = settings;
|
2021-01-16 19:22:52 +03:00
|
|
|
this.queries = queries;
|
|
|
|
this.eventProcessor = eventProcessor;
|
|
|
|
this.providers = providers;
|
2020-11-26 16:09:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
async fetchAll() {
|
2021-03-02 00:31:07 +03:00
|
|
|
const result = new EventProcessingResult();
|
|
|
|
|
2021-01-16 19:22:52 +03:00
|
|
|
const shouldFetchStats = await this.queries.shouldFetchStats();
|
|
|
|
if (!shouldFetchStats) {
|
|
|
|
debug('fetchAll: skipping - fetch requirements not met');
|
2020-11-26 16:09:38 +03:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
const startFetch = new Date();
|
|
|
|
debug('fetchAll: starting');
|
|
|
|
for (const [, provider] of Object.entries(this.providers)) {
|
|
|
|
const providerResults = await provider.fetchAll(this.processEventBatch.bind(this));
|
|
|
|
result.merge(providerResults);
|
|
|
|
}
|
|
|
|
debug(`fetchAll: finished (${Date.now() - startFetch}ms)`);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
async fetchLatest({maxEvents = Infinity} = {}) {
|
2021-03-02 00:31:07 +03:00
|
|
|
const result = new EventProcessingResult();
|
|
|
|
|
2021-01-16 19:22:52 +03:00
|
|
|
const shouldFetchStats = await this.queries.shouldFetchStats();
|
|
|
|
if (!shouldFetchStats) {
|
|
|
|
debug('fetchLatest: skipping - fetch requirements not met');
|
2020-12-01 13:15:01 +03:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-01-16 19:22:52 +03:00
|
|
|
const lastTimestamp = await this.queries.getLastSeenEventTimestamp();
|
|
|
|
|
2020-11-26 16:09:38 +03:00
|
|
|
const startFetch = new Date();
|
|
|
|
debug('fetchLatest: starting');
|
|
|
|
providersLoop:
|
|
|
|
for (const [, provider] of Object.entries(this.providers)) {
|
|
|
|
const providerResults = await provider.fetchLatest(lastTimestamp, this.processEventBatch.bind(this), {maxEvents});
|
|
|
|
result.merge(providerResults);
|
|
|
|
|
|
|
|
if (result.totalEvents >= maxEvents) {
|
|
|
|
break providersLoop;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
debug(`fetchLatest: finished in ${Date.now() - startFetch}ms. Fetched ${result.totalEvents} events`);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
async processEventBatch(events) {
|
|
|
|
const result = new EventProcessingResult();
|
|
|
|
|
|
|
|
for (const event of events) {
|
2022-11-29 13:15:19 +03:00
|
|
|
const batchResult = await this.processEvent(event);
|
2020-11-26 16:09:38 +03:00
|
|
|
result.merge(batchResult);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-11-29 13:15:19 +03:00
|
|
|
/**
|
2022-12-14 13:17:45 +03:00
|
|
|
*
|
|
|
|
* @param {{id: string, type: any; severity: any; recipientEmail: any; emailId: any; providerId: string; timestamp: Date; error: {code: number; message: string; enhandedCode: string|number} | null}} event
|
2022-11-29 13:15:19 +03:00
|
|
|
* @returns {Promise<EventProcessingResult>}
|
|
|
|
*/
|
|
|
|
async processEvent(event) {
|
|
|
|
if (event.type === 'delivered') {
|
|
|
|
const recipient = await this.eventProcessor.handleDelivered({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
|
|
|
|
|
|
if (recipient) {
|
|
|
|
return new EventProcessingResult({
|
|
|
|
delivered: 1,
|
|
|
|
emailIds: [recipient.emailId],
|
|
|
|
memberIds: [recipient.memberId]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return new EventProcessingResult({unprocessable: 1});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.type === 'opened') {
|
|
|
|
const recipient = await this.eventProcessor.handleOpened({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
|
|
|
|
|
|
if (recipient) {
|
|
|
|
return new EventProcessingResult({
|
|
|
|
opened: 1,
|
|
|
|
emailIds: [recipient.emailId],
|
|
|
|
memberIds: [recipient.memberId]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return new EventProcessingResult({unprocessable: 1});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.type === 'failed') {
|
|
|
|
if (event.severity === 'permanent') {
|
2022-12-01 12:00:53 +03:00
|
|
|
const recipient = await this.eventProcessor.handlePermanentFailed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, {id: event.id, timestamp: event.timestamp, error: event.error});
|
2022-11-29 13:15:19 +03:00
|
|
|
|
|
|
|
if (recipient) {
|
|
|
|
return new EventProcessingResult({
|
|
|
|
permanentFailed: 1,
|
|
|
|
emailIds: [recipient.emailId],
|
|
|
|
memberIds: [recipient.memberId]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return new EventProcessingResult({unprocessable: 1});
|
|
|
|
} else {
|
2022-12-01 12:00:53 +03:00
|
|
|
const recipient = await this.eventProcessor.handleTemporaryFailed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, {id: event.id, timestamp: event.timestamp, error: event.error});
|
2022-11-29 13:15:19 +03:00
|
|
|
|
|
|
|
if (recipient) {
|
|
|
|
return new EventProcessingResult({
|
|
|
|
temporaryFailed: 1,
|
|
|
|
emailIds: [recipient.emailId],
|
|
|
|
memberIds: [recipient.memberId]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return new EventProcessingResult({unprocessable: 1});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.type === 'unsubscribed') {
|
|
|
|
const recipient = await this.eventProcessor.handleUnsubscribed({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
|
|
|
|
|
|
if (recipient) {
|
|
|
|
return new EventProcessingResult({
|
|
|
|
unsubscribed: 1,
|
|
|
|
emailIds: [recipient.emailId],
|
|
|
|
memberIds: [recipient.memberId]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return new EventProcessingResult({unprocessable: 1});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.type === 'complained') {
|
|
|
|
const recipient = await this.eventProcessor.handleComplained({emailId: event.emailId, providerId: event.providerId, email: event.recipientEmail}, event.timestamp);
|
|
|
|
|
|
|
|
if (recipient) {
|
|
|
|
return new EventProcessingResult({
|
|
|
|
complained: 1,
|
|
|
|
emailIds: [recipient.emailId],
|
|
|
|
memberIds: [recipient.memberId]
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return new EventProcessingResult({unprocessable: 1});
|
|
|
|
}
|
|
|
|
|
|
|
|
return new EventProcessingResult({unhandled: 1});
|
|
|
|
}
|
|
|
|
|
2020-11-26 16:09:38 +03:00
|
|
|
async aggregateStats({emailIds = [], memberIds = []}) {
|
|
|
|
for (const emailId of emailIds) {
|
|
|
|
await this.aggregateEmailStats(emailId);
|
|
|
|
}
|
|
|
|
for (const memberId of memberIds) {
|
2020-12-08 15:43:10 +03:00
|
|
|
await this.aggregateMemberStats(memberId);
|
2020-11-26 16:09:38 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-02 00:31:07 +03:00
|
|
|
async aggregateEmailStats(emailId) {
|
2021-01-16 19:22:52 +03:00
|
|
|
return this.queries.aggregateEmailStats(emailId);
|
2020-11-26 16:09:38 +03:00
|
|
|
}
|
|
|
|
|
2021-03-02 00:31:07 +03:00
|
|
|
async aggregateMemberStats(memberId) {
|
2021-01-16 19:22:52 +03:00
|
|
|
return this.queries.aggregateMemberStats(memberId);
|
2020-11-26 16:09:38 +03:00
|
|
|
}
|
2021-01-16 19:22:52 +03:00
|
|
|
};
|