Compare commits

...

78 Commits

Author SHA1 Message Date
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
198 changed files with 4305 additions and 1306 deletions

View File

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

376
.pnp.cjs generated
View File

@@ -126,7 +126,7 @@ const RAW_RUNTIME_STATE =
["@lerna-lite/cli", "npm:1.6.0"],\
["@lerna-lite/list", "npm:1.6.0"],\
["@lerna-lite/run", "npm:1.6.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
@@ -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/",\
@@ -2447,6 +2494,16 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.28.1-a468033ea8-f29d747d3e.zip/node_modules/@sentry/core/",\
"packageDependencies": [\
["@sentry/core", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.28.1"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/hub", [\
@@ -2476,6 +2533,20 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.28.1-b0e124fdfc-b4922d1f0a.zip/node_modules/@sentry/node/",\
"packageDependencies": [\
["@sentry/node", "npm:7.28.1"],\
["@sentry/core", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.28.1"],\
["cookie", "npm:0.4.2"],\
["https-proxy-agent", "npm:5.0.1"],\
["lru_map", "npm:0.3.3"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/profiling-node", [\
@@ -2506,6 +2577,17 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-tracing-npm-7.28.1-e15d453d8e-be501ca9d7.zip/node_modules/@sentry/tracing/",\
"packageDependencies": [\
["@sentry/tracing", "npm:7.28.1"],\
["@sentry/core", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.28.1"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/types", [\
@@ -2515,6 +2597,13 @@ const RAW_RUNTIME_STATE =
["@sentry/types", "npm:7.27.0"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.28.1-42d9a8574c-7dc6639cb7.zip/node_modules/@sentry/types/",\
"packageDependencies": [\
["@sentry/types", "npm:7.28.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/utils", [\
@@ -2526,6 +2615,43 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}],\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.28.1-71eaeb767f-a4b5f73db0.zip/node_modules/@sentry/utils/",\
"packageDependencies": [\
["@sentry/utils", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@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", [\
@@ -2581,7 +2707,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/analytics", "workspace:packages/analytics"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -2589,6 +2715,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"],\
@@ -2600,6 +2727,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"],\
@@ -2633,7 +2761,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
@@ -2689,9 +2817,11 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/auth-server", "workspace:packages/auth"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/profiling-node", "npm:0.0.12"],\
["@sentry/tracing", "npm:7.27.0"],\
["@sentry/tracing", "npm:7.28.1"],\
["@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"],\
@@ -2915,7 +3045,7 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/files/",\
"packageDependencies": [\
["@standardnotes/files-server", "workspace:packages/files"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/config", "npm:2.4.3"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3050,7 +3180,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -3095,7 +3225,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3156,7 +3286,7 @@ const RAW_RUNTIME_STATE =
["@lerna-lite/cli", "npm:1.6.0"],\
["@lerna-lite/list", "npm:1.6.0"],\
["@lerna-lite/run", "npm:1.6.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
@@ -3222,9 +3352,9 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/profiling-node", "npm:0.0.12"],\
["@sentry/tracing", "npm:7.27.0"],\
["@sentry/tracing", "npm:7.28.1"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3324,7 +3454,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3364,7 +3494,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.27.0"],\
["@sentry/node", "npm:7.28.1"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -3771,6 +3901,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/",\
@@ -4818,6 +4957,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/",\
@@ -5031,6 +5195,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/",\
@@ -5049,6 +5222,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/",\
@@ -5070,6 +5252,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/",\
@@ -5137,6 +5328,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/",\
@@ -5372,6 +5572,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/",\
@@ -6496,6 +6707,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/",\
@@ -8002,6 +8229,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/",\
@@ -8020,6 +8258,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/",\
@@ -8110,6 +8360,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": [\
@@ -8400,6 +8659,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", [\
@@ -9602,6 +9868,15 @@ const RAW_RUNTIME_STATE =
"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", [\
["npm:1.4.1", {\
"packageLocation": "./.yarn/cache/jwa-npm-1.4.1-4f19d6572c-0cc3e68b68.zip/node_modules/jwa/",\
@@ -9614,6 +9889,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/",\
@@ -10227,6 +10514,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/",\
@@ -10351,6 +10656,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/",\
@@ -10646,6 +10961,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/",\
@@ -11702,6 +12026,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/",\
@@ -13386,6 +13729,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.

View File

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

View File

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

View File

@@ -3,6 +3,87 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.13.0",
"version": "2.18.0",
"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.27.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",

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,21 @@ export class ContainerConfigLoader {
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
.to(SubscriptionReactivatedEventHandler)
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
container
.bind<StatisticPersistenceRequestedEventHandler>(TYPES.StatisticPersistenceRequestedEventHandler)
.toConstantValue(
new StatisticPersistenceRequestedEventHandler(
container.get(TYPES.PersistStatistic),
container.get(TYPES.Timer),
container.get(TYPES.Logger),
),
)
// Maps
container
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
.to(RevenueModificationMap)
// Services
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
container
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
if (env.get('SNS_TOPIC_ARN', true)) {
container
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
} else {
container
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
)
}
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
@@ -222,6 +246,7 @@ export class ContainerConfigLoader {
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
['STATISTIC_PERSISTENCE_REQUESTED', container.get(TYPES.StatisticPersistenceRequestedEventHandler)],
])
if (env.get('SQS_QUEUE_URL', true)) {

View File

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

View File

@@ -2,9 +2,41 @@
import { TimerInterface } from '@standardnotes/time'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { Period } from '../Time/Period'
const countActiveUsers = (measureName: string, data: any): { yesterday: number; last30Days: number } => {
const totalActiveUsersLast30DaysIncludingToday = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === measureName && a.period === 27,
)
const totalActiveUsersYesterday =
totalActiveUsersLast30DaysIncludingToday.counts[totalActiveUsersLast30DaysIncludingToday.counts.length - 2]
.totalCount
const filteredCounts = totalActiveUsersLast30DaysIncludingToday.counts.filter(
(count: { totalCount: number }) => count.totalCount !== 0,
)
if (filteredCounts.length === 0) {
return {
yesterday: 0,
last30Days: 0,
}
}
const last30DaysNumbers = filteredCounts.map((count: { totalCount: number }) => count.totalCount)
const last30DaysCount = last30DaysNumbers.reduce((previousValue: number, currentValue: number) => {
return previousValue + currentValue
})
const averageActiveUsersLast30Days = Math.floor(last30DaysCount / last30DaysNumbers.length)
return {
yesterday: totalActiveUsersYesterday,
last30Days: averageActiveUsersLast30Days,
}
}
const getChartUrls = (
data: any,
): {
@@ -12,7 +44,6 @@ const getChartUrls = (
users: string
quarterlyPerformance: string
churn: string
mrr: string
mrrMonthly: string
} => {
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
@@ -237,82 +268,6 @@ const getChartUrls = (
},
}
const mrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
)
const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
)
const annualPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
)
const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
)
const proPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
)
const plusPlansMrrOverTime = data.statisticsOverTime.find(
(a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
)
const mrrOverTimeConfig = {
type: 'line',
data: {
labels: mrrOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
datasets: [
{
label: 'MRR',
backgroundColor: 'rgb(25, 255, 140)',
borderColor: 'rgb(25, 255, 140)',
data: mrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Monthly Plans',
backgroundColor: 'rgb(54, 162, 235)',
borderColor: 'rgb(54, 162, 235)',
data: monthlyPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Annual Plans',
backgroundColor: 'rgb(255, 221, 51)',
borderColor: 'rgb(255, 221, 51)',
data: annualPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - Five Year Plans',
backgroundColor: 'rgb(255, 120, 120)',
borderColor: 'rgb(255, 120, 120)',
data: fiveYearPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - PRO Plans',
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: proPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
{
label: 'MRR - PLUS Plans',
backgroundColor: 'rgb(221, 51, 255)',
borderColor: 'rgb(221, 51, 255)',
data: plusPlansMrrOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
fill: false,
pointRadius: 2,
},
],
},
}
const mrrMonthlyOverTime = data.statisticsOverTime
.find((a: { name: string; period: Period }) => a.name === 'mrr' && a.period === Period.ThisYear)
?.counts.map((count: { totalCount: number }) => +count.totalCount.toFixed(2))
@@ -371,7 +326,6 @@ const getChartUrls = (
JSON.stringify(quarterlyConfig),
)}`,
churn: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(churnConfig))}`,
mrr: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrOverTimeConfig))}`,
mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
}
}
@@ -417,156 +371,170 @@ export const html = (data: any, timer: TimerInterface) => {
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
)
const incomeMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Income && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.Yesterday,
)
const refundMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Refunds && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.Yesterday,
)
const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0
const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0
const revenueYesterday = incomeYesterday - refundsYesterday
const subscriptionLengthMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.Yesterday,
)
const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0),
)
const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
)
const subscriptionRemainingTimePercentageYesterday = Math.floor(
subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0,
)
const registrationLengthMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.Yesterday,
)
const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationLengthMeasureYesterday?.average ?? 0),
)
const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
)
const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0),
)
const incomeMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Income && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.ThisMonth,
)
const refundMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.Refunds && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.ThisMonth,
)
const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0
const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0
const revenueThisMonth = incomeThisMonth - refundsThisMonth
const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.SubscriptionLength && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.ThisMonth,
)
const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0),
)
const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
)
const subscriptionRemainingTimePercentageThisMonth = Math.floor(
subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0,
)
const registrationLengthMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationLength && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.ThisMonth,
)
const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationLengthMeasureThisMonth?.average ?? 0),
)
const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
)
const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0),
)
const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
a.period === Period.Yesterday,
)
const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
a.period === Period.Yesterday,
)
const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
a.period === Period.Yesterday,
)
const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
a.period === Period.Yesterday,
)
const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
)
const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
a.period === Period.Yesterday,
)
const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
a.period === Period.Yesterday,
)
const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome &&
a.period === Period.Yesterday,
)
const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
a.period === Period.ThisMonth,
)
const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
a.period === Period.ThisMonth,
)
const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
a.period === Period.ThisMonth,
)
const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
a.period === Period.ThisMonth,
)
const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
)
const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
a.period === Period.ThisMonth,
)
const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
a.period === Period.ThisMonth,
)
const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
(a: { name: StatisticsMeasure; period: Period }) =>
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
(a: { name: string; period: Period }) =>
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome &&
a.period === Period.ThisMonth,
)
const mrrOverTime = data.statisticsOverTime.find(
@@ -594,12 +562,39 @@ export const html = (data: any, timer: TimerInterface) => {
(value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
)
const totalActiveUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveUsers, data)
const totalActiveFreeUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveFreeUsers, data)
const totalActivePlusUsers = countActiveUsers(StatisticMeasureName.NAMES.ActivePlusUsers, data)
const totalActiveProUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveProUsers, data)
return ` <div>
<p>Hello,</p>
<p>
<strong>Here are some statistics from yesterday:</strong>
</p>
<ul>
<li>
<b>Active Users</b>
<ul>
<li>
<b>Total:</b> ${totalActiveUsers.yesterday.toLocaleString('en-US')}
</li>
<li>
<b>By Subscription Type:</b>
<ul>
<li>
<b>FREE:</b> ${totalActiveFreeUsers.yesterday.toLocaleString('en-US')}
</li>
<li>
<b>PLUS:</b> ${totalActivePlusUsers.yesterday.toLocaleString('en-US')}
</li>
<li>
<b>PRO:</b> ${totalActiveProUsers.yesterday.toLocaleString('en-US')}
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Payments</b>
<ul>
@@ -798,6 +793,28 @@ export const html = (data: any, timer: TimerInterface) => {
<strong>Here are some statistics from last 30 days:</strong>
</p>
<ul>
<li>
<b>Active Users (Average)</b>
<ul>
<li>
<b>Total:</b> ${totalActiveUsers.last30Days.toLocaleString('en-US')}
</li>
<li>
<b>By Subscription Type:</b>
<ul>
<li>
<b>FREE:</b> ${totalActiveFreeUsers.last30Days.toLocaleString('en-US')}
</li>
<li>
<b>PLUS:</b> ${totalActivePlusUsers.last30Days.toLocaleString('en-US')}
</li>
<li>
<b>PRO:</b> ${totalActiveProUsers.last30Days.toLocaleString('en-US')}
</li>
</ul>
</li>
</ul>
</li>
<li>
<b>Payments (This Month)</b>
<ul>
@@ -930,10 +947,6 @@ export const html = (data: any, timer: TimerInterface) => {
</ul>
</li>
</ul>
<p>
<strong>Here is the MRR chart over 30 days:</strong>
</p>
<img src=${chartUrls.mrr}></img>
<p>
<strong>Here is the MRR Monthly chart this year:</strong>
</p>

View File

@@ -1,69 +0,0 @@
import 'reflect-metadata'
import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events'
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
import { TimerInterface } from '@standardnotes/time'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
describe('AccountDeletionRequestedEventHandler', () => {
let event: AccountDeletionRequestedEvent
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let timer: TimerInterface
const createHandler = () =>
new AccountDeletionRequestedEventHandler(analyticsEntityRepository, analyticsStore, statisticsStore, timer)
beforeEach(() => {
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '1-2-3',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: '2-3-4',
}
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue({ id: 3 })
analyticsEntityRepository.remove = jest.fn()
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
})
it('should mark account deletion and registration length', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['DeleteAccount'], 3, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith('registration-length', 122, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
expect(analyticsEntityRepository.remove).toHaveBeenCalled()
})
it('should not mark anything if entity is not found', async () => {
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
await createHandler().handle(event)
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
expect(analyticsEntityRepository.remove).not.toHaveBeenCalled()
})
})

View File

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

View File

@@ -1,34 +0,0 @@
import 'reflect-metadata'
import { PaymentFailedEvent } from '@standardnotes/domain-events'
import { PaymentFailedEventHandler } from './PaymentFailedEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
describe('PaymentFailedEventHandler', () => {
let event: PaymentFailedEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
const createHandler = () => new PaymentFailedEventHandler(getUserAnalyticsId, analyticsStore)
beforeEach(() => {
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
event = {} as jest.Mocked<PaymentFailedEvent>
event.payload = {
userEmail: 'test@test.com',
}
})
it('should mark payment failed for analytics', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalled()
})
})

View File

@@ -1,75 +0,0 @@
import 'reflect-metadata'
import { PaymentSuccessEvent } from '@standardnotes/domain-events'
import { PaymentSuccessEventHandler } from './PaymentSuccessEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { Logger } from 'winston'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { Period } from '../Time/Period'
describe('PaymentSuccessEventHandler', () => {
let event: PaymentSuccessEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
let statisticsStore: StatisticsStoreInterface
let logger: Logger
const createHandler = () =>
new PaymentSuccessEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, logger)
beforeEach(() => {
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
statisticsStore.incrementMeasure = jest.fn()
event = {} as jest.Mocked<PaymentSuccessEvent>
event.payload = {
userEmail: 'test@test.com',
amount: 12.45,
billingFrequency: 12,
paymentType: 'initial',
subscriptionName: 'PRO_PLAN',
}
logger = {} as jest.Mocked<Logger>
logger.warn = jest.fn()
})
it('should mark payment success for analytics', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalled()
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(
2,
'pro-subscription-initial-annual-payments-income',
12.45,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
})
it('should mark non-detailed payment success statistics for analytics', async () => {
event.payload = {
userEmail: 'test@test.com',
amount: 12.45,
billingFrequency: 13,
paymentType: 'initial',
subscriptionName: 'PRO_PLAN',
}
await createHandler().handle(event)
expect(statisticsStore.incrementMeasure).toBeCalledTimes(1)
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(1, 'income', 12.45, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,46 +0,0 @@
import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
import { SubscriptionReactivatedEventHandler } from './SubscriptionReactivatedEventHandler'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { Period } from '../Time/Period'
describe('SubscriptionReactivatedEventHandler', () => {
let event: SubscriptionReactivatedEvent
let getUserAnalyticsId: GetUserAnalyticsId
let analyticsStore: AnalyticsStoreInterface
const createHandler = () => new SubscriptionReactivatedEventHandler(analyticsStore, getUserAnalyticsId)
beforeEach(() => {
event = {} as jest.Mocked<SubscriptionReactivatedEvent>
event.createdAt = new Date(1)
event.payload = {
previousSubscriptionId: 1,
currentSubscriptionId: 2,
userEmail: 'test@test.com',
subscriptionName: SubscriptionName.PlusPlan,
subscriptionExpiresAt: 5,
discountCode: 'exit-20',
}
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
analyticsStore.markActivity = jest.fn()
})
it('should mark subscription reactivated activity for analytics', async () => {
await createHandler().handle(event)
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['subscription-reactivated'], 3, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
})
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,12 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsEntity.id,
protocol_version: event.payload.protocolVersion,
})
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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

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'

View File

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

View File

@@ -0,0 +1,58 @@
import { inject } from 'inversify'
import { Request, Response } from 'express'
import { controller, BaseHttpController, httpPost, httpGet, httpDelete } from 'inversify-express-utils'
import TYPES from '../../Bootstrap/Types'
import { HttpServiceInterface } from '../../Service/Http/HttpServiceInterface'
@controller('/v1/authenticators', TYPES.AuthMiddleware)
export class AuthenticatorsController extends BaseHttpController {
constructor(@inject(TYPES.HTTPService) private httpService: HttpServiceInterface) {
super()
}
@httpDelete('/:authenticatorId')
async delete(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
response,
`authenticators/${request.params.authenticatorId}`,
request.body,
)
}
@httpGet('/')
async list(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'authenticators/', request.body)
}
@httpGet('/generate-registration-options')
async generateRegistrationOptions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
response,
'authenticators/generate-registration-options',
request.body,
)
}
@httpGet('/generate-authentication-options')
async generateAuthenticationOptions(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(
request,
response,
'authenticators/generate-authentication-options',
request.body,
)
}
@httpPost('/verify-registration')
async verifyRegistration(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'authenticators/verify-registration', request.body)
}
@httpPost('/verify-authentication')
async verifyAuthentication(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'authenticators/verify-authentication', request.body)
}
}

View File

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

View File

@@ -3,6 +3,152 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.79.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.78.0...@standardnotes/auth-server@1.79.0) (2022-12-29)
### Features
* **auth:** add removing authenticator ([de50d76](https://github.com/standardnotes/server/commit/de50d76800a4240729763b2df11c4a1718951670))
# [1.78.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.77.1...@standardnotes/auth-server@1.78.0) (2022-12-29)
### Features
* **auth:** add listing authenticators ([01837ea](https://github.com/standardnotes/server/commit/01837eaea9b1f219e7ad3be4d28cd0df099fe423))
## [1.77.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.77.0...@standardnotes/auth-server@1.77.1) (2022-12-29)
### Bug Fixes
* **auth:** specs for verifying authenticator authentication response ([5455972](https://github.com/standardnotes/server/commit/5455972be2c62d7862c351b1328beacf4bd5c3da))
# [1.77.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.76.0...@standardnotes/auth-server@1.77.0) (2022-12-29)
### Features
* **auth:** add http endpoints for authenticators ([b6fda90](https://github.com/standardnotes/server/commit/b6fda901ef66a3e66541bd1e3f041b8268a1c3f5))
# [1.76.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.75.0...@standardnotes/auth-server@1.76.0) (2022-12-29)
### Features
* **auth:** add verifying authenticator authentication response ([64525a6](https://github.com/standardnotes/server/commit/64525a65f2e1677f942868903f318d6700c34c74))
# [1.75.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.74.1...@standardnotes/auth-server@1.75.0) (2022-12-29)
### Features
* **auth:** add generating authenticator authentication options ([8c7c1e4](https://github.com/standardnotes/server/commit/8c7c1e4745647004f3dc361ec374014390952486))
## [1.74.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.74.0...@standardnotes/auth-server@1.74.1) (2022-12-28)
### Bug Fixes
* **auth:** migrations to not include unique index for credentials id ([384dfc8](https://github.com/standardnotes/server/commit/384dfc8da4b1b640964fa6da207a67fcd68dc7ec))
# [1.74.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.73.1...@standardnotes/auth-server@1.74.0) (2022-12-28)
### Features
* **auth:** add verifying authenticator registration response ([f5683cf](https://github.com/standardnotes/server/commit/f5683cfd9494db8e25010e9c4ef5fd4d8fcd6bc7))
## [1.73.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.73.0...@standardnotes/auth-server@1.73.1) (2022-12-28)
### Bug Fixes
* **auth:** temporarily remove credential id index due to mysql 5.6 limitations ([a5e7132](https://github.com/standardnotes/server/commit/a5e7132d3c4b74ed13877d7d437062c509201874))
# [1.73.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.72.0...@standardnotes/auth-server@1.73.0) (2022-12-28)
### Features
* **auth:** add generating authencator registration options ([51ad06b](https://github.com/standardnotes/server/commit/51ad06b303d7dc994920818872fdf8bd37fc445c))
# [1.72.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.71.1...@standardnotes/auth-server@1.72.0) (2022-12-28)
### Features
* **auth:** add authenticator challenges model ([fa9bf0b](https://github.com/standardnotes/server/commit/fa9bf0b448acb3f19ab44c4b431ce367dab37b76))
## [1.71.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.71.0...@standardnotes/auth-server@1.71.1) (2022-12-28)
### Bug Fixes
* **auth:** credential id field type ([98e3d18](https://github.com/standardnotes/server/commit/98e3d1833530dcd9e3e34a4c4a6b14a2a01afea1))
# [1.71.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.9...@standardnotes/auth-server@1.71.0) (2022-12-28)
### Features
* **auth:** add authenticators model ([1e69a13](https://github.com/standardnotes/server/commit/1e69a13a97c4d9022aa96397cce1b349d3cede89))
## [1.70.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.8...@standardnotes/auth-server@1.70.9) (2022-12-28)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.70.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.7...@standardnotes/auth-server@1.70.8) (2022-12-20)
### Bug Fixes
* **auth:** move tracing sessions to session creation instead of cross service token creation ([5255cfb](https://github.com/standardnotes/server/commit/5255cfbb257cc9e6ac437fe0c5b28d938e3e599b))
## [1.70.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.6...@standardnotes/auth-server@1.70.7) (2022-12-20)
### Bug Fixes
* **auth:** change severity on tracing session errors - most probably hazardous reads ([cf0b918](https://github.com/standardnotes/server/commit/cf0b91891370e1c1799ad80c10ee9f6b98087a94))
## [1.70.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.5...@standardnotes/auth-server@1.70.6) (2022-12-20)
### Bug Fixes
* **auth:** query for session traces ([14eb775](https://github.com/standardnotes/server/commit/14eb775749bfa9972dc3c07049505f3d15f0b556))
## [1.70.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.4...@standardnotes/auth-server@1.70.5) (2022-12-20)
### Bug Fixes
* **auth:** add session traces index ([b9e1e47](https://github.com/standardnotes/server/commit/b9e1e4787129f00fab8f98cb721141f2e7d75600))
## [1.70.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.3...@standardnotes/auth-server@1.70.4) (2022-12-20)
### Bug Fixes
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
## [1.70.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.2...@standardnotes/auth-server@1.70.3) (2022-12-20)
### Bug Fixes
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
## [1.70.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.1...@standardnotes/auth-server@1.70.2) (2022-12-20)
### Bug Fixes
* **auth:** docker command ([85d2f42](https://github.com/standardnotes/server/commit/85d2f42f473110e8dfca975bfecc7a56823bdef4))
## [1.70.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.0...@standardnotes/auth-server@1.70.1) (2022-12-20)
### Bug Fixes
* **auth:** saving subscription plan name in session traces ([3064d03](https://github.com/standardnotes/server/commit/3064d03aa9a2ac9ca3acfff30480ea8629faeb14))
# [1.70.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.1...@standardnotes/auth-server@1.70.0) (2022-12-19)
### Features
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
## [1.69.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.0...@standardnotes/auth-server@1.69.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.69.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.68.0...@standardnotes/auth-server@1.69.0) (2022-12-19)
### Features
* **auth:** add session traces cleanup procedure ([147d8fd](https://github.com/standardnotes/server/commit/147d8fd9af89d2b97cd68eefac36e53d14d511bf))
# [1.68.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.3...@standardnotes/auth-server@1.68.0) (2022-12-19)
### Features

View File

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

View File

@@ -21,6 +21,7 @@ import '../src/Controller/ListedController'
import '../src/Controller/SubscriptionSettingsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressAuthenticatorsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressSubscriptionInvitesController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressUserRequestsController'
import '../src/Infra/InversifyExpressUtils/InversifyExpressWebSocketsController'

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addAuthenticators1672223738686 implements MigrationInterface {
name = 'addAuthenticators1672223738686'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `authenticators` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `credential_id` varbinary(1024) NOT NULL, `credential_public_key` blob NOT NULL, `counter` bigint NOT NULL, `credential_device_type` varchar(32) NOT NULL, `credential_backed_up` tinyint NOT NULL, `transports` varchar(255) NULL, `created_at` bigint NOT NULL, `updated_at` bigint NOT NULL, PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TABLE `authentticators`')
}
}

View File

@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addAuthenticatorChallenges1672227471677 implements MigrationInterface {
name = 'addAuthenticatorChallenges1672227471677'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'CREATE TABLE `authenticator_challenges` (`uuid` varchar(36) NOT NULL, `user_uuid` varchar(36) NOT NULL, `challenge` varchar(255) NOT NULL, `created_at` bigint NOT NULL, INDEX `user_uuid_and_challenge` (`user_uuid`, `challenge`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `user_uuid_and_challenge` ON `authenticator_challenges`')
await queryRunner.query('DROP TABLE `authenticator_challenges`')
}
}

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class fixAuthenticatorDataTypes1672232035280 implements MigrationInterface {
name = 'fixAuthenticatorDataTypes1672232035280'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `created_at`')
await queryRunner.query('ALTER TABLE `authenticators` ADD `created_at` datetime NOT NULL')
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `updated_at`')
await queryRunner.query('ALTER TABLE `authenticators` ADD `updated_at` datetime NOT NULL')
await queryRunner.query('ALTER TABLE `authenticator_challenges` DROP COLUMN `created_at`')
await queryRunner.query('ALTER TABLE `authenticator_challenges` ADD `created_at` datetime NOT NULL')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `authenticator_challenges` DROP COLUMN `created_at`')
await queryRunner.query('ALTER TABLE `authenticator_challenges` ADD `created_at` bigint NOT NULL')
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `updated_at`')
await queryRunner.query('ALTER TABLE `authenticators` ADD `updated_at` bigint NOT NULL')
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `created_at`')
await queryRunner.query('ALTER TABLE `authenticators` ADD `created_at` bigint NOT NULL')
}
}

View File

@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addUniqueIndexOnChallenges1672299743840 implements MigrationInterface {
name = 'addUniqueIndexOnChallenges1672299743840'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `user_uuid_and_challenge` ON `authenticator_challenges`')
await queryRunner.query('CREATE UNIQUE INDEX `unique_user_uuid` ON `authenticator_challenges` (`user_uuid`)')
await queryRunner.query(
'CREATE INDEX `user_uuid_and_challenge` ON `authenticator_challenges` (`user_uuid`, `challenge`)',
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP INDEX `user_uuid_and_challenge` ON `authenticator_challenges`')
await queryRunner.query('DROP INDEX `unique_user_uuid` ON `authenticator_challenges`')
await queryRunner.query(
'CREATE INDEX `user_uuid_and_challenge` ON `authenticator_challenges` (`user_uuid`, `challenge`)',
)
}
}

View File

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

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class addAuthenticatorName1672317378817 implements MigrationInterface {
name = 'addAuthenticatorName1672317378817'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `authenticators` ADD `name` varchar(255) NOT NULL')
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE `authenticators` DROP COLUMN `name`')
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.68.0",
"version": "1.79.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -19,6 +19,8 @@
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
"start": "yarn node dist/bin/server.js",
"worker": "yarn node dist/bin/worker.js",
"cleanup": "yarn node dist/bin/cleanup.js",
"stats": "yarn node dist/bin/stats.js",
"daily-backup:email": "yarn node dist/bin/backup.js email daily",
"user-email-backup": "yarn node dist/bin/user_email_backup.js",
"daily-backup:dropbox": "yarn node dist/bin/backup.js dropbox daily",
@@ -31,9 +33,11 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.27.0",
"@sentry/node": "^7.28.1",
"@sentry/profiling-node": "^0.0.12",
"@sentry/tracing": "^7.27.0",
"@sentry/tracing": "^7.28.1",
"@simplewebauthn/server": "^6.2.2",
"@simplewebauthn/typescript-types": "^6.3.0-alpha.1",
"@standardnotes/api": "^1.19.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",

View File

@@ -201,6 +201,27 @@ import { SessionTracePersistenceMapper } from '../Mapping/SessionTracePersistenc
import { SessionTrace } from '../Domain/Session/SessionTrace'
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
import { TraceSession } from '../Domain/UseCase/TraceSession/TraceSession'
import { CleanupSessionTraces } from '../Domain/UseCase/CleanupSessionTraces/CleanupSessionTraces'
import { PersistStatistics } from '../Domain/UseCase/PersistStatistics/PersistStatistics'
import { TypeORMAuthenticator } from '../Infra/TypeORM/TypeORMAuthenticator'
import { Authenticator } from '../Domain/Authenticator/Authenticator'
import { AuthenticatorPersistenceMapper } from '../Mapping/AuthenticatorPersistenceMapper'
import { AuthenticatorChallenge } from '../Domain/Authenticator/AuthenticatorChallenge'
import { TypeORMAuthenticatorChallenge } from '../Infra/TypeORM/TypeORMAuthenticatorChallenge'
import { AuthenticatorChallengePersistenceMapper } from '../Mapping/AuthenticatorChallengePersistenceMapper'
import { AuthenticatorRepositoryInterface } from '../Domain/Authenticator/AuthenticatorRepositoryInterface'
import { MySQLAuthenticatorRepository } from '../Infra/MySQL/MySQLAuthenticatorRepository'
import { AuthenticatorChallengeRepositoryInterface } from '../Domain/Authenticator/AuthenticatorChallengeRepositoryInterface'
import { MySQLAuthenticatorChallengeRepository } from '../Infra/MySQL/MySQLAuthenticatorChallengeRepository'
import { GenerateAuthenticatorRegistrationOptions } from '../Domain/UseCase/GenerateAuthenticatorRegistrationOptions/GenerateAuthenticatorRegistrationOptions'
import { VerifyAuthenticatorRegistrationResponse } from '../Domain/UseCase/VerifyAuthenticatorRegistrationResponse/VerifyAuthenticatorRegistrationResponse'
import { GenerateAuthenticatorAuthenticationOptions } from '../Domain/UseCase/GenerateAuthenticatorAuthenticationOptions/GenerateAuthenticatorAuthenticationOptions'
import { VerifyAuthenticatorAuthenticationResponse } from '../Domain/UseCase/VerifyAuthenticatorAuthenticationResponse/VerifyAuthenticatorAuthenticationResponse'
import { AuthenticatorsController } from '../Controller/AuthenticatorsController'
import { ListAuthenticators } from '../Domain/UseCase/ListAuthenticators/ListAuthenticators'
import { AuthenticatorHttpProjection } from '../Infra/Http/Projection/AuthenticatorHttpProjection'
import { AuthenticatorHttpMapper } from '../Mapping/AuthenticatorHttpMapper'
import { DeleteAuthenticator } from '../Domain/UseCase/DeleteAuthenticator/DeleteAuthenticator'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -278,11 +299,17 @@ export class ContainerConfigLoader {
container
.bind<MapperInterface<SessionTrace, TypeORMSessionTrace>>(TYPES.SessionTracePersistenceMapper)
.toConstantValue(new SessionTracePersistenceMapper())
// Controller
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
container.bind<UserRequestsController>(TYPES.UserRequestsController).to(UserRequestsController)
container
.bind<MapperInterface<Authenticator, TypeORMAuthenticator>>(TYPES.AuthenticatorPersistenceMapper)
.toConstantValue(new AuthenticatorPersistenceMapper())
container
.bind<MapperInterface<Authenticator, AuthenticatorHttpProjection>>(TYPES.AuthenticatorHttpMapper)
.toConstantValue(new AuthenticatorHttpMapper())
container
.bind<MapperInterface<AuthenticatorChallenge, TypeORMAuthenticatorChallenge>>(
TYPES.AuthenticatorChallengePersistenceMapper,
)
.toConstantValue(new AuthenticatorChallengePersistenceMapper())
// ORM
container
@@ -314,6 +341,12 @@ export class ContainerConfigLoader {
container
.bind<Repository<TypeORMSessionTrace>>(TYPES.ORMSessionTraceRepository)
.toConstantValue(AppDataSource.getRepository(TypeORMSessionTrace))
container
.bind<Repository<TypeORMAuthenticator>>(TYPES.ORMAuthenticatorRepository)
.toConstantValue(AppDataSource.getRepository(TypeORMAuthenticator))
container
.bind<Repository<TypeORMAuthenticatorChallenge>>(TYPES.ORMAuthenticatorChallengeRepository)
.toConstantValue(AppDataSource.getRepository(TypeORMAuthenticatorChallenge))
// Repositories
container.bind<SessionRepositoryInterface>(TYPES.SessionRepository).to(MySQLSessionRepository)
@@ -353,6 +386,22 @@ export class ContainerConfigLoader {
container.get(TYPES.SessionTracePersistenceMapper),
),
)
container
.bind<AuthenticatorRepositoryInterface>(TYPES.AuthenticatorRepository)
.toConstantValue(
new MySQLAuthenticatorRepository(
container.get(TYPES.ORMAuthenticatorRepository),
container.get(TYPES.AuthenticatorPersistenceMapper),
),
)
container
.bind<AuthenticatorChallengeRepositoryInterface>(TYPES.AuthenticatorChallengeRepository)
.toConstantValue(
new MySQLAuthenticatorChallengeRepository(
container.get(TYPES.ORMAuthenticatorChallengeRepository),
container.get(TYPES.AuthenticatorChallengePersistenceMapper),
),
)
// Middleware
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware)
@@ -499,6 +548,58 @@ export class ContainerConfigLoader {
container.get(TYPES.SESSION_TRACE_DAYS_TTL),
),
)
container
.bind<PersistStatistics>(TYPES.PersistStatistics)
.toConstantValue(
new PersistStatistics(
container.get(TYPES.SessionTraceRepository),
container.get(TYPES.DomainEventPublisher),
container.get(TYPES.DomainEventFactory),
container.get(TYPES.Timer),
),
)
container
.bind<GenerateAuthenticatorRegistrationOptions>(TYPES.GenerateAuthenticatorRegistrationOptions)
.toConstantValue(
new GenerateAuthenticatorRegistrationOptions(
container.get(TYPES.AuthenticatorRepository),
container.get(TYPES.AuthenticatorChallengeRepository),
),
)
container
.bind<VerifyAuthenticatorRegistrationResponse>(TYPES.VerifyAuthenticatorRegistrationResponse)
.toConstantValue(
new VerifyAuthenticatorRegistrationResponse(
container.get(TYPES.AuthenticatorRepository),
container.get(TYPES.AuthenticatorChallengeRepository),
),
)
container
.bind<GenerateAuthenticatorAuthenticationOptions>(TYPES.GenerateAuthenticatorAuthenticationOptions)
.toConstantValue(
new GenerateAuthenticatorAuthenticationOptions(
container.get(TYPES.AuthenticatorRepository),
container.get(TYPES.AuthenticatorChallengeRepository),
),
)
container
.bind<VerifyAuthenticatorAuthenticationResponse>(TYPES.VerifyAuthenticatorAuthenticationResponse)
.toConstantValue(
new VerifyAuthenticatorAuthenticationResponse(
container.get(TYPES.AuthenticatorRepository),
container.get(TYPES.AuthenticatorChallengeRepository),
),
)
container
.bind<ListAuthenticators>(TYPES.ListAuthenticators)
.toConstantValue(new ListAuthenticators(container.get(TYPES.AuthenticatorRepository)))
container
.bind<DeleteAuthenticator>(TYPES.DeleteAuthenticator)
.toConstantValue(new DeleteAuthenticator(container.get(TYPES.AuthenticatorRepository)))
container
.bind<CleanupSessionTraces>(TYPES.CleanupSessionTraces)
.toConstantValue(new CleanupSessionTraces(container.get(TYPES.SessionTraceRepository)))
container.bind<AuthenticateUser>(TYPES.AuthenticateUser).to(AuthenticateUser)
container.bind<AuthenticateRequest>(TYPES.AuthenticateRequest).to(AuthenticateRequest)
container.bind<RefreshSessionToken>(TYPES.RefreshSessionToken).to(RefreshSessionToken)
@@ -549,6 +650,24 @@ export class ContainerConfigLoader {
container.bind<CreateCrossServiceToken>(TYPES.CreateCrossServiceToken).to(CreateCrossServiceToken)
container.bind<ProcessUserRequest>(TYPES.ProcessUserRequest).to(ProcessUserRequest)
// Controller
container.bind<AuthController>(TYPES.AuthController).to(AuthController)
container
.bind<AuthenticatorsController>(TYPES.AuthenticatorsController)
.toConstantValue(
new AuthenticatorsController(
container.get(TYPES.GenerateAuthenticatorRegistrationOptions),
container.get(TYPES.VerifyAuthenticatorRegistrationResponse),
container.get(TYPES.GenerateAuthenticatorAuthenticationOptions),
container.get(TYPES.VerifyAuthenticatorAuthenticationResponse),
container.get(TYPES.ListAuthenticators),
container.get(TYPES.DeleteAuthenticator),
container.get(TYPES.AuthenticatorHttpMapper),
),
)
container.bind<SubscriptionInvitesController>(TYPES.SubscriptionInvitesController).to(SubscriptionInvitesController)
container.bind<UserRequestsController>(TYPES.UserRequestsController).to(UserRequestsController)
// Handlers
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
container

View File

@@ -10,6 +10,8 @@ import { SharedSubscriptionInvitation } from '../Domain/SharedSubscription/Share
import { OfflineUserSubscription } from '../Domain/Subscription/OfflineUserSubscription'
import { UserSubscription } from '../Domain/Subscription/UserSubscription'
import { User } from '../Domain/User/User'
import { TypeORMAuthenticator } from '../Infra/TypeORM/TypeORMAuthenticator'
import { TypeORMAuthenticatorChallenge } from '../Infra/TypeORM/TypeORMAuthenticatorChallenge'
import { TypeORMSessionTrace } from '../Infra/TypeORM/TypeORMSessionTrace'
import { Env } from './Env'
@@ -58,6 +60,8 @@ export const AppDataSource = new DataSource({
SharedSubscriptionInvitation,
SubscriptionSetting,
TypeORMSessionTrace,
TypeORMAuthenticator,
TypeORMAuthenticatorChallenge,
],
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
migrationsRun: true,

View File

@@ -5,8 +5,12 @@ const TYPES = {
SQS: Symbol.for('SQS'),
// Mapping
SessionTracePersistenceMapper: Symbol.for('SessionTracePersistenceMapper'),
AuthenticatorChallengePersistenceMapper: Symbol.for('AuthenticatorChallengePersistenceMapper'),
AuthenticatorPersistenceMapper: Symbol.for('AuthenticatorPersistenceMapper'),
AuthenticatorHttpMapper: Symbol.for('AuthenticatorHttpMapper'),
// Controller
AuthController: Symbol.for('AuthController'),
AuthenticatorsController: Symbol.for('AuthenticatorsController'),
SubscriptionInvitesController: Symbol.for('SubscriptionInvitesController'),
UserRequestsController: Symbol.for('UserRequestsController'),
// Repositories
@@ -26,6 +30,8 @@ const TYPES = {
SharedSubscriptionInvitationRepository: Symbol.for('SharedSubscriptionInvitationRepository'),
PKCERepository: Symbol.for('PKCERepository'),
SessionTraceRepository: Symbol.for('SessionTraceRepository'),
AuthenticatorRepository: Symbol.for('AuthenticatorRepository'),
AuthenticatorChallengeRepository: Symbol.for('AuthenticatorChallengeRepository'),
// ORM
ORMOfflineSettingRepository: Symbol.for('ORMOfflineSettingRepository'),
ORMOfflineUserSubscriptionRepository: Symbol.for('ORMOfflineUserSubscriptionRepository'),
@@ -38,6 +44,8 @@ const TYPES = {
ORMUserRepository: Symbol.for('ORMUserRepository'),
ORMUserSubscriptionRepository: Symbol.for('ORMUserSubscriptionRepository'),
ORMSessionTraceRepository: Symbol.for('ORMSessionTraceRepository'),
ORMAuthenticatorRepository: Symbol.for('ORMAuthenticatorRepository'),
ORMAuthenticatorChallengeRepository: Symbol.for('ORMAuthenticatorChallengeRepository'),
// Middleware
AuthMiddleware: Symbol.for('AuthMiddleware'),
ApiGatewayAuthMiddleware: Symbol.for('ApiGatewayAuthMiddleware'),
@@ -125,6 +133,14 @@ const TYPES = {
CreateCrossServiceToken: Symbol.for('CreateCrossServiceToken'),
ProcessUserRequest: Symbol.for('ProcessUserRequest'),
TraceSession: Symbol.for('TraceSession'),
CleanupSessionTraces: Symbol.for('CleanupSessionTraces'),
PersistStatistics: Symbol.for('PersistStatistics'),
GenerateAuthenticatorRegistrationOptions: Symbol.for('GenerateAuthenticatorRegistrationOptions'),
VerifyAuthenticatorRegistrationResponse: Symbol.for('VerifyAuthenticatorRegistrationResponse'),
GenerateAuthenticatorAuthenticationOptions: Symbol.for('GenerateAuthenticatorAuthenticationOptions'),
VerifyAuthenticatorAuthenticationResponse: Symbol.for('VerifyAuthenticatorAuthenticationResponse'),
ListAuthenticators: Symbol.for('ListAuthenticators'),
DeleteAuthenticator: Symbol.for('DeleteAuthenticator'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),

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