mirror of
https://github.com/standardnotes/server
synced 2026-01-16 20:04:32 -05:00
feat: remove analytics scope from other services in favor of a separate service
This commit is contained in:
1
.pnp.cjs
generated
1
.pnp.cjs
generated
@@ -2651,7 +2651,6 @@ const RAW_RUNTIME_STATE =
|
||||
["@standardnotes/auth-server", "workspace:packages/auth"],\
|
||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||
["@sentry/node", "npm:7.5.0"],\
|
||||
["@standardnotes/analytics", "workspace:packages/analytics"],\
|
||||
["@standardnotes/api", "npm:1.19.0"],\
|
||||
["@standardnotes/common", "workspace:packages/common"],\
|
||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||
|
||||
@@ -34,8 +34,6 @@ const requestReport = async (
|
||||
}> = []
|
||||
|
||||
const thirtyDaysAnalyticsNames = [
|
||||
AnalyticsActivity.GeneralActivity,
|
||||
AnalyticsActivity.EditingItems,
|
||||
AnalyticsActivity.SubscriptionPurchased,
|
||||
AnalyticsActivity.Register,
|
||||
AnalyticsActivity.SubscriptionRenewed,
|
||||
@@ -80,9 +78,6 @@ const requestReport = async (
|
||||
}> = []
|
||||
const yesterdayActivityNames = [
|
||||
AnalyticsActivity.LimitedDiscountOfferPurchased,
|
||||
AnalyticsActivity.GeneralActivity,
|
||||
AnalyticsActivity.GeneralActivityFreeUsers,
|
||||
AnalyticsActivity.GeneralActivityPaidUsers,
|
||||
AnalyticsActivity.PaymentFailed,
|
||||
AnalyticsActivity.PaymentSuccess,
|
||||
AnalyticsActivity.NewCustomersChurn,
|
||||
@@ -116,9 +111,6 @@ const requestReport = async (
|
||||
StatisticsMeasure.SubscriptionLength,
|
||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
||||
StatisticsMeasure.NotesCountFreeUsers,
|
||||
StatisticsMeasure.NotesCountPaidUsers,
|
||||
StatisticsMeasure.FilesCount,
|
||||
StatisticsMeasure.NewCustomers,
|
||||
StatisticsMeasure.TotalCustomers,
|
||||
]
|
||||
@@ -141,29 +133,6 @@ const requestReport = async (
|
||||
}
|
||||
}
|
||||
|
||||
const periodKeys = periodKeyGenerator.getDiscretePeriodKeys(Period.Last7Days)
|
||||
const retentionOverDays: Array<{
|
||||
firstPeriodKey: string
|
||||
secondPeriodKey: string
|
||||
value: number
|
||||
}> = []
|
||||
for (let i = 0; i < periodKeys.length; i++) {
|
||||
for (let j = 0; j < periodKeys.length - i; j++) {
|
||||
const dailyRetention = await analyticsStore.calculateActivitiesRetention({
|
||||
firstActivity: AnalyticsActivity.Register,
|
||||
firstActivityPeriodKey: periodKeys[i],
|
||||
secondActivity: AnalyticsActivity.GeneralActivity,
|
||||
secondActivityPeriodKey: periodKeys[i + j],
|
||||
})
|
||||
|
||||
retentionOverDays.push({
|
||||
firstPeriodKey: periodKeys[i],
|
||||
secondPeriodKey: periodKeys[i + j],
|
||||
value: dailyRetention,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const monthlyPeriodKeys = periodKeyGenerator.getDiscretePeriodKeys(Period.ThisYear)
|
||||
const churnRates: Array<{
|
||||
rate: number
|
||||
@@ -207,16 +176,7 @@ const requestReport = async (
|
||||
activityStatistics: yesterdayActivityStatistics,
|
||||
activityStatisticsOverTime: analyticsOverTime,
|
||||
statisticMeasures,
|
||||
retentionStatistics: [
|
||||
{
|
||||
firstActivity: AnalyticsActivity.Register,
|
||||
secondActivity: AnalyticsActivity.GeneralActivity,
|
||||
retention: {
|
||||
periodKeys,
|
||||
values: retentionOverDays,
|
||||
},
|
||||
},
|
||||
],
|
||||
retentionStatistics: [],
|
||||
churn: {
|
||||
periodKeys: monthlyPeriodKeys,
|
||||
values: churnRates,
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "1.52.0",
|
||||
"version": "2.0.0",
|
||||
"engines": {
|
||||
"node": ">=14.0.0 <17.0.0"
|
||||
},
|
||||
"private": true,
|
||||
"description": "Analytics tools for Standard Notes projects",
|
||||
"main": "dist/src/index.js",
|
||||
"author": "Standard Notes",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"files": [
|
||||
"dist/src/**/*.js",
|
||||
"dist/src/**/*.d.ts"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@ import { SubscriptionRefundedEventHandler } from '../Domain/Handler/Subscription
|
||||
import { SubscriptionPurchasedEventHandler } from '../Domain/Handler/SubscriptionPurchasedEventHandler'
|
||||
import { SubscriptionExpiredEventHandler } from '../Domain/Handler/SubscriptionExpiredEventHandler'
|
||||
import { SubscriptionReactivatedEventHandler } from '../Domain/Handler/SubscriptionReactivatedEventHandler'
|
||||
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -149,6 +150,7 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
||||
.to(SubscriptionReactivatedEventHandler)
|
||||
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
||||
|
||||
// Services
|
||||
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
@@ -175,6 +177,16 @@ export class ContainerConfigLoader {
|
||||
|
||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
|
||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
||||
['PAYMENT_FAILED', container.get(TYPES.PaymentFailedEventHandler)],
|
||||
['PAYMENT_SUCCESS', container.get(TYPES.PaymentSuccessEventHandler)],
|
||||
['SUBSCRIPTION_CANCELLED', container.get(TYPES.SubscriptionCancelledEventHandler)],
|
||||
['SUBSCRIPTION_RENEWED', container.get(TYPES.SubscriptionRenewedEventHandler)],
|
||||
['SUBSCRIPTION_REFUNDED', container.get(TYPES.SubscriptionRefundedEventHandler)],
|
||||
['SUBSCRIPTION_PURCHASED', container.get(TYPES.SubscriptionPurchasedEventHandler)],
|
||||
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
|
||||
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
||||
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
||||
])
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
|
||||
@@ -28,6 +28,7 @@ const TYPES = {
|
||||
SubscriptionPurchasedEventHandler: Symbol.for('SubscriptionPurchasedEventHandler'),
|
||||
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
|
||||
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
||||
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
||||
// Services
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
export enum AnalyticsActivity {
|
||||
GeneralActivity = 'general-activity',
|
||||
GeneralActivityFreeUsers = 'general-activity-free-users',
|
||||
GeneralActivityPaidUsers = 'general-activity-paid-users',
|
||||
EditingItems = 'editing-items',
|
||||
CheckingIntegrity = 'checking-integrity',
|
||||
Login = 'login',
|
||||
Register = 'register',
|
||||
DeleteAccount = 'DeleteAccount',
|
||||
SubscriptionPurchased = 'subscription-purchased',
|
||||
@@ -13,8 +7,6 @@ export enum AnalyticsActivity {
|
||||
SubscriptionCancelled = 'subscription-cancelled',
|
||||
SubscriptionExpired = 'subscription-expired',
|
||||
SubscriptionReactivated = 'subscription-reactivated',
|
||||
EmailUnbackedUpData = 'email-unbacked-up-data',
|
||||
EmailBackup = 'email-backup',
|
||||
LimitedDiscountOfferPurchased = 'limited-discount-offer-purchased',
|
||||
PaymentFailed = 'payment-failed',
|
||||
PaymentSuccess = 'payment-success',
|
||||
|
||||
@@ -64,22 +64,7 @@ describe('DomainEventFactory', () => {
|
||||
},
|
||||
],
|
||||
outOfSyncIncidents: 324,
|
||||
retentionStatistics: [
|
||||
{
|
||||
firstActivity: AnalyticsActivity.Register,
|
||||
secondActivity: AnalyticsActivity.Login,
|
||||
retention: {
|
||||
periodKeys: ['2022-10-9'],
|
||||
values: [
|
||||
{
|
||||
firstPeriodKey: AnalyticsActivity.Register,
|
||||
secondPeriodKey: AnalyticsActivity.Login,
|
||||
value: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
retentionStatistics: [],
|
||||
churn: {
|
||||
periodKeys: ['2022-10-9'],
|
||||
values: [
|
||||
@@ -136,22 +121,7 @@ describe('DomainEventFactory', () => {
|
||||
],
|
||||
},
|
||||
outOfSyncIncidents: 324,
|
||||
retentionStatistics: [
|
||||
{
|
||||
firstActivity: 'register',
|
||||
retention: {
|
||||
periodKeys: ['2022-10-9'],
|
||||
values: [
|
||||
{
|
||||
firstPeriodKey: 'register',
|
||||
secondPeriodKey: 'login',
|
||||
value: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
secondActivity: 'login',
|
||||
},
|
||||
],
|
||||
retentionStatistics: [],
|
||||
snjsStatistics: [
|
||||
{
|
||||
count: 2,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { RefundProcessedEvent } from '@standardnotes/domain-events'
|
||||
import { Period, StatisticsMeasure, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
import { RefundProcessedEventHandler } from './RefundProcessedEventHandler'
|
||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
describe('RefundProcessedEventHandler', () => {
|
||||
let event: RefundProcessedEvent
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Period, StatisticsMeasure, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
@injectable()
|
||||
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -13,9 +13,6 @@ export enum StatisticsMeasure {
|
||||
RegistrationToSubscriptionTime = 'registration-to-subscription-time',
|
||||
RemainingSubscriptionTimePercentage = 'remaining-subscription-time-percentage',
|
||||
Refunds = 'refunds',
|
||||
NotesCountFreeUsers = 'notes-count-free-users',
|
||||
NotesCountPaidUsers = 'notes-count-paid-users',
|
||||
FilesCount = 'files-count',
|
||||
NewCustomers = 'new-customers',
|
||||
TotalCustomers = 'total-customers',
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export * from './Analytics/AnalyticsActivity'
|
||||
export * from './Analytics/AnalyticsStoreInterface'
|
||||
export * from './Statistics/StatisticsMeasure'
|
||||
export * from './Statistics/StatisticsStoreInterface'
|
||||
export * from './Time/Period'
|
||||
export * from './Time/PeriodKeyGenerator'
|
||||
export * from './Time/PeriodKeyGeneratorInterface'
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as IORedis from 'ioredis'
|
||||
import { Period } from '../../Domain'
|
||||
import { AnalyticsActivity } from '../../Domain/Analytics/AnalyticsActivity'
|
||||
import { Period } from '../../Domain/Time/Period'
|
||||
import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
|
||||
|
||||
import { RedisAnalyticsStore } from './RedisAnalyticsStore'
|
||||
@@ -35,24 +35,24 @@ describe('RedisAnalyticsStore', () => {
|
||||
|
||||
periodKeyGenerator.getDiscretePeriodKeys = jest.fn().mockReturnValue(['2022-4-24', '2022-4-25', '2022-4-26'])
|
||||
|
||||
await createStore().calculateActivityTotalCountOverTime(AnalyticsActivity.EditingItems, Period.Last30Days)
|
||||
await createStore().calculateActivityTotalCountOverTime(AnalyticsActivity.Register, Period.Last30Days)
|
||||
|
||||
expect(redisClient.bitop).toHaveBeenCalledTimes(1)
|
||||
expect(redisClient.bitop).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'OR',
|
||||
'bitmap:action:editing-items:timespan:2022-4-24-2022-4-26',
|
||||
'bitmap:action:editing-items:timespan:2022-4-24',
|
||||
'bitmap:action:editing-items:timespan:2022-4-25',
|
||||
'bitmap:action:editing-items:timespan:2022-4-26',
|
||||
'bitmap:action:register:timespan:2022-4-24-2022-4-26',
|
||||
'bitmap:action:register:timespan:2022-4-24',
|
||||
'bitmap:action:register:timespan:2022-4-25',
|
||||
'bitmap:action:register:timespan:2022-4-26',
|
||||
)
|
||||
expect(redisClient.bitcount).toHaveBeenCalledWith('bitmap:action:editing-items:timespan:2022-4-24-2022-4-26')
|
||||
expect(redisClient.bitcount).toHaveBeenCalledWith('bitmap:action:register:timespan:2022-4-24-2022-4-26')
|
||||
})
|
||||
|
||||
it('should not calculate total count over time of activities if period is unsupported', async () => {
|
||||
let caughtError = null
|
||||
try {
|
||||
await createStore().calculateActivityTotalCountOverTime(AnalyticsActivity.EditingItems, Period.LastWeek)
|
||||
await createStore().calculateActivityTotalCountOverTime(AnalyticsActivity.Register, Period.LastWeek)
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
@@ -66,7 +66,7 @@ describe('RedisAnalyticsStore', () => {
|
||||
redisClient.bitcount = jest.fn().mockReturnValueOnce(70).mockReturnValueOnce(71).mockReturnValueOnce(72)
|
||||
|
||||
expect(
|
||||
await createStore().calculateActivityChangesTotalCount(AnalyticsActivity.EditingItems, Period.Last30Days),
|
||||
await createStore().calculateActivityChangesTotalCount(AnalyticsActivity.Register, Period.Last30Days),
|
||||
).toEqual([
|
||||
{
|
||||
periodKey: '2022-4-24',
|
||||
@@ -82,9 +82,9 @@ describe('RedisAnalyticsStore', () => {
|
||||
},
|
||||
])
|
||||
|
||||
expect(redisClient.bitcount).toHaveBeenNthCalledWith(1, 'bitmap:action:editing-items:timespan:2022-4-24')
|
||||
expect(redisClient.bitcount).toHaveBeenNthCalledWith(2, 'bitmap:action:editing-items:timespan:2022-4-25')
|
||||
expect(redisClient.bitcount).toHaveBeenNthCalledWith(3, 'bitmap:action:editing-items:timespan:2022-4-26')
|
||||
expect(redisClient.bitcount).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:2022-4-24')
|
||||
expect(redisClient.bitcount).toHaveBeenNthCalledWith(2, 'bitmap:action:register:timespan:2022-4-25')
|
||||
expect(redisClient.bitcount).toHaveBeenNthCalledWith(3, 'bitmap:action:register:timespan:2022-4-26')
|
||||
})
|
||||
|
||||
it('should throw error on calculating total count changes of activities on unsupported period', async () => {
|
||||
@@ -94,7 +94,7 @@ describe('RedisAnalyticsStore', () => {
|
||||
|
||||
let caughtError = null
|
||||
try {
|
||||
await createStore().calculateActivityChangesTotalCount(AnalyticsActivity.EditingItems, Period.LastWeek)
|
||||
await createStore().calculateActivityChangesTotalCount(AnalyticsActivity.Register, Period.LastWeek)
|
||||
} catch (error) {
|
||||
caughtError = error
|
||||
}
|
||||
@@ -105,19 +105,17 @@ describe('RedisAnalyticsStore', () => {
|
||||
it('should calculate total count of activities by period', async () => {
|
||||
redisClient.bitcount = jest.fn().mockReturnValue(70)
|
||||
|
||||
expect(await createStore().calculateActivityTotalCount(AnalyticsActivity.EditingItems, Period.Yesterday)).toEqual(
|
||||
70,
|
||||
)
|
||||
expect(await createStore().calculateActivityTotalCount(AnalyticsActivity.Register, Period.Yesterday)).toEqual(70)
|
||||
|
||||
expect(redisClient.bitcount).toHaveBeenCalledWith('bitmap:action:editing-items:timespan:period-key')
|
||||
expect(redisClient.bitcount).toHaveBeenCalledWith('bitmap:action:register:timespan:period-key')
|
||||
})
|
||||
|
||||
it('should calculate total count of activities by period key', async () => {
|
||||
redisClient.bitcount = jest.fn().mockReturnValue(70)
|
||||
|
||||
expect(await createStore().calculateActivityTotalCount(AnalyticsActivity.EditingItems, '2022-10-03')).toEqual(70)
|
||||
expect(await createStore().calculateActivityTotalCount(AnalyticsActivity.Register, '2022-10-03')).toEqual(70)
|
||||
|
||||
expect(redisClient.bitcount).toHaveBeenCalledWith('bitmap:action:editing-items:timespan:2022-10-03')
|
||||
expect(redisClient.bitcount).toHaveBeenCalledWith('bitmap:action:register:timespan:2022-10-03')
|
||||
})
|
||||
|
||||
it('should calculate activity retention', async () => {
|
||||
@@ -125,7 +123,7 @@ describe('RedisAnalyticsStore', () => {
|
||||
|
||||
expect(
|
||||
await createStore().calculateActivityRetention(
|
||||
AnalyticsActivity.EditingItems,
|
||||
AnalyticsActivity.Register,
|
||||
Period.DayBeforeYesterday,
|
||||
Period.Yesterday,
|
||||
),
|
||||
@@ -133,44 +131,44 @@ describe('RedisAnalyticsStore', () => {
|
||||
|
||||
expect(redisClient.bitop).toHaveBeenCalledWith(
|
||||
'AND',
|
||||
'bitmap:action:editing-items-editing-items:timespan:period-key',
|
||||
'bitmap:action:editing-items:timespan:period-key',
|
||||
'bitmap:action:editing-items:timespan:period-key',
|
||||
'bitmap:action:register-register:timespan:period-key',
|
||||
'bitmap:action:register:timespan:period-key',
|
||||
'bitmap:action:register:timespan:period-key',
|
||||
)
|
||||
})
|
||||
|
||||
it('shoud tell if activity was done', async () => {
|
||||
await createStore().wasActivityDone(AnalyticsActivity.EditingItems, 123, Period.Yesterday)
|
||||
await createStore().wasActivityDone(AnalyticsActivity.Register, 123, Period.Yesterday)
|
||||
|
||||
expect(redisClient.getbit).toHaveBeenCalledWith('bitmap:action:editing-items:timespan:period-key', 123)
|
||||
expect(redisClient.getbit).toHaveBeenCalledWith('bitmap:action:register:timespan:period-key', 123)
|
||||
})
|
||||
|
||||
it('should mark activity as done', async () => {
|
||||
await createStore().markActivity([AnalyticsActivity.EditingItems], 123, [Period.Today])
|
||||
await createStore().markActivity([AnalyticsActivity.Register], 123, [Period.Today])
|
||||
|
||||
expect(pipeline.setbit).toBeCalledTimes(1)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:editing-items:timespan:period-key', 123, 1)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:period-key', 123, 1)
|
||||
expect(pipeline.exec).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should mark activities as done', async () => {
|
||||
await createStore().markActivity([AnalyticsActivity.EditingItems, AnalyticsActivity.EmailUnbackedUpData], 123, [
|
||||
await createStore().markActivity([AnalyticsActivity.Register, AnalyticsActivity.SubscriptionPurchased], 123, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
])
|
||||
|
||||
expect(pipeline.setbit).toBeCalledTimes(4)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:editing-items:timespan:period-key', 123, 1)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(2, 'bitmap:action:editing-items:timespan:period-key', 123, 1)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:period-key', 123, 1)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(2, 'bitmap:action:register:timespan:period-key', 123, 1)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'bitmap:action:email-unbacked-up-data:timespan:period-key',
|
||||
'bitmap:action:subscription-purchased:timespan:period-key',
|
||||
123,
|
||||
1,
|
||||
)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'bitmap:action:email-unbacked-up-data:timespan:period-key',
|
||||
'bitmap:action:subscription-purchased:timespan:period-key',
|
||||
123,
|
||||
1,
|
||||
)
|
||||
@@ -178,31 +176,31 @@ describe('RedisAnalyticsStore', () => {
|
||||
})
|
||||
|
||||
it('should unmark activity as done', async () => {
|
||||
await createStore().unmarkActivity([AnalyticsActivity.EditingItems], 123, [Period.Today])
|
||||
await createStore().unmarkActivity([AnalyticsActivity.Register], 123, [Period.Today])
|
||||
|
||||
expect(pipeline.setbit).toBeCalledTimes(1)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:editing-items:timespan:period-key', 123, 0)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:period-key', 123, 0)
|
||||
expect(pipeline.exec).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should unmark activities as done', async () => {
|
||||
await createStore().unmarkActivity([AnalyticsActivity.EditingItems, AnalyticsActivity.EmailUnbackedUpData], 123, [
|
||||
await createStore().unmarkActivity([AnalyticsActivity.Register, AnalyticsActivity.SubscriptionPurchased], 123, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
])
|
||||
|
||||
expect(pipeline.setbit).toBeCalledTimes(4)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:editing-items:timespan:period-key', 123, 0)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(2, 'bitmap:action:editing-items:timespan:period-key', 123, 0)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(1, 'bitmap:action:register:timespan:period-key', 123, 0)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(2, 'bitmap:action:register:timespan:period-key', 123, 0)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'bitmap:action:email-unbacked-up-data:timespan:period-key',
|
||||
'bitmap:action:subscription-purchased:timespan:period-key',
|
||||
123,
|
||||
0,
|
||||
)
|
||||
expect(pipeline.setbit).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'bitmap:action:email-unbacked-up-data:timespan:period-key',
|
||||
'bitmap:action:subscription-purchased:timespan:period-key',
|
||||
123,
|
||||
0,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as IORedis from 'ioredis'
|
||||
|
||||
import { Period, PeriodKeyGeneratorInterface } from '../../Domain'
|
||||
import { StatisticsMeasure } from '../../Domain/Statistics/StatisticsMeasure'
|
||||
import { Period } from '../../Domain/Time/Period'
|
||||
import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
|
||||
|
||||
import { RedisStatisticsStore } from './RedisStatisticsStore'
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as IORedis from 'ioredis'
|
||||
|
||||
import { Period, PeriodKeyGeneratorInterface } from '../../Domain'
|
||||
import { StatisticsMeasure } from '../../Domain/Statistics/StatisticsMeasure'
|
||||
|
||||
import { StatisticsStoreInterface } from '../../Domain/Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../../Domain/Time/Period'
|
||||
import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
|
||||
|
||||
export class RedisStatisticsStore implements StatisticsStoreInterface {
|
||||
constructor(private periodKeyGenerator: PeriodKeyGeneratorInterface, private redisClient: IORedis.Redis) {}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './Redis/RedisAnalyticsStore'
|
||||
export * from './Redis/RedisStatisticsStore'
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './Domain'
|
||||
export * from './Infra'
|
||||
@@ -22,7 +22,6 @@
|
||||
"dependencies": {
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/analytics": "workspace:*",
|
||||
"@standardnotes/common": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
|
||||
@@ -2,14 +2,6 @@ import * as winston from 'winston'
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import Redis from 'ioredis'
|
||||
import { Container } from 'inversify'
|
||||
import {
|
||||
AnalyticsStoreInterface,
|
||||
PeriodKeyGenerator,
|
||||
PeriodKeyGeneratorInterface,
|
||||
RedisAnalyticsStore,
|
||||
RedisStatisticsStore,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { Env } from './Env'
|
||||
@@ -18,7 +10,6 @@ import { AuthMiddleware } from '../Controller/AuthMiddleware'
|
||||
import { HttpServiceInterface } from '../Service/Http/HttpServiceInterface'
|
||||
import { HttpService } from '../Service/Http/HttpService'
|
||||
import { SubscriptionTokenAuthMiddleware } from '../Controller/SubscriptionTokenAuthMiddleware'
|
||||
import { StatisticsMiddleware } from '../Controller/StatisticsMiddleware'
|
||||
import { CrossServiceTokenCacheInterface } from '../Service/Cache/CrossServiceTokenCacheInterface'
|
||||
import { RedisCrossServiceTokenCache } from '../Infra/Redis/RedisCrossServiceTokenCache'
|
||||
import { WebSocketAuthMiddleware } from '../Controller/WebSocketAuthMiddleware'
|
||||
@@ -79,17 +70,9 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<SubscriptionTokenAuthMiddleware>(TYPES.SubscriptionTokenAuthMiddleware)
|
||||
.to(SubscriptionTokenAuthMiddleware)
|
||||
container.bind<StatisticsMiddleware>(TYPES.StatisticsMiddleware).to(StatisticsMiddleware)
|
||||
|
||||
// Services
|
||||
container.bind<HttpServiceInterface>(TYPES.HTTPService).to(HttpService)
|
||||
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
|
||||
container
|
||||
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
|
||||
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||
container
|
||||
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
||||
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||
container.bind<CrossServiceTokenCacheInterface>(TYPES.CrossServiceTokenCache).to(RedisCrossServiceTokenCache)
|
||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||
|
||||
|
||||
@@ -15,17 +15,13 @@ const TYPES = {
|
||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||
CROSS_SERVICE_TOKEN_CACHE_TTL: Symbol.for('CROSS_SERVICE_TOKEN_CACHE_TTL'),
|
||||
// Middleware
|
||||
StatisticsMiddleware: Symbol.for('StatisticsMiddleware'),
|
||||
AuthMiddleware: Symbol.for('AuthMiddleware'),
|
||||
WebSocketAuthMiddleware: Symbol.for('WebSocketAuthMiddleware'),
|
||||
SubscriptionTokenAuthMiddleware: Symbol.for('SubscriptionTokenAuthMiddleware'),
|
||||
// Services
|
||||
HTTPService: Symbol.for('HTTPService'),
|
||||
CrossServiceTokenCache: Symbol.for('CrossServiceTokenCache'),
|
||||
AnalyticsStore: Symbol.for('AnalyticsStore'),
|
||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||
Timer: Symbol.for('Timer'),
|
||||
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CrossServiceTokenData } from '@standardnotes/security'
|
||||
import { RoleName } from '@standardnotes/common'
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
@@ -21,7 +20,6 @@ export class AuthMiddleware extends BaseMiddleware {
|
||||
@inject(TYPES.CROSS_SERVICE_TOKEN_CACHE_TTL) private crossServiceTokenCacheTTL: number,
|
||||
@inject(TYPES.CrossServiceTokenCache) private crossServiceTokenCache: CrossServiceTokenCacheInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
@@ -80,17 +78,6 @@ export class AuthMiddleware extends BaseMiddleware {
|
||||
decodedToken.roles.length === 1 &&
|
||||
decodedToken.roles.find((role) => role.name === RoleName.CoreUser) !== undefined
|
||||
|
||||
await this.analyticsStore.markActivity(
|
||||
[
|
||||
AnalyticsActivity.GeneralActivity,
|
||||
response.locals.freeUser
|
||||
? AnalyticsActivity.GeneralActivityFreeUsers
|
||||
: AnalyticsActivity.GeneralActivityPaidUsers,
|
||||
],
|
||||
decodedToken.analyticsId as number,
|
||||
[Period.Today],
|
||||
)
|
||||
|
||||
if (this.crossServiceTokenCacheTTL && !crossServiceTokenFetchedFromCache) {
|
||||
await this.crossServiceTokenCache.set({
|
||||
authorizationHeaderValue: authHeaderValue,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { controller, all, BaseHttpController, httpPost, httpGet, results, httpDe
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('', TYPES.StatisticsMiddleware)
|
||||
@controller('')
|
||||
export class LegacyController extends BaseHttpController {
|
||||
private AUTH_ROUTES: Map<string, string>
|
||||
private PARAMETRIZED_AUTH_ROUTES: Map<string, string>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { BaseMiddleware } from 'inversify-express-utils'
|
||||
import { Logger } from 'winston'
|
||||
import { StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
import TYPES from '../Bootstrap/Types'
|
||||
|
||||
@injectable()
|
||||
export class StatisticsMiddleware extends BaseMiddleware {
|
||||
constructor(
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async handler(request: Request, _response: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const snjsVersion = request.headers['x-snjs-version'] ?? 'unknown'
|
||||
await this.statisticsStore.incrementSNJSVersionUsage(snjsVersion as string)
|
||||
|
||||
const applicationVersion = request.headers['x-application-version'] ?? 'unknown'
|
||||
await this.statisticsStore.incrementApplicationVersionUsage(applicationVersion as string)
|
||||
} catch (error) {
|
||||
this.logger.error(`Could not store analytics data: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-exp
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1', TYPES.StatisticsMiddleware)
|
||||
@controller('/v1')
|
||||
export class ActionsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseHttpController, controller, httpPost } from 'inversify-express-util
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/files', TYPES.StatisticsMiddleware)
|
||||
@controller('/v1/files')
|
||||
export class FilesController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { inject } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1', TYPES.StatisticsMiddleware)
|
||||
@controller('/v1')
|
||||
export class InvoicesController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-exp
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/items', TYPES.StatisticsMiddleware, TYPES.AuthMiddleware)
|
||||
@controller('/v1/items', TYPES.AuthMiddleware)
|
||||
export class ItemsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseHttpController, controller, httpGet, httpPost } from 'inversify-exp
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/offline', TYPES.StatisticsMiddleware)
|
||||
@controller('/v1/offline')
|
||||
export class OfflineController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { all, BaseHttpController, controller, httpDelete, httpGet, httpPost } fr
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1', TYPES.StatisticsMiddleware)
|
||||
@controller('/v1')
|
||||
export class PaymentsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BaseHttpController, controller, httpDelete, httpGet } from 'inversify-e
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/items/:item_id/revisions', TYPES.StatisticsMiddleware, TYPES.AuthMiddleware)
|
||||
@controller('/v1/items/:item_id/revisions', TYPES.AuthMiddleware)
|
||||
export class RevisionsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'i
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/sessions', TYPES.StatisticsMiddleware)
|
||||
@controller('/v1/sessions')
|
||||
export class SessionsController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseHttpController, controller, httpDelete, httpGet, httpPost } from 'i
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/subscription-invites', TYPES.StatisticsMiddleware)
|
||||
@controller('/v1/subscription-invites')
|
||||
export class SubscriptionInvitesController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseHttpController, controller, httpPost } from 'inversify-express-util
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v1/subscription-tokens', TYPES.StatisticsMiddleware)
|
||||
@controller('/v1/subscription-tokens')
|
||||
export class TokensController extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -16,7 +16,7 @@ import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
import { TokenAuthenticationMethod } from '../TokenAuthenticationMethod'
|
||||
|
||||
@controller('/v1/users', TYPES.StatisticsMiddleware)
|
||||
@controller('/v1/users')
|
||||
export class UsersController extends BaseHttpController {
|
||||
constructor(
|
||||
@inject(TYPES.HTTPService) private httpService: HttpServiceInterface,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BaseHttpController, controller, httpPost } from 'inversify-express-util
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v2', TYPES.StatisticsMiddleware)
|
||||
@controller('/v2')
|
||||
export class ActionsControllerV2 extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { inject } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
|
||||
|
||||
@controller('/v2', TYPES.StatisticsMiddleware)
|
||||
@controller('/v2')
|
||||
export class PaymentsControllerV2 extends BaseHttpController {
|
||||
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
|
||||
super()
|
||||
|
||||
@@ -67,6 +67,3 @@ VALET_TOKEN_SECRET=
|
||||
VALET_TOKEN_TTL=
|
||||
|
||||
WEB_SOCKET_CONNECTION_TOKEN_SECRET=
|
||||
|
||||
# (Optional) Analytics
|
||||
ANALYTICS_ENABLED=false
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Stream } from 'stream'
|
||||
import { Logger } from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
@@ -19,44 +18,17 @@ import { MuteFailedBackupsEmailsOption, MuteFailedCloudBackupsEmailsOption, Sett
|
||||
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../src/Domain/Analytics/AnalyticsEntityRepositoryInterface'
|
||||
|
||||
const inputArgs = process.argv.slice(2)
|
||||
const backupProvider = inputArgs[0]
|
||||
const backupFrequency = inputArgs[1]
|
||||
|
||||
const shouldEmailBackupBeTriggered = async (
|
||||
analyticsId: number,
|
||||
analyticsStore: AnalyticsStoreInterface,
|
||||
): Promise<boolean> => {
|
||||
let periods = [Period.Today, Period.Yesterday]
|
||||
if (backupFrequency === 'weekly') {
|
||||
periods = [Period.ThisWeek, Period.LastWeek]
|
||||
}
|
||||
|
||||
for (const period of periods) {
|
||||
const wasUnBackedUpDataCreatedInPeriod = await analyticsStore.wasActivityDone(
|
||||
AnalyticsActivity.EmailUnbackedUpData,
|
||||
analyticsId,
|
||||
period,
|
||||
)
|
||||
if (wasUnBackedUpDataCreatedInPeriod) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const requestBackups = async (
|
||||
settingRepository: SettingRepositoryInterface,
|
||||
roleService: RoleServiceInterface,
|
||||
settingService: SettingServiceInterface,
|
||||
domainEventFactory: DomainEventFactoryInterface,
|
||||
domainEventPublisher: DomainEventPublisherInterface,
|
||||
analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||
analyticsStore: AnalyticsStoreInterface,
|
||||
logger: Logger,
|
||||
): Promise<void> => {
|
||||
let settingName: SettingName,
|
||||
permissionName: PermissionName,
|
||||
@@ -123,23 +95,6 @@ const requestBackups = async (
|
||||
}
|
||||
|
||||
if (backupProvider === 'email') {
|
||||
const analyticsEntity = await analyticsEntityRepository.findOneByUserUuid(setting.setting_user_uuid)
|
||||
if (analyticsEntity === null) {
|
||||
callback()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const emailBackupsShouldBeTriggered = await shouldEmailBackupBeTriggered(
|
||||
analyticsEntity.id,
|
||||
analyticsStore,
|
||||
)
|
||||
if (!emailBackupsShouldBeTriggered) {
|
||||
logger.info(
|
||||
`Email backup for user ${setting.setting_user_uuid} should not be triggered due to inactivity. It will be triggered until further changes.`,
|
||||
)
|
||||
}
|
||||
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createEmailBackupRequestedEvent(
|
||||
setting.setting_user_uuid,
|
||||
@@ -148,15 +103,6 @@ const requestBackups = async (
|
||||
),
|
||||
)
|
||||
|
||||
await analyticsStore.markActivity([AnalyticsActivity.EmailBackup], analyticsEntity.id, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
])
|
||||
await analyticsStore.unmarkActivity([AnalyticsActivity.EmailUnbackedUpData], analyticsEntity.id, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
])
|
||||
|
||||
callback()
|
||||
|
||||
return
|
||||
@@ -206,20 +152,9 @@ void container.load().then((container) => {
|
||||
const settingService: SettingServiceInterface = container.get(TYPES.SettingService)
|
||||
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
|
||||
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
|
||||
const analyticsEntityRepository: AnalyticsEntityRepositoryInterface = container.get(TYPES.AnalyticsEntityRepository)
|
||||
const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore)
|
||||
|
||||
Promise.resolve(
|
||||
requestBackups(
|
||||
settingRepository,
|
||||
roleService,
|
||||
settingService,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
analyticsEntityRepository,
|
||||
analyticsStore,
|
||||
logger,
|
||||
),
|
||||
requestBackups(settingRepository, roleService, settingService, domainEventFactory, domainEventPublisher),
|
||||
)
|
||||
.then(() => {
|
||||
logger.info(`${backupFrequency} ${backupProvider} backup requesting complete`)
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'newrelic'
|
||||
import { Logger } from 'winston'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
@@ -16,7 +15,6 @@ import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingReposit
|
||||
import { MuteFailedBackupsEmailsOption, SettingName } from '@standardnotes/settings'
|
||||
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
|
||||
import { PermissionName } from '@standardnotes/features'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../src/Domain/Analytics/AnalyticsEntityRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../src/Domain/User/UserRepositoryInterface'
|
||||
|
||||
const inputArgs = process.argv.slice(2)
|
||||
@@ -28,8 +26,6 @@ const requestBackups = async (
|
||||
roleService: RoleServiceInterface,
|
||||
domainEventFactory: DomainEventFactoryInterface,
|
||||
domainEventPublisher: DomainEventPublisherInterface,
|
||||
analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||
analyticsStore: AnalyticsStoreInterface,
|
||||
): Promise<void> => {
|
||||
const permissionName = PermissionName.DailyEmailBackup
|
||||
const muteEmailsSettingName = SettingName.MuteFailedBackupsEmails
|
||||
@@ -55,11 +51,6 @@ const requestBackups = async (
|
||||
userHasEmailsMuted = emailsMutedSetting.value === muteEmailsSettingValue
|
||||
}
|
||||
|
||||
const analyticsEntity = await analyticsEntityRepository.findOneByUserUuid(user.uuid)
|
||||
if (analyticsEntity === null) {
|
||||
return
|
||||
}
|
||||
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createEmailBackupRequestedEvent(
|
||||
user.uuid,
|
||||
@@ -68,15 +59,6 @@ const requestBackups = async (
|
||||
),
|
||||
)
|
||||
|
||||
await analyticsStore.markActivity([AnalyticsActivity.EmailBackup], analyticsEntity.id, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
])
|
||||
await analyticsStore.unmarkActivity([AnalyticsActivity.EmailUnbackedUpData], analyticsEntity.id, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -96,19 +78,9 @@ void container.load().then((container) => {
|
||||
const roleService: RoleServiceInterface = container.get(TYPES.RoleService)
|
||||
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
|
||||
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
|
||||
const analyticsEntityRepository: AnalyticsEntityRepositoryInterface = container.get(TYPES.AnalyticsEntityRepository)
|
||||
const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore)
|
||||
|
||||
Promise.resolve(
|
||||
requestBackups(
|
||||
userRepository,
|
||||
settingRepository,
|
||||
roleService,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
analyticsEntityRepository,
|
||||
analyticsStore,
|
||||
),
|
||||
requestBackups(userRepository, settingRepository, roleService, domainEventFactory, domainEventPublisher),
|
||||
)
|
||||
.then(() => {
|
||||
logger.info(`Email backup requesting complete for ${backupEmail}`)
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"dependencies": {
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/analytics": "workspace:*",
|
||||
"@standardnotes/api": "^1.19.0",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
|
||||
@@ -9,13 +9,6 @@ import {
|
||||
} from '@standardnotes/domain-events'
|
||||
import { TimerInterface, Timer } from '@standardnotes/time'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
import {
|
||||
AnalyticsStoreInterface,
|
||||
PeriodKeyGenerator,
|
||||
RedisAnalyticsStore,
|
||||
RedisStatisticsStore,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
@@ -191,21 +184,13 @@ import { RoleRepositoryInterface } from '../Domain/Role/RoleRepositoryInterface'
|
||||
import { RevokedSessionRepositoryInterface } from '../Domain/Session/RevokedSessionRepositoryInterface'
|
||||
import { SessionRepositoryInterface } from '../Domain/Session/SessionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../Domain/User/UserRepositoryInterface'
|
||||
import { AnalyticsEntity } from '../Domain/Analytics/AnalyticsEntity'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../Domain/Analytics/AnalyticsEntityRepositoryInterface'
|
||||
import { MySQLAnalyticsEntityRepository } from '../Infra/MySQL/MySQLAnalyticsEntityRepository'
|
||||
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AuthController } from '../Controller/AuthController'
|
||||
import { VerifyPredicate } from '../Domain/UseCase/VerifyPredicate/VerifyPredicate'
|
||||
import { PredicateVerificationRequestedEventHandler } from '../Domain/Handler/PredicateVerificationRequestedEventHandler'
|
||||
import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventHandler'
|
||||
import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEventHandler'
|
||||
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
|
||||
import { SubscriptionInvitesController } from '../Controller/SubscriptionInvitesController'
|
||||
import { CreateCrossServiceToken } from '../Domain/UseCase/CreateCrossServiceToken/CreateCrossServiceToken'
|
||||
import { ProcessUserRequest } from '../Domain/UseCase/ProcessUserRequest/ProcessUserRequest'
|
||||
import { UserRequestsController } from '../Controller/UserRequestsController'
|
||||
import { SubscriptionReactivatedEventHandler } from '../Domain/Handler/SubscriptionReactivatedEventHandler'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -301,9 +286,6 @@ export class ContainerConfigLoader {
|
||||
.bind<SharedSubscriptionInvitationRepositoryInterface>(TYPES.SharedSubscriptionInvitationRepository)
|
||||
.to(MySQLSharedSubscriptionInvitationRepository)
|
||||
container.bind<PKCERepositoryInterface>(TYPES.PKCERepository).to(RedisPKCERepository)
|
||||
container
|
||||
.bind<AnalyticsEntityRepositoryInterface>(TYPES.AnalyticsEntityRepository)
|
||||
.to(MySQLAnalyticsEntityRepository)
|
||||
|
||||
// ORM
|
||||
container
|
||||
@@ -332,9 +314,6 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<Repository<UserSubscription>>(TYPES.ORMUserSubscriptionRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(UserSubscription))
|
||||
container
|
||||
.bind<Repository<AnalyticsEntity>>(TYPES.ORMAnalyticsEntityRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(AnalyticsEntity))
|
||||
|
||||
// Middleware
|
||||
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
|
||||
@@ -379,7 +358,6 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind(TYPES.DISABLE_USER_REGISTRATION)
|
||||
.toConstantValue(env.get('DISABLE_USER_REGISTRATION', true) === 'true')
|
||||
container.bind(TYPES.ANALYTICS_ENABLED).toConstantValue(env.get('ANALYTICS_ENABLED', true) === 'true')
|
||||
container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
|
||||
container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
|
||||
@@ -439,7 +417,6 @@ export class ContainerConfigLoader {
|
||||
.bind<ListSharedSubscriptionInvitations>(TYPES.ListSharedSubscriptionInvitations)
|
||||
.to(ListSharedSubscriptionInvitations)
|
||||
container.bind<GetSubscriptionSetting>(TYPES.GetSubscriptionSetting).to(GetSubscriptionSetting)
|
||||
container.bind<GetUserAnalyticsId>(TYPES.GetUserAnalyticsId).to(GetUserAnalyticsId)
|
||||
container.bind<VerifyPredicate>(TYPES.VerifyPredicate).to(VerifyPredicate)
|
||||
container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
|
||||
container.bind<ProcessUserRequest>(TYPES.ProcessUserRequest).to(ProcessUserRequest)
|
||||
@@ -491,12 +468,6 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<PredicateVerificationRequestedEventHandler>(TYPES.PredicateVerificationRequestedEventHandler)
|
||||
.to(PredicateVerificationRequestedEventHandler)
|
||||
container.bind<PaymentFailedEventHandler>(TYPES.PaymentFailedEventHandler).to(PaymentFailedEventHandler)
|
||||
container.bind<PaymentSuccessEventHandler>(TYPES.PaymentSuccessEventHandler).to(PaymentSuccessEventHandler)
|
||||
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
||||
container
|
||||
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
||||
.to(SubscriptionReactivatedEventHandler)
|
||||
|
||||
// Services
|
||||
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
|
||||
@@ -562,13 +533,6 @@ export class ContainerConfigLoader {
|
||||
.bind<SelectorInterface<boolean>>(TYPES.BooleanSelector)
|
||||
.toConstantValue(new DeterministicSelector<boolean>())
|
||||
container.bind<UserSubscriptionServiceInterface>(TYPES.UserSubscriptionService).to(UserSubscriptionService)
|
||||
const periodKeyGenerator = new PeriodKeyGenerator()
|
||||
container
|
||||
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
|
||||
.toConstantValue(new RedisAnalyticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
|
||||
container
|
||||
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
||||
.toConstantValue(new RedisStatisticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
|
||||
container.bind<ValidatorInterface<Uuid>>(TYPES.UuidValidator).toConstantValue(new UuidValidator())
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
@@ -605,10 +569,6 @@ export class ContainerConfigLoader {
|
||||
],
|
||||
['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.SharedSubscriptionInvitationCreatedEventHandler)],
|
||||
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.PredicateVerificationRequestedEventHandler)],
|
||||
['PAYMENT_FAILED', container.get(TYPES.PaymentFailedEventHandler)],
|
||||
['PAYMENT_SUCCESS', container.get(TYPES.PaymentSuccessEventHandler)],
|
||||
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
||||
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
||||
])
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DataSource, LoggerOptions } from 'typeorm'
|
||||
import { AnalyticsEntity } from '../Domain/Analytics/AnalyticsEntity'
|
||||
import { Permission } from '../Domain/Permission/Permission'
|
||||
import { Role } from '../Domain/Role/Role'
|
||||
import { RevokedSession } from '../Domain/Session/RevokedSession'
|
||||
@@ -57,7 +56,6 @@ export const AppDataSource = new DataSource({
|
||||
OfflineSetting,
|
||||
SharedSubscriptionInvitation,
|
||||
SubscriptionSetting,
|
||||
AnalyticsEntity,
|
||||
],
|
||||
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
|
||||
migrationsRun: true,
|
||||
|
||||
@@ -23,7 +23,6 @@ const TYPES = {
|
||||
OfflineSubscriptionTokenRepository: Symbol.for('OfflineSubscriptionTokenRepository'),
|
||||
SharedSubscriptionInvitationRepository: Symbol.for('SharedSubscriptionInvitationRepository'),
|
||||
PKCERepository: Symbol.for('PKCERepository'),
|
||||
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
|
||||
// ORM
|
||||
ORMOfflineSettingRepository: Symbol.for('ORMOfflineSettingRepository'),
|
||||
ORMOfflineUserSubscriptionRepository: Symbol.for('ORMOfflineUserSubscriptionRepository'),
|
||||
@@ -35,7 +34,6 @@ const TYPES = {
|
||||
ORMSubscriptionSettingRepository: Symbol.for('ORMSubscriptionSettingRepository'),
|
||||
ORMUserRepository: Symbol.for('ORMUserRepository'),
|
||||
ORMUserSubscriptionRepository: Symbol.for('ORMUserSubscriptionRepository'),
|
||||
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
|
||||
// Middleware
|
||||
AuthMiddleware: Symbol.for('AuthMiddleware'),
|
||||
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),
|
||||
@@ -71,7 +69,6 @@ const TYPES = {
|
||||
PSEUDO_KEY_PARAMS_KEY: Symbol.for('PSEUDO_KEY_PARAMS_KEY'),
|
||||
REDIS_URL: Symbol.for('REDIS_URL'),
|
||||
DISABLE_USER_REGISTRATION: Symbol.for('DISABLE_USER_REGISTRATION'),
|
||||
ANALYTICS_ENABLED: Symbol.for('ANALYTICS_ENABLED'),
|
||||
SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'),
|
||||
SNS_AWS_REGION: Symbol.for('SNS_AWS_REGION'),
|
||||
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
|
||||
@@ -119,7 +116,6 @@ const TYPES = {
|
||||
CancelSharedSubscriptionInvitation: Symbol.for('CancelSharedSubscriptionInvitation'),
|
||||
ListSharedSubscriptionInvitations: Symbol.for('ListSharedSubscriptionInvitations'),
|
||||
GetSubscriptionSetting: Symbol.for('GetSubscriptionSetting'),
|
||||
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
||||
VerifyPredicate: Symbol.for('VerifyPredicate'),
|
||||
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
|
||||
ProcessUserRequest: Symbol.for('ProcessUserRequest'),
|
||||
@@ -142,10 +138,6 @@ const TYPES = {
|
||||
UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'),
|
||||
SharedSubscriptionInvitationCreatedEventHandler: Symbol.for('SharedSubscriptionInvitationCreatedEventHandler'),
|
||||
PredicateVerificationRequestedEventHandler: Symbol.for('PredicateVerificationRequestedEventHandler'),
|
||||
PaymentFailedEventHandler: Symbol.for('PaymentFailedEventHandler'),
|
||||
PaymentSuccessEventHandler: Symbol.for('PaymentSuccessEventHandler'),
|
||||
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
||||
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
||||
// Services
|
||||
DeviceDetector: Symbol.for('DeviceDetector'),
|
||||
SessionService: Symbol.for('SessionService'),
|
||||
@@ -187,8 +179,6 @@ const TYPES = {
|
||||
ProtocolVersionSelector: Symbol.for('ProtocolVersionSelector'),
|
||||
BooleanSelector: Symbol.for('BooleanSelector'),
|
||||
UserSubscriptionService: Symbol.for('UserSubscriptionService'),
|
||||
AnalyticsStore: Symbol.for('AnalyticsStore'),
|
||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||
UuidValidator: Symbol.for('UuidValidator'),
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Role } from '../Domain/Role/Role'
|
||||
import { SettingServiceInterface } from '../Domain/Setting/SettingServiceInterface'
|
||||
import { Setting } from '../Domain/Setting/Setting'
|
||||
import { CrossServiceTokenData, TokenEncoderInterface } from '@standardnotes/security'
|
||||
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
describe('SubscriptionTokensController', () => {
|
||||
let createSubscriptionToken: CreateSubscriptionToken
|
||||
@@ -24,7 +23,6 @@ describe('SubscriptionTokensController', () => {
|
||||
let settingService: SettingServiceInterface
|
||||
let extensionKeySetting: Setting
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
|
||||
let request: express.Request
|
||||
let response: express.Response
|
||||
@@ -39,7 +37,6 @@ describe('SubscriptionTokensController', () => {
|
||||
userProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
getUserAnalyticsId,
|
||||
jwtTTL,
|
||||
)
|
||||
|
||||
@@ -78,9 +75,6 @@ describe('SubscriptionTokensController', () => {
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<CrossServiceTokenData>>
|
||||
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 123 })
|
||||
|
||||
request = {
|
||||
headers: {},
|
||||
body: {},
|
||||
@@ -137,7 +131,6 @@ describe('SubscriptionTokensController', () => {
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
extensionKey: 'abc123',
|
||||
roles: [
|
||||
{
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Role } from '../Domain/Role/Role'
|
||||
import { SettingServiceInterface } from '../Domain/Setting/SettingServiceInterface'
|
||||
import { AuthenticateSubscriptionToken } from '../Domain/UseCase/AuthenticateSubscriptionToken/AuthenticateSubscriptionToken'
|
||||
import { CreateSubscriptionToken } from '../Domain/UseCase/CreateSubscriptionToken/CreateSubscriptionToken'
|
||||
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { User } from '../Domain/User/User'
|
||||
import { ProjectorInterface } from '../Projection/ProjectorInterface'
|
||||
|
||||
@@ -29,7 +28,6 @@ export class SubscriptionTokensController extends BaseHttpController {
|
||||
@inject(TYPES.UserProjector) private userProjector: ProjectorInterface<User>,
|
||||
@inject(TYPES.RoleProjector) private roleProjector: ProjectorInterface<Role>,
|
||||
@inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number,
|
||||
) {
|
||||
super()
|
||||
@@ -88,13 +86,10 @@ export class SubscriptionTokensController extends BaseHttpController {
|
||||
|
||||
const roles = await user.roles
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
|
||||
const authTokenData: CrossServiceTokenData = {
|
||||
user: await this.projectUser(user),
|
||||
roles: await this.projectRoles(roles),
|
||||
extensionKey,
|
||||
analyticsId,
|
||||
}
|
||||
|
||||
const authToken = this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { User } from '../User/User'
|
||||
|
||||
@Entity({ name: 'analytics_entities' })
|
||||
export class AnalyticsEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
declare id: number
|
||||
|
||||
@OneToOne(
|
||||
/* istanbul ignore next */
|
||||
() => User,
|
||||
/* istanbul ignore next */
|
||||
(user) => user.analyticsEntity,
|
||||
/* istanbul ignore next */
|
||||
{ onDelete: 'CASCADE', nullable: false, lazy: true, eager: false },
|
||||
)
|
||||
@JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' })
|
||||
declare user: Promise<User>
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { AnalyticsEntity } from './AnalyticsEntity'
|
||||
|
||||
export interface AnalyticsEntityRepositoryInterface {
|
||||
save(analyticsEntity: AnalyticsEntity): Promise<AnalyticsEntity>
|
||||
findOneByUserUuid(userUuid: Uuid): Promise<AnalyticsEntity | null>
|
||||
}
|
||||
@@ -11,9 +11,6 @@ import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterfac
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AnalyticsStoreInterface, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
describe('AccountDeletionRequestedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -26,10 +23,6 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
let revokedSession: RevokedSession
|
||||
let user: User
|
||||
let event: AccountDeletionRequestedEvent
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
let timer: TimerInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new AccountDeletionRequestedEventHandler(
|
||||
@@ -37,10 +30,6 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
sessionRepository,
|
||||
ephemeralSessionRepository,
|
||||
revokedSessionRepository,
|
||||
getUserAnalyticsId,
|
||||
analyticsStore,
|
||||
statisticsStore,
|
||||
timer,
|
||||
logger,
|
||||
)
|
||||
|
||||
@@ -84,22 +73,9 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
regularSubscriptionUuid: '2-3-4',
|
||||
}
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
|
||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
||||
statisticsStore.incrementMeasure = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
||||
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(100)
|
||||
})
|
||||
|
||||
it('should remove a user', async () => {
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import {
|
||||
AnalyticsActivity,
|
||||
AnalyticsStoreInterface,
|
||||
Period,
|
||||
StatisticsMeasure,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { EphemeralSessionRepositoryInterface } from '../Session/EphemeralSessionRepositoryInterface'
|
||||
import { RevokedSessionRepositoryInterface } from '../Session/RevokedSessionRepositoryInterface'
|
||||
import { SessionRepositoryInterface } from '../Session/SessionRepositoryInterface'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
@@ -23,10 +14,6 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
@inject(TYPES.SessionRepository) private sessionRepository: SessionRepositoryInterface,
|
||||
@inject(TYPES.EphemeralSessionRepository) private ephemeralSessionRepository: EphemeralSessionRepositoryInterface,
|
||||
@inject(TYPES.RevokedSessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -41,21 +28,6 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
||||
|
||||
await this.removeSessions(event.payload.userUuid)
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.DeleteAccount], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
const registrationLength =
|
||||
this.timer.getTimestampInMicroseconds() - this.timer.convertDateToMicroseconds(user.createdAt)
|
||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.RegistrationLength, registrationLength, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
await this.userRepository.remove(user)
|
||||
|
||||
this.logger.info(`Finished account cleanup for user: ${event.payload.userUuid}`)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { PaymentFailedEvent } from '@standardnotes/domain-events'
|
||||
import { AnalyticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
import { PaymentFailedEventHandler } from './PaymentFailedEventHandler'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
describe('PaymentFailedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let event: PaymentFailedEvent
|
||||
let user: User
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
|
||||
const createHandler = () => new PaymentFailedEventHandler(userRepository, getUserAnalyticsId, analyticsStore)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<PaymentFailedEvent>
|
||||
event.payload = {
|
||||
userEmail: 'test@test.com',
|
||||
}
|
||||
})
|
||||
|
||||
it('should mark payment failed for analytics', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not mark payment failed for analytics if user is not found', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentFailedEvent): Promise<void> {
|
||||
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
|
||||
if (user === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentFailed], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { PaymentSuccessEvent } from '@standardnotes/domain-events'
|
||||
import { AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
import { PaymentSuccessEventHandler } from './PaymentSuccessEventHandler'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
describe('PaymentSuccessEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let event: PaymentSuccessEvent
|
||||
let user: User
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () =>
|
||||
new PaymentSuccessEventHandler(userRepository, getUserAnalyticsId, analyticsStore, statisticsStore, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
||||
statisticsStore.incrementMeasure = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<PaymentSuccessEvent>
|
||||
event.payload = {
|
||||
userEmail: 'test@test.com',
|
||||
amount: 12.45,
|
||||
billingFrequency: 12,
|
||||
paymentType: 'initial',
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should mark payment success for analytics', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
||||
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'pro-subscription-initial-annual-payments-income',
|
||||
12.45,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
})
|
||||
|
||||
it('should mark non-detailed payment success statistics for analytics', async () => {
|
||||
event.payload = {
|
||||
userEmail: 'test@test.com',
|
||||
amount: 12.45,
|
||||
billingFrequency: 13,
|
||||
paymentType: 'initial',
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
}
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(statisticsStore.incrementMeasure).toBeCalledTimes(1)
|
||||
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(1, 'income', 12.45, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
})
|
||||
|
||||
it('should not mark payment failed for analytics if user is not found', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,102 +0,0 @@
|
||||
import {
|
||||
AnalyticsActivity,
|
||||
AnalyticsStoreInterface,
|
||||
Period,
|
||||
StatisticsMeasure,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
|
||||
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
||||
private readonly DETAILED_MEASURES = new Map([
|
||||
[
|
||||
SubscriptionName.PlusPlan,
|
||||
new Map([
|
||||
[
|
||||
PaymentType.Initial,
|
||||
new Map([
|
||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome],
|
||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome],
|
||||
]),
|
||||
],
|
||||
[
|
||||
PaymentType.Renewal,
|
||||
new Map([
|
||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome],
|
||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
SubscriptionName.ProPlan,
|
||||
new Map([
|
||||
[
|
||||
PaymentType.Initial,
|
||||
new Map([
|
||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome],
|
||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome],
|
||||
]),
|
||||
],
|
||||
[
|
||||
PaymentType.Renewal,
|
||||
new Map([
|
||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome],
|
||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
],
|
||||
])
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentSuccessEvent): Promise<void> {
|
||||
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
|
||||
if (user === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentSuccess], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
const statisticMeasures = [StatisticsMeasure.Income]
|
||||
|
||||
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
|
||||
?.get(event.payload.paymentType as PaymentType)
|
||||
?.get(event.payload.billingFrequency as SubscriptionBillingFrequency)
|
||||
if (detailedMeasure !== undefined) {
|
||||
statisticMeasures.push(detailedMeasure)
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Could not find detailed measure for: subscription - ${event.payload.subscriptionName}, payment type - ${event.payload.paymentType}, billing frequency - ${event.payload.billingFrequency}`,
|
||||
)
|
||||
}
|
||||
|
||||
for (const measure of statisticMeasures) {
|
||||
await this.statisticsStore.incrementMeasure(measure, event.payload.amount, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,54 +8,19 @@ import * as dayjs from 'dayjs'
|
||||
import { SubscriptionCancelledEventHandler } from './SubscriptionCancelledEventHandler'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { AnalyticsStoreInterface, Period, StatisticsMeasure, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
|
||||
describe('SubscriptionCancelledEventHandler', () => {
|
||||
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
|
||||
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
|
||||
let event: SubscriptionCancelledEvent
|
||||
let userRepository: UserRepositoryInterface
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
let timestamp: number
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionCancelledEventHandler(
|
||||
userSubscriptionRepository,
|
||||
offlineUserSubscriptionRepository,
|
||||
userRepository,
|
||||
getUserAnalyticsId,
|
||||
analyticsStore,
|
||||
statisticsStore,
|
||||
)
|
||||
new SubscriptionCancelledEventHandler(userSubscriptionRepository, offlineUserSubscriptionRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
const user = { uuid: '1-2-3' } as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
||||
statisticsStore.incrementMeasure = jest.fn()
|
||||
|
||||
const userSubscription = {
|
||||
createdAt: 1642395451515000,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.updateCancelled = jest.fn()
|
||||
userSubscriptionRepository.findBySubscriptionId = jest.fn().mockReturnValue([userSubscription])
|
||||
|
||||
offlineUserSubscriptionRepository = {} as jest.Mocked<OfflineUserSubscriptionRepositoryInterface>
|
||||
offlineUserSubscriptionRepository.updateCancelled = jest.fn()
|
||||
@@ -83,35 +48,6 @@ describe('SubscriptionCancelledEventHandler', () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(userSubscriptionRepository.updateCancelled).toHaveBeenCalledWith(1, true, 1642395451516000)
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.SubscriptionLength, 1000, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
})
|
||||
|
||||
it('should not track statistics for subscriptions that are in a legacy 5 year plan', async () => {
|
||||
event.payload.timestamp = 1642395451516000
|
||||
|
||||
const userSubscription = {
|
||||
createdAt: 1642395451515000,
|
||||
endsAt: 1642395451515000 + 126_230_400_000_001,
|
||||
} as jest.Mocked<UserSubscription>
|
||||
userSubscriptionRepository.findBySubscriptionId = jest.fn().mockReturnValue([userSubscription])
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update subscription cancelled - user not found', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(userSubscriptionRepository.updateCancelled).toHaveBeenCalledWith(1, true, timestamp)
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update offline subscription cancelled', async () => {
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import {
|
||||
AnalyticsActivity,
|
||||
AnalyticsStoreInterface,
|
||||
Period,
|
||||
StatisticsMeasure,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -21,24 +11,9 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
||||
@inject(TYPES.UserSubscriptionRepository) private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.OfflineUserSubscriptionRepository)
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
|
||||
if (user !== null) {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionCancelled], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
|
||||
await this.trackSubscriptionStatistics(event)
|
||||
|
||||
if (event.payload.offline) {
|
||||
await this.updateOfflineSubscriptionCancelled(event.payload.subscriptionId, event.payload.timestamp)
|
||||
|
||||
@@ -55,39 +30,4 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
||||
private async updateOfflineSubscriptionCancelled(subscriptionId: number, timestamp: number): Promise<void> {
|
||||
await this.offlineUserSubscriptionRepository.updateCancelled(subscriptionId, true, timestamp)
|
||||
}
|
||||
|
||||
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
|
||||
const subscriptions = await this.userSubscriptionRepository.findBySubscriptionId(event.payload.subscriptionId)
|
||||
if (subscriptions.length !== 0) {
|
||||
const lastSubscription = subscriptions.shift() as UserSubscription
|
||||
if (this.isLegacy5yearSubscriptionPlan(lastSubscription)) {
|
||||
return
|
||||
}
|
||||
|
||||
const subscriptionLength = event.payload.timestamp - lastSubscription.createdAt
|
||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
const lastPurchaseTime = lastSubscription.renewedAt ?? lastSubscription.updatedAt
|
||||
const remainingSubscriptionTime = lastSubscription.endsAt - event.payload.timestamp
|
||||
const totalSubscriptionTime = lastSubscription.endsAt - lastPurchaseTime
|
||||
|
||||
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
||||
|
||||
await this.statisticsStore.incrementMeasure(
|
||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
||||
remainingSubscriptionPercentage,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private isLegacy5yearSubscriptionPlan(subscription: UserSubscription) {
|
||||
const fourYearsInMicroseconds = 126_230_400_000_000
|
||||
|
||||
return subscription.endsAt - subscription.createdAt > fourYearsInMicroseconds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscri
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { AnalyticsStoreInterface, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
describe('SubscriptionExpiredEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -25,9 +23,6 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
let user: User
|
||||
let event: SubscriptionExpiredEvent
|
||||
let timestamp: number
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionExpiredEventHandler(
|
||||
@@ -35,9 +30,6 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
userSubscriptionRepository,
|
||||
offlineUserSubscriptionRepository,
|
||||
roleService,
|
||||
getUserAnalyticsId,
|
||||
analyticsStore,
|
||||
statisticsStore,
|
||||
logger,
|
||||
)
|
||||
|
||||
@@ -82,15 +74,6 @@ describe('SubscriptionExpiredEventHandler', () => {
|
||||
totalActiveSubscriptionsCount: 123,
|
||||
}
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
||||
statisticsStore.setMeasure = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
|
||||
@@ -8,14 +8,6 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import {
|
||||
AnalyticsStoreInterface,
|
||||
AnalyticsActivity,
|
||||
Period,
|
||||
StatisticsMeasure,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -25,9 +17,6 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||
@inject(TYPES.OfflineUserSubscriptionRepository)
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.RoleService) private roleService: RoleServiceInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -47,21 +36,6 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
||||
|
||||
await this.updateSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)
|
||||
await this.removeRoleFromSubscriptionUsers(event.payload.subscriptionId, event.payload.subscriptionName)
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity(
|
||||
[AnalyticsActivity.SubscriptionExpired, AnalyticsActivity.ExistingCustomersChurn],
|
||||
analyticsId,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
|
||||
const activeSubscriptions = await this.userSubscriptionRepository.countActiveSubscriptions()
|
||||
await this.statisticsStore.setMeasure(StatisticsMeasure.TotalCustomers, activeSubscriptions, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
Period.ThisYear,
|
||||
])
|
||||
}
|
||||
|
||||
private async removeRoleFromSubscriptionUsers(
|
||||
|
||||
@@ -16,10 +16,6 @@ import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/Offl
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
import { AnalyticsEntity } from '../Analytics/AnalyticsEntity'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
describe('SubscriptionPurchasedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -33,11 +29,7 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
let event: SubscriptionPurchasedEvent
|
||||
let subscriptionExpiresAt: number
|
||||
let subscriptionSettingService: SubscriptionSettingServiceInterface
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let timestamp: number
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
let timer: TimerInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionPurchasedEventHandler(
|
||||
@@ -46,10 +38,6 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
offlineUserSubscriptionRepository,
|
||||
roleService,
|
||||
subscriptionSettingService,
|
||||
getUserAnalyticsId,
|
||||
analyticsStore,
|
||||
statisticsStore,
|
||||
timer,
|
||||
logger,
|
||||
)
|
||||
|
||||
@@ -71,13 +59,6 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
|
||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
||||
statisticsStore.incrementMeasure = jest.fn()
|
||||
statisticsStore.setMeasure = jest.fn()
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(1)
|
||||
|
||||
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
|
||||
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(0)
|
||||
userSubscriptionRepository.countActiveSubscriptions = jest.fn().mockReturnValue(13)
|
||||
@@ -114,13 +95,6 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
|
||||
subscriptionSettingService.applyDefaultSubscriptionSettingsForSubscription = jest.fn()
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
analyticsStore.unmarkActivity = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
@@ -166,37 +140,6 @@ describe('SubscriptionPurchasedEventHandler', () => {
|
||||
updatedAt: expect.any(Number),
|
||||
cancelled: false,
|
||||
})
|
||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should not measure registration to subscription time if this is not user's first subscription", async () => {
|
||||
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(1)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update analytics on limited discount offer purchasing', async () => {
|
||||
const analyticsEntity = { id: 3 } as jest.Mocked<AnalyticsEntity>
|
||||
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.CoreUser,
|
||||
},
|
||||
]),
|
||||
analyticsEntity: Promise.resolve(analyticsEntity),
|
||||
} as jest.Mocked<User>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
event.payload.discountCode = 'limited-10'
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['limited-discount-offer-purchased'], 3, [Period.Today])
|
||||
})
|
||||
|
||||
it('should create an offline subscription', async () => {
|
||||
|
||||
@@ -13,15 +13,6 @@ import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
|
||||
import { SubscriptionSettingServiceInterface } from '../Setting/SubscriptionSettingServiceInterface'
|
||||
import {
|
||||
AnalyticsActivity,
|
||||
AnalyticsStoreInterface,
|
||||
Period,
|
||||
StatisticsMeasure,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -32,10 +23,6 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.RoleService) private roleService: RoleServiceInterface,
|
||||
@inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -61,8 +48,6 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
return
|
||||
}
|
||||
|
||||
const previousSubscriptionCount = await this.userSubscriptionRepository.countByUserUuid(user.uuid)
|
||||
|
||||
const userSubscription = await this.createSubscription(
|
||||
event.payload.subscriptionId,
|
||||
event.payload.subscriptionName,
|
||||
@@ -78,48 +63,6 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
||||
event.payload.subscriptionName,
|
||||
user.uuid,
|
||||
)
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionPurchased], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
await this.analyticsStore.unmarkActivity(
|
||||
[AnalyticsActivity.ExistingCustomersChurn, AnalyticsActivity.NewCustomersChurn],
|
||||
analyticsId,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
|
||||
const limitedDiscountPurchased = ['limited-10', 'limited-20', 'exit-20'].includes(
|
||||
event.payload.discountCode as string,
|
||||
)
|
||||
if (limitedDiscountPurchased) {
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.LimitedDiscountOfferPurchased], analyticsId, [
|
||||
Period.Today,
|
||||
])
|
||||
}
|
||||
|
||||
if (previousSubscriptionCount === 0) {
|
||||
await this.statisticsStore.incrementMeasure(
|
||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
||||
event.payload.timestamp - this.timer.convertDateToMicroseconds(user.createdAt),
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.NewCustomers, 1, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
Period.ThisYear,
|
||||
])
|
||||
const activeSubscriptions = await this.userSubscriptionRepository.countActiveSubscriptions()
|
||||
await this.statisticsStore.setMeasure(StatisticsMeasure.TotalCustomers, activeSubscriptions, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
Period.ThisYear,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
private async addUserRole(user: User, subscriptionName: SubscriptionName): Promise<void> {
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { RoleName, SubscriptionName } from '@standardnotes/common'
|
||||
import { SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { SubscriptionReactivatedEventHandler } from './SubscriptionReactivatedEventHandler'
|
||||
import { AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
describe('SubscriptionReactivatedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let logger: Logger
|
||||
let user: User
|
||||
let event: SubscriptionReactivatedEvent
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionReactivatedEventHandler(userRepository, analyticsStore, getUserAnalyticsId, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {
|
||||
uuid: '123',
|
||||
email: 'test@test.com',
|
||||
roles: Promise.resolve([
|
||||
{
|
||||
name: RoleName.ProUser,
|
||||
},
|
||||
]),
|
||||
} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
userRepository.save = jest.fn().mockReturnValue(user)
|
||||
|
||||
event = {} as jest.Mocked<SubscriptionReactivatedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
previousSubscriptionId: 1,
|
||||
currentSubscriptionId: 2,
|
||||
userEmail: 'test@test.com',
|
||||
subscriptionName: SubscriptionName.PlusPlan,
|
||||
subscriptionExpiresAt: 5,
|
||||
discountCode: 'exit-20',
|
||||
}
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should mark subscription reactivated activity for analytics', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['subscription-reactivated'], 3, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
})
|
||||
|
||||
it('should not do anything if no user is found for specified email', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionReactivatedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
|
||||
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
|
||||
|
||||
if (user === null) {
|
||||
this.logger.warn(`Could not find user with email: ${event.payload.userEmail}`)
|
||||
return
|
||||
}
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionReactivated], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,6 @@ import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscri
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
describe('SubscriptionRefundedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -25,9 +23,6 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
let user: User
|
||||
let event: SubscriptionRefundedEvent
|
||||
let timestamp: number
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionRefundedEventHandler(
|
||||
@@ -35,9 +30,6 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
userSubscriptionRepository,
|
||||
offlineUserSubscriptionRepository,
|
||||
roleService,
|
||||
getUserAnalyticsId,
|
||||
analyticsStore,
|
||||
statisticsStore,
|
||||
logger,
|
||||
)
|
||||
|
||||
@@ -84,16 +76,6 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
totalActiveSubscriptionsCount: 1,
|
||||
}
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(true)
|
||||
|
||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
||||
statisticsStore.setMeasure = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.info = jest.fn()
|
||||
logger.warn = jest.fn()
|
||||
@@ -129,33 +111,4 @@ describe('SubscriptionRefundedEventHandler', () => {
|
||||
expect(roleService.removeUserRole).not.toHaveBeenCalled()
|
||||
expect(userSubscriptionRepository.updateEndsAt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should mark churn for new customer', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.NewCustomersChurn], 3, [
|
||||
Period.ThisMonth,
|
||||
])
|
||||
})
|
||||
|
||||
it('should mark churn for existing customer', async () => {
|
||||
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(3)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [
|
||||
Period.ThisMonth,
|
||||
])
|
||||
})
|
||||
|
||||
it('should not mark churn if customer did not purchase subscription in defined analytic periods', async () => {
|
||||
userSubscriptionRepository.countByUserUuid = jest.fn().mockReturnValue(3)
|
||||
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(false)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [
|
||||
Period.ThisMonth,
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SubscriptionName, Uuid } from '@standardnotes/common'
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
@@ -8,14 +8,6 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
|
||||
import {
|
||||
AnalyticsActivity,
|
||||
AnalyticsStoreInterface,
|
||||
Period,
|
||||
StatisticsMeasure,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionRefundedEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -25,9 +17,6 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
@inject(TYPES.OfflineUserSubscriptionRepository)
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.RoleService) private roleService: RoleServiceInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -47,15 +36,6 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
|
||||
await this.updateSubscriptionEndsAt(event.payload.subscriptionId, event.payload.timestamp)
|
||||
await this.removeRoleFromSubscriptionUsers(event.payload.subscriptionId, event.payload.subscriptionName)
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionRefunded], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
await this.markChurnActivity(analyticsId, user.uuid)
|
||||
}
|
||||
|
||||
private async removeRoleFromSubscriptionUsers(
|
||||
@@ -75,30 +55,4 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
||||
private async updateOfflineSubscriptionEndsAt(subscriptionId: number, timestamp: number): Promise<void> {
|
||||
await this.offlineUserSubscriptionRepository.updateEndsAt(subscriptionId, timestamp, timestamp)
|
||||
}
|
||||
|
||||
private async markChurnActivity(analyticsId: number, userUuid: Uuid): Promise<void> {
|
||||
const existingSubscriptionsCount = await this.userSubscriptionRepository.countByUserUuid(userUuid)
|
||||
|
||||
const churnActivity =
|
||||
existingSubscriptionsCount > 1 ? AnalyticsActivity.ExistingCustomersChurn : AnalyticsActivity.NewCustomersChurn
|
||||
|
||||
for (const period of [Period.ThisMonth, Period.ThisWeek, Period.Today]) {
|
||||
const customerPurchasedInPeriod = await this.analyticsStore.wasActivityDone(
|
||||
AnalyticsActivity.SubscriptionPurchased,
|
||||
analyticsId,
|
||||
period,
|
||||
)
|
||||
if (customerPurchasedInPeriod) {
|
||||
await this.analyticsStore.markActivity([churnActivity], analyticsId, [period])
|
||||
}
|
||||
}
|
||||
|
||||
const activeSubscriptions = await this.userSubscriptionRepository.countActiveSubscriptions()
|
||||
await this.statisticsStore.setMeasure(StatisticsMeasure.TotalCustomers, activeSubscriptions, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
Period.ThisYear,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AnalyticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
describe('SubscriptionRenewedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
@@ -28,8 +26,6 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
let event: SubscriptionRenewedEvent
|
||||
let subscriptionExpiresAt: number
|
||||
let timestamp: number
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
|
||||
const createHandler = () =>
|
||||
new SubscriptionRenewedEventHandler(
|
||||
@@ -37,8 +33,6 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
userSubscriptionRepository,
|
||||
offlineUserSubscriptionRepository,
|
||||
roleService,
|
||||
getUserAnalyticsId,
|
||||
analyticsStore,
|
||||
logger,
|
||||
)
|
||||
|
||||
@@ -89,13 +83,6 @@ describe('SubscriptionRenewedEventHandler', () => {
|
||||
offline: false,
|
||||
}
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
analyticsStore.unmarkActivity = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
|
||||
@@ -10,7 +9,6 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { Logger } from 'winston'
|
||||
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -20,8 +18,6 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
@inject(TYPES.OfflineUserSubscriptionRepository)
|
||||
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
|
||||
@inject(TYPES.RoleService) private roleService: RoleServiceInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -63,18 +59,6 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
||||
}
|
||||
|
||||
await this.addRoleToSubscriptionUsers(event.payload.subscriptionId, event.payload.subscriptionName)
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionRenewed], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
await this.analyticsStore.unmarkActivity(
|
||||
[AnalyticsActivity.ExistingCustomersChurn, AnalyticsActivity.NewCustomersChurn],
|
||||
analyticsId,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
}
|
||||
|
||||
private async addRoleToSubscriptionUsers(subscriptionId: number, subscriptionName: SubscriptionName): Promise<void> {
|
||||
|
||||
@@ -4,8 +4,6 @@ import { Logger } from 'winston'
|
||||
|
||||
import { UserRegisteredEventHandler } from './UserRegisteredEventHandler'
|
||||
import { AxiosInstance } from 'axios'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AnalyticsStoreInterface } from '@standardnotes/analytics'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
describe('UserRegisteredEventHandler', () => {
|
||||
@@ -13,19 +11,10 @@ describe('UserRegisteredEventHandler', () => {
|
||||
const userServerRegistrationUrl = 'https://user-server/registration'
|
||||
const userServerAuthKey = 'auth-key'
|
||||
let event: UserRegisteredEvent
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () =>
|
||||
new UserRegisteredEventHandler(
|
||||
httpClient,
|
||||
userServerRegistrationUrl,
|
||||
userServerAuthKey,
|
||||
getUserAnalyticsId,
|
||||
analyticsStore,
|
||||
logger,
|
||||
)
|
||||
new UserRegisteredEventHandler(httpClient, userServerRegistrationUrl, userServerAuthKey, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
httpClient = {} as jest.Mocked<AxiosInstance>
|
||||
@@ -39,12 +28,6 @@ describe('UserRegisteredEventHandler', () => {
|
||||
protocolVersion: ProtocolVersion.V004,
|
||||
}
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.debug = jest.fn()
|
||||
})
|
||||
@@ -71,14 +54,7 @@ describe('UserRegisteredEventHandler', () => {
|
||||
})
|
||||
|
||||
it('should not send a request to the user management server about a registration if url is not defined', async () => {
|
||||
const handler = new UserRegisteredEventHandler(
|
||||
httpClient,
|
||||
'',
|
||||
userServerAuthKey,
|
||||
getUserAnalyticsId,
|
||||
analyticsStore,
|
||||
logger,
|
||||
)
|
||||
const handler = new UserRegisteredEventHandler(httpClient, '', userServerAuthKey, logger)
|
||||
await handler.handle(event)
|
||||
|
||||
expect(httpClient.request).not.toHaveBeenCalled()
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
|
||||
import { AxiosInstance } from 'axios'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
@injectable()
|
||||
export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -13,8 +11,6 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
||||
@inject(TYPES.HTTPClient) private httpClient: AxiosInstance,
|
||||
@inject(TYPES.USER_SERVER_REGISTRATION_URL) private userServerRegistrationUrl: string,
|
||||
@inject(TYPES.USER_SERVER_AUTH_KEY) private userServerAuthKey: string,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -24,13 +20,6 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
||||
return
|
||||
}
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: event.payload.userUuid })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.Register], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
await this.httpClient.request({
|
||||
method: 'POST',
|
||||
url: this.userServerRegistrationUrl,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { Role } from '../../Role/Role'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { GetUserAnalyticsId } from '../GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
|
||||
|
||||
@@ -15,7 +14,6 @@ describe('CreateCrossServiceToken', () => {
|
||||
let sessionProjector: ProjectorInterface<Session>
|
||||
let roleProjector: ProjectorInterface<Role>
|
||||
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let userRepository: UserRepositoryInterface
|
||||
const jwtTTL = 60
|
||||
|
||||
@@ -23,17 +21,8 @@ describe('CreateCrossServiceToken', () => {
|
||||
let user: User
|
||||
let role: Role
|
||||
|
||||
const createUseCase = (analyticsEnabled = true) =>
|
||||
new CreateCrossServiceToken(
|
||||
userProjector,
|
||||
sessionProjector,
|
||||
roleProjector,
|
||||
tokenEncoder,
|
||||
getUserAnalyticsId,
|
||||
userRepository,
|
||||
analyticsEnabled,
|
||||
jwtTTL,
|
||||
)
|
||||
const createUseCase = () =>
|
||||
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
|
||||
|
||||
beforeEach(() => {
|
||||
session = {} as jest.Mocked<Session>
|
||||
@@ -54,9 +43,6 @@ describe('CreateCrossServiceToken', () => {
|
||||
tokenEncoder = {} as jest.Mocked<TokenEncoderInterface<CrossServiceTokenData>>
|
||||
tokenEncoder.encodeExpirableToken = jest.fn().mockReturnValue('foobar')
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 123 })
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
|
||||
})
|
||||
@@ -67,32 +53,6 @@ describe('CreateCrossServiceToken', () => {
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
uuid: '1-3-4',
|
||||
},
|
||||
],
|
||||
session: {
|
||||
test: 'test',
|
||||
},
|
||||
user: {
|
||||
bar: 'baz',
|
||||
},
|
||||
},
|
||||
60,
|
||||
)
|
||||
})
|
||||
|
||||
it('should create a cross service token for user - analytics disabled', async () => {
|
||||
await createUseCase(false).execute({
|
||||
user,
|
||||
session,
|
||||
})
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
roles: [
|
||||
@@ -119,7 +79,6 @@ describe('CreateCrossServiceToken', () => {
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
@@ -141,7 +100,6 @@ describe('CreateCrossServiceToken', () => {
|
||||
|
||||
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
|
||||
{
|
||||
analyticsId: 123,
|
||||
roles: [
|
||||
{
|
||||
name: 'role1',
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Role } from '../../Role/Role'
|
||||
import { Session } from '../../Session/Session'
|
||||
import { User } from '../../User/User'
|
||||
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
|
||||
import { GetUserAnalyticsId } from '../GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
|
||||
import { CreateCrossServiceTokenResponse } from './CreateCrossServiceTokenResponse'
|
||||
@@ -20,9 +19,7 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
@inject(TYPES.SessionProjector) private sessionProjector: ProjectorInterface<Session>,
|
||||
@inject(TYPES.RoleProjector) private roleProjector: ProjectorInterface<Role>,
|
||||
@inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.ANALYTICS_ENABLED) private analyticsEnabled: boolean,
|
||||
@inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number,
|
||||
) {}
|
||||
|
||||
@@ -43,11 +40,6 @@ export class CreateCrossServiceToken implements UseCaseInterface {
|
||||
roles: this.projectRoles(roles),
|
||||
}
|
||||
|
||||
if (this.analyticsEnabled) {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
authTokenData.analyticsId = analyticsId
|
||||
}
|
||||
|
||||
if (dto.session !== undefined) {
|
||||
authTokenData.session = this.projectSession(dto.session)
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { AnalyticsEntity } from '../../Analytics/AnalyticsEntity'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../../Analytics/AnalyticsEntityRepositoryInterface'
|
||||
|
||||
import { GetUserAnalyticsId } from './GetUserAnalyticsId'
|
||||
|
||||
describe('GetUserAnalyticsId', () => {
|
||||
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
|
||||
let analyticsEntity: AnalyticsEntity
|
||||
|
||||
const createUseCase = () => new GetUserAnalyticsId(analyticsEntityRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
analyticsEntity = { id: 123 } as jest.Mocked<AnalyticsEntity>
|
||||
|
||||
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
|
||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(analyticsEntity)
|
||||
})
|
||||
|
||||
it('should return analytics id for a user', async () => {
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ analyticsId: 123 })
|
||||
})
|
||||
|
||||
it('should throw error if user is missing analytics entity', async () => {
|
||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
||||
let error = null
|
||||
|
||||
try {
|
||||
await createUseCase().execute({ userUuid: '1-2-3' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,25 +0,0 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../../Analytics/AnalyticsEntityRepositoryInterface'
|
||||
import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { GetUserAnalyticsIdDTO } from './GetUserAnalyticsIdDTO'
|
||||
import { GetUserAnalyticsIdResponse } from './GetUserAnalyticsIdResponse'
|
||||
|
||||
@injectable()
|
||||
export class GetUserAnalyticsId implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetUserAnalyticsIdDTO): Promise<GetUserAnalyticsIdResponse> {
|
||||
const analyticsEntity = await this.analyticsEntityRepository.findOneByUserUuid(dto.userUuid)
|
||||
|
||||
if (analyticsEntity === null) {
|
||||
throw new Error(`Could not find analytics entity for user ${dto.userUuid}`)
|
||||
}
|
||||
|
||||
return {
|
||||
analyticsId: analyticsEntity.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export type GetUserAnalyticsIdDTO = {
|
||||
userUuid: Uuid
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type GetUserAnalyticsIdResponse = {
|
||||
analyticsId: number
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { User } from '../User/User'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { Register } from './Register'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../Analytics/AnalyticsEntityRepositoryInterface'
|
||||
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
|
||||
|
||||
describe('Register', () => {
|
||||
@@ -20,19 +19,9 @@ describe('Register', () => {
|
||||
let user: User
|
||||
let crypter: CrypterInterface
|
||||
let timer: TimerInterface
|
||||
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
|
||||
|
||||
const createUseCase = () =>
|
||||
new Register(
|
||||
userRepository,
|
||||
roleRepository,
|
||||
authResponseFactory,
|
||||
crypter,
|
||||
false,
|
||||
settingService,
|
||||
timer,
|
||||
analyticsEntityRepository,
|
||||
)
|
||||
new Register(userRepository, roleRepository, authResponseFactory, crypter, false, settingService, timer)
|
||||
|
||||
beforeEach(() => {
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
@@ -55,9 +44,6 @@ describe('Register', () => {
|
||||
|
||||
timer = {} as jest.Mocked<TimerInterface>
|
||||
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
|
||||
|
||||
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
|
||||
analyticsEntityRepository.save = jest.fn()
|
||||
})
|
||||
|
||||
it('should register a new user', async () => {
|
||||
@@ -91,8 +77,6 @@ describe('Register', () => {
|
||||
})
|
||||
|
||||
expect(settingService.applyDefaultSettingsUponRegistration).toHaveBeenCalled()
|
||||
|
||||
expect(analyticsEntityRepository.save).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should register a new user with default role', async () => {
|
||||
@@ -187,7 +171,6 @@ describe('Register', () => {
|
||||
true,
|
||||
settingService,
|
||||
timer,
|
||||
analyticsEntityRepository,
|
||||
).execute({
|
||||
email: 'test@test.te',
|
||||
password: 'asdzxc',
|
||||
|
||||
@@ -14,8 +14,6 @@ import { RoleRepositoryInterface } from '../Role/RoleRepositoryInterface'
|
||||
import { CrypterInterface } from '../Encryption/CrypterInterface'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../Analytics/AnalyticsEntityRepositoryInterface'
|
||||
import { AnalyticsEntity } from '../Analytics/AnalyticsEntity'
|
||||
import { AuthResponseFactory20200115 } from '../Auth/AuthResponseFactory20200115'
|
||||
import { AuthResponse20200115 } from '../Auth/AuthResponse20200115'
|
||||
|
||||
@@ -29,7 +27,6 @@ export class Register implements UseCaseInterface {
|
||||
@inject(TYPES.DISABLE_USER_REGISTRATION) private disableUserRegistration: boolean,
|
||||
@inject(TYPES.SettingService) private settingService: SettingServiceInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||
) {}
|
||||
|
||||
async execute(dto: RegisterDTO): Promise<RegisterResponse> {
|
||||
@@ -77,10 +74,6 @@ export class Register implements UseCaseInterface {
|
||||
|
||||
await this.settingService.applyDefaultSettingsUponRegistration(user)
|
||||
|
||||
const analyticsEntity = new AnalyticsEntity()
|
||||
analyticsEntity.user = Promise.resolve(user)
|
||||
await this.analyticsEntityRepository.save(analyticsEntity)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
authResponse: (await this.authResponseFactory20200115.createResponse({
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Column, Entity, Index, JoinTable, ManyToMany, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { Column, Entity, Index, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn } from 'typeorm'
|
||||
import { RevokedSession } from '../Session/RevokedSession'
|
||||
import { Role } from '../Role/Role'
|
||||
import { Setting } from '../Setting/Setting'
|
||||
import { UserSubscription } from '../Subscription/UserSubscription'
|
||||
import { AnalyticsEntity } from '../Analytics/AnalyticsEntity'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
@Entity({ name: 'users' })
|
||||
@@ -182,16 +181,6 @@ export class User {
|
||||
)
|
||||
declare subscriptions: Promise<UserSubscription[]>
|
||||
|
||||
@OneToOne(
|
||||
/* istanbul ignore next */
|
||||
() => AnalyticsEntity,
|
||||
/* istanbul ignore next */
|
||||
(analyticsEntity) => analyticsEntity.user,
|
||||
/* istanbul ignore next */
|
||||
{ lazy: true, eager: false },
|
||||
)
|
||||
declare analyticsEntity: Promise<AnalyticsEntity>
|
||||
|
||||
supportsSessions(): boolean {
|
||||
return parseInt(this.version) >= parseInt(ProtocolVersion.V004)
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { Repository, SelectQueryBuilder } from 'typeorm'
|
||||
|
||||
import { AnalyticsEntity } from '../../Domain/Analytics/AnalyticsEntity'
|
||||
|
||||
import { MySQLAnalyticsEntityRepository } from './MySQLAnalyticsEntityRepository'
|
||||
|
||||
describe('MySQLAnalyticsEntityRepository', () => {
|
||||
let ormRepository: Repository<AnalyticsEntity>
|
||||
let analyticsEntity: AnalyticsEntity
|
||||
let queryBuilder: SelectQueryBuilder<AnalyticsEntity>
|
||||
|
||||
const createRepository = () => new MySQLAnalyticsEntityRepository(ormRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
analyticsEntity = {} as jest.Mocked<AnalyticsEntity>
|
||||
|
||||
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<AnalyticsEntity>>
|
||||
|
||||
ormRepository = {} as jest.Mocked<Repository<AnalyticsEntity>>
|
||||
ormRepository.save = jest.fn()
|
||||
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
|
||||
})
|
||||
|
||||
it('should save', async () => {
|
||||
await createRepository().save(analyticsEntity)
|
||||
|
||||
expect(ormRepository.save).toHaveBeenCalledWith(analyticsEntity)
|
||||
})
|
||||
|
||||
it('should find one by user uuid', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(analyticsEntity)
|
||||
|
||||
const result = await createRepository().findOneByUserUuid('123')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('analytics_entity.user_uuid = :userUuid', { userUuid: '123' })
|
||||
|
||||
expect(result).toEqual(analyticsEntity)
|
||||
})
|
||||
})
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Repository } from 'typeorm'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsEntity } from '../../Domain/Analytics/AnalyticsEntity'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../../Domain/Analytics/AnalyticsEntityRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class MySQLAnalyticsEntityRepository implements AnalyticsEntityRepositoryInterface {
|
||||
constructor(
|
||||
@inject(TYPES.ORMAnalyticsEntityRepository)
|
||||
private ormRepository: Repository<AnalyticsEntity>,
|
||||
) {}
|
||||
|
||||
async findOneByUserUuid(userUuid: Uuid): Promise<AnalyticsEntity | null> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('analytics_entity')
|
||||
.where('analytics_entity.user_uuid = :userUuid', { userUuid })
|
||||
.getOne()
|
||||
}
|
||||
|
||||
async save(analyticsEntity: AnalyticsEntity): Promise<AnalyticsEntity> {
|
||||
return this.ormRepository.save(analyticsEntity)
|
||||
}
|
||||
}
|
||||
@@ -19,5 +19,4 @@ export type CrossServiceTokenData = {
|
||||
refresh_expiration: string
|
||||
}
|
||||
extensionKey?: string
|
||||
analyticsId?: number
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"dependencies": {
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.3.0",
|
||||
"@standardnotes/analytics": "workspace:*",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
|
||||
@@ -7,13 +7,6 @@ import {
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventSubscriberFactoryInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import {
|
||||
AnalyticsStoreInterface,
|
||||
PeriodKeyGenerator,
|
||||
RedisAnalyticsStore,
|
||||
RedisStatisticsStore,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
@@ -242,13 +235,6 @@ export class ContainerConfigLoader {
|
||||
container.bind<ExtensionsHttpServiceInterface>(TYPES.ExtensionsHttpService).to(ExtensionsHttpService)
|
||||
container.bind<ItemBackupServiceInterface>(TYPES.ItemBackupService).to(S3ItemBackupService)
|
||||
container.bind<RevisionServiceInterface>(TYPES.RevisionService).to(RevisionService)
|
||||
const periodKeyGenerator = new PeriodKeyGenerator()
|
||||
container
|
||||
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
|
||||
.toConstantValue(new RedisAnalyticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
|
||||
container
|
||||
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
||||
.toConstantValue(new RedisStatisticsStore(periodKeyGenerator, container.get(TYPES.Redis)))
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
container
|
||||
|
||||
@@ -71,8 +71,6 @@ const TYPES = {
|
||||
ContentTypeFilter: Symbol.for('ContentTypeFilter'),
|
||||
ContentFilter: Symbol.for('ContentFilter'),
|
||||
ItemFactory: Symbol.for('ItemFactory'),
|
||||
AnalyticsStore: Symbol.for('AnalyticsStore'),
|
||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||
ItemTransferCalculator: Symbol.for('ItemTransferCalculator'),
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ describe('AuthMiddleware', () => {
|
||||
name: RoleName.ProUser,
|
||||
},
|
||||
],
|
||||
analyticsId: 123,
|
||||
permissions: [],
|
||||
},
|
||||
jwtSecret,
|
||||
@@ -65,7 +64,6 @@ describe('AuthMiddleware', () => {
|
||||
expect(response.locals.roleNames).toEqual(['CORE_USER', 'PRO_USER'])
|
||||
expect(response.locals.session).toEqual({ uuid: '234' })
|
||||
expect(response.locals.readOnlyAccess).toBeFalsy()
|
||||
expect(response.locals.analyticsId).toEqual(123)
|
||||
expect(response.locals.freeUser).toEqual(false)
|
||||
|
||||
expect(next).toHaveBeenCalled()
|
||||
@@ -82,7 +80,6 @@ describe('AuthMiddleware', () => {
|
||||
name: RoleName.CoreUser,
|
||||
},
|
||||
],
|
||||
analyticsId: 123,
|
||||
permissions: [],
|
||||
},
|
||||
jwtSecret,
|
||||
@@ -116,7 +113,6 @@ describe('AuthMiddleware', () => {
|
||||
name: RoleName.ProUser,
|
||||
},
|
||||
],
|
||||
analyticsId: 123,
|
||||
permissions: [],
|
||||
},
|
||||
jwtSecret,
|
||||
@@ -131,7 +127,6 @@ describe('AuthMiddleware', () => {
|
||||
expect(response.locals.roleNames).toEqual(['CORE_USER', 'PRO_USER'])
|
||||
expect(response.locals.session).toEqual({ uuid: '234', readonly_access: true })
|
||||
expect(response.locals.readOnlyAccess).toBeTruthy()
|
||||
expect(response.locals.analyticsId).toEqual(123)
|
||||
|
||||
expect(next).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -32,7 +32,6 @@ export class AuthMiddleware extends BaseMiddleware {
|
||||
response.locals.roleNames.length === 1 && response.locals.roleNames[0] === RoleName.CoreUser
|
||||
response.locals.session = decodedToken.session
|
||||
response.locals.readOnlyAccess = decodedToken.session?.readonly_access ?? false
|
||||
response.locals.analyticsId = decodedToken.analyticsId
|
||||
|
||||
return next()
|
||||
} catch (error) {
|
||||
|
||||
@@ -74,7 +74,6 @@ describe('ItemsController', () => {
|
||||
response.locals.user = {
|
||||
uuid: '123',
|
||||
}
|
||||
response.locals.analyticsId = 123
|
||||
response.locals.freeUser = false
|
||||
|
||||
syncResponse = {} as jest.Mocked<SyncResponse20200115>
|
||||
@@ -133,7 +132,6 @@ describe('ItemsController', () => {
|
||||
},
|
||||
],
|
||||
userUuid: '123',
|
||||
analyticsId: 123,
|
||||
freeUser: false,
|
||||
})
|
||||
|
||||
@@ -150,7 +148,6 @@ describe('ItemsController', () => {
|
||||
expect(checkIntegrity.execute).toHaveBeenCalledWith({
|
||||
integrityPayloads: [],
|
||||
userUuid: '123',
|
||||
analyticsId: 123,
|
||||
freeUser: false,
|
||||
})
|
||||
|
||||
@@ -183,7 +180,6 @@ describe('ItemsController', () => {
|
||||
limit: 150,
|
||||
syncToken: 'MjoxNjE3MTk1MzQyLjc1ODEyMTc=',
|
||||
userUuid: '123',
|
||||
analyticsId: 123,
|
||||
sessionUuid: null,
|
||||
})
|
||||
|
||||
@@ -215,7 +211,6 @@ describe('ItemsController', () => {
|
||||
limit: 150,
|
||||
syncToken: 'MjoxNjE3MTk1MzQyLjc1ODEyMTc=',
|
||||
userUuid: '123',
|
||||
analyticsId: 123,
|
||||
sessionUuid: null,
|
||||
})
|
||||
|
||||
@@ -236,7 +231,6 @@ describe('ItemsController', () => {
|
||||
limit: 150,
|
||||
syncToken: 'MjoxNjE3MTk1MzQyLjc1ODEyMTc=',
|
||||
userUuid: '123',
|
||||
analyticsId: 123,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ export class ItemsController extends BaseHttpController {
|
||||
contentType: request.body.content_type,
|
||||
apiVersion: request.body.api ?? ApiVersion.v20161215,
|
||||
readOnlyAccess: response.locals.readOnlyAccess,
|
||||
analyticsId: response.locals.analyticsId,
|
||||
sessionUuid: response.locals.session ? response.locals.session.uuid : null,
|
||||
})
|
||||
|
||||
@@ -62,7 +61,6 @@ export class ItemsController extends BaseHttpController {
|
||||
const result = await this.checkIntegrity.execute({
|
||||
userUuid: response.locals.user.uuid,
|
||||
integrityPayloads,
|
||||
analyticsId: response.locals.analyticsId,
|
||||
freeUser: response.locals.freeUser,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
import { ItemRepositoryInterface } from '../../Item/ItemRepositoryInterface'
|
||||
|
||||
import { CheckIntegrity } from './CheckIntegrity'
|
||||
@@ -9,10 +7,8 @@ import { ContentType } from '@standardnotes/common'
|
||||
|
||||
describe('CheckIntegrity', () => {
|
||||
let itemRepository: ItemRepositoryInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
|
||||
const createUseCase = () => new CheckIntegrity(itemRepository, statisticsStore, analyticsStore)
|
||||
const createUseCase = () => new CheckIntegrity(itemRepository)
|
||||
|
||||
beforeEach(() => {
|
||||
itemRepository = {} as jest.Mocked<ItemRepositoryInterface>
|
||||
@@ -43,21 +39,12 @@ describe('CheckIntegrity', () => {
|
||||
content_type: ContentType.File,
|
||||
},
|
||||
])
|
||||
|
||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
||||
statisticsStore.incrementOutOfSyncIncidents = jest.fn()
|
||||
statisticsStore.incrementMeasure = jest.fn()
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(false)
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
})
|
||||
|
||||
it('should return an empty result if there are no integrity mismatches', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
analyticsId: 1,
|
||||
freeUser: false,
|
||||
integrityPayloads: [
|
||||
{
|
||||
@@ -87,7 +74,6 @@ describe('CheckIntegrity', () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
analyticsId: 1,
|
||||
freeUser: false,
|
||||
integrityPayloads: [
|
||||
{
|
||||
@@ -116,15 +102,12 @@ describe('CheckIntegrity', () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(statisticsStore.incrementOutOfSyncIncidents).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should return a mismatch item that is missing on the client side', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
analyticsId: 1,
|
||||
freeUser: false,
|
||||
integrityPayloads: [
|
||||
{
|
||||
@@ -150,87 +133,4 @@ describe('CheckIntegrity', () => {
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should count notes for statistics of free users', async () => {
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
analyticsId: 1,
|
||||
freeUser: true,
|
||||
integrityPayloads: [
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
updated_at_timestamp: 1,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
updated_at_timestamp: 1,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
updated_at_timestamp: 3,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith('notes-count-free-users', 3, [
|
||||
Period.Today,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['checking-integrity'], 1, [Period.Today])
|
||||
})
|
||||
|
||||
it('should count notes for statistics of paid users', async () => {
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
analyticsId: 1,
|
||||
freeUser: false,
|
||||
integrityPayloads: [
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
updated_at_timestamp: 1,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
updated_at_timestamp: 1,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
updated_at_timestamp: 3,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith('notes-count-paid-users', 3, [
|
||||
Period.Today,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['checking-integrity'], 1, [Period.Today])
|
||||
})
|
||||
|
||||
it('should not count notes for statistics if they were already counted today', async () => {
|
||||
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(true)
|
||||
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
analyticsId: 1,
|
||||
freeUser: false,
|
||||
integrityPayloads: [
|
||||
{
|
||||
uuid: '1-2-3',
|
||||
updated_at_timestamp: 1,
|
||||
},
|
||||
{
|
||||
uuid: '2-3-4',
|
||||
updated_at_timestamp: 1,
|
||||
},
|
||||
{
|
||||
uuid: '3-4-5',
|
||||
updated_at_timestamp: 3,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { IntegrityPayload } from '@standardnotes/payloads'
|
||||
import {
|
||||
AnalyticsActivity,
|
||||
AnalyticsStoreInterface,
|
||||
Period,
|
||||
StatisticsMeasure,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
import TYPES from '../../../Bootstrap/Types'
|
||||
import { ItemRepositoryInterface } from '../../Item/ItemRepositoryInterface'
|
||||
@@ -14,34 +8,19 @@ import { UseCaseInterface } from '../UseCaseInterface'
|
||||
import { CheckIntegrityDTO } from './CheckIntegrityDTO'
|
||||
import { CheckIntegrityResponse } from './CheckIntegrityResponse'
|
||||
import { ExtendedIntegrityPayload } from '../../Item/ExtendedIntegrityPayload'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
@injectable()
|
||||
export class CheckIntegrity implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
) {}
|
||||
constructor(@inject(TYPES.ItemRepository) private itemRepository: ItemRepositoryInterface) {}
|
||||
|
||||
async execute(dto: CheckIntegrityDTO): Promise<CheckIntegrityResponse> {
|
||||
const serverItemIntegrityPayloads = await this.itemRepository.findItemsForComputingIntegrityPayloads(dto.userUuid)
|
||||
|
||||
let notesCount = 0
|
||||
let filesCount = 0
|
||||
const serverItemIntegrityPayloadsMap = new Map<string, ExtendedIntegrityPayload>()
|
||||
for (const serverItemIntegrityPayload of serverItemIntegrityPayloads) {
|
||||
serverItemIntegrityPayloadsMap.set(serverItemIntegrityPayload.uuid, serverItemIntegrityPayload)
|
||||
if (serverItemIntegrityPayload.content_type === ContentType.Note) {
|
||||
notesCount++
|
||||
}
|
||||
if (serverItemIntegrityPayload.content_type === ContentType.File) {
|
||||
filesCount++
|
||||
}
|
||||
}
|
||||
|
||||
await this.saveNotesCountStatistics(dto.freeUser, dto.analyticsId, { notes: notesCount, files: filesCount })
|
||||
|
||||
const clientItemIntegrityPayloadsMap = new Map<string, number>()
|
||||
for (const clientItemIntegrityPayload of dto.integrityPayloads) {
|
||||
clientItemIntegrityPayloadsMap.set(
|
||||
@@ -83,41 +62,8 @@ export class CheckIntegrity implements UseCaseInterface {
|
||||
}
|
||||
}
|
||||
|
||||
if (mismatches.length > 0) {
|
||||
await this.statisticsStore.incrementOutOfSyncIncidents()
|
||||
}
|
||||
|
||||
return {
|
||||
mismatches,
|
||||
}
|
||||
}
|
||||
|
||||
private async saveNotesCountStatistics(
|
||||
freeUser: boolean,
|
||||
analyticsId: number,
|
||||
counts: { notes: number; files: number },
|
||||
) {
|
||||
const integrityWasCheckedToday = await this.analyticsStore.wasActivityDone(
|
||||
AnalyticsActivity.CheckingIntegrity,
|
||||
analyticsId,
|
||||
Period.Today,
|
||||
)
|
||||
|
||||
if (!integrityWasCheckedToday) {
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.CheckingIntegrity], analyticsId, [Period.Today])
|
||||
|
||||
await this.statisticsStore.incrementMeasure(
|
||||
freeUser ? StatisticsMeasure.NotesCountFreeUsers : StatisticsMeasure.NotesCountPaidUsers,
|
||||
counts.notes,
|
||||
[Period.Today, Period.ThisMonth],
|
||||
)
|
||||
|
||||
if (!freeUser) {
|
||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.FilesCount, counts.files, [
|
||||
Period.Today,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,4 @@ export type CheckIntegrityDTO = {
|
||||
userUuid: Uuid
|
||||
integrityPayloads: IntegrityPayload[]
|
||||
freeUser: boolean
|
||||
analyticsId: number
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
|
||||
import { ApiVersion } from '../Api/ApiVersion'
|
||||
import { Item } from '../Item/Item'
|
||||
@@ -16,9 +15,8 @@ describe('SyncItems', () => {
|
||||
let item2: Item
|
||||
let item3: Item
|
||||
let itemHash: ItemHash
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
|
||||
const createUseCase = () => new SyncItems(itemService, analyticsStore)
|
||||
const createUseCase = () => new SyncItems(itemService)
|
||||
|
||||
beforeEach(() => {
|
||||
item1 = {
|
||||
@@ -53,61 +51,9 @@ describe('SyncItems', () => {
|
||||
syncToken: 'qwerty',
|
||||
})
|
||||
itemService.frontLoadKeysItemsToTop = jest.fn().mockReturnValue([item3, item1])
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
})
|
||||
|
||||
it('should sync items', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
itemHashes: [itemHash],
|
||||
computeIntegrityHash: false,
|
||||
syncToken: 'foo',
|
||||
cursorToken: 'bar',
|
||||
limit: 10,
|
||||
readOnlyAccess: false,
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
analyticsId: 123,
|
||||
sessionUuid: '2-3-4',
|
||||
}),
|
||||
).toEqual({
|
||||
conflicts: [],
|
||||
cursorToken: 'asdzxc',
|
||||
retrievedItems: [item1],
|
||||
savedItems: [item2],
|
||||
syncToken: 'qwerty',
|
||||
})
|
||||
|
||||
expect(itemService.frontLoadKeysItemsToTop).not.toHaveBeenCalled()
|
||||
expect(itemService.getItems).toHaveBeenCalledWith({
|
||||
contentType: 'Note',
|
||||
cursorToken: 'bar',
|
||||
limit: 10,
|
||||
syncToken: 'foo',
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
expect(itemService.saveItems).toHaveBeenCalledWith({
|
||||
itemHashes: [itemHash],
|
||||
userUuid: '1-2-3',
|
||||
apiVersion: '20200115',
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: '2-3-4',
|
||||
})
|
||||
expect(analyticsStore.markActivity).toHaveBeenNthCalledWith(1, ['editing-items'], 123, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
expect(analyticsStore.markActivity).toHaveBeenNthCalledWith(2, ['email-unbacked-up-data'], 123, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
])
|
||||
})
|
||||
|
||||
it('should sync items - no analytics', async () => {
|
||||
expect(
|
||||
await createUseCase().execute({
|
||||
userUuid: '1-2-3',
|
||||
@@ -144,7 +90,6 @@ describe('SyncItems', () => {
|
||||
readOnlyAccess: false,
|
||||
sessionUuid: null,
|
||||
})
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should sync items and return items keys on top for first sync', async () => {
|
||||
@@ -158,7 +103,6 @@ describe('SyncItems', () => {
|
||||
sessionUuid: '2-3-4',
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
analyticsId: 123,
|
||||
}),
|
||||
).toEqual({
|
||||
conflicts: [],
|
||||
@@ -202,7 +146,6 @@ describe('SyncItems', () => {
|
||||
limit: 10,
|
||||
contentType: 'Note',
|
||||
apiVersion: ApiVersion.v20200115,
|
||||
analyticsId: 123,
|
||||
}),
|
||||
).toEqual({
|
||||
conflicts: [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { Item } from '../Item/Item'
|
||||
@@ -10,10 +9,7 @@ import { UseCaseInterface } from './UseCaseInterface'
|
||||
|
||||
@injectable()
|
||||
export class SyncItems implements UseCaseInterface {
|
||||
constructor(
|
||||
@inject(TYPES.ItemService) private itemService: ItemServiceInterface,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
) {}
|
||||
constructor(@inject(TYPES.ItemService) private itemService: ItemServiceInterface) {}
|
||||
|
||||
async execute(dto: SyncItemsDTO): Promise<SyncItemsResponse> {
|
||||
const getItemsResult = await this.itemService.getItems({
|
||||
@@ -37,19 +33,6 @@ export class SyncItems implements UseCaseInterface {
|
||||
retrievedItems = await this.itemService.frontLoadKeysItemsToTop(dto.userUuid, retrievedItems)
|
||||
}
|
||||
|
||||
if (dto.analyticsId && saveItemsResult.savedItems.length > 0) {
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.EditingItems], dto.analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.EmailUnbackedUpData], dto.analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
])
|
||||
}
|
||||
|
||||
const syncResponse: SyncItemsResponse = {
|
||||
retrievedItems,
|
||||
syncToken: saveItemsResult.syncToken,
|
||||
|
||||
@@ -9,7 +9,6 @@ export type SyncItemsDTO = {
|
||||
syncToken?: string | null
|
||||
cursorToken?: string | null
|
||||
contentType?: string
|
||||
analyticsId?: number
|
||||
apiVersion: string
|
||||
readOnlyAccess: boolean
|
||||
sessionUuid: Uuid | null
|
||||
|
||||
@@ -1899,7 +1899,6 @@ __metadata:
|
||||
dependencies:
|
||||
"@newrelic/winston-enricher": "npm:^4.0.0"
|
||||
"@sentry/node": "npm:^7.3.0"
|
||||
"@standardnotes/analytics": "workspace:*"
|
||||
"@standardnotes/api": "npm:^1.19.0"
|
||||
"@standardnotes/common": "workspace:*"
|
||||
"@standardnotes/domain-events": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user