Compare commits

..

195 Commits

Author SHA1 Message Date
standardci a6061ec2a9 chore(release): publish new version
- @standardnotes/revisions-server@1.10.12
2023-01-17 13:56:18 +00:00
Karol Sójko 51c777304b fix(revisions): fetching revisions metadata 2023-01-17 14:54:22 +01:00
standardci fbd535f2c5 chore(release): publish new version
- @standardnotes/auth-server@1.82.0
2023-01-17 13:18:05 +00:00
Aman Harwara 7d456671c2 feat: super editor permissions migration (#414) 2023-01-17 18:45:20 +05:30
standardci dd4924c925 chore(release): publish new version
- @standardnotes/analytics@2.19.4
 - @standardnotes/auth-server@1.81.11
 - @standardnotes/event-store@1.6.58
 - @standardnotes/revisions-server@1.10.11
 - @standardnotes/scheduler-server@1.16.8
 - @standardnotes/syncing-server@1.28.9
 - @standardnotes/workspace-server@1.19.7
2023-01-17 12:47:34 +00:00
Karol Sójko f73129cd7e fix: allow to run typeorm in non-replica mode 2023-01-17 13:45:32 +01:00
standardci 4983c8741e chore(release): publish new version
- @standardnotes/revisions-server@1.10.10
2023-01-17 10:55:57 +00:00
Karol Sójko c5798640ff fix(revisions): add debug logs for retrieving revisions metadata from mysql 2023-01-17 11:53:57 +01:00
standardci 5803a8018a chore(release): publish new version
- @standardnotes/revisions-server@1.10.9
2023-01-17 10:02:11 +00:00
Karol Sójko e2aae8ac8a fix(revisions): response structure 2023-01-17 11:00:05 +01:00
Karol Sójko 2917aeeb32 fix: turn some of the applications into a utility publishing workflow 2023-01-17 10:24:45 +01:00
standardci 9377c03c3f chore(release): publish new version
- @standardnotes/syncing-server@1.28.8
2023-01-17 09:09:03 +00:00
Karol Sójko 9b926fbad6 fix(syncing-server-js): creating directory for revision dumps 2023-01-17 10:07:01 +01:00
Karol Sójko 8db19c3e2b fix(syncing-server-js): add debug logs for dumping items for revisions creation 2023-01-17 10:02:17 +01:00
standardci ca970781c7 chore(release): publish new version
- @standardnotes/analytics@2.19.3
 - @standardnotes/auth-server@1.81.10
 - @standardnotes/domain-core@1.11.1
 - @standardnotes/revisions-server@1.10.8
 - @standardnotes/scheduler-server@1.16.7
 - @standardnotes/syncing-server@1.28.7
 - @standardnotes/workspace-server@1.19.6
2023-01-16 14:36:54 +00:00
Karol Sójko e7beee2788 fix(revisions): add required role to revisions list response 2023-01-16 15:34:20 +01:00
standardci d266eada88 chore(release): publish new version
- @standardnotes/revisions-server@1.10.7
2023-01-16 10:22:58 +00:00
Karol Sójko 11b8b078b4 fix(revisions): remove redundant specs 2023-01-16 11:20:58 +01:00
standardci 37912fa29a chore(release): publish new version
- @standardnotes/revisions-server@1.10.6
2023-01-16 10:14:55 +00:00
Karol Sójko b97dafe6f3 fix(revisions): mapping to snake case 2023-01-16 11:12:29 +01:00
standardci 2a29151395 chore(release): publish new version
- @standardnotes/revisions-server@1.10.5
2023-01-16 10:01:02 +00:00
Karol Sójko 8b988d89c0 fix(revisions): response structure 2023-01-16 10:58:39 +01:00
standardci c0908f1b58 chore(release): publish new version
- @standardnotes/api-gateway@1.46.0
2023-01-16 09:02:31 +00:00
Karol Sójko bb46044f7c Merge pull request #366 from standardnotes/switch_revisions
feat(api-gateway): switch to fetching revisions from reivsions server
2023-01-16 10:00:37 +01:00
Karol Sójko 60b3dd6138 feat(api-gateway): add all revisions endpoints on v2 2023-01-16 09:40:07 +01:00
Karol Sójko 22c1f936c3 feat(api-gateway): switch to fetching revisions from reivsions server 2023-01-16 09:33:13 +01:00
standardci e899874b04 chore(release): publish new version
- @standardnotes/api-gateway@1.45.3
2023-01-16 08:32:47 +00:00
Karol Sójko 04c6888cf6 fix(api-gateway): add noindex robots meta tag to api gateway homepage 2023-01-16 09:30:02 +01:00
standardci 29c56c6919 chore(release): publish new version
- @standardnotes/analytics@2.19.2
 - @standardnotes/api-gateway@1.45.2
 - @standardnotes/auth-server@1.81.9
 - @standardnotes/domain-events-infra@1.9.60
 - @standardnotes/domain-events@2.105.2
 - @standardnotes/event-store@1.6.57
 - @standardnotes/files-server@1.9.5
 - @standardnotes/revisions-server@1.10.4
 - @standardnotes/scheduler-server@1.16.6
 - @standardnotes/security@1.7.3
 - @standardnotes/syncing-server@1.28.6
 - @standardnotes/websockets-server@1.5.4
 - @standardnotes/workspace-server@1.19.5
2023-01-13 09:56:13 +00:00
Karol Sójko c98ed9cc85 chore: update jsonwebtoken 2023-01-13 10:53:57 +01:00
standardci 88f7530c13 chore(release): publish new version
- @standardnotes/api-gateway@1.45.1
 - @standardnotes/files-server@1.9.4
2023-01-13 09:05:13 +00:00
Karol Sójko bb820437af fix: add robots.txt setup for api-gateway and files server to disallow indexing 2023-01-13 10:03:03 +01:00
standardci d1a4bd38e0 chore(release): publish new version
- @standardnotes/auth-server@1.81.8
2023-01-11 12:49:19 +00:00
Karol Sójko d18f6ccd32 fix(auth): add relying party configuration options 2023-01-11 13:47:13 +01:00
standardci aa317c964e chore(release): publish new version
- @standardnotes/auth-server@1.81.7
2023-01-09 14:31:00 +00:00
Karol Sójko 7ae8845ae9 fix(auth): failure messages for debug logs upon signing in with recovery codes 2023-01-09 15:28:35 +01:00
standardci 123a6dbe0c chore(release): publish new version
- @standardnotes/auth-server@1.81.6
2023-01-09 13:53:44 +00:00
Karol Sójko dda8d79526 fix(auth): request parameters names 2023-01-09 14:51:48 +01:00
standardci de5293955a chore(release): publish new version
- @standardnotes/auth-server@1.81.5
2023-01-09 12:59:21 +00:00
Karol Sójko 96669bff5b fix(auth): debuggin recovery sign in 2023-01-09 13:56:56 +01:00
standardci a99762f004 chore(release): publish new version
- @standardnotes/auth-server@1.81.4
2023-01-09 12:49:05 +00:00
Karol Sójko 1fc3c9b83e fix(auth): error messages on account recovery 2023-01-09 13:47:11 +01:00
standardci af86b6f664 chore(release): publish new version
- @standardnotes/auth-server@1.81.3
2023-01-09 11:58:44 +00:00
Karol Sójko a0208dd5b3 fix(auth): remove mfa settings after recovery sign in 2023-01-09 12:56:50 +01:00
standardci 1c5c8b81d5 chore(release): publish new version
- @standardnotes/scheduler-server@1.16.5
2023-01-06 08:17:13 +00:00
Karol Sójko 79c3e33434 fix(scheduler): change email levels 2023-01-06 09:15:22 +01:00
standardci 5ab8729a31 chore(release): publish new version
- @standardnotes/auth-server@1.81.2
2023-01-05 13:36:08 +00:00
Karol Sójko db0baf92f1 fix(auth): return type to include user 2023-01-05 14:33:34 +01:00
standardci a8974094db chore(release): publish new version
- @standardnotes/auth-server@1.81.1
2023-01-05 11:31:08 +00:00
Karol Sójko 13c5c97ba7 fix(auth): allow retrieval of recovery codes setting 2023-01-05 12:28:56 +01:00
standardci 894ebb3edd chore(release): publish new version
- @standardnotes/api-gateway@1.45.0
 - @standardnotes/auth-server@1.81.0
2023-01-05 10:44:51 +00:00
Karol Sójko cac899a7e5 feat(auth): add recovery sign in with recovery codes 2023-01-05 11:42:55 +01:00
standardci 901e0dd93b chore(release): publish new version
- @standardnotes/auth-server@1.80.0
 - @standardnotes/settings@1.19.0
 - @standardnotes/syncing-server@1.28.5
2023-01-04 14:31:36 +00:00
Karol Sójko a360231fd0 feat(auth): add generating recovery codes 2023-01-04 15:29:15 +01:00
standardci 6ccc6ee42f chore(release): publish new version
- @standardnotes/auth-server@1.79.1
 - @standardnotes/syncing-server@1.28.4
2023-01-02 08:53:12 +00:00
Karol Sójko 9c72ad85a0 fix: remove @sentry/profiling-node integration as it is not compatible with ARM - fixes #383 2023-01-02 09:49:04 +01:00
Karol Sójko fa6d80a753 fix: remove @sentry/profiling-node as it is not compatible with ARM - fixes #383 2023-01-02 09:46:42 +01:00
standardci f6ab2ca9ba chore(release): publish new version
- @standardnotes/analytics@2.19.1
2022-12-30 14:08:32 +00:00
Karol Sójko ba1e1ad5ad fix(analytics): remove unnecesary context from mixpanel events 2022-12-30 15:06:04 +01:00
standardci 02705ea3ad chore(release): publish new version
- @standardnotes/analytics@2.19.0
2022-12-30 11:44:19 +00:00
Karol Sójko df6e3f06a6 feat(analytics): add mixpanel events tracking 2022-12-30 12:41:42 +01:00
standardci 1cb5ee9fd6 chore(release): publish new version
- @standardnotes/analytics@2.18.0
2022-12-30 07:41:42 +00:00
Karol Sójko 893d6176c3 feat(analytics): add mixpanel 2022-12-30 08:39:19 +01:00
standardci 2c1b512e40 chore(release): publish new version
- @standardnotes/api-gateway@1.44.0
 - @standardnotes/auth-server@1.79.0
2022-12-29 12:57:00 +00:00
Karol Sójko de50d76800 feat(auth): add removing authenticator 2022-12-29 13:55:08 +01:00
standardci 401b78e477 chore(release): publish new version
- @standardnotes/api-gateway@1.43.0
 - @standardnotes/auth-server@1.78.0
2022-12-29 12:39:51 +00:00
Karol Sójko 01837eaea9 feat(auth): add listing authenticators 2022-12-29 13:37:30 +01:00
standardci 7df699353c chore(release): publish new version
- @standardnotes/auth-server@1.77.1
2022-12-29 11:57:38 +00:00
Karol Sójko 5455972be2 fix(auth): specs for verifying authenticator authentication response 2022-12-29 12:55:43 +01:00
standardci 57488bcd16 chore(release): publish new version
- @standardnotes/api-gateway@1.42.0
 - @standardnotes/auth-server@1.77.0
2022-12-29 10:31:18 +00:00
Karol Sójko b6fda901ef feat(auth): add http endpoints for authenticators 2022-12-29 11:29:23 +01:00
standardci 14669df890 chore(release): publish new version
- @standardnotes/auth-server@1.76.0
2022-12-29 08:43:05 +00:00
Karol Sójko 64525a65f2 feat(auth): add verifying authenticator authentication response 2022-12-29 09:41:10 +01:00
standardci 61fc7efecb chore(release): publish new version
- @standardnotes/auth-server@1.75.0
2022-12-29 07:47:43 +00:00
Karol Sójko 8c7c1e4745 feat(auth): add generating authenticator authentication options 2022-12-29 08:45:32 +01:00
standardci f64d30ec88 chore(release): publish new version
- @standardnotes/auth-server@1.74.1
2022-12-28 15:18:15 +00:00
Karol Sójko 384dfc8da4 fix(auth): migrations to not include unique index for credentials id 2022-12-28 16:16:10 +01:00
standardci 841784ae8c chore(release): publish new version
- @standardnotes/auth-server@1.74.0
2022-12-28 14:53:11 +00:00
Karol Sójko f5683cfd94 feat(auth): add verifying authenticator registration response 2022-12-28 15:50:48 +01:00
standardci 0a420ce30e chore(release): publish new version
- @standardnotes/auth-server@1.73.1
2022-12-28 13:12:37 +00:00
Karol Sójko a5e7132d3c fix(auth): temporarily remove credential id index due to mysql 5.6 limitations 2022-12-28 14:10:40 +01:00
standardci 6dfb2be4a2 chore(release): publish new version
- @standardnotes/auth-server@1.73.0
2022-12-28 13:09:57 +00:00
Karol Sójko d81cbad550 Merge pull request #381 from standardnotes/authenticator_registration
feat(auth): add generating authencator registration options
2022-12-28 14:08:02 +01:00
Karol Sójko 51ad06b303 feat(auth): add generating authencator registration options 2022-12-28 13:56:06 +01:00
standardci 27048ad95c chore(release): publish new version
- @standardnotes/auth-server@1.72.0
2022-12-28 11:42:03 +00:00
Karol Sójko fa9bf0b448 feat(auth): add authenticator challenges model 2022-12-28 12:40:13 +01:00
standardci 305190b64e chore(release): publish new version
- @standardnotes/auth-server@1.71.1
2022-12-28 11:27:35 +00:00
Karol Sójko 98e3d18335 fix(auth): credential id field type 2022-12-28 12:25:36 +01:00
standardci 72e398956b chore(release): publish new version
- @standardnotes/auth-server@1.71.0
2022-12-28 10:39:38 +00:00
Karol Sójko 1e69a13a97 feat(auth): add authenticators model 2022-12-28 11:37:06 +01:00
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
standardci c12d354900 chore(release): publish new version
- @standardnotes/analytics@2.13.0
 - @standardnotes/api-gateway@1.41.0
 - @standardnotes/auth-server@1.68.0
 - @standardnotes/domain-events-infra@1.9.57
 - @standardnotes/domain-events@2.104.2
 - @standardnotes/event-store@1.6.54
 - @standardnotes/files-server@1.9.0
 - @standardnotes/revisions-server@1.10.0
 - @standardnotes/scheduler-server@1.16.0
 - @standardnotes/syncing-server@1.28.0
 - @standardnotes/websockets-server@1.5.0
 - @standardnotes/workspace-server@1.19.0
2022-12-19 11:27:50 +00:00
Karol Sójko 8bcb552783 feat(auth): add session traces 2022-12-19 12:25:15 +01:00
Karol Sójko f504a8288c fix(syncing-server): cleanup unused events 2022-12-19 12:25:15 +01:00
Karol Sójko 79f5b54228 fix(event-store): event handling cleanup 2022-12-19 12:25:15 +01:00
standardci 669a9855e6 chore(release): publish new version
- @standardnotes/syncing-server@1.27.0
2022-12-19 08:46:24 +00:00
Karol Sójko e3b96c3a1f feat(syncing-server): setup sentry profiling 2022-12-19 09:43:51 +01:00
standardci 7e0d4bef20 chore(release): publish new version
- @standardnotes/syncing-server@1.26.11
2022-12-19 07:38:08 +00:00
Karol Sójko 0bd0f48df3 feat(syncing-server-js) add sentry node profiling packages 2022-12-19 08:36:08 +01:00
standardci ae56126585 chore(release): publish new version
- @standardnotes/syncing-server@1.26.10
2022-12-15 14:41:40 +00:00
Karol Sójko 6dcf0ac124 fix(syncing-server): item query created_at condition 2022-12-15 15:38:56 +01:00
standardci 63e2ce43c2 chore(release): publish new version
- @standardnotes/syncing-server@1.26.9
2022-12-15 13:40:30 +00:00
Karol Sójko f27aa21eb5 fix(syncing-server): fetching items in raw mode 2022-12-15 14:38:32 +01:00
standardci 42926c663b chore(release): publish new version
- @standardnotes/syncing-server@1.26.8
2022-12-15 11:37:14 +00:00
Karol Sójko d38116183c fix(syncing-server): select fields in query 2022-12-15 12:35:26 +01:00
standardci 9ca373e208 chore(release): publish new version
- @standardnotes/analytics@2.12.27
 - @standardnotes/auth-server@1.67.3
 - @standardnotes/domain-core@1.11.0
 - @standardnotes/revisions-server@1.9.28
 - @standardnotes/scheduler-server@1.15.8
 - @standardnotes/syncing-server@1.26.7
 - @standardnotes/workspace-server@1.18.6
2022-12-15 11:26:36 +00:00
Karol Sójko 4084f2f5ec feat(domain-core): add legacy session model 2022-12-15 12:24:11 +01:00
standardci 684ffbadbc chore(release): publish new version
- @standardnotes/analytics@2.12.26
 - @standardnotes/auth-server@1.67.2
 - @standardnotes/domain-core@1.10.0
 - @standardnotes/revisions-server@1.9.27
 - @standardnotes/scheduler-server@1.15.7
 - @standardnotes/syncing-server@1.26.6
 - @standardnotes/workspace-server@1.18.5
2022-12-15 10:38:10 +00:00
Karol Sójko 1c4d4c57de feat(domain-core): add session model 2022-12-15 11:35:49 +01:00
standardci d83111a199 chore(release): publish new version
- @standardnotes/syncing-server@1.26.5
2022-12-15 09:40:56 +00:00
Karol Sójko f10fa839fb fix(syncing-server): revisions processing limit 2022-12-15 10:38:25 +01:00
standardci 1f20395ff3 chore(release): publish new version
- @standardnotes/syncing-server@1.26.4
2022-12-15 07:33:29 +00:00
Karol Sójko bfe6f4255a fix(syncing-server): user uuid field name 2022-12-15 08:31:30 +01:00
standardci b9032f3012 chore(release): publish new version
- @standardnotes/syncing-server@1.26.3
2022-12-15 06:31:07 +00:00
Karol Sójko ce53c459e6 fix(syncing-server): select fields in query for revisions 2022-12-15 07:28:43 +01:00
standardci 6df42fb0d5 chore(release): publish new version
- @standardnotes/syncing-server@1.26.2
2022-12-14 19:08:28 +00:00
Karol Sójko 1e2b496f4f fix(syncing-server): revisions procedure logs 2022-12-14 20:06:07 +01:00
standardci 528c1b0d57 chore(release): publish new version
- @standardnotes/syncing-server@1.26.1
2022-12-14 18:58:11 +00:00
Karol Sójko 22fba8ba80 fix(syncing-server): revisions procedure logs 2022-12-14 19:56:17 +01:00
standardci 6f26261ebe chore(release): publish new version
- @standardnotes/syncing-server@1.26.0
2022-12-14 15:51:14 +00:00
Karol Sójko 4b1fe3ba91 feat(syncing-server): change revisions procedure to pagination instead of streaming 2022-12-14 16:49:21 +01:00
standardci 9f95262bd4 chore(release): publish new version
- @standardnotes/syncing-server@1.25.6
2022-12-14 09:33:24 +00:00
Karol Sójko 2ec28e541e fix(syncing-server): additional stream events handling on revisions procedure 2022-12-14 10:31:06 +01:00
standardci 4764d4b19a chore(release): publish new version
- @standardnotes/syncing-server@1.25.5
2022-12-14 09:00:42 +00:00
Karol Sójko 9b27547dae fix(syncing-server): revisions procedure with env var defined ranges 2022-12-14 09:58:41 +01:00
standardci a96f2c9153 chore(release): publish new version
- @standardnotes/syncing-server@1.25.4
2022-12-13 11:14:35 +00:00
Karol Sójko 225e0aaf88 fix(syncing-server): logs on revisions procedure 2022-12-13 12:12:42 +01:00
standardci f0c85910bc chore(release): publish new version
- @standardnotes/syncing-server@1.25.3
2022-12-13 11:07:23 +00:00
Karol Sójko 124c443528 fix(syncing-server): revisions ownership procedure destructured 2022-12-13 12:05:10 +01:00
standardci 37c7f8d39f chore(release): publish new version
- @standardnotes/syncing-server@1.25.2
2022-12-13 07:19:55 +00:00
Karol Sójko c419f1ce22 fix(syncing-server): change revisions migration to notes 2022-12-13 08:17:55 +01:00
standardci 4949cdfe2f chore(release): publish new version
- @standardnotes/syncing-server@1.25.1
2022-12-13 06:03:06 +00:00
Karol Sójko cd101b96ea fix(syncing-server): revisions procedure properties 2022-12-13 07:01:06 +01:00
standardci 40d0e4631f chore(release): publish new version
- @standardnotes/syncing-server@1.25.0
2022-12-12 19:06:24 +00:00
Karol Sójko a55a995660 feat(syncing-server): fix streaming items for revisions update 2022-12-12 20:03:45 +01:00
standardci 1d576d48ad chore(release): publish new version
- @standardnotes/auth-server@1.67.1
 - @standardnotes/syncing-server@1.24.7
2022-12-12 13:20:47 +00:00
Karol Sójko 4ff8030f87 fix(syncing-server): revisions updating - select fields 2022-12-12 14:18:45 +01:00
Karol Sójko c15e2e2c8f fix: user signed in email template 2022-12-12 14:18:45 +01:00
standardci 41d31a8d75 chore(release): publish new version
- @standardnotes/auth-server@1.67.0
2022-12-12 13:00:40 +00:00
Karol Sójko 10e2a26352 feat(auth): add email subscription unsubscribed event handler 2022-12-12 13:58:35 +01:00
standardci 6e547f77d0 chore(release): publish new version
- @standardnotes/revisions-server@1.9.26
2022-12-12 12:14:52 +00:00
Karol Sójko 530a426601 fix(revisions): responses to match previous response structure 2022-12-12 13:12:46 +01:00
Karol Sójko 642d6bab77 chore: fix triggers for other repos dep 2022-12-12 12:56:28 +01:00
standardci 7980af3d82 chore(release): publish new version
- @standardnotes/analytics@2.12.25
 - @standardnotes/api-gateway@1.40.2
 - @standardnotes/auth-server@1.66.9
 - @standardnotes/domain-events-infra@1.9.56
 - @standardnotes/domain-events@2.104.1
 - @standardnotes/event-store@1.6.53
 - @standardnotes/files-server@1.8.52
 - @standardnotes/revisions-server@1.9.25
 - @standardnotes/scheduler-server@1.15.6
 - @standardnotes/syncing-server@1.24.6
 - @standardnotes/websockets-server@1.4.53
 - @standardnotes/workspace-server@1.18.4
2022-12-12 11:38:37 +00:00
Karol Sójko 2980c42e88 fix(domain-events): add additional domain event services 2022-12-12 12:36:09 +01:00
standardci b03994f9db chore(release): publish new version
- @standardnotes/analytics@2.12.24
2022-12-12 11:31:36 +00:00
Karol Sójko 41906ec2f9 fix(analytics): daily analytics report template 2022-12-12 12:29:16 +01:00
316 changed files with 7950 additions and 2581 deletions
+4 -5
View File
@@ -11,19 +11,18 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: analytics
workspace_name: "@standardnotes/analytics"
e2e_tag_parameter_name: analytics_image_tag
deploy_web: false
package_path: packages/analytics
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest
@@ -187,12 +187,12 @@ jobs:
tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
- name: Run E2E test suite
uses: convictional/trigger-workflow-and-wait@v1.6.3
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
+164
View File
@@ -0,0 +1,164 @@
name: Reusable Server Utility Workflow
on:
workflow_call:
inputs:
service_name:
required: true
type: string
workspace_name:
required: true
type: string
deploy_web:
required: false
default: true
type: boolean
deploy_worker:
required: false
default: true
type: boolean
package_path:
required: true
type: string
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
CI_PAT_TOKEN:
required: true
AWS_ACCESS_KEY_ID:
required: true
AWS_SECRET_ACCESS_KEY:
required: true
jobs:
build:
runs-on: ubuntu-latest
outputs:
temp_dir: ${{ steps.bundle-dir.outputs.temp_dir }}
steps:
- uses: actions/checkout@v3
- name: Create Bundle Dir
id: bundle-dir
run: echo "temp_dir=$(mktemp -d -t ${{ inputs.service_name }}-${{ github.sha }}-XXXXXXX)" >> $GITHUB_OUTPUT
- name: Cache build
id: cache-build
uses: actions/cache@v3
with:
path: |
packages/**/dist
${{ steps.bundle-dir.outputs.temp_dir }}
key: ${{ runner.os }}-${{ inputs.service_name }}-build-${{ github.sha }}
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build
run: yarn build ${{ inputs.package_path }}
- name: Bundle
run: yarn workspace ${{ inputs.workspace_name }} bundle --no-compress --output-directory ${{ steps.bundle-dir.outputs.temp_dir }}
lint:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v3
- name: Cache build
id: cache-build
uses: actions/cache@v3
with:
path: |
packages/**/dist
${{ needs.build.outputs.temp_dir }}
key: ${{ runner.os }}-${{ inputs.service_name }}-build-${{ github.sha }}
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build
if: steps.cache-build.outputs.cache-hit != 'true'
run: yarn build ${{ inputs.package_path }}
- name: Lint
run: yarn lint:${{ inputs.service_name }}
test:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v3
- name: Cache build
id: cache-build
uses: actions/cache@v3
with:
path: |
packages/**/dist
${{ needs.build.outputs.temp_dir }}
key: ${{ runner.os }}-${{ inputs.service_name }}-build-${{ github.sha }}
- name: Set up Node
uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version-file: '.nvmrc'
- name: Build
if: steps.cache-build.outputs.cache-hit != 'true'
run: yarn build ${{ inputs.package_path }}
- name: Test
run: yarn test ${{ inputs.package_path }}
publish:
needs: [ build, test, lint ]
name: Publish Docker Image
uses: standardnotes/server/.github/workflows/common-docker-image.yml@main
with:
service_name: ${{ inputs.service_name }}
bundle_dir: ${{ needs.build.outputs.temp_dir }}
package_path: ${{ inputs.package_path }}
workspace_name: ${{ inputs.workspace_name }}
secrets: inherit
deploy-web:
if: ${{ inputs.deploy_web }}
needs: publish
name: Deploy Web
uses: standardnotes/server/.github/workflows/common-deploy.yml@main
with:
service_name: ${{ inputs.service_name }}
docker_image: ${{ inputs.service_name }}:${{ github.sha }}
secrets: inherit
deploy-worker:
if: ${{ inputs.deploy_worker }}
needs: publish
name: Deploy Worker
uses: standardnotes/server/.github/workflows/common-deploy.yml@main
with:
service_name: ${{ inputs.service_name }}-worker
docker_image: ${{ inputs.service_name }}:${{ github.sha }}
secrets: inherit
+4 -5
View File
@@ -11,19 +11,18 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: event-store
workspace_name: "@standardnotes/event-store"
e2e_tag_parameter_name: event_store_image_tag
deploy_web: false
package_path: packages/event-store
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest
+4 -5
View File
@@ -11,19 +11,18 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: scheduler
workspace_name: "@standardnotes/scheduler-server"
e2e_tag_parameter_name: scheduler_image_tag
deploy_web: false
package_path: packages/scheduler
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest
+4 -5
View File
@@ -11,18 +11,17 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: websockets
workspace_name: "@standardnotes/websockets-server"
e2e_tag_parameter_name: websockets_image_tag
package_path: packages/websockets
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest
steps:
+4 -5
View File
@@ -11,18 +11,17 @@ on:
workflow_dispatch:
jobs:
call_server_application_workflow:
name: Server Application
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
call_server_utility_workflow:
name: Server Utility
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
with:
service_name: workspace
workspace_name: "@standardnotes/workspace-server"
e2e_tag_parameter_name: workspace_image_tag
package_path: packages/workspace
secrets: inherit
newrelic:
needs: call_server_application_workflow
needs: call_server_utility_workflow
runs-on: ubuntu-latest
steps:
Generated
+398 -45
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.19.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"],\
@@ -1968,6 +1968,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@noble/ed25519", [\
["npm:1.7.1", {\
"packageLocation": "./.yarn/cache/@noble-ed25519-npm-1.7.1-177d9beb01-b1aa4b9264.zip/node_modules/@noble/ed25519/",\
"packageDependencies": [\
["@noble/ed25519", "npm:1.7.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@nodelib/fs.scandir", [\
["npm:2.1.5", {\
"packageLocation": "./.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-5f309a3b37.zip/node_modules/@nodelib/fs.scandir/",\
@@ -2324,6 +2333,44 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@peculiar/asn1-android", [\
["npm:2.3.3", {\
"packageLocation": "./.yarn/cache/@peculiar-asn1-android-npm-2.3.3-28df67d7a3-0c7cad544e.zip/node_modules/@peculiar/asn1-android/",\
"packageDependencies": [\
["@peculiar/asn1-android", "npm:2.3.3"],\
["@peculiar/asn1-schema", "npm:2.3.3"],\
["asn1js", "npm:3.0.5"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@peculiar/asn1-schema", [\
["npm:2.3.3", {\
"packageLocation": "./.yarn/cache/@peculiar-asn1-schema-npm-2.3.3-7c2b9469c4-f584f79d5a.zip/node_modules/@peculiar/asn1-schema/",\
"packageDependencies": [\
["@peculiar/asn1-schema", "npm:2.3.3"],\
["asn1js", "npm:3.0.5"],\
["pvtsutils", "npm:1.3.2"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@peculiar/asn1-x509", [\
["npm:2.3.4", {\
"packageLocation": "./.yarn/cache/@peculiar-asn1-x509-npm-2.3.4-a579005836-10a8659980.zip/node_modules/@peculiar/asn1-x509/",\
"packageDependencies": [\
["@peculiar/asn1-x509", "npm:2.3.4"],\
["@peculiar/asn1-schema", "npm:2.3.3"],\
["asn1js", "npm:3.0.5"],\
["ipaddr.js", "npm:2.0.1"],\
["pvtsutils", "npm:1.3.2"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@pnpm/network.ca-file", [\
["npm:1.0.1", {\
"packageLocation": "./.yarn/cache/@pnpm-network.ca-file-npm-1.0.1-42bfe40bec-ed952a5574.zip/node_modules/@pnpm/network.ca-file/",\
@@ -2438,25 +2485,25 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@sentry/core", [\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip/node_modules/@sentry/core/",\
["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.19.0"],\
["@sentry/types", "npm:7.19.0"],\
["@sentry/utils", "npm:7.19.0"],\
["@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/node", [\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip/node_modules/@sentry/node/",\
["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.19.0"],\
["@sentry/core", "npm:7.19.0"],\
["@sentry/types", "npm:7.19.0"],\
["@sentry/utils", "npm:7.19.0"],\
["@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"],\
@@ -2465,26 +2512,67 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@sentry/types", [\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip/node_modules/@sentry/types/",\
["@sentry/tracing", [\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-tracing-npm-7.28.1-e15d453d8e-be501ca9d7.zip/node_modules/@sentry/tracing/",\
"packageDependencies": [\
["@sentry/types", "npm:7.19.0"]\
["@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", [\
["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", [\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip/node_modules/@sentry/utils/",\
["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.19.0"],\
["@sentry/types", "npm:7.19.0"],\
["@sentry/utils", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@simplewebauthn/server", [\
["npm:6.2.2", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-6.2.2-ca870b05c2-5ffb9b1c15.zip/node_modules/@simplewebauthn/server/",\
"packageDependencies": [\
["@simplewebauthn/server", "npm:6.2.2"],\
["@noble/ed25519", "npm:1.7.1"],\
["@peculiar/asn1-android", "npm:2.3.3"],\
["@peculiar/asn1-schema", "npm:2.3.3"],\
["@peculiar/asn1-x509", "npm:2.3.4"],\
["base64url", "npm:3.0.1"],\
["cbor", "npm:5.2.0"],\
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
["jsrsasign", "npm:10.6.1"],\
["jwk-to-pem", "npm:2.0.5"],\
["node-fetch", "virtual:25a5f5382d53dbf298bf7a1191760bc2e0a523a619eeb0e667b99a8649e8ad183f9e2e0b45f6fb831b92f4078b61622aa567cf79565f6aa5af9597e3c84864f6#npm:2.6.7"]\
],\
"linkType": "HARD"\
}]\
]],\
["@simplewebauthn/typescript-types", [\
["npm:6.3.0-alpha.1", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-typescript-types-npm-6.3.0-alpha.1-629da05c10-5667c214e9.zip/node_modules/@simplewebauthn/typescript-types/",\
"packageDependencies": [\
["@simplewebauthn/typescript-types", "npm:6.3.0-alpha.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sinclair/typebox", [\
["npm:0.24.44", {\
"packageLocation": "./.yarn/cache/@sinclair-typebox-npm-0.24.44-38506ddef6-f37b9d28bf.zip/node_modules/@sinclair/typebox/",\
@@ -2538,7 +2626,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/analytics", "workspace:packages/analytics"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.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"],\
@@ -2546,6 +2634,7 @@ const RAW_RUNTIME_STATE =
["@standardnotes/time", "workspace:packages/time"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/mixpanel", "npm:2.14.4"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.30.5"],\
@@ -2557,6 +2646,7 @@ const RAW_RUNTIME_STATE =
["inversify", "npm:6.0.1"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["mixpanel", "npm:0.17.0"],\
["mysql2", "npm:2.3.3"],\
["newrelic", "npm:9.6.0"],\
["reflect-metadata", "npm:0.1.13"],\
@@ -2590,7 +2680,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.19.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"],\
@@ -2600,7 +2690,7 @@ const RAW_RUNTIME_STATE =
["@types/express", "npm:4.17.14"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/prettyjson", "npm:0.0.30"],\
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
@@ -2611,12 +2701,13 @@ const RAW_RUNTIME_STATE =
["eslint", "npm:8.25.0"],\
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
["express", "npm:4.18.2"],\
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
["helmet", "npm:6.0.0"],\
["inversify", "npm:6.0.1"],\
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["jsonwebtoken", "npm:8.5.1"],\
["jsonwebtoken", "npm:9.0.0"],\
["newrelic", "npm:9.6.0"],\
["nodemon", "npm:2.0.20"],\
["npm-check-updates", "npm:16.0.1"],\
@@ -2646,7 +2737,10 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/auth-server", "workspace:packages/auth"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/tracing", "npm:7.28.1"],\
["@simplewebauthn/server", "npm:6.2.2"],\
["@simplewebauthn/typescript-types", "npm:6.3.0-alpha.1"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -2737,10 +2831,6 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/domain-core/",\
"packageDependencies": [\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.53.1"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/security", "workspace:packages/security"],\
["@types/jest", "npm:29.1.1"],\
["@types/uuid", "npm:8.3.4"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
@@ -2874,7 +2964,7 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/files/",\
"packageDependencies": [\
["@standardnotes/files-server", "workspace:packages/files"],\
["@sentry/node", "npm:7.19.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"],\
@@ -2888,7 +2978,7 @@ const RAW_RUNTIME_STATE =
["@types/express", "npm:4.17.14"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/prettyjson", "npm:0.0.30"],\
["@types/uuid", "npm:8.3.4"],\
@@ -2901,13 +2991,14 @@ const RAW_RUNTIME_STATE =
["eslint", "npm:8.25.0"],\
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
["express", "npm:4.18.2"],\
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
["express-winston", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0"],\
["helmet", "npm:6.0.0"],\
["inversify", "npm:6.0.1"],\
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["jsonwebtoken", "npm:8.5.1"],\
["jsonwebtoken", "npm:9.0.0"],\
["newrelic", "npm:9.6.0"],\
["nodemon", "npm:2.0.20"],\
["npm-check-updates", "npm:16.0.1"],\
@@ -3009,7 +3100,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.19.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"],\
@@ -3054,7 +3145,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.19.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"],\
@@ -3093,11 +3184,11 @@ const RAW_RUNTIME_STATE =
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/common", "workspace:packages/common"],\
["@types/jest", "npm:29.1.1"],\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["jsonwebtoken", "npm:8.5.1"],\
["jsonwebtoken", "npm:9.0.0"],\
["reflect-metadata", "npm:0.1.13"],\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"]\
@@ -3115,7 +3206,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.19.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"],\
@@ -3181,7 +3272,8 @@ 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.19.0"],\
["@sentry/node", "npm:7.28.1"],\
["@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"],\
@@ -3197,7 +3289,7 @@ const RAW_RUNTIME_STATE =
["@types/inversify-express-utils", "npm:2.0.0"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/prettyjson", "npm:0.0.30"],\
["@types/ua-parser-js", "npm:0.7.36"],\
@@ -3215,7 +3307,7 @@ const RAW_RUNTIME_STATE =
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["jsonwebtoken", "npm:8.5.1"],\
["jsonwebtoken", "npm:9.0.0"],\
["mysql2", "npm:2.3.3"],\
["newrelic", "npm:9.6.0"],\
["nodemon", "npm:2.0.20"],\
@@ -3281,7 +3373,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.19.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"],\
@@ -3321,7 +3413,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.19.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"],\
@@ -3664,10 +3756,10 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@types/jsonwebtoken", [\
["npm:8.5.9", {\
"packageLocation": "./.yarn/cache/@types-jsonwebtoken-npm-8.5.9-79c2843a81-3f15a76cd5.zip/node_modules/@types/jsonwebtoken/",\
["npm:9.0.1", {\
"packageLocation": "./.yarn/cache/@types-jsonwebtoken-npm-9.0.1-5f660fdf38-44d3fccc6b.zip/node_modules/@types/jsonwebtoken/",\
"packageDependencies": [\
["@types/jsonwebtoken", "npm:8.5.9"],\
["@types/jsonwebtoken", "npm:9.0.1"],\
["@types/node", "npm:18.0.3"]\
],\
"linkType": "HARD"\
@@ -3728,6 +3820,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@types/mixpanel", [\
["npm:2.14.4", {\
"packageLocation": "./.yarn/cache/@types-mixpanel-npm-2.14.4-34bd98306f-a2bf6e633e.zip/node_modules/@types/mixpanel/",\
"packageDependencies": [\
["@types/mixpanel", "npm:2.14.4"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/newrelic", [\
["npm:7.0.4", {\
"packageLocation": "./.yarn/cache/@types-newrelic-npm-7.0.4-4f0b179b51-b44215b3ab.zip/node_modules/@types/newrelic/",\
@@ -4775,6 +4876,31 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["asn1.js", [\
["npm:5.4.1", {\
"packageLocation": "./.yarn/cache/asn1.js-npm-5.4.1-37c7edbcb0-5c36f81388.zip/node_modules/asn1.js/",\
"packageDependencies": [\
["asn1.js", "npm:5.4.1"],\
["bn.js", "npm:4.12.0"],\
["inherits", "npm:2.0.4"],\
["minimalistic-assert", "npm:1.0.1"],\
["safer-buffer", "npm:2.1.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["asn1js", [\
["npm:3.0.5", {\
"packageLocation": "./.yarn/cache/asn1js-npm-3.0.5-cf5558af33-d0bc57da97.zip/node_modules/asn1js/",\
"packageDependencies": [\
["asn1js", "npm:3.0.5"],\
["pvtsutils", "npm:1.3.2"],\
["pvutils", "npm:1.1.3"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["async", [\
["npm:3.2.4", {\
"packageLocation": "./.yarn/cache/async-npm-3.2.4-aba13508f9-9719e38d24.zip/node_modules/async/",\
@@ -4988,6 +5114,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["base64url", [\
["npm:3.0.1", {\
"packageLocation": "./.yarn/cache/base64url-npm-3.0.1-4c171c4917-72e1401ffe.zip/node_modules/base64url/",\
"packageDependencies": [\
["base64url", "npm:3.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["bcryptjs", [\
["npm:2.4.3", {\
"packageLocation": "./.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-bf6a43e9c4.zip/node_modules/bcryptjs/",\
@@ -5006,6 +5141,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["bignumber.js", [\
["npm:9.1.1", {\
"packageLocation": "./.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-e44d008049.zip/node_modules/bignumber.js/",\
"packageDependencies": [\
["bignumber.js", "npm:9.1.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["binary-extensions", [\
["npm:2.2.0", {\
"packageLocation": "./.yarn/cache/binary-extensions-npm-2.2.0-180c33fec7-16cf7c0cfd.zip/node_modules/binary-extensions/",\
@@ -5027,6 +5171,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["bn.js", [\
["npm:4.12.0", {\
"packageLocation": "./.yarn/cache/bn.js-npm-4.12.0-3ec6c884f6-bfb4590775.zip/node_modules/bn.js/",\
"packageDependencies": [\
["bn.js", "npm:4.12.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["body-parser", [\
["npm:1.20.1", {\
"packageLocation": "./.yarn/cache/body-parser-npm-1.20.1-759fd14db9-33f202c9d5.zip/node_modules/body-parser/",\
@@ -5094,6 +5247,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["brorand", [\
["npm:1.1.0", {\
"packageLocation": "./.yarn/cache/brorand-npm-1.1.0-ea86634c4b-f736e127fb.zip/node_modules/brorand/",\
"packageDependencies": [\
["brorand", "npm:1.1.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["browserslist", [\
["npm:4.21.1", {\
"packageLocation": "./.yarn/cache/browserslist-npm-4.21.1-930e90b93a-617d624493.zip/node_modules/browserslist/",\
@@ -5329,6 +5491,17 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["cbor", [\
["npm:5.2.0", {\
"packageLocation": "./.yarn/cache/cbor-npm-5.2.0-4f6440587f-d60986b9d0.zip/node_modules/cbor/",\
"packageDependencies": [\
["cbor", "npm:5.2.0"],\
["bignumber.js", "npm:9.1.1"],\
["nofilter", "npm:1.0.4"]\
],\
"linkType": "HARD"\
}]\
]],\
["chalk", [\
["npm:2.4.2", {\
"packageLocation": "./.yarn/cache/chalk-npm-2.4.2-3ea16dd91e-befd2fe888.zip/node_modules/chalk/",\
@@ -6453,6 +6626,22 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["elliptic", [\
["npm:6.5.4", {\
"packageLocation": "./.yarn/cache/elliptic-npm-6.5.4-0ca8204a86-4453b008cf.zip/node_modules/elliptic/",\
"packageDependencies": [\
["elliptic", "npm:6.5.4"],\
["bn.js", "npm:4.12.0"],\
["brorand", "npm:1.1.0"],\
["hash.js", "npm:1.1.7"],\
["hmac-drbg", "npm:1.0.1"],\
["inherits", "npm:2.0.4"],\
["minimalistic-assert", "npm:1.0.1"],\
["minimalistic-crypto-utils", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["emittery", [\
["npm:0.10.2", {\
"packageLocation": "./.yarn/cache/emittery-npm-0.10.2-aac10498b5-c55b286714.zip/node_modules/emittery/",\
@@ -7129,6 +7318,28 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["express-robots-txt", [\
["npm:1.0.0", {\
"packageLocation": "./.yarn/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-54f066f6c3.zip/node_modules/express-robots-txt/",\
"packageDependencies": [\
["express-robots-txt", "npm:1.0.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0", {\
"packageLocation": "./.yarn/__virtual__/express-robots-txt-virtual-0a3eb9f2f5/0/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-54f066f6c3.zip/node_modules/express-robots-txt/",\
"packageDependencies": [\
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
["@types/express", "npm:4.17.14"],\
["express", "npm:4.18.2"]\
],\
"packagePeers": [\
"@types/express",\
"express"\
],\
"linkType": "HARD"\
}]\
]],\
["express-winston", [\
["npm:4.2.0", {\
"packageLocation": "./.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-2d4b37671d.zip/node_modules/express-winston/",\
@@ -7959,6 +8170,17 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["hash.js", [\
["npm:1.1.7", {\
"packageLocation": "./.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e4266370d1.zip/node_modules/hash.js/",\
"packageDependencies": [\
["hash.js", "npm:1.1.7"],\
["inherits", "npm:2.0.4"],\
["minimalistic-assert", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["helmet", [\
["npm:6.0.0", {\
"packageLocation": "./.yarn/cache/helmet-npm-6.0.0-2285459f57-73b6ba802d.zip/node_modules/helmet/",\
@@ -7977,6 +8199,18 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["hmac-drbg", [\
["npm:1.0.1", {\
"packageLocation": "./.yarn/cache/hmac-drbg-npm-1.0.1-3499ad31cd-4e88d58ffc.zip/node_modules/hmac-drbg/",\
"packageDependencies": [\
["hmac-drbg", "npm:1.0.1"],\
["hash.js", "npm:1.1.7"],\
["minimalistic-assert", "npm:1.0.1"],\
["minimalistic-crypto-utils", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["hosted-git-info", [\
["npm:2.8.9", {\
"packageLocation": "./.yarn/cache/hosted-git-info-npm-2.8.9-62c44fa93f-c24da52f98.zip/node_modules/hosted-git-info/",\
@@ -8067,6 +8301,15 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["https-proxy-agent", [\
["npm:5.0.0", {\
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.0-bb777903c3-77d11b0e2c.zip/node_modules/https-proxy-agent/",\
"packageDependencies": [\
["https-proxy-agent", "npm:5.0.0"],\
["agent-base", "npm:6.0.2"],\
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"]\
],\
"linkType": "HARD"\
}],\
["npm:5.0.1", {\
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.1-42d65f358e-8e767faec9.zip/node_modules/https-proxy-agent/",\
"packageDependencies": [\
@@ -8357,6 +8600,13 @@ const RAW_RUNTIME_STATE =
["ipaddr.js", "npm:1.9.1"]\
],\
"linkType": "HARD"\
}],\
["npm:2.0.1", {\
"packageLocation": "./.yarn/cache/ipaddr.js-npm-2.0.1-04e97280d7-04ce6c896c.zip/node_modules/ipaddr.js/",\
"packageDependencies": [\
["ipaddr.js", "npm:2.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["is-arguments", [\
@@ -9557,6 +9807,26 @@ const RAW_RUNTIME_STATE =
["semver", "npm:5.7.1"]\
],\
"linkType": "HARD"\
}],\
["npm:9.0.0", {\
"packageLocation": "./.yarn/cache/jsonwebtoken-npm-9.0.0-36fd1594c0-7ccbd0b7bf.zip/node_modules/jsonwebtoken/",\
"packageDependencies": [\
["jsonwebtoken", "npm:9.0.0"],\
["jws", "npm:3.2.2"],\
["lodash", "npm:4.17.21"],\
["ms", "npm:2.1.3"],\
["semver", "npm:7.3.8"]\
],\
"linkType": "HARD"\
}]\
]],\
["jsrsasign", [\
["npm:10.6.1", {\
"packageLocation": "./.yarn/cache/jsrsasign-npm-10.6.1-a8fa295369-e8e9c1b24f.zip/node_modules/jsrsasign/",\
"packageDependencies": [\
["jsrsasign", "npm:10.6.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["jwa", [\
@@ -9571,6 +9841,18 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["jwk-to-pem", [\
["npm:2.0.5", {\
"packageLocation": "./.yarn/cache/jwk-to-pem-npm-2.0.5-aff7d9f125-fced3a75b0.zip/node_modules/jwk-to-pem/",\
"packageDependencies": [\
["jwk-to-pem", "npm:2.0.5"],\
["asn1.js", "npm:5.4.1"],\
["elliptic", "npm:6.5.4"],\
["safe-buffer", "npm:5.2.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["jws", [\
["npm:3.2.2", {\
"packageLocation": "./.yarn/cache/jws-npm-3.2.2-c1ae59c7af-347ed7c334.zip/node_modules/jws/",\
@@ -10184,6 +10466,24 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["minimalistic-assert", [\
["npm:1.0.1", {\
"packageLocation": "./.yarn/cache/minimalistic-assert-npm-1.0.1-dc8bb23d29-e2310081d8.zip/node_modules/minimalistic-assert/",\
"packageDependencies": [\
["minimalistic-assert", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["minimalistic-crypto-utils", [\
["npm:1.0.1", {\
"packageLocation": "./.yarn/cache/minimalistic-crypto-utils-npm-1.0.1-e66b10822e-7d909decd2.zip/node_modules/minimalistic-crypto-utils/",\
"packageDependencies": [\
["minimalistic-crypto-utils", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["minimatch", [\
["npm:3.1.2", {\
"packageLocation": "./.yarn/cache/minimatch-npm-3.1.2-9405269906-97f5615ee8.zip/node_modules/minimatch/",\
@@ -10308,6 +10608,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["mixpanel", [\
["npm:0.17.0", {\
"packageLocation": "./.yarn/cache/mixpanel-npm-0.17.0-3073ce9949-5a945bdbdd.zip/node_modules/mixpanel/",\
"packageDependencies": [\
["mixpanel", "npm:0.17.0"],\
["https-proxy-agent", "npm:5.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["mkdirp", [\
["npm:1.0.4", {\
"packageLocation": "./.yarn/cache/mkdirp-npm-1.0.4-37f6ef56b9-1233611198.zip/node_modules/mkdirp/",\
@@ -10568,6 +10878,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["nofilter", [\
["npm:1.0.4", {\
"packageLocation": "./.yarn/cache/nofilter-npm-1.0.4-1cbdc6c03a-9a26874e7d.zip/node_modules/nofilter/",\
"packageDependencies": [\
["nofilter", "npm:1.0.4"]\
],\
"linkType": "HARD"\
}]\
]],\
["nopt", [\
["npm:1.0.10", {\
"packageLocation": "./.yarn/cache/nopt-npm-1.0.10-f3db192976-efa5a9c2c1.zip/node_modules/nopt/",\
@@ -11616,6 +11935,25 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["pvtsutils", [\
["npm:1.3.2", {\
"packageLocation": "./.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-eb22d3df60.zip/node_modules/pvtsutils/",\
"packageDependencies": [\
["pvtsutils", "npm:1.3.2"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["pvutils", [\
["npm:1.1.3", {\
"packageLocation": "./.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-0cb4f4878f.zip/node_modules/pvutils/",\
"packageDependencies": [\
["pvutils", "npm:1.1.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["q", [\
["npm:1.5.1", {\
"packageLocation": "./.yarn/cache/q-npm-1.5.1-a28b3cfeaf-276b7e93fc.zip/node_modules/q/",\
@@ -12194,6 +12532,14 @@ const RAW_RUNTIME_STATE =
["lru-cache", "npm:6.0.0"]\
],\
"linkType": "HARD"\
}],\
["npm:7.3.8", {\
"packageLocation": "./.yarn/cache/semver-npm-7.3.8-25a996cb4f-94ad80ee14.zip/node_modules/semver/",\
"packageDependencies": [\
["semver", "npm:7.3.8"],\
["lru-cache", "npm:6.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["semver-diff", [\
@@ -13300,6 +13646,13 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.4.0"]\
],\
"linkType": "HARD"\
}],\
["npm:2.4.1", {\
"packageLocation": "./.yarn/cache/tslib-npm-2.4.1-36f0ed04db-a739a21e3f.zip/node_modules/tslib/",\
"packageDependencies": [\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["tsutils", [\
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -61,7 +61,7 @@
},
"packageManager": "yarn@4.0.0-rc.25",
"dependencies": {
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.28.1",
"newrelic": "^9.6.0"
}
}
+5
View File
@@ -10,6 +10,8 @@ DB_DATABASE=analytics
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
DB_MIGRATIONS_PATH=dist/migrations/*.js
ADMIN_EMAILS=test@standardnotes.com
REDIS_URL=redis://cache
REDIS_EVENTS_CHANNEL=events
@@ -26,3 +28,6 @@ NEW_RELIC_NO_CONFIG_FILE=true
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
NEW_RELIC_LOG_ENABLED=false
NEW_RELIC_LOG_LEVEL=info
# (Optional) Mixpanel
MIXPANEL_TOKEN=
+131
View File
@@ -3,6 +3,137 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.19.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.3...@standardnotes/analytics@2.19.4) (2023-01-17)
### Bug Fixes
* allow to run typeorm in non-replica mode ([f73129c](https://github.com/standardnotes/server/commit/f73129cd7e7d6a9b8a63e5c80284467597557982))
## [2.19.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.2...@standardnotes/analytics@2.19.3) (2023-01-16)
**Note:** Version bump only for package @standardnotes/analytics
## [2.19.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.1...@standardnotes/analytics@2.19.2) (2023-01-13)
**Note:** Version bump only for package @standardnotes/analytics
## [2.19.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.0...@standardnotes/analytics@2.19.1) (2022-12-30)
### Bug Fixes
* **analytics:** remove unnecesary context from mixpanel events ([ba1e1ad](https://github.com/standardnotes/server/commit/ba1e1ad5ad82b052be4cc2d1cc2abdaf3b72cf4c))
# [2.19.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.18.0...@standardnotes/analytics@2.19.0) (2022-12-30)
### Features
* **analytics:** add mixpanel events tracking ([df6e3f0](https://github.com/standardnotes/server/commit/df6e3f06a6868e30e60dd98431122983724644b4))
# [2.18.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.8...@standardnotes/analytics@2.18.0) (2022-12-30)
### Features
* **analytics:** add mixpanel ([893d617](https://github.com/standardnotes/server/commit/893d6176c3b0b56c45e5188fe982232db2ceedc4))
## [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
* **auth:** add session traces ([8bcb552](https://github.com/standardnotes/server/commit/8bcb552783b2d12f3296b3195752168482790bc8))
## [2.12.27](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.26...@standardnotes/analytics@2.12.27) (2022-12-15)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.26](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.25...@standardnotes/analytics@2.12.26) (2022-12-15)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.25](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.24...@standardnotes/analytics@2.12.25) (2022-12-12)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.24](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.23...@standardnotes/analytics@2.12.24) (2022-12-12)
### Bug Fixes
* **analytics:** daily analytics report template ([41906ec](https://github.com/standardnotes/server/commit/41906ec2f9fd4d605b1c002826173e14fb534e00))
## [2.12.23](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.22...@standardnotes/analytics@2.12.23) (2022-12-12)
**Note:** Version bump only for package @standardnotes/analytics
+33 -26
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)
@@ -263,7 +270,7 @@ void container.load().then((container) => {
)
const adminEmails = container.get(TYPES.ADMIN_EMAILS) as string[]
logger.info('Sending report to following admins: %O', adminEmails)
logger.info(`Sending report to following admins: ${adminEmails}`)
Promise.resolve(
requestReport(
+1 -1
View File
@@ -7,5 +7,5 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/'],
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/', '/Handler/'],
}
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.12.23",
"version": "2.19.4",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -27,6 +27,7 @@
"devDependencies": {
"@types/ioredis": "^5.0.0",
"@types/jest": "^29.1.1",
"@types/mixpanel": "^2.14.4",
"@types/newrelic": "^7.0.4",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.30.0",
@@ -38,7 +39,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.28.1",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
@@ -49,6 +50,7 @@
"dotenv": "^16.0.1",
"inversify": "^6.0.1",
"ioredis": "^5.2.4",
"mixpanel": "^0.17.0",
"mysql2": "^2.3.3",
"newrelic": "^9.6.0",
"reflect-metadata": "^0.1.13",
+49 -23
View File
@@ -8,6 +8,8 @@ import {
DomainEventSubscriberFactoryInterface,
} from '@standardnotes/domain-events'
import { MapperInterface } from '@standardnotes/domain-core'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Mixpanel = require('mixpanel')
import { Env } from './Env'
import TYPES from './Types'
@@ -52,6 +54,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')
@@ -131,6 +136,33 @@ export class ContainerConfigLoader {
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
container.bind(TYPES.MIXPANEL_TOKEN).toConstantValue(env.get('MIXPANEL_TOKEN', true))
// 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)),
)
}
if (env.get('MIXPANEL_TOKEN', true)) {
container.bind<Mixpanel>(TYPES.MixpanelClient).toConstantValue(Mixpanel.init(env.get('MIXPANEL_TOKEN', true)))
}
// Repositories
container
@@ -139,6 +171,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 +189,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 +219,22 @@ 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),
env.get('MIXPANEL_TOKEN', true) ? container.get(TYPES.MixpanelClient) : null,
),
)
// 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 +247,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)) {
+29 -19
View File
@@ -12,31 +12,41 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
: 45_000
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
const replicationConfig = {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST', true),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
restoreNodeTimeout: 5,
}
export const AppDataSource = new DataSource({
type: 'mysql',
charset: 'utf8mb4',
supportBigNumbers: true,
bigNumberStrings: false,
maxQueryExecutionTime,
replication: {
master: {
host: env.get('DB_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
slaves: [
{
host: env.get('DB_REPLICA_HOST'),
port: parseInt(env.get('DB_PORT')),
username: env.get('DB_USERNAME'),
password: env.get('DB_PASSWORD'),
database: env.get('DB_DATABASE'),
},
],
removeNodeErrorCount: 10,
},
replication: inReplicaMode ? replicationConfig : undefined,
host: inReplicaMode ? undefined : env.get('DB_HOST'),
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
entities: [AnalyticsEntity, TypeORMRevenueModification],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,
@@ -12,9 +12,11 @@ const TYPES = {
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
ADMIN_EMAILS: Symbol.for('ADMIN_EMAILS'),
MIXPANEL_TOKEN: Symbol.for('MIXPANEL_TOKEN'),
// Repositories
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
StatisticMeasureRepository: Symbol.for('StatisticMeasureRepository'),
// ORM
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
@@ -22,6 +24,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 +37,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
@@ -45,6 +49,7 @@ const TYPES = {
StatisticsStore: Symbol.for('StatisticsStore'),
Timer: Symbol.for('Timer'),
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
MixpanelClient: Symbol.for('MixpanelClient'),
}
export default TYPES
@@ -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,13 +326,12 @@ 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))}`,
}
}
export const html = (data: any, timer: TimerInterface) => {
const chartUrls = getChartUrls(event)
const chartUrls = getChartUrls(data)
const successfullPaymentsActivity = data.activityStatistics.find(
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentSuccess && Period.Yesterday,
@@ -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>
@@ -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()
})
})
@@ -1,12 +1,13 @@
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
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'
@@ -17,6 +18,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
@@ -33,12 +35,19 @@ 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,
])
await this.analyticsEntityRepository.remove(analyticsEntity)
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsEntity.id.toString(),
user_created_at: this.timer.convertMicrosecondsToDate(event.payload.userCreatedAtTimestamp),
})
}
}
}
@@ -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()
})
})
@@ -1,5 +1,6 @@
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -12,6 +13,7 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: PaymentFailedEvent): Promise<void> {
@@ -21,5 +23,11 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
})
}
}
}
@@ -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,
])
})
})
@@ -1,12 +1,13 @@
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
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 +21,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 +52,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,
],
]),
],
]),
@@ -59,6 +84,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: PaymentSuccessEvent): Promise<void> {
@@ -69,7 +95,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)
@@ -89,5 +115,19 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
Period.ThisMonth,
])
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
amount: event.payload.amount,
billing_frequency: event.payload.billingFrequency,
payment_type: event.payload.paymentType,
subscription_name: event.payload.subscriptionName,
})
this.mixpanelClient.people.track_charge(analyticsId.toString(), event.payload.amount)
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}
@@ -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,
])
})
})
@@ -1,20 +1,36 @@
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
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'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@injectable()
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {}
constructor(
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: RefundProcessedEvent): Promise<void> {
await this.statisticsStore.incrementMeasure(StatisticsMeasure.Refunds, event.payload.amount, [
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
amount: event.payload.amount,
})
this.mixpanelClient.people.track_charge(analyticsId.toString(), -event.payload.amount)
}
}
}
@@ -0,0 +1,34 @@
import { DomainEventHandlerInterface, StatisticPersistenceRequestedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { Logger } from 'winston'
import { PersistStatistic } from '../UseCase/PersistStatistic/PersistStatistic'
import { Mixpanel } from 'mixpanel'
export class StatisticPersistenceRequestedEventHandler implements DomainEventHandlerInterface {
constructor(
private persistStatistic: PersistStatistic,
private timer: TimerInterface,
private logger: Logger,
private mixpanelClient: Mixpanel | null,
) {}
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())
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: 'global-stats',
statistic: event.payload.statisticMeasureName,
value: event.payload.value,
})
}
}
}
@@ -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()
})
})
@@ -1,18 +1,20 @@
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Logger } from 'winston'
import { Username } from '@standardnotes/domain-core'
import { Mixpanel } from 'mixpanel'
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'
import { TimerInterface } from '@standardnotes/time'
@injectable()
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
@@ -22,6 +24,8 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionCancelledEvent): Promise<void> {
@@ -50,6 +54,22 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_created_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionCreatedAt),
subscription_updated_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionUpdatedAt),
last_payed_at: this.timer.convertMicrosecondsToDate(event.payload.lastPayedAt),
subscription_ends_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionEndsAt),
offline: event.payload.offline,
replaced: event.payload.replaced,
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
}
}
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
@@ -58,7 +78,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 +90,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],
)
@@ -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()
})
})
@@ -1,12 +1,13 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Logger } from 'winston'
import { Mixpanel } from 'mixpanel'
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'
@@ -22,6 +23,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: SubscriptionExpiredEvent): Promise<void> {
@@ -33,7 +35,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],
)
@@ -54,5 +56,18 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
offline: event.payload.offline,
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
}
}
}
@@ -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()
})
})
@@ -1,12 +1,14 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable, optional } from 'inversify'
import { Logger } from 'winston'
import { Mixpanel } from 'mixpanel'
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'
@@ -22,6 +24,8 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
@@ -45,18 +49,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],
)
@@ -78,5 +82,22 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
offline: event.payload.offline,
discount_code: event.payload.discountCode,
limited_discount_purchased: event.payload.limitedDiscountPurchased,
new_subscriber: event.payload.newSubscriber,
user_registered_at: this.timer.convertMicrosecondsToDate(event.payload.userRegisteredAt),
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}
@@ -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,
])
})
})
@@ -1,5 +1,7 @@
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -12,6 +14,8 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
constructor(
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
@@ -21,5 +25,16 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
discount_code: event.payload.discountCode,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}
@@ -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()
})
})
@@ -1,12 +1,13 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Logger } from 'winston'
import { Mixpanel } from 'mixpanel'
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'
@@ -22,6 +23,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: SubscriptionRefundedEvent): Promise<void> {
@@ -50,6 +52,18 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
offline: event.payload.offline,
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
}
}
private async markChurnActivity(analyticsId: number, event: SubscriptionRefundedEvent): Promise<void> {
@@ -70,7 +84,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],
)
@@ -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()
})
})
@@ -1,6 +1,7 @@
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Username } from '@standardnotes/domain-core'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@@ -11,6 +12,7 @@ import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/Save
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
import { Logger } from 'winston'
import { TimerInterface } from '@standardnotes/time'
@injectable()
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
@@ -19,6 +21,8 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionRenewedEvent): Promise<void> {
@@ -50,5 +54,17 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
offline: event.payload.offline,
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}
@@ -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,
])
})
})
@@ -1,5 +1,6 @@
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -13,6 +14,7 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: UserRegisteredEvent): Promise<void> {
@@ -26,5 +28,17 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsEntity.id.toString(),
protocol_version: event.payload.protocolVersion,
})
this.mixpanelClient.people.set(analyticsEntity.id.toString(), {
subscription: 'free',
protocol_version: event.payload.protocolVersion,
})
}
}
}
@@ -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))
}
}
@@ -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()
}
})
})
@@ -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 }))
}
}
}
@@ -0,0 +1,3 @@
export interface StatisticMeasureNameProps {
value: string
}
@@ -0,0 +1,7 @@
import { StatisticMeasureName } from './StatisticMeasureName'
export interface StatisticMeasureProps {
name: StatisticMeasureName
value: number
date: Date
}
@@ -0,0 +1,5 @@
import { StatisticMeasure } from './StatisticMeasure'
export interface StatisticMeasureRepositoryInterface {
save(statisticMeasure: StatisticMeasure): Promise<void>
}
@@ -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',
}
@@ -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 }>>
}
@@ -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)}`
@@ -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[]
}
@@ -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,
@@ -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,
@@ -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)
}
}
@@ -0,0 +1,5 @@
export interface PersistStatisticDTO {
statisticMeasureName: string
value: number
date: Date
}
@@ -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) {
+69
View File
@@ -3,6 +3,75 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.46.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.3...@standardnotes/api-gateway@1.46.0) (2023-01-16)
### Features
* **api-gateway:** add all revisions endpoints on v2 ([60b3dd6](https://github.com/standardnotes/api-gateway/commit/60b3dd6138ef9b8e9a717873548afc2d3924a0d7))
* **api-gateway:** switch to fetching revisions from reivsions server ([22c1f93](https://github.com/standardnotes/api-gateway/commit/22c1f936c3a770a82dc1a1e6aa136e183d308aa6))
## [1.45.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.2...@standardnotes/api-gateway@1.45.3) (2023-01-16)
### Bug Fixes
* **api-gateway:** add noindex robots meta tag to api gateway homepage ([04c6888](https://github.com/standardnotes/api-gateway/commit/04c6888cf65f9f1315fc2fb8af069d26bfbc31b1))
## [1.45.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.1...@standardnotes/api-gateway@1.45.2) (2023-01-13)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.45.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.0...@standardnotes/api-gateway@1.45.1) (2023-01-13)
### Bug Fixes
* add robots.txt setup for api-gateway and files server to disallow indexing ([bb82043](https://github.com/standardnotes/api-gateway/commit/bb820437af2b9644d7597de045b5840037b81db3))
# [1.45.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.44.0...@standardnotes/api-gateway@1.45.0) (2023-01-05)
### Features
* **auth:** add recovery sign in with recovery codes ([cac899a](https://github.com/standardnotes/api-gateway/commit/cac899a7e558d066895dfb3ba28418d94072f2b7))
# [1.44.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.43.0...@standardnotes/api-gateway@1.44.0) (2022-12-29)
### Features
* **auth:** add removing authenticator ([de50d76](https://github.com/standardnotes/api-gateway/commit/de50d76800a4240729763b2df11c4a1718951670))
# [1.43.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.42.0...@standardnotes/api-gateway@1.43.0) (2022-12-29)
### Features
* **auth:** add listing authenticators ([01837ea](https://github.com/standardnotes/api-gateway/commit/01837eaea9b1f219e7ad3be4d28cd0df099fe423))
# [1.42.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.3...@standardnotes/api-gateway@1.42.0) (2022-12-29)
### Features
* **auth:** add http endpoints for authenticators ([b6fda90](https://github.com/standardnotes/api-gateway/commit/b6fda901ef66a3e66541bd1e3f041b8268a1c3f5))
## [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
* **auth:** add session traces ([8bcb552](https://github.com/standardnotes/api-gateway/commit/8bcb552783b2d12f3296b3195752168482790bc8))
## [1.40.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.1...@standardnotes/api-gateway@1.40.2) (2022-12-12)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.40.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.0...@standardnotes/api-gateway@1.40.1) (2022-12-12)
**Note:** Version bump only for package @standardnotes/api-gateway
+9
View File
@@ -21,6 +21,7 @@ import '../src/Controller/v1/FilesController'
import '../src/Controller/v1/SubscriptionInvitesController'
import '../src/Controller/v1/WorkspacesController'
import '../src/Controller/v1/InvitesController'
import '../src/Controller/v1/AuthenticatorsController'
import '../src/Controller/v2/PaymentsControllerV2'
import '../src/Controller/v2/ActionsControllerV2'
@@ -30,6 +31,8 @@ import helmet from 'helmet'
import * as cors from 'cors'
import { text, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
import * as winston from 'winston'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const robots = require('express-robots-txt')
import { InversifyExpressServer } from 'inversify-express-utils'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
@@ -77,6 +80,12 @@ void container.load().then((container) => {
}),
)
app.use(cors())
app.use(
robots({
UserAgent: '*',
Disallow: '/',
}),
)
if (env.get('SENTRY_DSN', true)) {
Sentry.init({
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.40.1",
"version": "1.46.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -21,7 +21,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@sentry/node": "^7.28.1",
"@standardnotes/common": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
@@ -32,11 +32,12 @@
"cors": "2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.2",
"express-robots-txt": "^1.0.0",
"helmet": "^6.0.0",
"inversify": "^6.0.1",
"inversify-express-utils": "^6.4.3",
"ioredis": "^5.2.4",
"jsonwebtoken": "8.5.1",
"jsonwebtoken": "^9.0.0",
"newrelic": "^9.6.0",
"prettyjson": "^1.2.5",
"reflect-metadata": "0.1.13",
@@ -47,7 +48,7 @@
"@types/express": "^4.17.14",
"@types/ioredis": "^5.0.0",
"@types/jest": "^29.1.1",
"@types/jsonwebtoken": "^8.5.0",
"@types/jsonwebtoken": "^9.0.1",
"@types/newrelic": "^7.0.4",
"@types/prettyjson": "^0.0.30",
"@typescript-eslint/eslint-plugin": "^5.29.0",
@@ -57,7 +57,9 @@ export class LegacyController extends BaseHttpController {
@all('*')
async legacyProxyToSyncingServer(request: Request, response: Response): Promise<void> {
if (request.path === '/') {
response.send('Welcome to the Standard Notes server infrastructure. Learn more at https://docs.standardnotes.com')
response.send(
'<!DOCTYPE html><html lang="en"><head><meta name="robots" content="noindex"></head><body>Welcome to the Standard Notes server infrastructure. Learn more at https://docs.standardnotes.com</body></html>',
)
return
}

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