Compare commits

..

25 Commits

Author SHA1 Message Date
standardci
59fc4a089c chore(release): publish new version
- @standardnotes/analytics@2.17.5
2022-12-20 13:16:57 +00:00
Karol Sójko
ef26dc8cbb fix(analytics): accessing analytics in report 2022-12-20 14:13:54 +01:00
standardci
8a0fbb28b0 chore(release): publish new version
- @standardnotes/analytics@2.17.4
2022-12-20 12:48:03 +00:00
Karol Sójko
618d8d5b1a tmp(analytics): add console logs for html generation on the report 2022-12-20 13:44:22 +01:00
standardci
3a936dc9c1 chore(release): publish new version
- @standardnotes/analytics@2.17.3
2022-12-20 12:15:31 +00:00
Karol Sójko
031fcd75ee fix(analytics): add debug logs for the report 2022-12-20 13:13:14 +01:00
standardci
c8cd23cb32 chore(release): publish new version
- @standardnotes/analytics@2.17.2
2022-12-20 11:27:58 +00:00
Karol Sójko
a3049938a3 fix(analytics): calculating active users 2022-12-20 12:26:06 +01:00
standardci
b23488e862 chore(release): publish new version
- @standardnotes/workspace-server@1.19.3
2022-12-20 10:32:06 +00:00
Karol Sójko
c8203cf04c fix(workspace): specs 2022-12-20 11:30:09 +01:00
standardci
4f2616ef0a chore(release): publish new version
- @standardnotes/analytics@2.17.1
 - @standardnotes/api-gateway@1.41.2
 - @standardnotes/auth-server@1.70.4
 - @standardnotes/domain-events-infra@1.9.59
 - @standardnotes/domain-events@2.105.1
 - @standardnotes/event-store@1.6.56
 - @standardnotes/files-server@1.9.2
 - @standardnotes/revisions-server@1.10.2
 - @standardnotes/scheduler-server@1.16.2
 - @standardnotes/syncing-server@1.28.2
 - @standardnotes/websockets-server@1.5.2
 - @standardnotes/workspace-server@1.19.2
2022-12-20 10:04:02 +00:00
Karol Sójko
04ffc69e00 fix(analytics): container binding 2022-12-20 10:59:26 +01:00
Karol Sójko
5b4bb6e7a7 fix(auth): replace date object with number timestamp 2022-12-20 10:54:31 +01:00
standardci
2e953ba998 chore(release): publish new version
- @standardnotes/analytics@2.17.0
2022-12-20 09:14:37 +00:00
Karol Sójko
ed5a4eb960 feat(analytics): add users activit to the report email 2022-12-20 10:12:42 +01:00
standardci
31b2c05084 chore(release): publish new version
- @standardnotes/analytics@2.16.0
2022-12-20 07:54:12 +00:00
Karol Sójko
6e1662038c feat(analytics): add active users stats to report 2022-12-20 08:52:19 +01:00
standardci
df78d88f79 chore(release): publish new version
- @standardnotes/analytics@2.15.1
 - @standardnotes/auth-server@1.70.3
2022-12-20 07:47:55 +00:00
Karol Sójko
addedb3091 fix(auth): add persisting statistics for all subscription plans 2022-12-20 08:45:43 +01:00
standardci
2ea17b2dea chore(release): publish new version
- @standardnotes/auth-server@1.70.2
2022-12-20 07:21:06 +00:00
Karol Sójko
85d2f42f47 fix(auth): docker command 2022-12-20 08:18:36 +01:00
standardci
cdb655c1bd chore(release): publish new version
- @standardnotes/auth-server@1.70.1
2022-12-20 07:06:06 +00:00
Karol Sójko
3064d03aa9 fix(auth): saving subscription plan name in session traces 2022-12-20 08:04:11 +01:00
standardci
6af6417ca2 chore(release): publish new version
- @standardnotes/analytics@2.15.0
 - @standardnotes/auth-server@1.70.0
2022-12-19 14:22:24 +00:00
Karol Sójko
a35271fbb3 feat(auth): add requesting persisting statistics 2022-12-19 15:19:49 +01:00
47 changed files with 475 additions and 106 deletions

View File

@@ -3,6 +3,59 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.17.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.4...@standardnotes/analytics@2.17.5) (2022-12-20)
### Bug Fixes
* **analytics:** accessing analytics in report ([ef26dc8](https://github.com/standardnotes/server/commit/ef26dc8cbb967e088ae7387ff6dbec1e60dc3ee4))
## [2.17.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.3...@standardnotes/analytics@2.17.4) (2022-12-20)
**Note:** Version bump only for package @standardnotes/analytics
## [2.17.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.2...@standardnotes/analytics@2.17.3) (2022-12-20)
### Bug Fixes
* **analytics:** add debug logs for the report ([031fcd7](https://github.com/standardnotes/server/commit/031fcd75eecdcf4c2f17257754a0ba3f24ba6d6e))
## [2.17.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.1...@standardnotes/analytics@2.17.2) (2022-12-20)
### Bug Fixes
* **analytics:** calculating active users ([a304993](https://github.com/standardnotes/server/commit/a3049938a31e21a5867a314ac62bee6aa4990d57))
## [2.17.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.0...@standardnotes/analytics@2.17.1) (2022-12-20)
### Bug Fixes
* **analytics:** container binding ([04ffc69](https://github.com/standardnotes/server/commit/04ffc69e000803107d8834c286de97b3d213a842))
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
# [2.17.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.16.0...@standardnotes/analytics@2.17.0) (2022-12-20)
### Features
* **analytics:** add users activit to the report email ([ed5a4eb](https://github.com/standardnotes/server/commit/ed5a4eb960a6c8fe9d0c77331f29dc3c7ffb9100))
# [2.16.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.1...@standardnotes/analytics@2.16.0) (2022-12-20)
### Features
* **analytics:** add active users stats to report ([6e16620](https://github.com/standardnotes/server/commit/6e1662038c3340fb60939464616789bab7639160))
## [2.15.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.0...@standardnotes/analytics@2.15.1) (2022-12-20)
### Bug Fixes
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
# [2.15.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.14.0...@standardnotes/analytics@2.15.0) (2022-12-19)
### Features
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
# [2.14.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.13.0...@standardnotes/analytics@2.14.0) (2022-12-19)
### Features

View File

@@ -121,6 +121,10 @@ const requestReport = async (
StatisticMeasureName.NAMES.FiveYearPlansMRR,
StatisticMeasureName.NAMES.PlusPlansMRR,
StatisticMeasureName.NAMES.ProPlansMRR,
StatisticMeasureName.NAMES.ActiveUsers,
StatisticMeasureName.NAMES.ActiveFreeUsers,
StatisticMeasureName.NAMES.ActivePlusUsers,
StatisticMeasureName.NAMES.ActiveProUsers,
]
for (const statisticName of thirtyDaysStatisticsNames) {
statisticsOverTime.push({

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.14.0",
"version": "2.17.5",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -218,6 +218,7 @@ export class ContainerConfigLoader {
.toConstantValue(
new StatisticPersistenceRequestedEventHandler(
container.get(TYPES.PersistStatistic),
container.get(TYPES.Timer),
container.get(TYPES.Logger),
),
)

View File

@@ -5,6 +5,31 @@ import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { Period } from '../Time/Period'
const countActiveUsers = (measureName: string, data: any): { yesterday: number; last30Days: number } => {
const totalActiveUsersLast30DaysIncludingToday = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === measureName && a.period === 27,
)
const totalActiveUsersYesterday =
totalActiveUsersLast30DaysIncludingToday.counts[totalActiveUsersLast30DaysIncludingToday.counts.length - 2]
.totalCount
const filteredCounts = totalActiveUsersLast30DaysIncludingToday.counts.filter(
(count: { totalCount: number }) => count.totalCount !== 0,
)
const averageActiveUsersLast30Days = Math.floor(
filteredCounts.reduce((previousValue: { totalCount: any }, currentValue: { totalCount: any }) => {
return previousValue.totalCount + currentValue.totalCount
}) / filteredCounts.length,
)
return {
yesterday: totalActiveUsersYesterday,
last30Days: averageActiveUsersLast30Days,
}
}
const getChartUrls = (
data: any,
): {
@@ -12,7 +37,6 @@ const getChartUrls = (
users: string
quarterlyPerformance: string
churn: string
mrr: string
mrrMonthly: string
} => {
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
@@ -237,82 +261,6 @@ const getChartUrls = (
},
}
const mrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
)
const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
)
const annualPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
)
const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
)
const proPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
)
const plusPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
)
const mrrOverTimeConfig = {
type: 'line',
data: {
labels: mrrOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
datasets: [
{
label: 'MRR',
backgroundColor: 'rgb(25, 255, 140)',
borderColor: 'rgb(25, 255, 140)',
data: mrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Monthly Plans',
backgroundColor: 'rgb(54, 162, 235)',
borderColor: 'rgb(54, 162, 235)',
data: monthlyPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Annual Plans',
backgroundColor: 'rgb(255, 221, 51)',
borderColor: 'rgb(255, 221, 51)',
data: annualPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Five Year Plans',
backgroundColor: 'rgb(255, 120, 120)',
borderColor: 'rgb(255, 120, 120)',
data: fiveYearPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - PRO Plans',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: proPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - PLUS Plans',
backgroundColor: 'rgb(221, 51, 255)',
borderColor: 'rgb(221, 51, 255)',
data: plusPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
],
},
}
const mrrMonthlyOverTime = data.statisticsOverTime
.find((a: { name: string; period: Period }) => a.name === 'mrr' && a.period === Period.ThisYear)
?.counts.map((count: { totalCount: number }) => +count.totalCount.toFixed(2))
@@ -371,7 +319,6 @@ const getChartUrls = (
JSON.stringify(quarterlyConfig),
)}`,
churn: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(churnConfig))}`,
mrr: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrOverTimeConfig))}`,
mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
}
}
@@ -608,12 +555,39 @@ export const html = (data: any, timer: TimerInterface) => {
(value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
)
const totalActiveUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveUsers, data)
const totalActiveFreeUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveFreeUsers, data)
const totalActivePlusUsers = countActiveUsers(StatisticMeasureName.NAMES.ActivePlusUsers, data)
const totalActiveProUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveProUsers, data)
return ` <div>
<p>Hello,</p>
<p>
<strong>Here are some statistics from yesterday:</strong>
</p>
<ul>
<li>
<b>Active Users</b>
<ul>
<li>
<b>Total:</b> ${totalActiveUsers.yesterday.toLocaleString('en-US')}
</li>
<li>
<b>By Subscription Type:</b>
<ul>
<li>
<b>FREE:</b> ${totalActiveFreeUsers.yesterday.toLocaleString('en-US')}
</li>
<li>
<b>PLUS:</b> ${totalActivePlusUsers.yesterday.toLocaleString('en-US')}
</li>
<li>
<b>PRO:</b> ${totalActiveProUsers.yesterday.toLocaleString('en-US')}
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Payments</b>
<ul>
@@ -812,6 +786,28 @@ export const html = (data: any, timer: TimerInterface) => {
<strong>Here are some statistics from last 30 days:</strong>
</p>
<ul>
<li>
<b>Active Users (Average)</b>
<ul>
<li>
<b>Total:</b> ${totalActiveUsers.last30Days.toLocaleString('en-US')}
</li>
<li>
<b>By Subscription Type:</b>
<ul>
<li>
<b>FREE:</b> ${totalActiveFreeUsers.last30Days.toLocaleString('en-US')}
</li>
<li>
<b>PLUS:</b> ${totalActivePlusUsers.last30Days.toLocaleString('en-US')}
</li>
<li>
<b>PRO:</b> ${totalActiveProUsers.last30Days.toLocaleString('en-US')}
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Payments (This Month)</b>
<ul>
@@ -944,10 +940,6 @@ export const html = (data: any, timer: TimerInterface) => {
</ul>
</li>
</ul>
<p>
<strong>Here is the MRR chart over 30 days:</strong>
</p>
<img src=${chartUrls.mrr}></img>
<p>
<strong>Here is the MRR Monthly chart this year:</strong>
</p>

View File

@@ -1,13 +1,14 @@
import { DomainEventHandlerInterface, StatisticPersistenceRequestedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston'
import { PersistStatistic } from '../UseCase/PersistStatistic/PersistStatistic'
export class StatisticPersistenceRequestedEventHandler implements DomainEventHandlerInterface {
constructor(private persistStatistic: PersistStatistic, private logger: Logger) {}
constructor(private persistStatistic: PersistStatistic, private timer: TimerInterface, private logger: Logger) {}
async handle(event: StatisticPersistenceRequestedEvent): Promise<void> {
const result = await this.persistStatistic.execute({
date: event.payload.date,
date: this.timer.convertMicrosecondsToDate(event.payload.date),
statisticMeasureName: event.payload.statisticMeasureName,
value: event.payload.value,
})

View File

@@ -26,6 +26,10 @@ export class StatisticMeasureName extends ValueObject<StatisticMeasureNameProps>
FiveYearPlansMRR: 'five-year-plans-mrr',
ProPlansMRR: 'pro-plans-mrr',
PlusPlansMRR: 'plus-plans-mrr',
ActiveUsers: 'active-users',
ActiveProUsers: 'active-pro-users',
ActivePlusUsers: 'active-plus-users',
ActiveFreeUsers: 'active-free-users',
}
get value(): string {

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.41.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.1...@standardnotes/api-gateway@1.41.2) (2022-12-20)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.41.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.0...@standardnotes/api-gateway@1.41.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/api-gateway

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.41.1",
"version": "1.41.2",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.70.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.3...@standardnotes/auth-server@1.70.4) (2022-12-20)
### Bug Fixes
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
## [1.70.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.2...@standardnotes/auth-server@1.70.3) (2022-12-20)
### Bug Fixes
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
## [1.70.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.1...@standardnotes/auth-server@1.70.2) (2022-12-20)
### Bug Fixes
* **auth:** docker command ([85d2f42](https://github.com/standardnotes/server/commit/85d2f42f473110e8dfca975bfecc7a56823bdef4))
## [1.70.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.0...@standardnotes/auth-server@1.70.1) (2022-12-20)
### Bug Fixes
* **auth:** saving subscription plan name in session traces ([3064d03](https://github.com/standardnotes/server/commit/3064d03aa9a2ac9ca3acfff30480ea8629faeb14))
# [1.70.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.1...@standardnotes/auth-server@1.70.0) (2022-12-19)
### Features
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
## [1.69.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.0...@standardnotes/auth-server@1.69.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/auth-server

View File

@@ -0,0 +1,40 @@
import 'reflect-metadata'
import 'newrelic'
import { Logger } from 'winston'
import { TimerInterface } from '@standardnotes/time'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { PersistStatistics } from '../src/Domain/UseCase/PersistStatistics/PersistStatistics'
const container = new ContainerConfigLoader()
void container.load().then((container) => {
const env: Env = new Env()
env.load()
const logger: Logger = container.get(TYPES.Logger)
logger.info('Starting session traces cleanup')
const persistStats: PersistStatistics = container.get(TYPES.PersistStatistics)
const timer: TimerInterface = container.get(TYPES.Timer)
Promise.resolve(
persistStats.execute({
sessionsInADay: timer.getUTCDateNDaysAgo(1),
}),
)
.then(() => {
logger.info('Stats persisted.')
process.exit(0)
})
.catch((error) => {
logger.error(`Could not persist stats: ${error.message}`)
process.exit(1)
})
})

View File

@@ -24,6 +24,11 @@ case "$COMMAND" in
yarn workspace @standardnotes/auth-server cleanup
;;
'stats' )
echo "[Docker] Starting Persisting Stats..."
yarn workspace @standardnotes/auth-server stats
;;
'email-daily-backup' )
echo "[Docker] Starting Email Daily Backup..."
yarn workspace @standardnotes/auth-server daily-backup:email

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.69.1",
"version": "1.70.4",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -20,6 +20,7 @@
"start": "yarn node dist/bin/server.js",
"worker": "yarn node dist/bin/worker.js",
"cleanup": "yarn node dist/bin/cleanup.js",
"stats": "yarn node dist/bin/stats.js",
"daily-backup:email": "yarn node dist/bin/backup.js email daily",
"user-email-backup": "yarn node dist/bin/user_email_backup.js",
"daily-backup:dropbox": "yarn node dist/bin/backup.js dropbox daily",

View File

@@ -202,6 +202,7 @@ import { SessionTrace } from '../Domain/Session/SessionTrace'
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
import { TraceSession } from '../Domain/UseCase/TraceSession/TraceSession'
import { CleanupSessionTraces } from '../Domain/UseCase/CleanupSessionTraces/CleanupSessionTraces'
import { PersistStatistics } from '../Domain/UseCase/PersistStatistics/PersistStatistics'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -500,6 +501,16 @@ export class ContainerConfigLoader {
container.get(TYPES.SESSION_TRACE_DAYS_TTL),
),
)
container
.bind<PersistStatistics>(TYPES.PersistStatistics)
.toConstantValue(
new PersistStatistics(
container.get(TYPES.SessionTraceRepository),
container.get(TYPES.DomainEventPublisher),
container.get(TYPES.DomainEventFactory),
container.get(TYPES.Timer),
),
)
container
.bind<CleanupSessionTraces>(TYPES.CleanupSessionTraces)

View File

@@ -126,6 +126,7 @@ const TYPES = {
ProcessUserRequest: Symbol.for('ProcessUserRequest'),
TraceSession: Symbol.for('TraceSession'),
CleanupSessionTraces: Symbol.for('CleanupSessionTraces'),
PersistStatistics: Symbol.for('PersistStatistics'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),

View File

@@ -19,6 +19,7 @@ import {
UserContentSizeRecalculationRequestedEvent,
MuteEmailsSettingChangedEvent,
EmailRequestedEvent,
StatisticPersistenceRequestedEvent,
} from '@standardnotes/domain-events'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import { TimerInterface } from '@standardnotes/time'
@@ -31,6 +32,25 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createStatisticPersistenceRequestedEvent(dto: {
statisticMeasureName: string
value: number
date: number
}): StatisticPersistenceRequestedEvent {
return {
type: 'STATISTIC_PERSISTENCE_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: '-',
userIdentifierType: 'email',
},
origin: DomainEventService.Auth,
},
payload: dto,
}
}
createMuteEmailsSettingChangedEvent(dto: {
username: string
mute: boolean

View File

@@ -17,6 +17,7 @@ import {
UserContentSizeRecalculationRequestedEvent,
MuteEmailsSettingChangedEvent,
EmailRequestedEvent,
StatisticPersistenceRequestedEvent,
} from '@standardnotes/domain-events'
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
@@ -88,4 +89,9 @@ export interface DomainEventFactoryInterface {
mute: boolean
emailSubscriptionRejectionLevel: string
}): MuteEmailsSettingChangedEvent
createStatisticPersistenceRequestedEvent(dto: {
statisticMeasureName: string
value: number
date: number
}): StatisticPersistenceRequestedEvent
}

View File

@@ -41,4 +41,23 @@ describe('RoleToSubscriptionMap', () => {
},
])
})
it('should filter our subscription roles from an array of roles', () => {
const roles = [
{
name: RoleName.CoreUser,
} as jest.Mocked<Role>,
{
name: RoleName.FilesBetaUser,
} as jest.Mocked<Role>,
{
name: RoleName.PlusUser,
} as jest.Mocked<Role>,
]
expect(createMap().filterSubscriptionRoles(roles)).toEqual([
{
name: RoleName.PlusUser,
},
])
})
})

View File

@@ -17,6 +17,10 @@ export class RoleToSubscriptionMap implements RoleToSubscriptionMapInterface {
return roles.filter((role) => this.nonSubscriptionRoles.includes(role.name as RoleName))
}
filterSubscriptionRoles(roles: Role[]): Array<Role> {
return roles.filter((role) => !this.nonSubscriptionRoles.includes(role.name as RoleName))
}
getSubscriptionNameForRoleName(roleName: RoleName): SubscriptionName | undefined {
return this.roleNameToSubscriptionNameMap.get(roleName)
}

View File

@@ -3,6 +3,7 @@ import { Role } from './Role'
export interface RoleToSubscriptionMapInterface {
filterNonSubscriptionRoles(roles: Role[]): Array<Role>
filterSubscriptionRoles(roles: Role[]): Array<Role>
getSubscriptionNameForRoleName(roleName: RoleName): SubscriptionName | undefined
getRoleNameForSubscriptionName(subscriptionName: SubscriptionName): RoleName | undefined
}

View File

@@ -1,4 +1,4 @@
import { Uuid } from '@standardnotes/domain-core'
import { SubscriptionPlanName, Uuid } from '@standardnotes/domain-core'
import { SessionTrace } from './SessionTrace'
@@ -6,4 +6,6 @@ export interface SessionTraceRepositoryInterface {
save(sessionTrace: SessionTrace): Promise<void>
removeExpiredBefore(date: Date): Promise<void>
findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null>
countByDate(date: Date): Promise<number>
countByDateAndSubscriptionPlanName(date: Date, subscriptionPlanName: SubscriptionPlanName): Promise<number>
}

View File

@@ -67,7 +67,7 @@ describe('CreateCrossServiceToken', () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
roleToSubscriptionMap = {} as jest.Mocked<RoleToSubscriptionMapInterface>
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([RoleName.NAMES.PlusUser])
roleToSubscriptionMap.filterSubscriptionRoles = jest.fn().mockReturnValue([RoleName.NAMES.PlusUser])
roleToSubscriptionMap.getSubscriptionNameForRoleName = jest
.fn()
.mockReturnValue(SubscriptionPlanName.NAMES.PlusPlan)
@@ -170,7 +170,7 @@ describe('CreateCrossServiceToken', () => {
})
it('should trace session without a subscription role', async () => {
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([])
roleToSubscriptionMap.filterSubscriptionRoles = jest.fn().mockReturnValue([])
await createUseCase().execute({
user,

View File

@@ -102,7 +102,7 @@ export class CreateCrossServiceToken implements UseCaseInterface {
}
private getSubscriptionNameFromRoles(roles: Array<Role>): string | null {
const nonSubscriptionRoles = this.roleToSubscriptionMap.filterNonSubscriptionRoles(roles)
const nonSubscriptionRoles = this.roleToSubscriptionMap.filterSubscriptionRoles(roles)
if (nonSubscriptionRoles.length === 0) {
return null
}

View File

@@ -0,0 +1,40 @@
import { DomainEventPublisherInterface, StatisticPersistenceRequestedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
import { PersistStatistics } from './PersistStatistics'
describe('PersistStatistics', () => {
let sessionTracesRepository: SessionTraceRepositoryInterface
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let timer: TimerInterface
const createUseCase = () =>
new PersistStatistics(sessionTracesRepository, domainEventPublisher, domainEventFactory, timer)
beforeEach(() => {
sessionTracesRepository = {} as jest.Mocked<SessionTraceRepositoryInterface>
sessionTracesRepository.countByDate = jest.fn().mockReturnValue(1)
sessionTracesRepository.countByDateAndSubscriptionPlanName = jest.fn().mockReturnValue(2)
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createStatisticPersistenceRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<StatisticPersistenceRequestedEvent>)
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.convertDateToMicroseconds = jest.fn().mockReturnValue(3)
})
it('should request statistic persistence', async () => {
await createUseCase().execute({ sessionsInADay: new Date() })
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(4)
})
})

View File

@@ -0,0 +1,64 @@
import { Result, SubscriptionPlanName, UseCaseInterface } from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
import { PersistStatisticsDTO } from './PersistStatisticsDTO'
export class PersistStatistics implements UseCaseInterface<string> {
constructor(
private sessionTracesRepository: SessionTraceRepositoryInterface,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private timer: TimerInterface,
) {}
async execute(dto: PersistStatisticsDTO): Promise<Result<string>> {
const countSessionsInADay = await this.sessionTracesRepository.countByDate(dto.sessionsInADay)
await this.domainEventPublisher.publish(
this.domainEventFactory.createStatisticPersistenceRequestedEvent({
statisticMeasureName: 'active-users',
value: countSessionsInADay,
date: this.timer.convertDateToMicroseconds(dto.sessionsInADay),
}),
)
const proSubscriptionPlanName = SubscriptionPlanName.create(SubscriptionPlanName.NAMES.ProPlan).getValue()
const countProSessionsInADay = await this.sessionTracesRepository.countByDateAndSubscriptionPlanName(
dto.sessionsInADay,
proSubscriptionPlanName,
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createStatisticPersistenceRequestedEvent({
statisticMeasureName: 'active-pro-users',
value: countProSessionsInADay,
date: this.timer.convertDateToMicroseconds(dto.sessionsInADay),
}),
)
const plusSubscriptionPlanName = SubscriptionPlanName.create(SubscriptionPlanName.NAMES.PlusPlan).getValue()
const countPlusSessionsInADay = await this.sessionTracesRepository.countByDateAndSubscriptionPlanName(
dto.sessionsInADay,
plusSubscriptionPlanName,
)
await this.domainEventPublisher.publish(
this.domainEventFactory.createStatisticPersistenceRequestedEvent({
statisticMeasureName: 'active-plus-users',
value: countPlusSessionsInADay,
date: this.timer.convertDateToMicroseconds(dto.sessionsInADay),
}),
)
const countFreeSessionsInADay = countSessionsInADay - countProSessionsInADay - countPlusSessionsInADay
await this.domainEventPublisher.publish(
this.domainEventFactory.createStatisticPersistenceRequestedEvent({
statisticMeasureName: 'active-free-users',
value: countFreeSessionsInADay,
date: this.timer.convertDateToMicroseconds(dto.sessionsInADay),
}),
)
return Result.ok('Statistics persisted.')
}
}

View File

@@ -0,0 +1,3 @@
export interface PersistStatisticsDTO {
sessionsInADay: Date
}

View File

@@ -1,4 +1,4 @@
import { MapperInterface, Uuid } from '@standardnotes/domain-core'
import { MapperInterface, SubscriptionPlanName, Uuid } from '@standardnotes/domain-core'
import { Repository } from 'typeorm'
import { SessionTrace } from '../../Domain/Session/SessionTrace'
import { SessionTraceRepositoryInterface } from '../../Domain/Session/SessionTraceRepositoryInterface'
@@ -10,6 +10,27 @@ export class MySQLSessionTraceRepository implements SessionTraceRepositoryInterf
private mapper: MapperInterface<SessionTrace, TypeORMSessionTrace>,
) {}
async countByDateAndSubscriptionPlanName(date: Date, subscriptionPlanName: SubscriptionPlanName): Promise<number> {
return this.ormRepository
.createQueryBuilder('trace')
.where('trace.creation_date = :creationDate', {
creationDate: new Date(date.getFullYear(), date.getMonth(), date.getDate()),
})
.andWhere('trace.subscription_plan_name = :subscriptionPlanName', {
subscriptionPlanName: subscriptionPlanName.value,
})
.getCount()
}
async countByDate(date: Date): Promise<number> {
return this.ormRepository
.createQueryBuilder('trace')
.where('trace.creation_date = :creationDate', {
creationDate: new Date(date.getFullYear(), date.getMonth(), date.getDate()),
})
.getCount()
}
async removeExpiredBefore(date: Date): Promise<void> {
await this.ormRepository
.createQueryBuilder()

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.59](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.58...@standardnotes/domain-events-infra@1.9.59) (2022-12-20)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.58](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.57...@standardnotes/domain-events-infra@1.9.58) (2022-12-19)
**Note:** Version bump only for package @standardnotes/domain-events-infra

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.9.58",
"version": "1.9.59",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.105.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.105.0...@standardnotes/domain-events@2.105.1) (2022-12-20)
### Bug Fixes
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
# [2.105.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.104.2...@standardnotes/domain-events@2.105.0) (2022-12-19)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.105.0",
"version": "2.105.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -1,5 +1,5 @@
export interface StatisticPersistenceRequestedEventPayload {
statisticMeasureName: string
value: number
date: Date
date: number
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.56](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.55...@standardnotes/event-store@1.6.56) (2022-12-20)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.55](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.54...@standardnotes/event-store@1.6.55) (2022-12-19)
**Note:** Version bump only for package @standardnotes/event-store

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.6.55",
"version": "1.6.56",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.2](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.9.1...@standardnotes/files-server@1.9.2) (2022-12-20)
**Note:** Version bump only for package @standardnotes/files-server
## [1.9.1](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.9.0...@standardnotes/files-server@1.9.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/files-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.9.1",
"version": "1.9.2",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.1...@standardnotes/revisions-server@1.10.2) (2022-12-20)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.10.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.0...@standardnotes/revisions-server@1.10.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/revisions-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.10.1",
"version": "1.10.2",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.16.2](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.1...@standardnotes/scheduler-server@1.16.2) (2022-12-20)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.16.1](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.0...@standardnotes/scheduler-server@1.16.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/scheduler-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.16.1",
"version": "1.16.2",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.2](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.1...@standardnotes/syncing-server@1.28.2) (2022-12-20)
**Note:** Version bump only for package @standardnotes/syncing-server
## [1.28.1](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.0...@standardnotes/syncing-server@1.28.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/syncing-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.28.1",
"version": "1.28.2",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.5.2](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.5.1...@standardnotes/websockets-server@1.5.2) (2022-12-20)
**Note:** Version bump only for package @standardnotes/websockets-server
## [1.5.1](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.5.0...@standardnotes/websockets-server@1.5.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/websockets-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.5.1",
"version": "1.5.2",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.3](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.2...@standardnotes/workspace-server@1.19.3) (2022-12-20)
### Bug Fixes
* **workspace:** specs ([c8203cf](https://github.com/standardnotes/server/commit/c8203cf04cb93cc65d30b69f10fb275f5e6be449))
## [1.19.2](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.1...@standardnotes/workspace-server@1.19.2) (2022-12-20)
**Note:** Version bump only for package @standardnotes/workspace-server
## [1.19.1](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.0...@standardnotes/workspace-server@1.19.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/workspace-server

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/workspace-server",
"version": "1.19.1",
"version": "1.19.3",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -5,7 +5,7 @@ import { WorkspaceInviteRepositoryInterface } from '../../Invite/WorkspaceInvite
import { InviteToWorkspace } from './InviteToWorkspace'
import { DomainEventFactoryInterface } from '../../Event/DomainEventFactoryInterface'
import { DomainEventPublisherInterface, WorkspaceInviteCreatedEvent } from '@standardnotes/domain-events'
import { DomainEventPublisherInterface, EmailRequestedEvent } from '@standardnotes/domain-events'
import { WorkspaceAccessLevel } from '@standardnotes/common'
describe('InviteToWorkspace', () => {
@@ -33,9 +33,7 @@ describe('InviteToWorkspace', () => {
domainEventPublisher.publish = jest.fn()
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createWorkspaceInviteCreatedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<WorkspaceInviteCreatedEvent>)
domainEventFactory.createEmailRequestedEvent = jest.fn().mockReturnValue({} as jest.Mocked<EmailRequestedEvent>)
})
it('should create an invite', async () => {