UNPKG

ghost

Version:

The professional publishing platform

165 lines (137 loc) 6.67 kB
const logging = require('@tryghost/logging'); class EmailAnalyticsServiceWrapper { init() { if (this.service) { return; } const EmailAnalyticsService = require('./EmailAnalyticsService'); const EmailEventStorage = require('../email-service/EmailEventStorage'); const EmailEventProcessor = require('../email-service/EmailEventProcessor'); const MailgunProvider = require('./EmailAnalyticsProviderMailgun'); const {EmailRecipientFailure, EmailSpamComplaintEvent, Email} = require('../../models'); const StartEmailAnalyticsJobEvent = require('./events/StartEmailAnalyticsJobEvent'); const domainEvents = require('@tryghost/domain-events'); const config = require('../../../shared/config'); const settings = require('../../../shared/settings-cache'); const db = require('../../data/db'); const queries = require('./lib/queries'); const membersService = require('../members'); const membersRepository = membersService.api.members; const emailSuppressionList = require('../email-suppression-list'); const prometheusClient = require('../../../shared/prometheus-client'); this.eventStorage = new EmailEventStorage({ db, membersRepository, models: { Email, EmailRecipientFailure, EmailSpamComplaintEvent }, emailSuppressionList, prometheusClient }); // Since this is running in a worker thread, we cant dispatch directly // So we post the events as a message to the job manager const eventProcessor = new EmailEventProcessor({ domainEvents, db, eventStorage: this.eventStorage, prometheusClient }); this.service = new EmailAnalyticsService({ config, settings, eventProcessor, providers: [ new MailgunProvider({config, settings}) ], queries, domainEvents, prometheusClient }); // We currently cannot trigger a non-offloaded job from the job manager // So the email analytics jobs simply emits an event. domainEvents.subscribe(StartEmailAnalyticsJobEvent, async () => { await this.startFetch(); }); } async fetchLatestOpenedEvents({maxEvents} = {maxEvents: Infinity}) { logging.info('[EmailAnalytics] Fetch latest opened events started'); const fetchStartDate = new Date(); const totalEvents = await this.service.fetchLatestOpenedEvents({maxEvents}); const fetchEndDate = new Date(); logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (latest opens)`); return totalEvents; } async fetchLatestNonOpenedEvents({maxEvents} = {maxEvents: Infinity}) { logging.info('[EmailAnalytics] Fetch latest non-opened events started'); const fetchStartDate = new Date(); const totalEvents = await this.service.fetchLatestNonOpenedEvents({maxEvents}); const fetchEndDate = new Date(); logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (latest)`); return totalEvents; } async fetchMissing({maxEvents} = {maxEvents: Infinity}) { logging.info('[EmailAnalytics] Fetch missing events started'); const fetchStartDate = new Date(); const totalEvents = await this.service.fetchMissing({maxEvents}); const fetchEndDate = new Date(); logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (missing)`); return totalEvents; } async fetchScheduled({maxEvents}) { if (maxEvents < 300) { return 0; } logging.info('[EmailAnalytics] Fetch scheduled events started'); const fetchStartDate = new Date(); const totalEvents = await this.service.fetchScheduled({maxEvents}); const fetchEndDate = new Date(); logging.info(`[EmailAnalytics] Fetched ${totalEvents} events and aggregated stats in ${fetchEndDate.getTime() - fetchStartDate.getTime()}ms (scheduled)`); return totalEvents; } async startFetch() { if (this.fetching) { logging.info('Email analytics fetch already running, skipping'); return; } this.fetching = true; // NOTE: Data shows we can process ~2500 events per minute on Pro for a large-ish db (150k members). // This can vary locally, but we should be conservative with the number of events we fetch. try { // Prioritize opens since they are the most important (only data directly displayed to users) const c1 = await this.fetchLatestOpenedEvents({maxEvents: 10000}); if (c1 >= 10000) { this._restartFetch('high opened event count'); return; } // Set limits on how much we fetch without checkings for opened events. During surge events (following newsletter send) // we want to make sure we don't spend too much time collecting delivery data. const c2 = await this.fetchLatestNonOpenedEvents({maxEvents: 10000 - c1}); const c3 = await this.fetchMissing({maxEvents: 10000 - c1 - c2}); // Always restart immediately instead of waiting for the next scheduled job if we're fetching a lot of events if ((c1 + c2 + c3) > 10000) { this._restartFetch('high event count'); return; } // Only backfill if we're not currently fetching a lot of events const c4 = await this.fetchScheduled({maxEvents: 10000}); if (c4 > 0) { this._restartFetch('scheduled backfill'); return; } this.fetching = false; } catch (e) { logging.error(e, 'Error while fetching email analytics'); // Log again only the error, otherwise we lose the stack trace logging.error(e); } this.fetching = false; } _restartFetch(reason) { this.fetching = false; logging.info(`[EmailAnalytics] Restarting fetch due to ${reason}`); this.startFetch(); } } module.exports = EmailAnalyticsServiceWrapper;