feat: remove analytics scope from other services in favor of a separate service

This commit is contained in:
Karol Sójko
2022-11-07 11:50:40 +01:00
parent 77a06b2fe7
commit ff1d5db12c
92 changed files with 95 additions and 1788 deletions

1
.pnp.cjs generated
View File

@@ -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"],\

View File

@@ -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,

View File

@@ -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"
},

View File

@@ -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)) {

View File

@@ -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'),

View File

@@ -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',

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -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',
}

View File

@@ -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'

View File

@@ -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,
)

View File

@@ -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'

View File

@@ -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) {}

View File

@@ -1,2 +0,0 @@
export * from './Redis/RedisAnalyticsStore'
export * from './Redis/RedisStatisticsStore'

View File

@@ -1,2 +0,0 @@
export * from './Domain'
export * from './Infra'

View File

@@ -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:*",

View File

@@ -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())

View File

@@ -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

View File

@@ -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,

View File

@@ -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>

View File

@@ -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()
}
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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,

View File

@@ -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()

View File

@@ -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()

View File

@@ -67,6 +67,3 @@ VALET_TOKEN_SECRET=
VALET_TOKEN_TTL=
WEB_SOCKET_CONNECTION_TOKEN_SECRET=
# (Optional) Analytics
ANALYTICS_ENABLED=false

View File

@@ -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`)

View File

@@ -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}`)

View File

@@ -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:*",

View File

@@ -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)) {

View File

@@ -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,

View File

@@ -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'),
}

View File

@@ -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: [
{

View File

@@ -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)

View File

@@ -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>
}

View File

@@ -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>
}

View File

@@ -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 () => {

View File

@@ -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}`)

View File

@@ -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()
})
})

View File

@@ -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,
])
}
}

View File

@@ -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()
})
})

View File

@@ -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,
])
}
}
}

View File

@@ -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 () => {

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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(

View File

@@ -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 () => {

View File

@@ -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> {

View File

@@ -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()
})
})

View File

@@ -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,
])
}
}

View File

@@ -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,
])
})
})

View File

@@ -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,
])
}
}

View File

@@ -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()
})

View File

@@ -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> {

View File

@@ -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()

View File

@@ -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,

View File

@@ -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',

View File

@@ -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)
}

View File

@@ -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()
})
})

View File

@@ -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,
}
}
}

View File

@@ -1,5 +0,0 @@
import { Uuid } from '@standardnotes/common'
export type GetUserAnalyticsIdDTO = {
userUuid: Uuid
}

View File

@@ -1,3 +0,0 @@
export type GetUserAnalyticsIdResponse = {
analyticsId: number
}

View File

@@ -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',

View File

@@ -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({

View File

@@ -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)
}

View File

@@ -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)
})
})

View File

@@ -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)
}
}

View File

@@ -19,5 +19,4 @@ export type CrossServiceTokenData = {
refresh_expiration: string
}
extensionKey?: string
analyticsId?: number
}

View File

@@ -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:*",

View File

@@ -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

View File

@@ -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'),
}

View File

@@ -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()
})

View File

@@ -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) {

View File

@@ -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',
})

View File

@@ -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,
})

View File

@@ -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()
})
})

View File

@@ -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,
])
}
}
}
}

View File

@@ -5,5 +5,4 @@ export type CheckIntegrityDTO = {
userUuid: Uuid
integrityPayloads: IntegrityPayload[]
freeUser: boolean
analyticsId: number
}

View File

@@ -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: [

View File

@@ -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,

View File

@@ -9,7 +9,6 @@ export type SyncItemsDTO = {
syncToken?: string | null
cursorToken?: string | null
contentType?: string
analyticsId?: number
apiVersion: string
readOnlyAccess: boolean
sessionUuid: Uuid | null

View File

@@ -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:*"