Compare commits

..

8 Commits

Author SHA1 Message Date
standardci
a9293f6ce1 chore(release): publish new version
- @standardnotes/analytics@1.18.0
 - @standardnotes/api-gateway@1.14.3
 - @standardnotes/auth-server@1.19.0
 - @standardnotes/syncing-server@1.6.48
2022-08-15 11:12:04 +00:00
Karol Sójko
58c5b586a9 feat(auth): add payment failed event handler 2022-08-15 13:10:36 +02:00
standardci
21d224da22 chore(release): publish new version
- @standardnotes/analytics@1.17.2
 - @standardnotes/api-gateway@1.14.2
 - @standardnotes/auth-server@1.18.4
 - @standardnotes/syncing-server@1.6.47
2022-08-15 10:59:57 +00:00
Karol Sójko
43d957c8d3 fix(analytics): quarterly calculations over time 2022-08-15 12:58:25 +02:00
standardci
917fad510a chore(release): publish new version
- @standardnotes/analytics@1.17.1
 - @standardnotes/api-gateway@1.14.1
 - @standardnotes/auth-server@1.18.3
 - @standardnotes/syncing-server@1.6.46
2022-08-15 10:43:54 +00:00
Karol Sójko
269eef7ef3 fix(analytics): expire bitop keys 2022-08-15 12:42:26 +02:00
standardci
b811f4527b chore(release): publish new version
- @standardnotes/analytics@1.17.0
 - @standardnotes/api-gateway@1.14.0
 - @standardnotes/auth-server@1.18.2
 - @standardnotes/syncing-server@1.6.45
2022-08-15 09:57:53 +00:00
Karol Sójko
67378e4535 feat(api-gateway): add quarterly analytics 2022-08-15 11:55:51 +02:00
16 changed files with 239 additions and 139 deletions

View File

@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.18.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.17.2...@standardnotes/analytics@1.18.0) (2022-08-15)
### Features
* **auth:** add payment failed event handler ([58c5b58](https://github.com/standardnotes/server/commit/58c5b586a904cf1fd179cc28783a6ae7da688063))
## [1.17.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.17.1...@standardnotes/analytics@1.17.2) (2022-08-15)
### Bug Fixes
* **analytics:** quarterly calculations over time ([43d957c](https://github.com/standardnotes/server/commit/43d957c8d382b501e8101b51e30b33f18a4dd871))
## [1.17.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.17.0...@standardnotes/analytics@1.17.1) (2022-08-15)
### Bug Fixes
* **analytics:** expire bitop keys ([269eef7](https://github.com/standardnotes/server/commit/269eef7ef31343390c6909350bf1bfede94c24b3))
# [1.17.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.16.0...@standardnotes/analytics@1.17.0) (2022-08-15)
### Features
* **api-gateway:** add quarterly analytics ([67378e4](https://github.com/standardnotes/server/commit/67378e4535ef2760cfe3fe27256ffe117ee11a71))
# [1.16.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.15.0...@standardnotes/analytics@1.16.0) (2022-08-15)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "1.16.0",
"version": "1.18.0",
"engines": {
"node": ">=14.0.0 <17.0.0"
},

View File

@@ -11,4 +11,5 @@ export enum AnalyticsActivity {
EmailUnbackedUpData = 'email-unbacked-up-data',
EmailBackup = 'email-backup',
LimitedDiscountOfferPurchased = 'limited-discount-offer-purchased',
PaymentFailed = 'payment-failed',
}

View File

@@ -24,6 +24,7 @@ describe('RedisAnalyticsStore', () => {
redisClient.setbit = jest.fn()
redisClient.getbit = jest.fn().mockReturnValue(1)
redisClient.bitop = jest.fn()
redisClient.expire = jest.fn()
periodKeyGenerator = {} as jest.Mocked<PeriodKeyGeneratorInterface>
periodKeyGenerator.getPeriodKey = jest.fn().mockReturnValue('period-key')

View File

@@ -15,13 +15,18 @@ export class RedisAnalyticsStore implements AnalyticsStoreInterface {
throw new Error(`Unsuporrted period: ${period}`)
}
const periodKeys = this.periodKeyGenerator.getDiscretePeriodKeys(Period.Last30Days)
const periodKeys = this.periodKeyGenerator.getDiscretePeriodKeys(period)
await this.redisClient.bitop(
'OR',
`bitmap:action:${activity}:timespan:${periodKeys[0]}-${periodKeys[periodKeys.length - 1]}`,
...periodKeys.map((p) => `bitmap:action:${activity}:timespan:${p}`),
)
await this.redisClient.expire(
`bitmap:action:${activity}:timespan:${periodKeys[0]}-${periodKeys[periodKeys.length - 1]}`,
3600,
)
return this.redisClient.bitcount(
`bitmap:action:${activity}:timespan:${periodKeys[0]}-${periodKeys[periodKeys.length - 1]}`,
)
@@ -31,11 +36,13 @@ export class RedisAnalyticsStore implements AnalyticsStoreInterface {
activity: AnalyticsActivity,
period: Period,
): Promise<Array<{ periodKey: string; totalCount: number }>> {
if (period !== Period.Last30Days) {
if (
![Period.Last30Days, Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear].includes(period)
) {
throw new Error(`Unsuporrted period: ${period}`)
}
const periodKeys = this.periodKeyGenerator.getDiscretePeriodKeys(Period.Last30Days)
const periodKeys = this.periodKeyGenerator.getDiscretePeriodKeys(period)
const counts = []
for (const periodKey of periodKeys) {
counts.push({
@@ -105,6 +112,8 @@ export class RedisAnalyticsStore implements AnalyticsStoreInterface {
`bitmap:action:${activity}:timespan:${subsequentPeriodKey}`,
)
await this.redisClient.expire(diffKey, 3600)
const retainedTotalInActivity = await this.redisClient.bitcount(diffKey)
const initialTotalInActivity = await this.redisClient.bitcount(

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.14.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.14.2...@standardnotes/api-gateway@1.14.3) (2022-08-15)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.14.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.14.1...@standardnotes/api-gateway@1.14.2) (2022-08-15)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.14.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.14.0...@standardnotes/api-gateway@1.14.1) (2022-08-15)
**Note:** Version bump only for package @standardnotes/api-gateway
# [1.14.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.13.1...@standardnotes/api-gateway@1.14.0) (2022-08-15)
### Features
* **api-gateway:** add quarterly analytics ([67378e4](https://github.com/standardnotes/api-gateway/commit/67378e4535ef2760cfe3fe27256ffe117ee11a71))
## [1.13.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.13.0...@standardnotes/api-gateway@1.13.1) (2022-08-15)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -19,6 +19,64 @@ const requestReport = async (
statisticsStore: StatisticsStoreInterface,
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
const analyticsOverTime = []
const thirtyDaysAnalyticsNames = [
AnalyticsActivity.GeneralActivity,
AnalyticsActivity.EditingItems,
AnalyticsActivity.SubscriptionPurchased,
AnalyticsActivity.Register,
AnalyticsActivity.SubscriptionRenewed,
AnalyticsActivity.DeleteAccount,
AnalyticsActivity.SubscriptionCancelled,
AnalyticsActivity.SubscriptionRefunded,
]
for (const analyticsName of thirtyDaysAnalyticsNames) {
analyticsOverTime.push({
name: analyticsName,
period: Period.Last30Days,
counts: await analyticsStore.calculateActivityChangesTotalCount(analyticsName, Period.Last30Days),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(analyticsName, Period.Last30Days),
})
}
const quarterlyAnalyticsNames = [
AnalyticsActivity.Register,
AnalyticsActivity.SubscriptionPurchased,
AnalyticsActivity.SubscriptionRenewed,
]
for (const analyticsName of quarterlyAnalyticsNames) {
for (const period of [Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear]) {
analyticsOverTime.push({
name: analyticsName,
period: period,
counts: await analyticsStore.calculateActivityChangesTotalCount(analyticsName, period),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(analyticsName, period),
})
}
}
const yesterdayActivityStatistics = []
const yesterdayActivityNames = [
AnalyticsActivity.EditingItems,
AnalyticsActivity.LimitedDiscountOfferPurchased,
AnalyticsActivity.GeneralActivity,
]
for (const activityName of yesterdayActivityNames) {
yesterdayActivityStatistics.push({
name: activityName,
retention: await analyticsStore.calculateActivityRetention(
activityName,
Period.DayBeforeYesterday,
Period.Yesterday,
),
totalCount: await analyticsStore.calculateActivityTotalCount(activityName, Period.Yesterday),
})
}
const event: DailyAnalyticsReportGeneratedEvent = {
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
createdAt: new Date(),
@@ -33,138 +91,8 @@ const requestReport = async (
applicationStatistics: await statisticsStore.getYesterdayApplicationUsage(),
snjsStatistics: await statisticsStore.getYesterdaySNJSUsage(),
outOfSyncIncidents: await statisticsStore.getYesterdayOutOfSyncIncidents(),
activityStatistics: [
{
name: AnalyticsActivity.EditingItems,
retention: await analyticsStore.calculateActivityRetention(
AnalyticsActivity.EditingItems,
Period.DayBeforeYesterday,
Period.Yesterday,
),
totalCount: await analyticsStore.calculateActivityTotalCount(
AnalyticsActivity.EditingItems,
Period.Yesterday,
),
},
{
name: AnalyticsActivity.LimitedDiscountOfferPurchased,
retention: 0,
totalCount: await analyticsStore.calculateActivityTotalCount(
AnalyticsActivity.LimitedDiscountOfferPurchased,
Period.Yesterday,
),
},
{
name: AnalyticsActivity.GeneralActivity,
retention: await analyticsStore.calculateActivityRetention(
AnalyticsActivity.GeneralActivity,
Period.DayBeforeYesterday,
Period.Yesterday,
),
totalCount: await analyticsStore.calculateActivityTotalCount(
AnalyticsActivity.GeneralActivity,
Period.Yesterday,
),
},
],
activityStatisticsOverTime: [
{
name: AnalyticsActivity.GeneralActivity,
period: Period.Last30Days,
counts: await analyticsStore.calculateActivityChangesTotalCount(
AnalyticsActivity.GeneralActivity,
Period.Last30Days,
),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(
AnalyticsActivity.GeneralActivity,
Period.Last30Days,
),
},
{
name: AnalyticsActivity.EditingItems,
period: Period.Last30Days,
counts: await analyticsStore.calculateActivityChangesTotalCount(
AnalyticsActivity.EditingItems,
Period.Last30Days,
),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(
AnalyticsActivity.EditingItems,
Period.Last30Days,
),
},
{
name: AnalyticsActivity.SubscriptionPurchased,
period: Period.Last30Days,
counts: await analyticsStore.calculateActivityChangesTotalCount(
AnalyticsActivity.SubscriptionPurchased,
Period.Last30Days,
),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(
AnalyticsActivity.SubscriptionPurchased,
Period.Last30Days,
),
},
{
name: AnalyticsActivity.Register,
period: Period.Last30Days,
counts: await analyticsStore.calculateActivityChangesTotalCount(
AnalyticsActivity.Register,
Period.Last30Days,
),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(
AnalyticsActivity.Register,
Period.Last30Days,
),
},
{
name: AnalyticsActivity.SubscriptionRenewed,
period: Period.Last30Days,
counts: await analyticsStore.calculateActivityChangesTotalCount(
AnalyticsActivity.SubscriptionRenewed,
Period.Last30Days,
),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(
AnalyticsActivity.SubscriptionRenewed,
Period.Last30Days,
),
},
{
name: AnalyticsActivity.DeleteAccount,
period: Period.Last30Days,
counts: await analyticsStore.calculateActivityChangesTotalCount(
AnalyticsActivity.DeleteAccount,
Period.Last30Days,
),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(
AnalyticsActivity.DeleteAccount,
Period.Last30Days,
),
},
{
name: AnalyticsActivity.SubscriptionCancelled,
period: Period.Last30Days,
counts: await analyticsStore.calculateActivityChangesTotalCount(
AnalyticsActivity.SubscriptionCancelled,
Period.Last30Days,
),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(
AnalyticsActivity.SubscriptionCancelled,
Period.Last30Days,
),
},
{
name: AnalyticsActivity.SubscriptionRefunded,
period: Period.Last30Days,
counts: await analyticsStore.calculateActivityChangesTotalCount(
AnalyticsActivity.SubscriptionRefunded,
Period.Last30Days,
),
totalCount: await analyticsStore.calculateActivityTotalCountOverTime(
AnalyticsActivity.SubscriptionRefunded,
Period.Last30Days,
),
},
],
activityStatistics: yesterdayActivityStatistics,
activityStatisticsOverTime: analyticsOverTime,
},
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.13.1",
"version": "1.14.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.19.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.18.4...@standardnotes/auth-server@1.19.0) (2022-08-15)
### Features
* **auth:** add payment failed event handler ([58c5b58](https://github.com/standardnotes/server/commit/58c5b586a904cf1fd179cc28783a6ae7da688063))
## [1.18.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.18.3...@standardnotes/auth-server@1.18.4) (2022-08-15)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.18.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.18.2...@standardnotes/auth-server@1.18.3) (2022-08-15)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.18.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.18.1...@standardnotes/auth-server@1.18.2) (2022-08-15)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.18.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.18.0...@standardnotes/auth-server@1.18.1) (2022-08-15)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.18.1",
"version": "1.19.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},

View File

@@ -191,6 +191,7 @@ import { AuthController } from '../Controller/AuthController'
import { VerifyPredicate } from '../Domain/UseCase/VerifyPredicate/VerifyPredicate'
import { PredicateVerificationRequestedEventHandler } from '../Domain/Handler/PredicateVerificationRequestedEventHandler'
import { MuteMarketingEmails } from '../Domain/UseCase/MuteMarketingEmails/MuteMarketingEmails'
import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventHandler'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -478,6 +479,7 @@ export class ContainerConfigLoader {
container
.bind<PredicateVerificationRequestedEventHandler>(TYPES.PredicateVerificationRequestedEventHandler)
.to(PredicateVerificationRequestedEventHandler)
container.bind<PaymentFailedEventHandler>(TYPES.PaymentFailedEventHandler).to(PaymentFailedEventHandler)
// Services
container.bind<UAParser>(TYPES.DeviceDetector).toConstantValue(new UAParser())
@@ -576,6 +578,7 @@ export class ContainerConfigLoader {
],
['SHARED_SUBSCRIPTION_INVITATION_CREATED', container.get(TYPES.SharedSubscriptionInvitationCreatedEventHandler)],
['PREDICATE_VERIFICATION_REQUESTED', container.get(TYPES.PredicateVerificationRequestedEventHandler)],
['PAYMENT_FAILED', container.get(TYPES.PaymentFailedEventHandler)],
])
if (env.get('SQS_QUEUE_URL', true)) {

View File

@@ -143,6 +143,7 @@ const TYPES = {
UserDisabledSessionUserAgentLoggingEventHandler: Symbol.for('UserDisabledSessionUserAgentLoggingEventHandler'),
SharedSubscriptionInvitationCreatedEventHandler: Symbol.for('SharedSubscriptionInvitationCreatedEventHandler'),
PredicateVerificationRequestedEventHandler: Symbol.for('PredicateVerificationRequestedEventHandler'),
PaymentFailedEventHandler: Symbol.for('PaymentFailedEventHandler'),
// Services
DeviceDetector: Symbol.for('DeviceDetector'),
SessionService: Symbol.for('SessionService'),

View File

@@ -0,0 +1,51 @@
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

@@ -0,0 +1,30 @@
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

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.48](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.47...@standardnotes/syncing-server@1.6.48) (2022-08-15)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.47](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.46...@standardnotes/syncing-server@1.6.47) (2022-08-15)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.46](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.45...@standardnotes/syncing-server@1.6.46) (2022-08-15)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.45](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.44...@standardnotes/syncing-server@1.6.45) (2022-08-15)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.6.44](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.6.43...@standardnotes/syncing-server@1.6.44) (2022-08-15)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.6.44",
"version": "1.6.48",
"engines": {
"node": ">=16.0.0 <17.0.0"
},