report.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import 'reflect-metadata'
  2. import { Logger } from 'winston'
  3. import { EmailLevel } from '@standardnotes/domain-core'
  4. import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
  5. import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
  6. import { Period } from '../src/Domain/Time/Period'
  7. import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface'
  8. import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface'
  9. import { PeriodKeyGeneratorInterface } from '../src/Domain/Time/PeriodKeyGeneratorInterface'
  10. import { ContainerConfigLoader } from '../src/Bootstrap/Container'
  11. import TYPES from '../src/Bootstrap/Types'
  12. import { Env } from '../src/Bootstrap/Env'
  13. import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
  14. import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
  15. import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
  16. import { TimerInterface } from '@standardnotes/time'
  17. import { StatisticMeasureName } from '../src/Domain/Statistics/StatisticMeasureName'
  18. const requestReport = async (
  19. analyticsStore: AnalyticsStoreInterface,
  20. statisticsStore: StatisticsStoreInterface,
  21. domainEventFactory: DomainEventFactoryInterface,
  22. domainEventPublisher: DomainEventPublisherInterface,
  23. periodKeyGenerator: PeriodKeyGeneratorInterface,
  24. calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
  25. timer: TimerInterface,
  26. adminEmails: string[],
  27. ): Promise<void> => {
  28. await calculateMonthlyRecurringRevenue.execute({})
  29. const analyticsOverTime: Array<{
  30. name: string
  31. period: number
  32. counts: Array<{
  33. periodKey: string
  34. totalCount: number
  35. }>
  36. totalCount: number
  37. }> = []
  38. const thirtyDaysAnalyticsNames = [
  39. AnalyticsActivity.SubscriptionPurchased,
  40. AnalyticsActivity.Register,
  41. AnalyticsActivity.SubscriptionRenewed,
  42. AnalyticsActivity.DeleteAccount,
  43. AnalyticsActivity.SubscriptionCancelled,
  44. AnalyticsActivity.SubscriptionRefunded,
  45. AnalyticsActivity.ExistingCustomersChurn,
  46. AnalyticsActivity.NewCustomersChurn,
  47. AnalyticsActivity.SubscriptionReactivated,
  48. ]
  49. for (const analyticsName of thirtyDaysAnalyticsNames) {
  50. analyticsOverTime.push({
  51. name: analyticsName,
  52. period: Period.Last30Days,
  53. counts: await analyticsStore.calculateActivityChangesTotalCount(analyticsName, Period.Last30Days),
  54. totalCount: await analyticsStore.calculateActivityTotalCountOverTime(analyticsName, Period.Last30Days),
  55. })
  56. }
  57. const quarterlyAnalyticsNames = [
  58. AnalyticsActivity.Register,
  59. AnalyticsActivity.SubscriptionPurchased,
  60. AnalyticsActivity.SubscriptionRenewed,
  61. ]
  62. for (const analyticsName of quarterlyAnalyticsNames) {
  63. for (const period of [Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear]) {
  64. analyticsOverTime.push({
  65. name: analyticsName,
  66. period: period,
  67. counts: await analyticsStore.calculateActivityChangesTotalCount(analyticsName, period),
  68. totalCount: await analyticsStore.calculateActivityTotalCountOverTime(analyticsName, period),
  69. })
  70. }
  71. }
  72. const yesterdayActivityStatistics: Array<{
  73. name: string
  74. retention: number
  75. totalCount: number
  76. }> = []
  77. const yesterdayActivityNames = [
  78. AnalyticsActivity.LimitedDiscountOfferPurchased,
  79. AnalyticsActivity.PaymentFailed,
  80. AnalyticsActivity.PaymentSuccess,
  81. AnalyticsActivity.NewCustomersChurn,
  82. AnalyticsActivity.ExistingCustomersChurn,
  83. ]
  84. for (const activityName of yesterdayActivityNames) {
  85. yesterdayActivityStatistics.push({
  86. name: activityName,
  87. retention: await analyticsStore.calculateActivityRetention(
  88. activityName,
  89. Period.DayBeforeYesterday,
  90. Period.Yesterday,
  91. ),
  92. totalCount: await analyticsStore.calculateActivityTotalCount(activityName, Period.Yesterday),
  93. })
  94. }
  95. const statisticsOverTime: Array<{
  96. name: string
  97. period: number
  98. counts: Array<{
  99. periodKey: string
  100. totalCount: number
  101. }>
  102. }> = []
  103. const thirtyDaysStatisticsNames = [
  104. StatisticMeasureName.NAMES.MRR,
  105. StatisticMeasureName.NAMES.AnnualPlansMRR,
  106. StatisticMeasureName.NAMES.MonthlyPlansMRR,
  107. StatisticMeasureName.NAMES.FiveYearPlansMRR,
  108. StatisticMeasureName.NAMES.PlusPlansMRR,
  109. StatisticMeasureName.NAMES.ProPlansMRR,
  110. StatisticMeasureName.NAMES.ActiveUsers,
  111. StatisticMeasureName.NAMES.ActiveFreeUsers,
  112. StatisticMeasureName.NAMES.ActivePlusUsers,
  113. StatisticMeasureName.NAMES.ActiveProUsers,
  114. ]
  115. for (const statisticName of thirtyDaysStatisticsNames) {
  116. statisticsOverTime.push({
  117. name: statisticName,
  118. period: Period.Last30DaysIncludingToday,
  119. counts: await statisticsStore.calculateTotalCountOverPeriod(statisticName, Period.Last30DaysIncludingToday),
  120. })
  121. }
  122. const monthlyStatisticsNames = [StatisticMeasureName.NAMES.MRR]
  123. for (const statisticName of monthlyStatisticsNames) {
  124. statisticsOverTime.push({
  125. name: statisticName,
  126. period: Period.ThisYear,
  127. counts: await statisticsStore.calculateTotalCountOverPeriod(statisticName, Period.ThisYear),
  128. })
  129. }
  130. const statisticMeasureNames = [
  131. StatisticMeasureName.NAMES.Income,
  132. StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
  133. StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
  134. StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome,
  135. StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome,
  136. StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
  137. StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
  138. StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome,
  139. StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome,
  140. StatisticMeasureName.NAMES.Refunds,
  141. StatisticMeasureName.NAMES.RegistrationLength,
  142. StatisticMeasureName.NAMES.SubscriptionLength,
  143. StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
  144. StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
  145. StatisticMeasureName.NAMES.NewCustomers,
  146. StatisticMeasureName.NAMES.TotalCustomers,
  147. ]
  148. const statisticMeasures: Array<{
  149. name: string
  150. totalValue: number
  151. average: number
  152. increments: number
  153. period: number
  154. }> = []
  155. for (const statisticMeasureName of statisticMeasureNames) {
  156. for (const period of [Period.Yesterday, Period.ThisMonth]) {
  157. statisticMeasures.push({
  158. name: statisticMeasureName,
  159. period,
  160. totalValue: await statisticsStore.getMeasureTotal(statisticMeasureName, period),
  161. average: await statisticsStore.getMeasureAverage(statisticMeasureName, period),
  162. increments: await statisticsStore.getMeasureIncrementCounts(statisticMeasureName, period),
  163. })
  164. }
  165. }
  166. const monthlyPeriodKeys = periodKeyGenerator.getDiscretePeriodKeys(Period.ThisYear)
  167. const churnRates: Array<{
  168. rate: number
  169. periodKey: string
  170. averageCustomersCount: number
  171. existingCustomersChurn: number
  172. newCustomersChurn: number
  173. }> = []
  174. for (const monthPeriodKey of monthlyPeriodKeys) {
  175. const monthPeriod = periodKeyGenerator.convertPeriodKeyToPeriod(monthPeriodKey)
  176. const dailyPeriodKeys = periodKeyGenerator.getDiscretePeriodKeys(monthPeriod)
  177. const totalCustomerCounts: Array<number> = []
  178. for (const dailyPeriodKey of dailyPeriodKeys) {
  179. const customersCount = await statisticsStore.getMeasureTotal(
  180. StatisticMeasureName.NAMES.TotalCustomers,
  181. dailyPeriodKey,
  182. )
  183. totalCustomerCounts.push(customersCount)
  184. }
  185. const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count)
  186. const averageCustomersCount = filteredTotalCustomerCounts.length
  187. ? filteredTotalCustomerCounts.reduce((total, current) => total + current, 0) / filteredTotalCustomerCounts.length
  188. : 0
  189. const existingCustomersChurn = await analyticsStore.calculateActivityTotalCount(
  190. AnalyticsActivity.ExistingCustomersChurn,
  191. monthPeriodKey,
  192. )
  193. const newCustomersChurn = await analyticsStore.calculateActivityTotalCount(
  194. AnalyticsActivity.NewCustomersChurn,
  195. monthPeriodKey,
  196. )
  197. const totalChurn = existingCustomersChurn + newCustomersChurn
  198. churnRates.push({
  199. periodKey: monthPeriodKey,
  200. rate: averageCustomersCount ? (totalChurn / averageCustomersCount) * 100 : 0,
  201. averageCustomersCount,
  202. existingCustomersChurn,
  203. newCustomersChurn,
  204. })
  205. }
  206. for (const adminEmail of adminEmails) {
  207. await domainEventPublisher.publish(
  208. domainEventFactory.createEmailRequestedEvent({
  209. messageIdentifier: 'VERSION_ADOPTION_REPORT',
  210. subject: getSubject(),
  211. body: getBody(
  212. {
  213. activityStatistics: yesterdayActivityStatistics,
  214. activityStatisticsOverTime: analyticsOverTime,
  215. statisticsOverTime,
  216. statisticMeasures,
  217. churn: {
  218. periodKeys: monthlyPeriodKeys,
  219. values: churnRates,
  220. },
  221. },
  222. timer,
  223. ),
  224. level: EmailLevel.LEVELS.System,
  225. userEmail: adminEmail,
  226. }),
  227. )
  228. }
  229. }
  230. const container = new ContainerConfigLoader()
  231. void container.load().then((container) => {
  232. const env: Env = new Env()
  233. env.load()
  234. const logger: Logger = container.get(TYPES.Logger)
  235. logger.info('Starting usage report generation...')
  236. const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore)
  237. const statisticsStore: StatisticsStoreInterface = container.get(TYPES.StatisticsStore)
  238. const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
  239. const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
  240. const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
  241. const timer: TimerInterface = container.get(TYPES.Timer)
  242. const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
  243. TYPES.CalculateMonthlyRecurringRevenue,
  244. )
  245. const adminEmails = container.get(TYPES.ADMIN_EMAILS) as string[]
  246. logger.info(`Sending report to following admins: ${adminEmails}`)
  247. Promise.resolve(
  248. requestReport(
  249. analyticsStore,
  250. statisticsStore,
  251. domainEventFactory,
  252. domainEventPublisher,
  253. periodKeyGenerator,
  254. calculateMonthlyRecurringRevenue,
  255. timer,
  256. adminEmails,
  257. ),
  258. )
  259. .then(() => {
  260. logger.info('Usage report generation complete')
  261. process.exit(0)
  262. })
  263. .catch((error) => {
  264. logger.error(`Could not finish usage report generation: ${error.message}`)
  265. process.exit(1)
  266. })
  267. })