import 'reflect-metadata' import { Logger } from 'winston' import { DomainEventPublisherInterface } from '@standardnotes/domain-events' import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity' import { Period } from '../src/Domain/Time/Period' import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface' import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface' import { PeriodKeyGeneratorInterface } from '../src/Domain/Time/PeriodKeyGeneratorInterface' import { ContainerConfigLoader } from '../src/Bootstrap/Container' import TYPES from '../src/Bootstrap/Types' import { Env } from '../src/Bootstrap/Env' import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface' import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue' import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport' import { TimerInterface } from '@standardnotes/time' import { StatisticMeasureName } from '../src/Domain/Statistics/StatisticMeasureName' import { EmailLevel } from '@standardnotes/domain-core' const requestReport = async ( analyticsStore: AnalyticsStoreInterface, statisticsStore: StatisticsStoreInterface, domainEventFactory: DomainEventFactoryInterface, domainEventPublisher: DomainEventPublisherInterface, periodKeyGenerator: PeriodKeyGeneratorInterface, calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue, timer: TimerInterface, adminEmails: string[], ): Promise => { await calculateMonthlyRecurringRevenue.execute({}) const analyticsOverTime: Array<{ name: string period: number counts: Array<{ periodKey: string totalCount: number }> totalCount: number }> = [] const thirtyDaysAnalyticsNames = [ AnalyticsActivity.SubscriptionPurchased, AnalyticsActivity.Register, AnalyticsActivity.SubscriptionRenewed, AnalyticsActivity.DeleteAccount, AnalyticsActivity.SubscriptionCancelled, AnalyticsActivity.SubscriptionRefunded, AnalyticsActivity.ExistingCustomersChurn, AnalyticsActivity.NewCustomersChurn, AnalyticsActivity.SubscriptionReactivated, ] for (const analyticsName of thirtyDaysAnalyticsNames) { analyticsOverTime.push({ name: analyticsName, period: Period.Last30Days, counts: await analyticsStore.calculateActivityChangesTotalCount(analyticsName, Period.Last30Days), totalCount: await analyticsStore.calculateActivityTotalCountOverTime(analyticsName, Period.Last30Days), }) } const quarterlyAnalyticsNames = [ AnalyticsActivity.Register, AnalyticsActivity.SubscriptionPurchased, AnalyticsActivity.SubscriptionRenewed, ] for (const analyticsName of quarterlyAnalyticsNames) { for (const period of [Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear]) { analyticsOverTime.push({ name: analyticsName, period: period, counts: await analyticsStore.calculateActivityChangesTotalCount(analyticsName, period), totalCount: await analyticsStore.calculateActivityTotalCountOverTime(analyticsName, period), }) } } const yesterdayActivityStatistics: Array<{ name: string retention: number totalCount: number }> = [] const yesterdayActivityNames = [ AnalyticsActivity.LimitedDiscountOfferPurchased, AnalyticsActivity.PaymentFailed, AnalyticsActivity.PaymentSuccess, AnalyticsActivity.NewCustomersChurn, AnalyticsActivity.ExistingCustomersChurn, ] for (const activityName of yesterdayActivityNames) { yesterdayActivityStatistics.push({ name: activityName, retention: await analyticsStore.calculateActivityRetention( activityName, Period.DayBeforeYesterday, Period.Yesterday, ), totalCount: await analyticsStore.calculateActivityTotalCount(activityName, Period.Yesterday), }) } const statisticsOverTime: Array<{ name: string period: number counts: Array<{ periodKey: string totalCount: number }> }> = [] const thirtyDaysStatisticsNames = [ StatisticMeasureName.NAMES.MRR, StatisticMeasureName.NAMES.AnnualPlansMRR, StatisticMeasureName.NAMES.MonthlyPlansMRR, StatisticMeasureName.NAMES.FiveYearPlansMRR, StatisticMeasureName.NAMES.PlusPlansMRR, StatisticMeasureName.NAMES.ProPlansMRR, StatisticMeasureName.NAMES.ActiveUsers, StatisticMeasureName.NAMES.ActiveFreeUsers, StatisticMeasureName.NAMES.ActivePlusUsers, StatisticMeasureName.NAMES.ActiveProUsers, ] for (const statisticName of thirtyDaysStatisticsNames) { statisticsOverTime.push({ name: statisticName, period: Period.Last30DaysIncludingToday, counts: await statisticsStore.calculateTotalCountOverPeriod(statisticName, Period.Last30DaysIncludingToday), }) } const monthlyStatisticsNames = [StatisticMeasureName.NAMES.MRR] for (const statisticName of monthlyStatisticsNames) { statisticsOverTime.push({ name: statisticName, period: Period.ThisYear, counts: await statisticsStore.calculateTotalCountOverPeriod(statisticName, Period.ThisYear), }) } const statisticMeasureNames = [ StatisticMeasureName.NAMES.Income, StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome, StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome, StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome, StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome, StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome, StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome, StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome, StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome, StatisticMeasureName.NAMES.Refunds, StatisticMeasureName.NAMES.RegistrationLength, StatisticMeasureName.NAMES.SubscriptionLength, StatisticMeasureName.NAMES.RegistrationToSubscriptionTime, StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage, StatisticMeasureName.NAMES.NewCustomers, StatisticMeasureName.NAMES.TotalCustomers, ] const statisticMeasures: Array<{ name: string totalValue: number average: number increments: number period: number }> = [] for (const statisticMeasureName of statisticMeasureNames) { for (const period of [Period.Yesterday, Period.ThisMonth]) { statisticMeasures.push({ name: statisticMeasureName, period, totalValue: await statisticsStore.getMeasureTotal(statisticMeasureName, period), average: await statisticsStore.getMeasureAverage(statisticMeasureName, period), increments: await statisticsStore.getMeasureIncrementCounts(statisticMeasureName, period), }) } } const monthlyPeriodKeys = periodKeyGenerator.getDiscretePeriodKeys(Period.ThisYear) const churnRates: Array<{ rate: number periodKey: string averageCustomersCount: number existingCustomersChurn: number newCustomersChurn: number }> = [] for (const monthPeriodKey of monthlyPeriodKeys) { const monthPeriod = periodKeyGenerator.convertPeriodKeyToPeriod(monthPeriodKey) const dailyPeriodKeys = periodKeyGenerator.getDiscretePeriodKeys(monthPeriod) const totalCustomerCounts: Array = [] for (const dailyPeriodKey of dailyPeriodKeys) { const customersCount = await statisticsStore.getMeasureTotal( StatisticMeasureName.NAMES.TotalCustomers, dailyPeriodKey, ) totalCustomerCounts.push(customersCount) } const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count) const averageCustomersCount = filteredTotalCustomerCounts.length ? filteredTotalCustomerCounts.reduce((total, current) => total + current, 0) / filteredTotalCustomerCounts.length : 0 const existingCustomersChurn = await analyticsStore.calculateActivityTotalCount( AnalyticsActivity.ExistingCustomersChurn, monthPeriodKey, ) const newCustomersChurn = await analyticsStore.calculateActivityTotalCount( AnalyticsActivity.NewCustomersChurn, monthPeriodKey, ) const totalChurn = existingCustomersChurn + newCustomersChurn churnRates.push({ periodKey: monthPeriodKey, rate: averageCustomersCount ? (totalChurn / averageCustomersCount) * 100 : 0, averageCustomersCount, existingCustomersChurn, newCustomersChurn, }) } for (const adminEmail of adminEmails) { await domainEventPublisher.publish( domainEventFactory.createEmailRequestedEvent({ messageIdentifier: 'VERSION_ADOPTION_REPORT', subject: getSubject(), body: getBody( { activityStatistics: yesterdayActivityStatistics, activityStatisticsOverTime: analyticsOverTime, statisticsOverTime, statisticMeasures, churn: { periodKeys: monthlyPeriodKeys, values: churnRates, }, }, timer, ), level: EmailLevel.LEVELS.System, userEmail: adminEmail, }), ) } } const container = new ContainerConfigLoader() void container.load().then((container) => { const env: Env = new Env() env.load() const logger: Logger = container.get(TYPES.Logger) logger.info('Starting usage report generation...') const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore) const statisticsStore: StatisticsStoreInterface = container.get(TYPES.StatisticsStore) const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory) const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher) const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator) const timer: TimerInterface = container.get(TYPES.Timer) const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get( TYPES.CalculateMonthlyRecurringRevenue, ) const adminEmails = container.get(TYPES.ADMIN_EMAILS) as string[] logger.info(`Sending report to following admins: ${adminEmails}`) Promise.resolve( requestReport( analyticsStore, statisticsStore, domainEventFactory, domainEventPublisher, periodKeyGenerator, calculateMonthlyRecurringRevenue, timer, adminEmails, ), ) .then(() => { logger.info('Usage report generation complete') process.exit(0) }) .catch((error) => { logger.error(`Could not finish usage report generation: ${error.message}`) process.exit(1) }) })