Compare commits

..

49 Commits

Author SHA1 Message Date
standardci
7f9e6e2f44 chore(release): publish new version
- @standardnotes/analytics@2.17.8
 - @standardnotes/api-gateway@1.41.3
 - @standardnotes/auth-server@1.70.9
 - @standardnotes/files-server@1.9.3
 - @standardnotes/revisions-server@1.10.3
 - @standardnotes/scheduler-server@1.16.4
 - @standardnotes/syncing-server@1.28.3
 - @standardnotes/websockets-server@1.5.3
 - @standardnotes/workspace-server@1.19.4
2022-12-28 07:07:42 +00:00
Karol Sójko
d3c6c0d48e chore(upgrade): sentry deps 2022-12-28 08:05:42 +01:00
Karol Sójko
6c83476fd2 chore: workflow disptach name 2022-12-27 15:50:40 +01:00
Karol Sójko
9cdf7e2c51 Revert "feat: add workflow for tagging latest versions as stable"
This reverts commit a2c484e0f3.
2022-12-27 15:37:32 +01:00
Karol Sójko
599119e14e chore: move e2e test suite to self-hosted repo 2022-12-27 15:00:11 +01:00
Karol Sójko
a2c484e0f3 feat: add workflow for tagging latest versions as stable 2022-12-27 14:43:36 +01:00
standardci
97ff4d5ac2 chore(release): publish new version
- @standardnotes/auth-server@1.70.8
2022-12-20 20:24:56 +00:00
Karol Sójko
5255cfbb25 fix(auth): move tracing sessions to session creation instead of cross service token creation 2022-12-20 21:22:24 +01:00
standardci
780358368b chore(release): publish new version
- @standardnotes/auth-server@1.70.7
2022-12-20 19:55:43 +00:00
Karol Sójko
cf0b918913 fix(auth): change severity on tracing session errors - most probably hazardous reads 2022-12-20 20:53:26 +01:00
standardci
4ea690204e chore(release): publish new version
- @standardnotes/auth-server@1.70.6
2022-12-20 18:54:47 +00:00
Karol Sójko
14eb775749 fix(auth): query for session traces 2022-12-20 19:52:32 +01:00
standardci
bf4a3be6d9 chore(release): publish new version
- @standardnotes/auth-server@1.70.5
2022-12-20 18:48:19 +00:00
Karol Sójko
b9e1e47871 fix(auth): add session traces index 2022-12-20 19:46:05 +01:00
standardci
ff532ecb22 chore(release): publish new version
- @standardnotes/scheduler-server@1.16.3
2022-12-20 18:21:59 +00:00
Karol Sójko
eb21872db1 fix(scheduler): new pricing for subscription encouragement email 2022-12-20 19:19:59 +01:00
standardci
8e3df184dc chore(release): publish new version
- @standardnotes/analytics@2.17.7
2022-12-20 14:43:24 +00:00
Karol Sójko
b34bbcac8b fix(analytics): monthly numbers of active users 2022-12-20 15:41:03 +01:00
standardci
226965a1d7 chore(release): publish new version
- @standardnotes/analytics@2.17.6
2022-12-20 14:04:05 +00:00
Karol Sójko
17b2ea126c fix(analytics): filtered counts for user activity check 2022-12-20 15:02:09 +01:00
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
standardci
63aef71f60 chore(release): publish new version
- @standardnotes/analytics@2.14.0
 - @standardnotes/api-gateway@1.41.1
 - @standardnotes/auth-server@1.69.1
 - @standardnotes/domain-events-infra@1.9.58
 - @standardnotes/domain-events@2.105.0
 - @standardnotes/event-store@1.6.55
 - @standardnotes/files-server@1.9.1
 - @standardnotes/revisions-server@1.10.1
 - @standardnotes/scheduler-server@1.16.1
 - @standardnotes/syncing-server@1.28.1
 - @standardnotes/websockets-server@1.5.1
 - @standardnotes/workspace-server@1.19.1
2022-12-19 13:22:13 +00:00
Karol Sójko
0f8457534c feat(analytics): add persisting statistics on demand 2022-12-19 14:20:16 +01:00
standardci
2984582e62 chore(release): publish new version
- @standardnotes/auth-server@1.69.0
2022-12-19 11:52:23 +00:00
Karol Sójko
147d8fd9af feat(auth): add session traces cleanup procedure 2022-12-19 12:49:59 +01:00
103 changed files with 1449 additions and 1298 deletions

View File

@@ -190,9 +190,9 @@ jobs:
uses: convictional/trigger-workflow-and-wait@master
with:
owner: standardnotes
repo: e2e
repo: self-hosted
github_token: ${{ secrets.CI_PAT_TOKEN }}
workflow_file_name: testing-with-stable-client.yml
workflow_file_name: testing-with-updating-client-and-server.yml
wait_interval: 30
client_payload: '{"${{ inputs.e2e_tag_parameter_name }}": "${{ github.sha }}"}'
propagate_failure: true

77
.pnp.cjs generated
View File

@@ -126,7 +126,7 @@ const RAW_RUNTIME_STATE =
["@lerna-lite/cli", "npm:1.6.0"],\
["@lerna-lite/list", "npm:1.6.0"],\
["@lerna-lite/run", "npm:1.6.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
@@ -2447,6 +2447,16 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.28.1-a468033ea8-f29d747d3e.zip/node_modules/@sentry/core/",\
"packageDependencies": [\
["@sentry/core", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.28.1"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/hub", [\
@@ -2476,6 +2486,20 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.28.1-b0e124fdfc-b4922d1f0a.zip/node_modules/@sentry/node/",\
"packageDependencies": [\
["@sentry/node", "npm:7.28.1"],\
["@sentry/core", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.28.1"],\
["cookie", "npm:0.4.2"],\
["https-proxy-agent", "npm:5.0.1"],\
["lru_map", "npm:0.3.3"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/profiling-node", [\
@@ -2506,6 +2530,17 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-tracing-npm-7.28.1-e15d453d8e-be501ca9d7.zip/node_modules/@sentry/tracing/",\
"packageDependencies": [\
["@sentry/tracing", "npm:7.28.1"],\
["@sentry/core", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.28.1"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/types", [\
@@ -2515,6 +2550,13 @@ const RAW_RUNTIME_STATE =
["@sentry/types", "npm:7.27.0"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.28.1-42d9a8574c-7dc6639cb7.zip/node_modules/@sentry/types/",\
"packageDependencies": [\
["@sentry/types", "npm:7.28.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/utils", [\
@@ -2526,6 +2568,15 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.28.1-71eaeb767f-a4b5f73db0.zip/node_modules/@sentry/utils/",\
"packageDependencies": [\
["@sentry/utils", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sinclair/typebox", [\
@@ -2581,7 +2632,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/analytics", "workspace:packages/analytics"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -2633,7 +2684,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
@@ -2689,9 +2740,9 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/auth-server", "workspace:packages/auth"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/profiling-node", "npm:0.0.12"],\
["@sentry/tracing", "npm:7.27.0"],\
["@sentry/tracing", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -2915,7 +2966,7 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/files/",\
"packageDependencies": [\
["@standardnotes/files-server", "workspace:packages/files"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/config", "npm:2.4.3"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3050,7 +3101,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -3095,7 +3146,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3156,7 +3207,7 @@ const RAW_RUNTIME_STATE =
["@lerna-lite/cli", "npm:1.6.0"],\
["@lerna-lite/list", "npm:1.6.0"],\
["@lerna-lite/run", "npm:1.6.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
@@ -3222,9 +3273,9 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/profiling-node", "npm:0.0.12"],\
["@sentry/tracing", "npm:7.27.0"],\
["@sentry/tracing", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3324,7 +3375,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3364,7 +3415,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -61,7 +61,7 @@
},
"packageManager": "yarn@4.0.0-rc.25",
"dependencies": {
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"newrelic": "^9.6.0"
}
}

View File

@@ -3,6 +3,81 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.17.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.7...@standardnotes/analytics@2.17.8) (2022-12-28)
**Note:** Version bump only for package @standardnotes/analytics
## [2.17.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.6...@standardnotes/analytics@2.17.7) (2022-12-20)
### Bug Fixes
* **analytics:** monthly numbers of active users ([b34bbca](https://github.com/standardnotes/server/commit/b34bbcac8b9604283b3a5959ab3218c468ce8a00))
## [2.17.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.5...@standardnotes/analytics@2.17.6) (2022-12-20)
### Bug Fixes
* **analytics:** filtered counts for user activity check ([17b2ea1](https://github.com/standardnotes/server/commit/17b2ea126c5ad2d7cf07657def63f9977f239a3c))
## [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
* **analytics:** add persisting statistics on demand ([0f84575](https://github.com/standardnotes/server/commit/0f8457534c1829c58f3c036749d262307ddeb779))
# [2.13.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.27...@standardnotes/analytics@2.13.0) (2022-12-19)
### Features

View File

@@ -8,7 +8,6 @@ import { EmailLevel } from '@standardnotes/domain-core'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
import { Period } from '../src/Domain/Time/Period'
import { StatisticsMeasure } from '../src/Domain/Statistics/StatisticsMeasure'
import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface'
import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface'
import { PeriodKeyGeneratorInterface } from '../src/Domain/Time/PeriodKeyGeneratorInterface'
@@ -19,6 +18,7 @@ import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFact
import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
import { TimerInterface } from '@standardnotes/time'
import { StatisticMeasureName } from '../src/Domain/Statistics/StatisticMeasureName'
const requestReport = async (
analyticsStore: AnalyticsStoreInterface,
@@ -115,12 +115,16 @@ const requestReport = async (
}> = []
const thirtyDaysStatisticsNames = [
StatisticsMeasure.MRR,
StatisticsMeasure.AnnualPlansMRR,
StatisticsMeasure.MonthlyPlansMRR,
StatisticsMeasure.FiveYearPlansMRR,
StatisticsMeasure.PlusPlansMRR,
StatisticsMeasure.ProPlansMRR,
StatisticMeasureName.NAMES.MRR,
StatisticMeasureName.NAMES.AnnualPlansMRR,
StatisticMeasureName.NAMES.MonthlyPlansMRR,
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({
@@ -130,7 +134,7 @@ const requestReport = async (
})
}
const monthlyStatisticsNames = [StatisticsMeasure.MRR]
const monthlyStatisticsNames = [StatisticMeasureName.NAMES.MRR]
for (const statisticName of monthlyStatisticsNames) {
statisticsOverTime.push({
name: statisticName,
@@ -140,22 +144,22 @@ const requestReport = async (
}
const statisticMeasureNames = [
StatisticsMeasure.Income,
StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome,
StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome,
StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome,
StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome,
StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome,
StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome,
StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome,
StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome,
StatisticsMeasure.Refunds,
StatisticsMeasure.RegistrationLength,
StatisticsMeasure.SubscriptionLength,
StatisticsMeasure.RegistrationToSubscriptionTime,
StatisticsMeasure.RemainingSubscriptionTimePercentage,
StatisticsMeasure.NewCustomers,
StatisticsMeasure.TotalCustomers,
StatisticMeasureName.NAMES.Income,
StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome,
StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome,
StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome,
StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome,
StatisticMeasureName.NAMES.Refunds,
StatisticMeasureName.NAMES.RegistrationLength,
StatisticMeasureName.NAMES.SubscriptionLength,
StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
StatisticMeasureName.NAMES.NewCustomers,
StatisticMeasureName.NAMES.TotalCustomers,
]
const statisticMeasures: Array<{
name: string
@@ -190,7 +194,10 @@ const requestReport = async (
const totalCustomerCounts: Array<number> = []
for (const dailyPeriodKey of dailyPeriodKeys) {
const customersCount = await statisticsStore.getMeasureTotal(StatisticsMeasure.TotalCustomers, dailyPeriodKey)
const customersCount = await statisticsStore.getMeasureTotal(
StatisticMeasureName.NAMES.TotalCustomers,
dailyPeriodKey,
)
totalCustomerCounts.push(customersCount)
}
const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count)

View File

@@ -7,5 +7,5 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/'],
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/', '/Handler/'],
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.13.0",
"version": "2.17.8",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -38,7 +38,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",

View File

@@ -52,6 +52,9 @@ import { RevenueModification } from '../Domain/Revenue/RevenueModification'
import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap'
import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification'
import { CalculateMonthlyRecurringRevenue } from '../Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
import { PersistStatistic } from '../Domain/UseCase/PersistStatistic/PersistStatistic'
import { StatisticMeasureRepositoryInterface } from '../Domain/Statistics/StatisticMeasureRepositoryInterface'
import { StatisticPersistenceRequestedEventHandler } from '../Domain/Handler/StatisticPersistenceRequestedEventHandler'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -132,6 +135,29 @@ export class ContainerConfigLoader {
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
// Services
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
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<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
if (env.get('SNS_TOPIC_ARN', true)) {
container
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
} else {
container
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
)
}
// Repositories
container
.bind<AnalyticsEntityRepositoryInterface>(TYPES.AnalyticsEntityRepository)
@@ -139,6 +165,9 @@ export class ContainerConfigLoader {
container
.bind<RevenueModificationRepositoryInterface>(TYPES.RevenueModificationRepository)
.to(MySQLRevenueModificationRepository)
container
.bind<StatisticMeasureRepositoryInterface>(TYPES.StatisticMeasureRepository)
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
// ORM
container
@@ -154,6 +183,9 @@ export class ContainerConfigLoader {
container
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
.to(CalculateMonthlyRecurringRevenue)
container
.bind<PersistStatistic>(TYPES.PersistStatistic)
.toConstantValue(new PersistStatistic(container.get(TYPES.StatisticMeasureRepository)))
// Hanlders
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
@@ -181,35 +213,21 @@ export class ContainerConfigLoader {
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
.to(SubscriptionReactivatedEventHandler)
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
container
.bind<StatisticPersistenceRequestedEventHandler>(TYPES.StatisticPersistenceRequestedEventHandler)
.toConstantValue(
new StatisticPersistenceRequestedEventHandler(
container.get(TYPES.PersistStatistic),
container.get(TYPES.Timer),
container.get(TYPES.Logger),
),
)
// Maps
container
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
.to(RevenueModificationMap)
// Services
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
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<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
if (env.get('SNS_TOPIC_ARN', true)) {
container
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
} else {
container
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
)
}
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
@@ -222,6 +240,7 @@ export class ContainerConfigLoader {
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
['STATISTIC_PERSISTENCE_REQUESTED', container.get(TYPES.StatisticPersistenceRequestedEventHandler)],
])
if (env.get('SQS_QUEUE_URL', true)) {

View File

@@ -15,6 +15,7 @@ const TYPES = {
// Repositories
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
StatisticMeasureRepository: Symbol.for('StatisticMeasureRepository'),
// ORM
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
@@ -22,6 +23,7 @@ const TYPES = {
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
PersistStatistic: Symbol.for('PersistStatistic'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
@@ -34,6 +36,7 @@ const TYPES = {
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
StatisticPersistenceRequestedEventHandler: Symbol.for('StatisticPersistenceRequestedEventHandler'),
// Maps
RevenueModificationMap: Symbol.for('RevenueModificationMap'),
// Services

View File

@@ -2,9 +2,41 @@
import { TimerInterface } from '@standardnotes/time'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
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,
)
if (filteredCounts.length === 0) {
return {
yesterday: 0,
last30Days: 0,
}
}
const last30DaysNumbers = filteredCounts.map((count: { totalCount: number }) => count.totalCount)
const last30DaysCount = last30DaysNumbers.reduce((previousValue: number, currentValue: number) => {
return previousValue + currentValue
})
const averageActiveUsersLast30Days = Math.floor(last30DaysCount / last30DaysNumbers.length)
return {
yesterday: totalActiveUsersYesterday,
last30Days: averageActiveUsersLast30Days,
}
}
const getChartUrls = (
data: any,
): {
@@ -12,7 +44,6 @@ const getChartUrls = (
users: string
quarterlyPerformance: string
churn: string
mrr: string
mrrMonthly: string
} => {
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
@@ -237,82 +268,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 +326,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))}`,
}
}
@@ -417,156 +371,170 @@ export const html = (data: any, timer: TimerInterface) => {
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
)
const incomeMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Income && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.Yesterday,
)
const refundMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Refunds && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.Yesterday,
)
const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0
const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0
const revenueYesterday = incomeYesterday - refundsYesterday
const subscriptionLengthMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.Yesterday,
)
const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0),
)
const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
)
const subscriptionRemainingTimePercentageYesterday = Math.floor(
subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0,
)
const registrationLengthMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.Yesterday,
)
const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationLengthMeasureYesterday?.average ?? 0),
)
const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
)
const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0),
)
const incomeMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Income && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.ThisMonth,
)
const refundMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Refunds && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.ThisMonth,
)
const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0
const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0
const revenueThisMonth = incomeThisMonth - refundsThisMonth
const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.ThisMonth,
)
const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0),
)
const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
)
const subscriptionRemainingTimePercentageThisMonth = Math.floor(
subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0,
)
const registrationLengthMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.ThisMonth,
)
const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationLengthMeasureThisMonth?.average ?? 0),
)
const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
)
const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0),
)
const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
a.period === Period.Yesterday,
)
const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
a.period === Period.Yesterday,
)
const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
a.period === Period.Yesterday,
)
const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
a.period === Period.Yesterday,
)
const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
a.period === Period.Yesterday,
)
const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
a.period === Period.Yesterday,
)
const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome &&
a.period === Period.Yesterday,
)
const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
a.period === Period.ThisMonth,
)
const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
a.period === Period.ThisMonth,
)
const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
a.period === Period.ThisMonth,
)
const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
a.period === Period.ThisMonth,
)
const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
a.period === Period.ThisMonth,
)
const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
a.period === Period.ThisMonth,
)
const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome &&
a.period === Period.ThisMonth,
)
const mrrOverTime = data.statisticsOverTime.find(
@@ -594,12 +562,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>
@@ -798,6 +793,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>
@@ -930,10 +947,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,69 +0,0 @@
import 'reflect-metadata'
import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events'
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
import { TimerInterface } from '@standardnotes/time'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
describe('AccountDeletionRequestedEventHandler', () => {
let event: AccountDeletionRequestedEvent
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let timer: TimerInterface
const createHandler = () =>
new AccountDeletionRequestedEventHandler(analyticsEntityRepository, analyticsStore, statisticsStore, timer)
beforeEach(() => {
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '1-2-3',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: '2-3-4',
}
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue({ id: 3 })
analyticsEntityRepository.remove = jest.fn()
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
})
it('should mark account deletion and registration length', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['DeleteAccount'], 3, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith('registration-length', 122, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
expect(analyticsEntityRepository.remove).toHaveBeenCalled()
})
it('should not mark anything if entity is not found', async () => {
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
expect(analyticsEntityRepository.remove).not.toHaveBeenCalled()
})
})

View File

@@ -6,7 +6,7 @@ import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
@@ -33,7 +33,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
])
const registrationLength = this.timer.getTimestampInMicroseconds() - event.payload.userCreatedAtTimestamp
await this.statisticsStore.incrementMeasure(StatisticsMeasure.RegistrationLength, registrationLength, [
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.RegistrationLength, registrationLength, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,

View File

@@ -1,34 +0,0 @@
import 'reflect-metadata'
import { PaymentFailedEvent } from '@standardnotes/domain-events'
import { PaymentFailedEventHandler } from './PaymentFailedEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
describe('PaymentFailedEventHandler', () => {
let event: PaymentFailedEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
const createHandler = () => new PaymentFailedEventHandler(getUserAnalyticsId, analyticsStore)
beforeEach(() => {
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()
})
})

View File

@@ -1,75 +0,0 @@
import 'reflect-metadata'
import { PaymentSuccessEvent } from '@standardnotes/domain-events'
import { PaymentSuccessEventHandler } from './PaymentSuccessEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { Logger } from 'winston'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { Period } from '../Time/Period'
describe('PaymentSuccessEventHandler', () => {
let event: PaymentSuccessEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let logger: Logger
const createHandler = () =>
new PaymentSuccessEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, logger)
beforeEach(() => {
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,
])
})
})

View File

@@ -6,7 +6,7 @@ import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@@ -20,15 +20,27 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
[
PaymentType.Initial,
new Map([
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome],
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome],
[
SubscriptionBillingFrequency.Monthly,
StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
],
[
SubscriptionBillingFrequency.Annual,
StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
],
]),
],
[
PaymentType.Renewal,
new Map([
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome],
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome],
[
SubscriptionBillingFrequency.Monthly,
StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome,
],
[
SubscriptionBillingFrequency.Annual,
StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome,
],
]),
],
]),
@@ -39,15 +51,27 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
[
PaymentType.Initial,
new Map([
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome],
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome],
[
SubscriptionBillingFrequency.Monthly,
StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
],
[
SubscriptionBillingFrequency.Annual,
StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
],
]),
],
[
PaymentType.Renewal,
new Map([
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome],
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome],
[
SubscriptionBillingFrequency.Monthly,
StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome,
],
[
SubscriptionBillingFrequency.Annual,
StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome,
],
]),
],
]),
@@ -69,7 +93,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
Period.ThisMonth,
])
const statisticMeasures = [StatisticsMeasure.Income]
const statisticMeasures = [StatisticMeasureName.NAMES.Income]
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
?.get(event.payload.paymentType as PaymentType)

View File

@@ -1,36 +0,0 @@
import 'reflect-metadata'
import { RefundProcessedEvent } from '@standardnotes/domain-events'
import { RefundProcessedEventHandler } from './RefundProcessedEventHandler'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
describe('RefundProcessedEventHandler', () => {
let event: RefundProcessedEvent
let statisticsStore: StatisticsStoreInterface
const createHandler = () => new RefundProcessedEventHandler(statisticsStore)
beforeEach(() => {
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
event = {} as jest.Mocked<RefundProcessedEvent>
event.payload = {
userEmail: 'test@test.com',
amount: 12.45,
}
})
it('should mark refunds for statistics', async () => {
await createHandler().handle(event)
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.Refunds, 12.45, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
})
})

View File

@@ -2,7 +2,7 @@ import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnote
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
@@ -11,7 +11,7 @@ export class RefundProcessedEventHandler implements DomainEventHandlerInterface
constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {}
async handle(event: RefundProcessedEvent): Promise<void> {
await this.statisticsStore.incrementMeasure(StatisticsMeasure.Refunds, event.payload.amount, [
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,

View File

@@ -0,0 +1,20 @@
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 timer: TimerInterface, private logger: Logger) {}
async handle(event: StatisticPersistenceRequestedEvent): Promise<void> {
const result = await this.persistStatistic.execute({
date: this.timer.convertMicrosecondsToDate(event.payload.date),
statisticMeasureName: event.payload.statisticMeasureName,
value: event.payload.value,
})
if (result.isFailed()) {
this.logger.error(result.getError())
}
}
}

View File

@@ -1,104 +0,0 @@
import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { Result } from '@standardnotes/domain-core'
import { SubscriptionCancelledEvent } from '@standardnotes/domain-events'
import { SubscriptionCancelledEventHandler } from './SubscriptionCancelledEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
import { RevenueModification } from '../Revenue/RevenueModification'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { Logger } from 'winston'
describe('SubscriptionCancelledEventHandler', () => {
let event: SubscriptionCancelledEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let saveRevenueModification: SaveRevenueModification
let logger: Logger
const createHandler = () =>
new SubscriptionCancelledEventHandler(
getUserAnalyticsId,
analyticsStore,
statisticsStore,
saveRevenueModification,
logger,
)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
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<SubscriptionCancelledEvent>
event.createdAt = new Date(1)
event.type = 'SUBSCRIPTION_CANCELLED'
event.payload = {
subscriptionId: 1,
userEmail: 'test@test.com',
subscriptionName: SubscriptionName.ProPlan,
subscriptionCreatedAt: 1642395451515000,
subscriptionUpdatedAt: 1642395451515001,
lastPayedAt: 1642395451515001,
subscriptionEndsAt: 1642395451515000 + 10,
timestamp: 1,
offline: false,
replaced: false,
userExistingSubscriptionsCount: 1,
billingFrequency: 1,
payAmount: 12.99,
}
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
})
it('should track subscription cancelled statistics', async () => {
event.payload.timestamp = 1642395451516000
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalled()
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.SubscriptionLength, 1000, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
expect(saveRevenueModification.execute).toHaveBeenCalled()
})
it('should not track statistics for subscriptions that are in a legacy 5 year plan', async () => {
event.payload.timestamp = 1642395451516000
event.payload.subscriptionEndsAt = 1642395451515000 + 126_230_400_000_001
event.payload.subscriptionCreatedAt = 1642395451515000
await createHandler().handle(event)
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
expect(saveRevenueModification.execute).toHaveBeenCalled()
})
it('should log failure to save revenue modification', async () => {
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
event.payload.timestamp = 1642395451516000
await createHandler().handle(event)
expect(logger.error).toHaveBeenCalled()
})
})

View File

@@ -6,13 +6,13 @@ import { Username } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
import { Period } from '../Time/Period'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
@injectable()
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
@@ -58,7 +58,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
}
const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.SubscriptionLength, subscriptionLength, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
@@ -70,7 +70,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
await this.statisticsStore.incrementMeasure(
StatisticsMeasure.RemainingSubscriptionTimePercentage,
StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
remainingSubscriptionPercentage,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)

View File

@@ -1,79 +0,0 @@
import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionExpiredEvent } from '@standardnotes/domain-events'
import { Result } from '@standardnotes/domain-core'
import { SubscriptionExpiredEventHandler } from './SubscriptionExpiredEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { RevenueModification } from '../Revenue/RevenueModification'
import { Logger } from 'winston'
describe('SubscriptionExpiredEventHandler', () => {
let event: SubscriptionExpiredEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let saveRevenueModification: SaveRevenueModification
let logger: Logger
const createHandler = () =>
new SubscriptionExpiredEventHandler(
getUserAnalyticsId,
analyticsStore,
statisticsStore,
saveRevenueModification,
logger,
)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
event = {} as jest.Mocked<SubscriptionExpiredEvent>
event.createdAt = new Date(1)
event.type = 'SUBSCRIPTION_EXPIRED'
event.payload = {
subscriptionId: 1,
userEmail: 'test@test.com',
subscriptionName: SubscriptionName.PlusPlan,
timestamp: 1,
offline: false,
totalActiveSubscriptionsCount: 123,
userExistingSubscriptionsCount: 2,
billingFrequency: 1,
payAmount: 12.99,
}
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()
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
})
it('should update analytics and statistics', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalled()
expect(statisticsStore.setMeasure).toHaveBeenCalled()
expect(saveRevenueModification.execute).toHaveBeenCalled()
})
it('should log failure to save revenue modification', async () => {
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
await createHandler().handle(event)
expect(logger.error).toHaveBeenCalled()
})
})

View File

@@ -6,7 +6,7 @@ import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
@@ -33,7 +33,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
)
await this.statisticsStore.setMeasure(
StatisticsMeasure.TotalCustomers,
StatisticMeasureName.NAMES.TotalCustomers,
event.payload.totalActiveSubscriptionsCount,
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
)

View File

@@ -1,102 +0,0 @@
import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
import { Result } from '@standardnotes/domain-core'
import { SubscriptionPurchasedEventHandler } from './SubscriptionPurchasedEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { RevenueModification } from '../Revenue/RevenueModification'
import { Logger } from 'winston'
describe('SubscriptionPurchasedEventHandler', () => {
let event: SubscriptionPurchasedEvent
let subscriptionExpiresAt: number
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let saveRevenueModification: SaveRevenueModification
let logger: Logger
const createHandler = () =>
new SubscriptionPurchasedEventHandler(
getUserAnalyticsId,
analyticsStore,
statisticsStore,
saveRevenueModification,
logger,
)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
statisticsStore.setMeasure = jest.fn()
event = {} as jest.Mocked<SubscriptionPurchasedEvent>
event.createdAt = new Date(1)
event.type = 'SUBSCRIPTION_PURCHASED'
event.payload = {
subscriptionId: 1,
userEmail: 'test@test.com',
subscriptionName: SubscriptionName.ProPlan,
subscriptionExpiresAt,
timestamp: 60,
offline: false,
discountCode: null,
limitedDiscountPurchased: false,
newSubscriber: true,
totalActiveSubscriptionsCount: 123,
userRegisteredAt: 23,
billingFrequency: 12,
payAmount: 29.99,
}
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()
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
})
it('should mark subscription creation statistics', async () => {
await createHandler().handle(event)
expect(statisticsStore.incrementMeasure).toHaveBeenCalled()
expect(saveRevenueModification.execute).toHaveBeenCalled()
})
it("should not measure registration to subscription time if this is not user's first subscription", async () => {
event.payload.newSubscriber = false
await createHandler().handle(event)
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
})
it('should update analytics on limited discount offer purchasing', async () => {
event.payload.limitedDiscountPurchased = true
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['limited-discount-offer-purchased'], 3, [Period.Today])
})
it('should log failure to save revenue modification', async () => {
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
await createHandler().handle(event)
expect(logger.error).toHaveBeenCalled()
})
})

View File

@@ -6,7 +6,7 @@ import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
@@ -45,18 +45,18 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
if (event.payload.newSubscriber) {
await this.statisticsStore.incrementMeasure(
StatisticsMeasure.RegistrationToSubscriptionTime,
StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
event.payload.timestamp - event.payload.userRegisteredAt,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
await this.statisticsStore.incrementMeasure(StatisticsMeasure.NewCustomers, 1, [
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.NewCustomers, 1, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
Period.ThisYear,
])
await this.statisticsStore.setMeasure(
StatisticsMeasure.TotalCustomers,
StatisticMeasureName.NAMES.TotalCustomers,
event.payload.totalActiveSubscriptionsCount,
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
)

View File

@@ -1,46 +0,0 @@
import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
import { SubscriptionReactivatedEventHandler } from './SubscriptionReactivatedEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { Period } from '../Time/Period'
describe('SubscriptionReactivatedEventHandler', () => {
let event: SubscriptionReactivatedEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
const createHandler = () => new SubscriptionReactivatedEventHandler(analyticsStore, getUserAnalyticsId)
beforeEach(() => {
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()
})
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,
])
})
})

View File

@@ -1,110 +0,0 @@
import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionRefundedEvent } from '@standardnotes/domain-events'
import { Result } from '@standardnotes/domain-core'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { SubscriptionRefundedEventHandler } from './SubscriptionRefundedEventHandler'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { Period } from '../Time/Period'
import { RevenueModification } from '../Revenue/RevenueModification'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { Logger } from 'winston'
describe('SubscriptionRefundedEventHandler', () => {
let event: SubscriptionRefundedEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let saveRevenueModification: SaveRevenueModification
let logger: Logger
const createHandler = () =>
new SubscriptionRefundedEventHandler(
getUserAnalyticsId,
analyticsStore,
statisticsStore,
saveRevenueModification,
logger,
)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
event = {} as jest.Mocked<SubscriptionRefundedEvent>
event.createdAt = new Date(1)
event.type = 'SUBSCRIPTION_REFUNDED'
event.payload = {
subscriptionId: 1,
userEmail: 'test@test.com',
subscriptionName: SubscriptionName.PlusPlan,
timestamp: 1,
offline: false,
userExistingSubscriptionsCount: 3,
totalActiveSubscriptionsCount: 1,
billingFrequency: 1,
payAmount: 12.99,
}
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()
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
})
it('should mark churn for new customer', async () => {
event.payload.userExistingSubscriptionsCount = 1
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.SubscriptionRefunded], 3, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.NewCustomersChurn], 3, [
Period.ThisMonth,
])
expect(saveRevenueModification.execute).toHaveBeenCalled()
})
it('should mark churn for existing customer', async () => {
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 () => {
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(false)
await createHandler().handle(event)
expect(analyticsStore.markActivity).not.toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [
Period.ThisMonth,
])
})
it('should log failure to save revenue modification', async () => {
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
await createHandler().handle(event)
expect(logger.error).toHaveBeenCalled()
})
})

View File

@@ -6,7 +6,7 @@ import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
@@ -70,7 +70,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
}
await this.statisticsStore.setMeasure(
StatisticsMeasure.TotalCustomers,
StatisticMeasureName.NAMES.TotalCustomers,
event.payload.totalActiveSubscriptionsCount,
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
)

View File

@@ -1,68 +0,0 @@
import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionRenewedEvent } from '@standardnotes/domain-events'
import { Result } from '@standardnotes/domain-core'
import { SubscriptionRenewedEventHandler } from './SubscriptionRenewedEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { RevenueModification } from '../Revenue/RevenueModification'
import { Logger } from 'winston'
describe('SubscriptionRenewedEventHandler', () => {
let event: SubscriptionRenewedEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let saveRevenueModification: SaveRevenueModification
let logger: Logger
const createHandler = () =>
new SubscriptionRenewedEventHandler(getUserAnalyticsId, analyticsStore, saveRevenueModification, logger)
beforeEach(() => {
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
event = {} as jest.Mocked<SubscriptionRenewedEvent>
event.createdAt = new Date(1)
event.type = 'SUBSCRIPTION_RENEWED'
event.payload = {
subscriptionId: 1,
userEmail: 'test@test.com',
subscriptionName: SubscriptionName.ProPlan,
subscriptionExpiresAt: 2,
timestamp: 1,
offline: false,
billingFrequency: 1,
payAmount: 12.99,
}
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()
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
})
it('should track subscription renewed statistics', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalled()
expect(analyticsStore.unmarkActivity).toHaveBeenCalled()
expect(saveRevenueModification.execute).toHaveBeenCalled()
})
it('should log failure to save revenue modification', async () => {
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
await createHandler().handle(event)
expect(logger.error).toHaveBeenCalled()
})
})

View File

@@ -1,47 +0,0 @@
import 'reflect-metadata'
import { UserRegisteredEvent } from '@standardnotes/domain-events'
import { ProtocolVersion } from '@standardnotes/common'
import { UserRegisteredEventHandler } from './UserRegisteredEventHandler'
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { Period } from '../Time/Period'
describe('UserRegisteredEventHandler', () => {
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
let event: UserRegisteredEvent
let analyticsStore: AnalyticsStoreInterface
const createHandler = () => new UserRegisteredEventHandler(analyticsEntityRepository, analyticsStore)
beforeEach(() => {
event = {} as jest.Mocked<UserRegisteredEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '1-2-3',
email: 'test@test.te',
protocolVersion: ProtocolVersion.V004,
}
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
analyticsEntityRepository.save = jest.fn().mockImplementation((entity) => ({
...entity,
id: 1,
}))
})
it('should save analytics entity upon user registration', async () => {
await createHandler().handle(event)
expect(analyticsEntityRepository.save).toHaveBeenCalled()
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['register'], 1, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
})
})

View File

@@ -0,0 +1,25 @@
import { Result, Entity, UniqueEntityId } from '@standardnotes/domain-core'
import { StatisticMeasureProps } from './StatisticMeasureProps'
export class StatisticMeasure extends Entity<StatisticMeasureProps> {
get id(): UniqueEntityId {
return this._id
}
get name(): string {
return this.props.name.value
}
get value(): number {
return this.props.value
}
private constructor(props: StatisticMeasureProps, id?: UniqueEntityId) {
super(props, id)
}
static create(props: StatisticMeasureProps, id?: UniqueEntityId): Result<StatisticMeasure> {
return Result.ok<StatisticMeasure>(new StatisticMeasure(props, id))
}
}

View File

@@ -0,0 +1,18 @@
import { StatisticMeasureName } from './StatisticMeasureName'
describe('StatisticMeasureName', () => {
it('should create a value object', () => {
const valueOrError = StatisticMeasureName.create('pro-subscription-initial-monthly-payments-income')
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('pro-subscription-initial-monthly-payments-income')
})
it('should not create an invalid value object', () => {
for (const value of ['', undefined, null, 0, 'foobar']) {
const valueOrError = StatisticMeasureName.create(value as string)
expect(valueOrError.isFailed()).toBeTruthy()
}
})
})

View File

@@ -0,0 +1,51 @@
import { ValueObject, Result } from '@standardnotes/domain-core'
import { StatisticMeasureNameProps } from './StatisticMeasureNameProps'
export class StatisticMeasureName extends ValueObject<StatisticMeasureNameProps> {
static readonly NAMES = {
Income: 'income',
PlusSubscriptionInitialMonthlyPaymentsIncome: 'plus-subscription-initial-monthly-payments-income',
ProSubscriptionInitialMonthlyPaymentsIncome: 'pro-subscription-initial-monthly-payments-income',
PlusSubscriptionInitialAnnualPaymentsIncome: 'plus-subscription-initial-annual-payments-income',
ProSubscriptionInitialAnnualPaymentsIncome: 'pro-subscription-initial-annual-payments-income',
PlusSubscriptionRenewingMonthlyPaymentsIncome: 'plus-subscription-renewing-monthly-payments-income',
ProSubscriptionRenewingMonthlyPaymentsIncome: 'pro-subscription-renewing-monthly-payments-income',
PlusSubscriptionRenewingAnnualPaymentsIncome: 'plus-subscription-renewing-annual-payments-income',
ProSubscriptionRenewingAnnualPaymentsIncome: 'pro-subscription-renewing-annual-payments-income',
SubscriptionLength: 'subscription-length',
RegistrationLength: 'registration-length',
RegistrationToSubscriptionTime: 'registration-to-subscription-time',
RemainingSubscriptionTimePercentage: 'remaining-subscription-time-percentage',
Refunds: 'refunds',
NewCustomers: 'new-customers',
TotalCustomers: 'total-customers',
MRR: 'mrr',
MonthlyPlansMRR: 'monthly-plans-mrr',
AnnualPlansMRR: 'annual-plans-mrr',
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 {
return this.props.value
}
private constructor(props: StatisticMeasureNameProps) {
super(props)
}
static create(name: string): Result<StatisticMeasureName> {
const isValidName = Object.values(this.NAMES).includes(name)
if (!isValidName) {
return Result.fail<StatisticMeasureName>(`Invalid statistics measure name: ${name}`)
} else {
return Result.ok<StatisticMeasureName>(new StatisticMeasureName({ value: name }))
}
}
}

View File

@@ -0,0 +1,3 @@
export interface StatisticMeasureNameProps {
value: string
}

View File

@@ -0,0 +1,7 @@
import { StatisticMeasureName } from './StatisticMeasureName'
export interface StatisticMeasureProps {
name: StatisticMeasureName
value: number
date: Date
}

View File

@@ -0,0 +1,5 @@
import { StatisticMeasure } from './StatisticMeasure'
export interface StatisticMeasureRepositoryInterface {
save(statisticMeasure: StatisticMeasure): Promise<void>
}

View File

@@ -1,24 +0,0 @@
export enum StatisticsMeasure {
Income = 'income',
PlusSubscriptionInitialMonthlyPaymentsIncome = 'plus-subscription-initial-monthly-payments-income',
ProSubscriptionInitialMonthlyPaymentsIncome = 'pro-subscription-initial-monthly-payments-income',
PlusSubscriptionInitialAnnualPaymentsIncome = 'plus-subscription-initial-annual-payments-income',
ProSubscriptionInitialAnnualPaymentsIncome = 'pro-subscription-initial-annual-payments-income',
PlusSubscriptionRenewingMonthlyPaymentsIncome = 'plus-subscription-renewing-monthly-payments-income',
ProSubscriptionRenewingMonthlyPaymentsIncome = 'pro-subscription-renewing-monthly-payments-income',
PlusSubscriptionRenewingAnnualPaymentsIncome = 'plus-subscription-renewing-annual-payments-income',
ProSubscriptionRenewingAnnualPaymentsIncome = 'pro-subscription-renewing-annual-payments-income',
SubscriptionLength = 'subscription-length',
RegistrationLength = 'registration-length',
RegistrationToSubscriptionTime = 'registration-to-subscription-time',
RemainingSubscriptionTimePercentage = 'remaining-subscription-time-percentage',
Refunds = 'refunds',
NewCustomers = 'new-customers',
TotalCustomers = 'total-customers',
MRR = 'mrr',
MonthlyPlansMRR = 'monthly-plans-mrr',
AnnualPlansMRR = 'annual-plans-mrr',
FiveYearPlansMRR = 'five-year-plans-mrr',
ProPlansMRR = 'pro-plans-mrr',
PlusPlansMRR = 'plus-plans-mrr',
}

View File

@@ -1,5 +1,4 @@
import { Period } from '../Time/Period'
import { StatisticsMeasure } from './StatisticsMeasure'
export interface StatisticsStoreInterface {
incrementSNJSVersionUsage(snjsVersion: string): Promise<void>
@@ -8,13 +7,13 @@ export interface StatisticsStoreInterface {
getYesterdaySNJSUsage(): Promise<Array<{ version: string; count: number }>>
getYesterdayApplicationUsage(): Promise<Array<{ version: string; count: number }>>
getYesterdayOutOfSyncIncidents(): Promise<number>
incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number>
getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise<number>
getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise<number>
incrementMeasure(measure: string, value: number, periods: Period[]): Promise<void>
setMeasure(measure: string, value: number, periods: Period[]): Promise<void>
getMeasureAverage(measure: string, period: Period): Promise<number>
getMeasureTotal(measure: string, periodOrPeriodKey: Period | string): Promise<number>
getMeasureIncrementCounts(measure: string, period: Period): Promise<number>
calculateTotalCountOverPeriod(
measure: StatisticsMeasure,
measure: string,
period: Period,
): Promise<Array<{ periodKey: string; totalCount: number }>>
}

View File

@@ -137,7 +137,7 @@ export class PeriodKeyGenerator implements PeriodKeyGeneratorInterface {
return `${this.getYear(date)}-${this.getMonth(date)}`
}
private getDailyKey(date?: Date): string {
getDailyKey(date?: Date): string {
date = date ?? new Date()
return `${this.getYear(date)}-${this.getMonth(date)}-${this.getDayOfTheMonth(date)}`

View File

@@ -2,6 +2,7 @@ import { Period } from './Period'
export interface PeriodKeyGeneratorInterface {
getPeriodKey(period: Period): string
getDailyKey(date?: Date): string
convertPeriodKeyToPeriod(periodKey: string): Period
getDiscretePeriodKeys(period: Period): string[]
}

View File

@@ -1,7 +1,7 @@
import 'reflect-metadata'
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure'
import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName'
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
import { Period } from '../../Time/Period'
@@ -24,7 +24,7 @@ describe('CalculateMonthlyRecurringRevenue', () => {
it('should calculate the MRR diff and persist it as a statistic', async () => {
await createUseCase().execute({})
expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticsMeasure.MRR, 123.45, [
expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticMeasureName.NAMES.MRR, 123.45, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,

View File

@@ -5,11 +5,11 @@ import { Result } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types'
import { MonthlyRevenue } from '../../Revenue/MonthlyRevenue'
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
import { Period } from '../../Time/Period'
import { DomainUseCaseInterface } from '../DomainUseCaseInterface'
import { CalculateMonthlyRecurringRevenueDTO } from './CalculateMonthlyRecurringRevenueDTO'
import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName'
@injectable()
export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<MonthlyRevenue> {
@@ -24,7 +24,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
})
await this.statisticsStore.setMeasure(StatisticsMeasure.MRR, mrrDiff, [
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.MRR, mrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
@@ -34,7 +34,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
billingFrequencies: [SubscriptionBillingFrequency.Monthly],
})
await this.statisticsStore.setMeasure(StatisticsMeasure.MonthlyPlansMRR, monthlyPlansMrrDiff, [
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.MonthlyPlansMRR, monthlyPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
@@ -44,7 +44,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
billingFrequencies: [SubscriptionBillingFrequency.Annual],
})
await this.statisticsStore.setMeasure(StatisticsMeasure.AnnualPlansMRR, annualPlansMrrDiff, [
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.AnnualPlansMRR, annualPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
@@ -54,7 +54,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
billingFrequencies: [SubscriptionBillingFrequency.FiveYear],
})
await this.statisticsStore.setMeasure(StatisticsMeasure.FiveYearPlansMRR, fiveYearPlansMrrDiff, [
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.FiveYearPlansMRR, fiveYearPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
@@ -65,7 +65,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
})
await this.statisticsStore.setMeasure(StatisticsMeasure.ProPlansMRR, proPlansMrrDiff, [
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.ProPlansMRR, proPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,
@@ -76,7 +76,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
})
await this.statisticsStore.setMeasure(StatisticsMeasure.PlusPlansMRR, plusPlansMrrDiff, [
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.PlusPlansMRR, plusPlansMrrDiff, [
Period.Today,
Period.ThisMonth,
Period.ThisYear,

View File

@@ -0,0 +1,31 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { StatisticMeasure } from '../../Statistics/StatisticMeasure'
import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName'
import { StatisticMeasureRepositoryInterface } from '../../Statistics/StatisticMeasureRepositoryInterface'
import { PersistStatisticDTO } from './PersistStatisticDTO'
export class PersistStatistic implements UseCaseInterface<StatisticMeasure> {
constructor(private statisticMeasureRepository: StatisticMeasureRepositoryInterface) {}
async execute(dto: PersistStatisticDTO): Promise<Result<StatisticMeasure>> {
const statisticMeasureNameOrError = StatisticMeasureName.create(dto.statisticMeasureName)
if (statisticMeasureNameOrError.isFailed()) {
return Result.fail(`Could not persist statistic measure: ${statisticMeasureNameOrError.getError()}`)
}
const statisticMeasureOrError = StatisticMeasure.create({
date: dto.date,
name: statisticMeasureNameOrError.getValue(),
value: dto.value,
})
if (statisticMeasureOrError.isFailed()) {
return Result.fail(`Could not persist statistic measure: ${statisticMeasureOrError.getError()}`)
}
const statisticMeasure = statisticMeasureOrError.getValue()
await this.statisticMeasureRepository.save(statisticMeasure)
return Result.ok(statisticMeasure)
}
}

View File

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

View File

@@ -1,16 +1,23 @@
import * as IORedis from 'ioredis'
import { StatisticsMeasure } from '../../Domain/Statistics/StatisticsMeasure'
import { StatisticMeasure } from '../../Domain/Statistics/StatisticMeasure'
import { StatisticMeasureRepositoryInterface } from '../../Domain/Statistics/StatisticMeasureRepositoryInterface'
import { StatisticsStoreInterface } from '../../Domain/Statistics/StatisticsStoreInterface'
import { Period } from '../../Domain/Time/Period'
import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
export class RedisStatisticsStore implements StatisticsStoreInterface {
export class RedisStatisticsStore implements StatisticsStoreInterface, StatisticMeasureRepositoryInterface {
constructor(private periodKeyGenerator: PeriodKeyGeneratorInterface, private redisClient: IORedis.Redis) {}
async save(statisticMeasure: StatisticMeasure): Promise<void> {
const periodKey = this.periodKeyGenerator.getDailyKey(statisticMeasure.props.date)
await this.setMeasure(statisticMeasure.name, statisticMeasure.value, [periodKey])
}
async calculateTotalCountOverPeriod(
measure: StatisticsMeasure,
measure: string,
period: Period,
): Promise<{ periodKey: string; totalCount: number }[]> {
if (
@@ -38,7 +45,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
return counts
}
async getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise<number> {
async getMeasureIncrementCounts(measure: string, period: Period): Promise<number> {
const increments = await this.redisClient.get(
`count:increments:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
)
@@ -49,17 +56,22 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
return +increments
}
async setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void> {
async setMeasure(measure: string, value: number, periodsOrPeriodKeys: Period[] | string[]): Promise<void> {
const pipeline = this.redisClient.pipeline()
for (const period of periods) {
pipeline.set(`count:measure:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`, value)
for (const periodOrPeriodKey of periodsOrPeriodKeys) {
let periodKey = periodOrPeriodKey
if (!isNaN(+periodOrPeriodKey)) {
periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period)
}
pipeline.set(`count:measure:${measure}:timespan:${periodKey}`, value)
}
await pipeline.exec()
}
async getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise<number> {
async getMeasureTotal(measure: string, periodOrPeriodKey: Period | string): Promise<number> {
let periodKey = periodOrPeriodKey
if (!isNaN(+periodOrPeriodKey)) {
periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period)
@@ -74,7 +86,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
return +totalValue
}
async incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void> {
async incrementMeasure(measure: string, value: number, periods: Period[]): Promise<void> {
const pipeline = this.redisClient.pipeline()
for (const period of periods) {
@@ -85,7 +97,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
await pipeline.exec()
}
async getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number> {
async getMeasureAverage(measure: string, period: Period): Promise<number> {
const increments = await this.getMeasureIncrementCounts(measure, period)
if (increments === 0) {

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.41.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.2...@standardnotes/api-gateway@1.41.3) (2022-12-28)
**Note:** Version bump only for package @standardnotes/api-gateway
## [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
# [1.41.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.2...@standardnotes/api-gateway@1.41.0) (2022-12-19)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.41.0",
"version": "1.41.3",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -21,7 +21,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"@standardnotes/common": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",

View File

@@ -13,8 +13,8 @@ ENCRYPTION_SERVER_KEY=change-me-!
PORT=3000
DB_HOST=localhost
DB_REPLICA_HOST=localhost
DB_HOST=127.0.0.1
DB_REPLICA_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=auth
DB_PASSWORD=changeme123

View File

@@ -3,6 +3,74 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.70.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.8...@standardnotes/auth-server@1.70.9) (2022-12-28)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.70.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.7...@standardnotes/auth-server@1.70.8) (2022-12-20)
### Bug Fixes
* **auth:** move tracing sessions to session creation instead of cross service token creation ([5255cfb](https://github.com/standardnotes/server/commit/5255cfbb257cc9e6ac437fe0c5b28d938e3e599b))
## [1.70.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.6...@standardnotes/auth-server@1.70.7) (2022-12-20)
### Bug Fixes
* **auth:** change severity on tracing session errors - most probably hazardous reads ([cf0b918](https://github.com/standardnotes/server/commit/cf0b91891370e1c1799ad80c10ee9f6b98087a94))
## [1.70.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.5...@standardnotes/auth-server@1.70.6) (2022-12-20)
### Bug Fixes
* **auth:** query for session traces ([14eb775](https://github.com/standardnotes/server/commit/14eb775749bfa9972dc3c07049505f3d15f0b556))
## [1.70.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.4...@standardnotes/auth-server@1.70.5) (2022-12-20)
### Bug Fixes
* **auth:** add session traces index ([b9e1e47](https://github.com/standardnotes/server/commit/b9e1e4787129f00fab8f98cb721141f2e7d75600))
## [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
# [1.69.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.68.0...@standardnotes/auth-server@1.69.0) (2022-12-19)
### Features
* **auth:** add session traces cleanup procedure ([147d8fd](https://github.com/standardnotes/server/commit/147d8fd9af89d2b97cd68eefac36e53d14d511bf))
# [1.68.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.3...@standardnotes/auth-server@1.68.0) (2022-12-19)
### Features

View File

@@ -0,0 +1,38 @@
import 'reflect-metadata'
import 'newrelic'
import { Logger } from 'winston'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { CleanupSessionTraces } from '../src/Domain/UseCase/CleanupSessionTraces/CleanupSessionTraces'
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 cleanupSessionTraces: CleanupSessionTraces = container.get(TYPES.CleanupSessionTraces)
Promise.resolve(
cleanupSessionTraces.execute({
date: new Date(),
}),
)
.then(() => {
logger.info('Expired session traces cleaned.')
process.exit(0)
})
.catch((error) => {
logger.error(`Could not clean session traces: ${error.message}`)
process.exit(1)
})
})

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

@@ -19,6 +19,16 @@ case "$COMMAND" in
yarn workspace @standardnotes/auth-server worker
;;
'cleanup' )
echo "[Docker] Starting Cleanup..."
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

@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addSessionTracesCompoundIndex1671561748264 implements MigrationInterface {
name = 'addSessionTracesCompoundIndex1671561748264'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE UNIQUE INDEX `user_uuid_and_creation_date` ON `session_traces` (`user_uuid`, `creation_date`)',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `user_uuid_and_creation_date` ON `session_traces`')
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.68.0",
"version": "1.70.9",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -19,6 +19,8 @@
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
"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",
@@ -31,9 +33,9 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"@sentry/profiling-node": "^0.0.12",
"@sentry/tracing": "^7.27.0",
"@sentry/tracing": "^7.28.1",
"@standardnotes/api": "^1.19.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",

View File

@@ -201,6 +201,8 @@ import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenc
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')
@@ -499,6 +501,20 @@ 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)
.toConstantValue(new CleanupSessionTraces(container.get(TYPES.SessionTraceRepository)))
container.bind<AuthenticateUser>(TYPES.AuthenticateUser).to(AuthenticateUser)
container.bind<AuthenticateRequest>(TYPES.AuthenticateRequest).to(AuthenticateRequest)
container.bind<RefreshSessionToken>(TYPES.RefreshSessionToken).to(RefreshSessionToken)

View File

@@ -125,6 +125,8 @@ const TYPES = {
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
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

@@ -15,6 +15,10 @@ import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { LogSessionUserAgentOption } from '@standardnotes/settings'
import { Setting } from '../Setting/Setting'
import { CryptoNode } from '@standardnotes/sncrypto-node'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { TraceSession } from '../UseCase/TraceSession/TraceSession'
import { UserSubscription } from '../Subscription/UserSubscription'
import { Result } from '@standardnotes/domain-core'
describe('SessionService', () => {
let sessionRepository: SessionRepositoryInterface
@@ -28,6 +32,8 @@ describe('SessionService', () => {
let timer: TimerInterface
let logger: winston.Logger
let cryptoNode: CryptoNode
let traceSession: TraceSession
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
const createService = () =>
new SessionService(
@@ -41,6 +47,8 @@ describe('SessionService', () => {
234,
settingService,
cryptoNode,
traceSession,
userSubscriptionRepository,
)
beforeEach(() => {
@@ -106,6 +114,14 @@ describe('SessionService', () => {
cryptoNode = {} as jest.Mocked<CryptoNode>
cryptoNode.generateRandomKey = jest.fn().mockReturnValue('foo bar')
cryptoNode.base64URLEncode = jest.fn().mockReturnValue('foobar')
traceSession = {} as jest.Mocked<TraceSession>
traceSession.execute = jest.fn()
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue({
planName: 'PRO_PLAN',
} as jest.Mocked<UserSubscription>)
})
it('should mark a revoked session as received', async () => {
@@ -204,6 +220,129 @@ describe('SessionService', () => {
})
})
it('should trace a session', async () => {
const user = {} as jest.Mocked<User>
user.uuid = '123'
user.email = 'test@test.te'
await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
readonlyAccess: false,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '123',
username: 'test@test.te',
subscriptionPlanName: 'PRO_PLAN',
})
})
it('should trace a session without a subscription', async () => {
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
const user = {} as jest.Mocked<User>
user.uuid = '123'
user.email = 'test@test.te'
await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
readonlyAccess: false,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '123',
username: 'test@test.te',
subscriptionPlanName: null,
})
})
it('should create a session if tracing session throws an error', async () => {
traceSession.execute = jest.fn().mockRejectedValue(new Error('foo bar'))
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
const user = {} as jest.Mocked<User>
user.uuid = '123'
user.email = 'test@test.te'
const sessionPayload = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
readonlyAccess: false,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '123',
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(sessionPayload).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
refresh_token: expect.any(String),
readonly_access: false,
})
})
it('should create a session if tracing session throws an error', async () => {
traceSession.execute = jest.fn().mockRejectedValue(new Error('foo bar'))
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
const user = {} as jest.Mocked<User>
user.uuid = '123'
user.email = 'test@test.te'
const sessionPayload = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
readonlyAccess: false,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '123',
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(sessionPayload).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
refresh_token: expect.any(String),
readonly_access: false,
})
})
it('should create a session if tracing session fails', async () => {
traceSession.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
userSubscriptionRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
const user = {} as jest.Mocked<User>
user.uuid = '123'
user.email = 'test@test.te'
const sessionPayload = await createService().createNewSessionForUser({
user,
apiVersion: '003',
userAgent: 'Google Chrome',
readonlyAccess: false,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '123',
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(sessionPayload).toEqual({
access_expiration: 123,
access_token: expect.any(String),
refresh_expiration: 123,
refresh_token: expect.any(String),
readonly_access: false,
})
})
it('should create new ephemeral session for a user', async () => {
const user = {} as jest.Mocked<User>
user.uuid = '123'

View File

@@ -1,10 +1,14 @@
import * as crypto from 'crypto'
import * as winston from 'winston'
import * as dayjs from 'dayjs'
import { UAParser } from 'ua-parser-js'
import { inject, injectable } from 'inversify'
import { v4 as uuidv4 } from 'uuid'
import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston'
import { LogSessionUserAgentOption, SettingName } from '@standardnotes/settings'
import { SessionBody } from '@standardnotes/responses'
import { Uuid } from '@standardnotes/common'
import { CryptoNode } from '@standardnotes/sncrypto-node'
import TYPES from '../../Bootstrap/Types'
import { Session } from './Session'
@@ -16,10 +20,8 @@ import { EphemeralSession } from './EphemeralSession'
import { RevokedSession } from './RevokedSession'
import { RevokedSessionRepositoryInterface } from './RevokedSessionRepositoryInterface'
import { SettingServiceInterface } from '../Setting/SettingServiceInterface'
import { LogSessionUserAgentOption, SettingName } from '@standardnotes/settings'
import { SessionBody } from '@standardnotes/responses'
import { Uuid } from '@standardnotes/common'
import { CryptoNode } from '@standardnotes/sncrypto-node'
import { TraceSession } from '../UseCase/TraceSession/TraceSession'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
@injectable()
export class SessionService implements SessionServiceInterface {
@@ -31,11 +33,13 @@ export class SessionService implements SessionServiceInterface {
@inject(TYPES.RevokedSessionRepository) private revokedSessionRepository: RevokedSessionRepositoryInterface,
@inject(TYPES.DeviceDetector) private deviceDetector: UAParser,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.Logger) private logger: winston.Logger,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.ACCESS_TOKEN_AGE) private accessTokenAge: number,
@inject(TYPES.REFRESH_TOKEN_AGE) private refreshTokenAge: number,
@inject(TYPES.SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.CryptoNode) private cryptoNode: CryptoNode,
@inject(TYPES.TraceSession) private traceSession: TraceSession,
@inject(TYPES.UserSubscriptionRepository) private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
) {}
async createNewSessionForUser(dto: {
@@ -53,6 +57,20 @@ export class SessionService implements SessionServiceInterface {
await this.sessionRepository.save(session)
try {
const userSubscription = await this.userSubscriptionRepository.findOneByUserUuid(dto.user.uuid)
const traceSessionResult = await this.traceSession.execute({
userUuid: dto.user.uuid,
username: dto.user.email,
subscriptionPlanName: userSubscription ? userSubscription.planName : null,
})
if (traceSessionResult.isFailed()) {
this.logger.error(traceSessionResult.getError())
}
} catch (error) {
this.logger.error(`Could not trace session while creating cross service token.: ${(error as Error).message}`)
}
return sessionPayload
}

View File

@@ -1,8 +1,11 @@
import { Uuid } from '@standardnotes/domain-core'
import { SubscriptionPlanName, Uuid } from '@standardnotes/domain-core'
import { SessionTrace } from './SessionTrace'
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

@@ -0,0 +1,19 @@
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
import { CleanupSessionTraces } from './CleanupSessionTraces'
describe('CleanupSessionTraces', () => {
let sessionTracesRepository: SessionTraceRepositoryInterface
const createUseCase = () => new CleanupSessionTraces(sessionTracesRepository)
beforeEach(() => {
sessionTracesRepository = {} as jest.Mocked<SessionTraceRepositoryInterface>
sessionTracesRepository.removeExpiredBefore = jest.fn()
})
it('should remove stale session traces', async () => {
await createUseCase().execute({ date: new Date() })
expect(sessionTracesRepository.removeExpiredBefore).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,14 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { SessionTraceRepositoryInterface } from '../../Session/SessionTraceRepositoryInterface'
import { CleanupSessionTracesDTO } from './CleanupSessionTracesDTO'
export class CleanupSessionTraces implements UseCaseInterface<string> {
constructor(private sessionTracesRepository: SessionTraceRepositoryInterface) {}
async execute(dto: CleanupSessionTracesDTO): Promise<Result<string>> {
await this.sessionTracesRepository.removeExpiredBefore(dto.date)
return Result.ok('Session traces removed')
}
}

View File

@@ -0,0 +1,3 @@
export interface CleanupSessionTracesDTO {
date: Date
}

View File

@@ -8,10 +8,6 @@ import { Role } from '../../Role/Role'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CreateCrossServiceToken } from './CreateCrossServiceToken'
import { RoleToSubscriptionMapInterface } from '../../Role/RoleToSubscriptionMapInterface'
import { TraceSession } from '../TraceSession/TraceSession'
import { Logger } from 'winston'
import { Result, RoleName, SubscriptionPlanName } from '@standardnotes/domain-core'
describe('CreateCrossServiceToken', () => {
let userProjector: ProjectorInterface<User>
@@ -19,9 +15,6 @@ describe('CreateCrossServiceToken', () => {
let roleProjector: ProjectorInterface<Role>
let tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>
let userRepository: UserRepositoryInterface
let roleToSubscriptionMap: RoleToSubscriptionMapInterface
let traceSession: TraceSession
let logger: Logger
const jwtTTL = 60
let session: Session
@@ -29,17 +22,7 @@ describe('CreateCrossServiceToken', () => {
let role: Role
const createUseCase = () =>
new CreateCrossServiceToken(
userProjector,
sessionProjector,
roleProjector,
tokenEncoder,
userRepository,
jwtTTL,
roleToSubscriptionMap,
traceSession,
logger,
)
new CreateCrossServiceToken(userProjector, sessionProjector, roleProjector, tokenEncoder, userRepository, jwtTTL)
beforeEach(() => {
session = {} as jest.Mocked<Session>
@@ -65,18 +48,6 @@ describe('CreateCrossServiceToken', () => {
userRepository = {} as jest.Mocked<UserRepositoryInterface>
userRepository.findOneByUuid = jest.fn().mockReturnValue(user)
roleToSubscriptionMap = {} as jest.Mocked<RoleToSubscriptionMapInterface>
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([RoleName.NAMES.PlusUser])
roleToSubscriptionMap.getSubscriptionNameForRoleName = jest
.fn()
.mockReturnValue(SubscriptionPlanName.NAMES.PlusPlan)
traceSession = {} as jest.Mocked<TraceSession>
traceSession.execute = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.error = jest.fn()
})
it('should create a cross service token for user', async () => {
@@ -85,11 +56,6 @@ describe('CreateCrossServiceToken', () => {
session,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '1-2-3',
username: 'test@test.te',
subscriptionPlanName: 'PLUS_PLAN',
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
@@ -168,126 +134,4 @@ describe('CreateCrossServiceToken', () => {
expect(caughtError).not.toBeNull()
})
it('should trace session without a subscription role', async () => {
roleToSubscriptionMap.filterNonSubscriptionRoles = jest.fn().mockReturnValue([])
await createUseCase().execute({
user,
session,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '1-2-3',
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
)
})
it('should trace session without a subscription', async () => {
roleToSubscriptionMap.getSubscriptionNameForRoleName = jest.fn().mockReturnValue(undefined)
await createUseCase().execute({
user,
session,
})
expect(traceSession.execute).toHaveBeenCalledWith({
userUuid: '1-2-3',
username: 'test@test.te',
subscriptionPlanName: null,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
)
})
it('should create token if tracing session throws an error', async () => {
traceSession.execute = jest.fn().mockRejectedValue(new Error('test'))
await createUseCase().execute({
user,
session,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
)
})
it('should create token if tracing session fails', async () => {
traceSession.execute = jest.fn().mockReturnValue(Result.fail('Ooops'))
await createUseCase().execute({
user,
session,
})
expect(tokenEncoder.encodeExpirableToken).toHaveBeenCalledWith(
{
roles: [
{
name: 'role1',
uuid: '1-3-4',
},
],
session: {
test: 'test',
},
user: {
email: 'test@test.te',
uuid: '1-2-3',
},
},
60,
)
})
})

View File

@@ -1,16 +1,13 @@
import { RoleName } from '@standardnotes/common'
import { TokenEncoderInterface, CrossServiceTokenData } from '@standardnotes/security'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../../Bootstrap/Types'
import { ProjectorInterface } from '../../../Projection/ProjectorInterface'
import { Role } from '../../Role/Role'
import { RoleToSubscriptionMapInterface } from '../../Role/RoleToSubscriptionMapInterface'
import { Session } from '../../Session/Session'
import { User } from '../../User/User'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { TraceSession } from '../TraceSession/TraceSession'
import { UseCaseInterface } from '../UseCaseInterface'
import { CreateCrossServiceTokenDTO } from './CreateCrossServiceTokenDTO'
@@ -25,9 +22,6 @@ export class CreateCrossServiceToken implements UseCaseInterface {
@inject(TYPES.CrossServiceTokenEncoder) private tokenEncoder: TokenEncoderInterface<CrossServiceTokenData>,
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.AUTH_JWT_TTL) private jwtTTL: number,
@inject(TYPES.RoleToSubscriptionMap) private roleToSubscriptionMap: RoleToSubscriptionMapInterface,
@inject(TYPES.TraceSession) private traceSession: TraceSession,
@inject(TYPES.Logger) private logger: Logger,
) {}
async execute(dto: CreateCrossServiceTokenDTO): Promise<CreateCrossServiceTokenResponse> {
@@ -51,19 +45,6 @@ export class CreateCrossServiceToken implements UseCaseInterface {
authTokenData.session = this.projectSession(dto.session)
}
try {
const traceSessionResult = await this.traceSession.execute({
userUuid: user.uuid,
username: user.email,
subscriptionPlanName: this.getSubscriptionNameFromRoles(roles),
})
if (traceSessionResult.isFailed()) {
this.logger.error(traceSessionResult.getError())
}
} catch (error) {
this.logger.error(`Could not trace session while creating cross service token: ${(error as Error).message}`)
}
return {
token: this.tokenEncoder.encodeExpirableToken(authTokenData, this.jwtTTL),
}
@@ -100,17 +81,4 @@ export class CreateCrossServiceToken implements UseCaseInterface {
private projectRoles(roles: Array<Role>): Array<{ uuid: string; name: RoleName }> {
return roles.map((role) => <{ uuid: string; name: RoleName }>this.roleProjector.projectSimple(role))
}
private getSubscriptionNameFromRoles(roles: Array<Role>): string | null {
const nonSubscriptionRoles = this.roleToSubscriptionMap.filterNonSubscriptionRoles(roles)
if (nonSubscriptionRoles.length === 0) {
return null
}
const subscriptionName = this.roleToSubscriptionMap.getSubscriptionNameForRoleName(
nonSubscriptionRoles[0].name as RoleName,
)
return subscriptionName === undefined ? null : subscriptionName
}
}

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,12 +10,41 @@ 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: `${date.getFullYear()}-${date.getMonth() + 1}-${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: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`,
})
.getCount()
}
async removeExpiredBefore(date: Date): Promise<void> {
await this.ormRepository
.createQueryBuilder()
.delete()
.where('expires_at < :date', { date: date.toISOString() })
.execute()
}
async findOneByUserUuidAndDate(userUuid: Uuid, date: Date): Promise<SessionTrace | null> {
const typeOrm = await this.ormRepository
.createQueryBuilder('trace')
.where('trace.user_uuid = :userUuid AND trace.creation_date = :creationDate', {
userUuid: userUuid.value,
creationDate: new Date(date.getFullYear(), date.getMonth(), date.getDate()),
creationDate: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`,
})
.getOne()

View File

@@ -1,6 +1,7 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
@Entity({ name: 'session_traces' })
@Index('user_uuid_and_creation_date', ['userUuid', 'creationDate'], { unique: true })
export class TypeORMSessionTrace {
@PrimaryGeneratedColumn('uuid')
declare uuid: string

View File

@@ -3,6 +3,14 @@
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
## [1.9.57](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.56...@standardnotes/domain-events-infra@1.9.57) (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.57",
"version": "1.9.59",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -3,6 +3,18 @@
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
* **analytics:** add persisting statistics on demand ([0f84575](https://github.com/standardnotes/server/commit/0f8457534c1829c58f3c036749d262307ddeb779))
## [2.104.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.104.1...@standardnotes/domain-events@2.104.2) (2022-12-19)
### Bug Fixes

View File

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

View File

@@ -0,0 +1,8 @@
import { DomainEventInterface } from './DomainEventInterface'
import { StatisticPersistenceRequestedEventPayload } from './StatisticPersistenceRequestedEventPayload'
export interface StatisticPersistenceRequestedEvent extends DomainEventInterface {
type: 'STATISTIC_PERSISTENCE_REQUESTED'
payload: StatisticPersistenceRequestedEventPayload
}

View File

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

View File

@@ -58,6 +58,8 @@ export * from './Event/SharedSubscriptionInvitationCanceledEvent'
export * from './Event/SharedSubscriptionInvitationCanceledEventPayload'
export * from './Event/SharedSubscriptionInvitationCreatedEvent'
export * from './Event/SharedSubscriptionInvitationCreatedEventPayload'
export * from './Event/StatisticPersistenceRequestedEvent'
export * from './Event/StatisticPersistenceRequestedEventPayload'
export * from './Event/SubscriptionCancelledEvent'
export * from './Event/SubscriptionCancelledEventPayload'
export * from './Event/SubscriptionPurchasedEvent'

View File

@@ -3,6 +3,14 @@
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
## [1.6.54](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.53...@standardnotes/event-store@1.6.54) (2022-12-19)
### Bug Fixes

View File

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

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.9.2...@standardnotes/files-server@1.9.3) (2022-12-28)
**Note:** Version bump only for package @standardnotes/files-server
## [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
# [1.9.0](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.52...@standardnotes/files-server@1.9.0) (2022-12-19)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.9.0",
"version": "1.9.3",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -25,7 +25,7 @@
"upgrade:snjs": "yarn ncu -u '@standardnotes/*'"
},
"dependencies": {
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.10.2...@standardnotes/revisions-server@1.10.3) (2022-12-28)
**Note:** Version bump only for package @standardnotes/revisions-server
## [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
# [1.10.0](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.28...@standardnotes/revisions-server@1.10.0) (2022-12-19)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.10.0",
"version": "1.10.3",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -24,7 +24,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"@standardnotes/api": "^1.19.0",
"@standardnotes/common": "workspace:^",
"@standardnotes/domain-core": "workspace:^",

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.16.4](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.3...@standardnotes/scheduler-server@1.16.4) (2022-12-28)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.16.3](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.16.2...@standardnotes/scheduler-server@1.16.3) (2022-12-20)
### Bug Fixes
* **scheduler:** new pricing for subscription encouragement email ([eb21872](https://github.com/standardnotes/server/commit/eb21872db1726c4f8a55b5d16de712650c5e946b))
## [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
# [1.16.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.15.8...@standardnotes/scheduler-server@1.16.0) (2022-12-19)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.16.0",
"version": "1.16.4",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -25,7 +25,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",

View File

@@ -5,7 +5,5 @@ export function getSubject(): string {
}
export function getBody(registrationDate: string): string {
const body = html
return body.replace('%%REGISTRATION_DATE%%', registrationDate)
return html(registrationDate, 90, 120)
}

View File

@@ -1,4 +1,4 @@
export const html = `<div>
export const html = (registrationDate: string, annualPlusPrice: number, annualProPrice: number) => `<div>
<p>Hi there,</p>
<p>
We hope you've been finding great use out of Standard Notes. We built Standard Notes to be a secure place for
@@ -7,7 +7,7 @@ export const html = `<div>
<p>
As a reminder,
<strong>
<em>you signed up for the Standard Notes free plan on %%REGISTRATION_DATE%%</em>
<em>you signed up for the Standard Notes free plan on ${registrationDate}</em>
</strong>
Your free account comes with standard features like end-to-end encryption, multiple-device sync, and
two-factor authentication.
@@ -19,13 +19,13 @@ export const html = `<div>
<ul>
<li>
<p>
<strong>Productivity</strong> <strong>($59/year)</strong> powers up your editing experience with unique
<strong>Productivity</strong> <strong>($${annualPlusPrice}/year)</strong> powers up your editing experience with unique
and special-built note-types for markdown, rich text, spreadsheets, todo, and more.
</p>
</li>
<li>
<p>
<strong>Professional</strong> <strong>($99/year)</strong> gives you all the power of Productivity plus
<strong>Professional</strong> <strong>($${annualProPrice}/year)</strong> gives you all the power of Productivity plus
100GB of end-to-end encrypted file storage for your private photos, videos, and documents, plus family
subscription sharing with up to 5 people.
</p>

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.3](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.28.2...@standardnotes/syncing-server@1.28.3) (2022-12-28)
**Note:** Version bump only for package @standardnotes/syncing-server
## [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
# [1.28.0](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.27.0...@standardnotes/syncing-server@1.28.0) (2022-12-19)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/syncing-server",
"version": "1.28.0",
"version": "1.28.3",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -27,9 +27,9 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"@sentry/profiling-node": "^0.0.12",
"@sentry/tracing": "^7.27.0",
"@sentry/tracing": "^7.28.1",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.5.3](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.5.2...@standardnotes/websockets-server@1.5.3) (2022-12-28)
**Note:** Version bump only for package @standardnotes/websockets-server
## [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
# [1.5.0](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.53...@standardnotes/websockets-server@1.5.0) (2022-12-19)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/websockets-server",
"version": "1.5.0",
"version": "1.5.3",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -23,7 +23,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"@standardnotes/api": "^1.19.0",
"@standardnotes/common": "workspace:^",
"@standardnotes/domain-events": "workspace:^",

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.4](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.19.3...@standardnotes/workspace-server@1.19.4) (2022-12-28)
**Note:** Version bump only for package @standardnotes/workspace-server
## [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
# [1.19.0](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.18.6...@standardnotes/workspace-server@1.19.0) (2022-12-19)
### Features

Some files were not shown because too many files have changed in this diff Show More