RedisAnalyticsStore.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import * as IORedis from 'ioredis'
  2. import { Period } from '../../Domain/Time/Period'
  3. import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
  4. import { AnalyticsActivity } from '../../Domain/Analytics/AnalyticsActivity'
  5. import { AnalyticsStoreInterface } from '../../Domain/Analytics/AnalyticsStoreInterface'
  6. export class RedisAnalyticsStore implements AnalyticsStoreInterface {
  7. constructor(
  8. private periodKeyGenerator: PeriodKeyGeneratorInterface,
  9. private redisClient: IORedis.Redis,
  10. ) {}
  11. async calculateActivityTotalCountOverTime(activity: AnalyticsActivity, period: Period): Promise<number> {
  12. if (
  13. ![Period.Last30Days, Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear].includes(period)
  14. ) {
  15. throw new Error(`Unsuporrted period: ${period}`)
  16. }
  17. const periodKeys = this.periodKeyGenerator.getDiscretePeriodKeys(period)
  18. await this.redisClient.bitop(
  19. 'OR',
  20. `bitmap:action:${activity}:timespan:${periodKeys[0]}-${periodKeys[periodKeys.length - 1]}`,
  21. ...periodKeys.map((p) => `bitmap:action:${activity}:timespan:${p}`),
  22. )
  23. await this.redisClient.expire(
  24. `bitmap:action:${activity}:timespan:${periodKeys[0]}-${periodKeys[periodKeys.length - 1]}`,
  25. 3600,
  26. )
  27. return this.redisClient.bitcount(
  28. `bitmap:action:${activity}:timespan:${periodKeys[0]}-${periodKeys[periodKeys.length - 1]}`,
  29. )
  30. }
  31. async calculateActivityChangesTotalCount(
  32. activity: AnalyticsActivity,
  33. period: Period,
  34. ): Promise<Array<{ periodKey: string; totalCount: number }>> {
  35. if (
  36. ![Period.Last30Days, Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear].includes(period)
  37. ) {
  38. throw new Error(`Unsuporrted period: ${period}`)
  39. }
  40. const periodKeys = this.periodKeyGenerator.getDiscretePeriodKeys(period)
  41. const counts = []
  42. for (const periodKey of periodKeys) {
  43. counts.push({
  44. periodKey,
  45. totalCount: await this.redisClient.bitcount(`bitmap:action:${activity}:timespan:${periodKey}`),
  46. })
  47. }
  48. return counts
  49. }
  50. async markActivity(activities: AnalyticsActivity[], analyticsId: number, periods: Period[]): Promise<void> {
  51. const pipeline = this.redisClient.pipeline()
  52. for (const activity of activities) {
  53. for (const period of periods) {
  54. pipeline.setbit(
  55. `bitmap:action:${activity}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
  56. analyticsId,
  57. 1,
  58. )
  59. }
  60. }
  61. await pipeline.exec()
  62. }
  63. async unmarkActivity(activities: AnalyticsActivity[], analyticsId: number, periods: Period[]): Promise<void> {
  64. const pipeline = this.redisClient.pipeline()
  65. for (const activity of activities) {
  66. for (const period of periods) {
  67. pipeline.setbit(
  68. `bitmap:action:${activity}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
  69. analyticsId,
  70. 0,
  71. )
  72. }
  73. }
  74. await pipeline.exec()
  75. }
  76. async wasActivityDone(activity: AnalyticsActivity, analyticsId: number, period: Period): Promise<boolean> {
  77. const bitValue = await this.redisClient.getbit(
  78. `bitmap:action:${activity}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
  79. analyticsId,
  80. )
  81. return bitValue === 1
  82. }
  83. async calculateActivitiesRetention(parameters: {
  84. firstActivity: AnalyticsActivity
  85. firstActivityPeriodKey: string
  86. secondActivity: AnalyticsActivity
  87. secondActivityPeriodKey: string
  88. }): Promise<number> {
  89. const diffKey = `bitmap:action:${parameters.firstActivity}-${parameters.secondActivity}:timespan:${parameters.secondActivityPeriodKey}`
  90. await this.redisClient.bitop(
  91. 'AND',
  92. diffKey,
  93. `bitmap:action:${parameters.firstActivity}:timespan:${parameters.firstActivityPeriodKey}`,
  94. `bitmap:action:${parameters.secondActivity}:timespan:${parameters.secondActivityPeriodKey}`,
  95. )
  96. await this.redisClient.expire(diffKey, 3600)
  97. const retainedTotalInActivity = await this.redisClient.bitcount(diffKey)
  98. const initialTotalInActivity = await this.redisClient.bitcount(
  99. `bitmap:action:${parameters.firstActivity}:timespan:${parameters.firstActivityPeriodKey}`,
  100. )
  101. return Math.ceil((retainedTotalInActivity * 100) / initialTotalInActivity)
  102. }
  103. async calculateActivityRetention(
  104. activity: AnalyticsActivity,
  105. firstPeriod: Period,
  106. secondPeriod: Period,
  107. ): Promise<number> {
  108. return this.calculateActivitiesRetention({
  109. firstActivity: activity,
  110. firstActivityPeriodKey: this.periodKeyGenerator.getPeriodKey(firstPeriod),
  111. secondActivity: activity,
  112. secondActivityPeriodKey: this.periodKeyGenerator.getPeriodKey(secondPeriod),
  113. })
  114. }
  115. async calculateActivityTotalCount(activity: AnalyticsActivity, periodOrPeriodKey: Period | string): Promise<number> {
  116. let periodKey = periodOrPeriodKey
  117. if (!isNaN(+periodOrPeriodKey)) {
  118. periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period)
  119. }
  120. return this.redisClient.bitcount(`bitmap:action:${activity}:timespan:${periodKey}`)
  121. }
  122. }