mirror of
https://github.com/standardnotes/server
synced 2026-01-18 08:04:28 -05:00
Compare commits
325 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff02ce0747 | ||
|
|
a6056600eb | ||
|
|
24c94326d5 | ||
|
|
48c0cb5e62 | ||
|
|
9968efe1b2 | ||
|
|
6368342149 | ||
|
|
b5f73db210 | ||
|
|
22d6a02d04 | ||
|
|
4e0bcfcccf | ||
|
|
104313c15d | ||
|
|
814289af46 | ||
|
|
3096cd98d5 | ||
|
|
45dfefbc7a | ||
|
|
20d92149a8 | ||
|
|
9c01fffca5 | ||
|
|
61c1cfff4b | ||
|
|
7e74261f62 | ||
|
|
32601f34f1 | ||
|
|
aef69a1a96 | ||
|
|
130f90bdb6 | ||
|
|
851c7de87f | ||
|
|
118156c62d | ||
|
|
cdad3143c9 | ||
|
|
00fe32136e | ||
|
|
52f56eeb68 | ||
|
|
b595264e31 | ||
|
|
bf04262170 | ||
|
|
fd589922bb | ||
|
|
fb7029f5c1 | ||
|
|
cc4b4f9bf8 | ||
|
|
b048d6d7e3 | ||
|
|
cffc1f442f | ||
|
|
91fe710741 | ||
|
|
5a1eb9fdac | ||
|
|
a64ef6e750 | ||
|
|
47d2012b3d | ||
|
|
2c99cd2e21 | ||
|
|
435cd2f66a | ||
|
|
372b12dfc2 | ||
|
|
3a12f5c1c4 | ||
|
|
781de224b6 | ||
|
|
eff09454c3 | ||
|
|
473feba6a8 | ||
|
|
e9f0704fb0 | ||
|
|
8c99469d88 | ||
|
|
8ec1311dfc | ||
|
|
e48cca6b45 | ||
|
|
d660721f95 | ||
|
|
c52bb93d79 | ||
|
|
ffb6bfd0c9 | ||
|
|
6e0855f9b3 | ||
|
|
ec9e9ec387 | ||
|
|
fa75aa40f0 | ||
|
|
b865953c22 | ||
|
|
2542cf6f9a | ||
|
|
cb9499b87f | ||
|
|
c351f01f67 | ||
|
|
c87561fca7 | ||
|
|
a363c143fa | ||
|
|
fb81d2b926 | ||
|
|
05b1b8f079 | ||
|
|
7848dc06d4 | ||
|
|
3a005719b7 | ||
|
|
6928988f78 | ||
|
|
a521894d7c | ||
|
|
b7fb1d9c08 | ||
|
|
5f67e45911 | ||
|
|
fddf9fccbd | ||
|
|
2bedbd7bd2 | ||
|
|
02f3c85796 | ||
|
|
3b5bd6a47f | ||
|
|
06fd404d44 | ||
|
|
d931c52508 | ||
|
|
800fe9e4c8 | ||
|
|
8b3d78678f | ||
|
|
2351cd3ad6 | ||
|
|
dd86c5bcdf | ||
|
|
d0c00e306e | ||
|
|
6cd68ddd6a | ||
|
|
02639cddb2 | ||
|
|
0f67aa4058 | ||
|
|
78c3403d5f | ||
|
|
fc8f8c574d | ||
|
|
3972ee580d | ||
|
|
b0a994d5be | ||
|
|
80df28a0c4 | ||
|
|
1c6c6a9296 | ||
|
|
7bb698e442 | ||
|
|
784728cd54 | ||
|
|
4b883b68de | ||
|
|
dec2cc2aaf | ||
|
|
b4e8971ad2 | ||
|
|
84e436265e | ||
|
|
ac8a69f8d4 | ||
|
|
b912e050ea | ||
|
|
284561d093 | ||
|
|
efc355982c | ||
|
|
8907879a19 | ||
|
|
86f6057207 | ||
|
|
4c92698c73 | ||
|
|
8407c3b649 | ||
|
|
ed8f82617d | ||
|
|
31d040d1b6 | ||
|
|
25a6796e63 | ||
|
|
ff091918aa | ||
|
|
91b76edce1 | ||
|
|
5ae5c83bf5 | ||
|
|
9d90f276de | ||
|
|
245f091e22 | ||
|
|
ae2f8f086b | ||
|
|
5e5eb7f937 | ||
|
|
748630e1f1 | ||
|
|
43064c8c55 | ||
|
|
4559a3047c | ||
|
|
de8064ee5c | ||
|
|
48c8dba342 | ||
|
|
31a515b2f1 | ||
|
|
294f56e189 | ||
|
|
70596a0aac | ||
|
|
74bc79116b | ||
|
|
e6bd50ae77 | ||
|
|
308662550f | ||
|
|
d94a7e7157 | ||
|
|
630b264754 | ||
|
|
5f2be44b85 | ||
|
|
f68ece68af | ||
|
|
70c829a2c9 | ||
|
|
e3b6ac4874 | ||
|
|
a762d5a22c | ||
|
|
3686a26019 | ||
|
|
80daec748d | ||
|
|
94359f1299 | ||
|
|
59dda1bb99 | ||
|
|
806a732cbc | ||
|
|
7816be7ba7 | ||
|
|
5f3bd5137f | ||
|
|
6c9fc5fb86 | ||
|
|
f7e0b68643 | ||
|
|
b283bbaca9 | ||
|
|
92ba759b1c | ||
|
|
0acc9d8d68 | ||
|
|
daa7a9ff61 | ||
|
|
455f35e0c1 | ||
|
|
1fa655b56e | ||
|
|
e553222b4b | ||
|
|
f1b6f48926 | ||
|
|
14ab1cae69 | ||
|
|
5f9cf90b16 | ||
|
|
97b367d4ee | ||
|
|
47119fb346 | ||
|
|
d77eb7f5f1 | ||
|
|
1b0a2bb34c | ||
|
|
a363039fa1 | ||
|
|
32c740b58e | ||
|
|
822ee890af | ||
|
|
b0406dd8aa | ||
|
|
8d152ddfcb | ||
|
|
1a16d2e4f4 | ||
|
|
1ca8531305 | ||
|
|
6190e7d092 | ||
|
|
a6542dd638 | ||
|
|
840777a851 | ||
|
|
5c9dff38c9 | ||
|
|
abfbacb8c2 | ||
|
|
03afdbf431 | ||
|
|
507d43b328 | ||
|
|
be214c0599 | ||
|
|
91f36c3a3f | ||
|
|
f60c15ed2e | ||
|
|
1ec072373d | ||
|
|
a7d039082e | ||
|
|
d5c06bfa58 | ||
|
|
c8f3a0ce7b | ||
|
|
edbedc181b | ||
|
|
94afa34780 | ||
|
|
74dd0ab6cd | ||
|
|
6c43a331d0 | ||
|
|
67835ba0c0 | ||
|
|
fe1b2a0e07 | ||
|
|
2e82be47ed | ||
|
|
15dfd6dcba | ||
|
|
dfd38943b0 | ||
|
|
500756d582 | ||
|
|
f855f541d8 | ||
|
|
590ec6643d | ||
|
|
b9efd35b50 | ||
|
|
3be1bfe58a | ||
|
|
bfbd2de778 | ||
|
|
50f7ae338a | ||
|
|
280fdc89c1 | ||
|
|
0f94e2ad0c | ||
|
|
d0036600e9 | ||
|
|
f766fefbf0 | ||
|
|
2178ed2a31 | ||
|
|
79511aea5f | ||
|
|
19bb77273b | ||
|
|
7ca377f1b8 | ||
|
|
6f5e9b7b5a | ||
|
|
f03a58079d | ||
|
|
8715fe1822 | ||
|
|
e10cb9adaf | ||
|
|
3030832711 | ||
|
|
7c638ef28a | ||
|
|
447a4b5e04 | ||
|
|
dd1ba6e302 | ||
|
|
08556b751f | ||
|
|
11d2190310 | ||
|
|
46519bb710 | ||
|
|
7b9290382d | ||
|
|
85e55cf0e4 | ||
|
|
7016854b7f | ||
|
|
01a4151763 | ||
|
|
311f758cd8 | ||
|
|
3bba36742a | ||
|
|
ea52ba51ca | ||
|
|
7e404ae71a | ||
|
|
3ad95afa84 | ||
|
|
1a13861647 | ||
|
|
6d84c819c0 | ||
|
|
36ec39d2fb | ||
|
|
eaafc12c8a | ||
|
|
a03c5bceea | ||
|
|
53c51fd204 | ||
|
|
9b593f2a6b | ||
|
|
363609cb1b | ||
|
|
68e6d30093 | ||
|
|
c53a40ef8d | ||
|
|
3c2ac05c60 | ||
|
|
bffab433f6 | ||
|
|
200b6ce01f | ||
|
|
0d29dc1012 | ||
|
|
b92c4ae650 | ||
|
|
e15d1e52bd | ||
|
|
ce3e259bde | ||
|
|
87361f90b1 | ||
|
|
81be06598c | ||
|
|
9492da6789 | ||
|
|
fce47a0a37 | ||
|
|
92ba682198 | ||
|
|
8df0482eb4 | ||
|
|
37a5cb347d | ||
|
|
77e50655f6 | ||
|
|
eacd2abc00 | ||
|
|
7393954ff6 | ||
|
|
68744379a6 | ||
|
|
90aef905af | ||
|
|
c7cbc8966e | ||
|
|
89502bed63 | ||
|
|
4952b48db6 | ||
|
|
52a257abb1 | ||
|
|
7480fb089b | ||
|
|
0f65c051ab | ||
|
|
7b62c7a967 | ||
|
|
5c3db2cb29 | ||
|
|
7008cbd363 | ||
|
|
cdb7fcf831 | ||
|
|
628aafdd42 | ||
|
|
9d3ef24ba9 | ||
|
|
4189f11fd7 | ||
|
|
5ea9941519 | ||
|
|
7a64494d07 | ||
|
|
4928685198 | ||
|
|
0103233d4a | ||
|
|
ee7075fe60 | ||
|
|
49feadd32a | ||
|
|
45758bf554 | ||
|
|
535d566a94 | ||
|
|
ff1d5db12c | ||
|
|
77a06b2fe7 | ||
|
|
6359030030 | ||
|
|
006f1fccec | ||
|
|
c0f5817d47 | ||
|
|
3da952fa52 | ||
|
|
f1834d58d2 | ||
|
|
b0cde4ab75 | ||
|
|
197c9914ca | ||
|
|
d7ef6898be | ||
|
|
2aa57f1f0d | ||
|
|
dcc0e38707 | ||
|
|
037fb2398a | ||
|
|
182512d07c | ||
|
|
a3be4b063d | ||
|
|
a97be4c342 | ||
|
|
5902cbb621 | ||
|
|
afc26d42ca | ||
|
|
51b12d05d4 | ||
|
|
3fe7b4ae24 | ||
|
|
2720a7c827 | ||
|
|
8d89b8ef12 | ||
|
|
5383e0cf52 | ||
|
|
7b05bf8991 | ||
|
|
b4c5b5a84e | ||
|
|
e115523acd | ||
|
|
35611fbc07 | ||
|
|
034aa38153 | ||
|
|
795728ab31 | ||
|
|
262d295121 | ||
|
|
4e5ac0a47b | ||
|
|
51b8cbdab2 | ||
|
|
f315b1ac5c | ||
|
|
2feaa8d956 | ||
|
|
5329f2a2fb | ||
|
|
5d9d2d0c8d | ||
|
|
34e11fd5b0 | ||
|
|
dc1f19ed04 | ||
|
|
ff7c52a05e | ||
|
|
d5684326b1 | ||
|
|
017c55d190 | ||
|
|
2504887e8d | ||
|
|
805e63379c | ||
|
|
dcb20e6ea6 | ||
|
|
786b94380b | ||
|
|
460d6a8d0f | ||
|
|
0dbc929c8e | ||
|
|
0c5305acf6 | ||
|
|
34139efafb | ||
|
|
eb53c3896f | ||
|
|
2af4c6fb55 | ||
|
|
d66f784538 | ||
|
|
f127241857 | ||
|
|
5b0d9dd394 | ||
|
|
ee29d18484 | ||
|
|
2255f856f9 | ||
|
|
f2415527f0 | ||
|
|
59eb70ce62 |
39
.github/workflows/analytics.yml
vendored
Normal file
39
.github/workflows/analytics.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Analytics Server
|
||||
|
||||
concurrency:
|
||||
group: analytics
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*standardnotes/analytics*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call_server_application_workflow:
|
||||
name: Server Application
|
||||
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
|
||||
with:
|
||||
service_name: analytics
|
||||
workspace_name: "@standardnotes/analytics"
|
||||
e2e_tag_parameter_name: analytics_image_tag
|
||||
deploy_web: false
|
||||
package_path: packages/analytics
|
||||
secrets: inherit
|
||||
|
||||
newrelic:
|
||||
needs: call_server_application_workflow
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Create New Relic deployment marker for Worker
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_ANALYTICS_WORKER_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
46
.github/workflows/revisions.yml
vendored
Normal file
46
.github/workflows/revisions.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Revisions Server
|
||||
|
||||
concurrency:
|
||||
group: revisions_server
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*standardnotes/revisions-server*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
call_server_application_workflow:
|
||||
name: Server Application
|
||||
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
|
||||
with:
|
||||
service_name: revisions
|
||||
workspace_name: "@standardnotes/revisions-server"
|
||||
e2e_tag_parameter_name: revisions_image_tag
|
||||
package_path: packages/revisions
|
||||
secrets: inherit
|
||||
|
||||
newrelic:
|
||||
needs: call_server_application_workflow
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create New Relic deployment marker for Web
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_REVISIONS_WEB_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
- name: Create New Relic deployment marker for Worker
|
||||
uses: newrelic/deployment-marker-action@v1
|
||||
with:
|
||||
accountId: ${{ secrets.NEW_RELIC_ACCOUNT_ID }}
|
||||
apiKey: ${{ secrets.NEW_RELIC_API_KEY }}
|
||||
applicationId: ${{ secrets.NEW_RELIC_APPLICATION_ID_REVISIONS_WORKER_PROD }}
|
||||
revision: "${{ github.sha }}"
|
||||
description: "Automated Deployment via Github Actions"
|
||||
user: "${{ github.actor }}"
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"semi": false
|
||||
}
|
||||
BIN
.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip
vendored
Normal file
BIN
.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip
vendored
Normal file
BIN
.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip
vendored
Normal file
BIN
.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip
vendored
Normal file
BIN
.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip
vendored
Normal file
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.
BIN
.yarn/cache/@standardnotes-utils-npm-1.11.0-afbc24024c-9e7d9c1257.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-utils-npm-1.11.0-afbc24024c-9e7d9c1257.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@types-ioredis-npm-5.0.0-6efa70abfa-439770c9da.zip
vendored
Normal file
BIN
.yarn/cache/@types-ioredis-npm-5.0.0-6efa70abfa-439770c9da.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@types-newrelic-npm-7.0.4-4f0b179b51-b44215b3ab.zip
vendored
Normal file
BIN
.yarn/cache/@types-newrelic-npm-7.0.4-4f0b179b51-b44215b3ab.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@types-node-npm-18.11.9-d21dd6ec05-7b7d90894d.zip
vendored
Normal file
BIN
.yarn/cache/@types-node-npm-18.11.9-d21dd6ec05-7b7d90894d.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/axios-npm-1.1.3-4b63965ac1-2e28acd01c.zip
vendored
Normal file
BIN
.yarn/cache/axios-npm-1.1.3-4b63965ac1-2e28acd01c.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/newrelic-npm-9.6.0-f10080c2de-eb378acde1.zip
vendored
Normal file
BIN
.yarn/cache/newrelic-npm-9.6.0-f10080c2de-eb378acde1.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/proxy-from-env-npm-1.1.0-c13d07f26b-0bba2ef7c8.zip
vendored
Normal file
BIN
.yarn/cache/proxy-from-env-npm-1.1.0-c13d07f26b-0bba2ef7c8.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/shallow-equal-object-npm-1.1.1-a41b289b2e-9e5e0cd10b.zip
vendored
Normal file
BIN
.yarn/cache/shallow-equal-object-npm-1.1.1-a41b289b2e-9e5e0cd10b.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/ua-parser-js-npm-1.0.32-95b0b6a78d-9d320c6742.zip
vendored
Normal file
BIN
.yarn/cache/ua-parser-js-npm-1.0.32-95b0b6a78d-9d320c6742.zip
vendored
Normal file
Binary file not shown.
@@ -326,8 +326,8 @@ ifeq ($(strip $(foreach prefix,$(NO_LOAD),\
|
||||
endif
|
||||
|
||||
quiet_cmd_regen_makefile = ACTION Regenerating $@
|
||||
cmd_regen_makefile = cd $(srcdir); /Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/karolsojko/Library/Caches/node-gyp/16.15.1" "-Dnode_gyp_dir=/Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp" "-Dnode_lib_file=/Users/karolsojko/Library/Caches/node-gyp/16.15.1/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/karolsojko/workspace/server/.yarn/unplugged/@newrelic-native-metrics-npm-9.0.0-590d2e713a/node_modules/@newrelic/native-metrics" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/karolsojko/workspace/server/.yarn/unplugged/@newrelic-native-metrics-npm-9.0.0-590d2e713a/node_modules/@newrelic/native-metrics/build/config.gypi -I/Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/common.gypi "--toplevel-dir=." binding.gyp
|
||||
Makefile: $(srcdir)/binding.gyp $(srcdir)/../../../../node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi $(srcdir)/../../../../../../../../Library/Caches/node-gyp/16.15.1/include/node/common.gypi $(srcdir)/build/config.gypi
|
||||
cmd_regen_makefile = cd $(srcdir); /Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/Users/karolsojko/Library/Caches/node-gyp/18.12.1" "-Dnode_gyp_dir=/Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp" "-Dnode_lib_file=/Users/karolsojko/Library/Caches/node-gyp/18.12.1/<(target_arch)/node.lib" "-Dmodule_root_dir=/Users/karolsojko/workspace/server/.yarn/unplugged/@newrelic-native-metrics-npm-9.0.0-590d2e713a/node_modules/@newrelic/native-metrics" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/Users/karolsojko/workspace/server/.yarn/unplugged/@newrelic-native-metrics-npm-9.0.0-590d2e713a/node_modules/@newrelic/native-metrics/build/config.gypi -I/Users/karolsojko/workspace/server/.yarn/unplugged/node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi -I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/include/node/common.gypi "--toplevel-dir=." binding.gyp
|
||||
Makefile: $(srcdir)/build/config.gypi $(srcdir)/../../../../node-gyp-npm-9.0.0-0eccfca4d1/node_modules/node-gyp/addon.gypi $(srcdir)/../../../../../../../../Library/Caches/node-gyp/18.12.1/include/node/common.gypi $(srcdir)/binding.gyp
|
||||
$(call do_cmd,regen_makefile)
|
||||
|
||||
# "all" is a concatenation of the "all" targets from all the included
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
cmd_Release/native_metrics.node := c++ -bundle -undefined dynamic_lookup -Wl,-search_paths_first -mmacosx-version-min=10.13 -arch x86_64 -L./Release -stdlib=libc++ -o Release/native_metrics.node Release/obj.target/native_metrics/src/native_metrics.o Release/obj.target/native_metrics/src/GCBinder.o Release/obj.target/native_metrics/src/LoopChecker.o
|
||||
@@ -1,69 +0,0 @@
|
||||
cmd_Release/obj.target/native_metrics/src/GCBinder.o := c++ -o Release/obj.target/native_metrics/src/GCBinder.o ../src/GCBinder.cpp '-DNODE_GYP_MODULE_NAME=native_metrics' '-DUSING_UV_SHARED=1' '-DUSING_V8_SHARED=1' '-DV8_DEPRECATION_WARNINGS=1' '-DV8_DEPRECATION_WARNINGS' '-DV8_IMMINENT_DEPRECATION_WARNINGS' '-D_GLIBCXX_USE_CXX11_ABI=1' '-D_DARWIN_USE_64_BIT_INODE=1' '-D_LARGEFILE_SOURCE' '-D_FILE_OFFSET_BITS=64' '-DOPENSSL_NO_PINSHARED' '-DOPENSSL_THREADS' '-DNOMINMAX' '-DBUILDING_NODE_EXTENSION' -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/src -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/config -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/openssl/include -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/uv/include -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/zlib -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/v8/include -I../src -I../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan -O3 -gdwarf-2 -mmacosx-version-min=10.13 -arch x86_64 -Wall -Wendif-labels -W -Wno-unused-parameter -std=gnu++14 -stdlib=libc++ -fno-rtti -fno-exceptions -fno-strict-aliasing -MMD -MF ./Release/.deps/Release/obj.target/native_metrics/src/GCBinder.o.d.raw -c
|
||||
Release/obj.target/native_metrics/src/GCBinder.o: ../src/GCBinder.cpp \
|
||||
../src/GCBinder.hpp \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_version.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/errno.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/version.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/unix.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/threadpool.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/darwin.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/cppgc/common.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8config.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-internal.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-version.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-platform.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_buffer.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_object_wrap.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks_12_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_maybe_43_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters_43_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_new.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_implementation_12_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_persistent_12_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_weak.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_object_wrap.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_private.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_typedarray_contents.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_json.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_scriptorigin.h \
|
||||
../src/Metric.hpp
|
||||
../src/GCBinder.cpp:
|
||||
../src/GCBinder.hpp:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_version.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/errno.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/version.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/unix.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/threadpool.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/darwin.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/cppgc/common.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8config.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-internal.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-version.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-platform.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_buffer.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_object_wrap.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks_12_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_maybe_43_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters_43_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_new.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_implementation_12_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_persistent_12_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_weak.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_object_wrap.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_private.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_typedarray_contents.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_json.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_scriptorigin.h:
|
||||
../src/Metric.hpp:
|
||||
@@ -1,70 +0,0 @@
|
||||
cmd_Release/obj.target/native_metrics/src/LoopChecker.o := c++ -o Release/obj.target/native_metrics/src/LoopChecker.o ../src/LoopChecker.cpp '-DNODE_GYP_MODULE_NAME=native_metrics' '-DUSING_UV_SHARED=1' '-DUSING_V8_SHARED=1' '-DV8_DEPRECATION_WARNINGS=1' '-DV8_DEPRECATION_WARNINGS' '-DV8_IMMINENT_DEPRECATION_WARNINGS' '-D_GLIBCXX_USE_CXX11_ABI=1' '-D_DARWIN_USE_64_BIT_INODE=1' '-D_LARGEFILE_SOURCE' '-D_FILE_OFFSET_BITS=64' '-DOPENSSL_NO_PINSHARED' '-DOPENSSL_THREADS' '-DNOMINMAX' '-DBUILDING_NODE_EXTENSION' -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/src -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/config -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/openssl/include -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/uv/include -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/zlib -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/v8/include -I../src -I../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan -O3 -gdwarf-2 -mmacosx-version-min=10.13 -arch x86_64 -Wall -Wendif-labels -W -Wno-unused-parameter -std=gnu++14 -stdlib=libc++ -fno-rtti -fno-exceptions -fno-strict-aliasing -MMD -MF ./Release/.deps/Release/obj.target/native_metrics/src/LoopChecker.o.d.raw -c
|
||||
Release/obj.target/native_metrics/src/LoopChecker.o: \
|
||||
../src/LoopChecker.cpp \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/errno.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/version.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/unix.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/threadpool.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/darwin.h \
|
||||
../src/LoopChecker.hpp \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_version.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/cppgc/common.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8config.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-internal.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-version.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-platform.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_buffer.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_object_wrap.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks_12_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_maybe_43_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters_43_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_new.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_implementation_12_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_persistent_12_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_weak.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_object_wrap.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_private.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_typedarray_contents.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_json.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_scriptorigin.h \
|
||||
../src/Metric.hpp
|
||||
../src/LoopChecker.cpp:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/errno.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/version.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/unix.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/threadpool.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/darwin.h:
|
||||
../src/LoopChecker.hpp:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_version.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/cppgc/common.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8config.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-internal.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-version.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-platform.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_buffer.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_object_wrap.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks_12_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_maybe_43_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters_43_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_new.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_implementation_12_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_persistent_12_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_weak.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_object_wrap.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_private.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_typedarray_contents.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_json.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_scriptorigin.h:
|
||||
../src/Metric.hpp:
|
||||
@@ -1,70 +0,0 @@
|
||||
cmd_Release/obj.target/native_metrics/src/native_metrics.o := c++ -o Release/obj.target/native_metrics/src/native_metrics.o ../src/native_metrics.cpp '-DNODE_GYP_MODULE_NAME=native_metrics' '-DUSING_UV_SHARED=1' '-DUSING_V8_SHARED=1' '-DV8_DEPRECATION_WARNINGS=1' '-DV8_DEPRECATION_WARNINGS' '-DV8_IMMINENT_DEPRECATION_WARNINGS' '-D_GLIBCXX_USE_CXX11_ABI=1' '-D_DARWIN_USE_64_BIT_INODE=1' '-D_LARGEFILE_SOURCE' '-D_FILE_OFFSET_BITS=64' '-DOPENSSL_NO_PINSHARED' '-DOPENSSL_THREADS' '-DNOMINMAX' '-DBUILDING_NODE_EXTENSION' -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/src -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/config -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/openssl/include -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/uv/include -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/zlib -I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/v8/include -I../src -I../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan -O3 -gdwarf-2 -mmacosx-version-min=10.13 -arch x86_64 -Wall -Wendif-labels -W -Wno-unused-parameter -std=gnu++14 -stdlib=libc++ -fno-rtti -fno-exceptions -fno-strict-aliasing -MMD -MF ./Release/.deps/Release/obj.target/native_metrics/src/native_metrics.o.d.raw -c
|
||||
Release/obj.target/native_metrics/src/native_metrics.o: \
|
||||
../src/native_metrics.cpp \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_version.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/errno.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/version.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/unix.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/threadpool.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/darwin.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/cppgc/common.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8config.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-internal.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-version.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-platform.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_buffer.h \
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_object_wrap.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks_12_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_maybe_43_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters_43_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_new.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_implementation_12_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_persistent_12_inl.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_weak.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_object_wrap.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_private.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_typedarray_contents.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_json.h \
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_scriptorigin.h \
|
||||
../src/GCBinder.hpp ../src/Metric.hpp ../src/LoopChecker.hpp
|
||||
../src/native_metrics.cpp:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_version.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/errno.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/version.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/unix.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/threadpool.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/uv/darwin.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/cppgc/common.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8config.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-internal.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-version.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/v8-platform.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_buffer.h:
|
||||
/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node/node_object_wrap.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_callbacks_12_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_maybe_43_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_converters_43_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_new.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_implementation_12_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_persistent_12_inl.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_weak.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_object_wrap.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_private.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_typedarray_contents.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_json.h:
|
||||
../../../../../nan-npm-2.16.0-cac314a230/node_modules/nan/nan_scriptorigin.h:
|
||||
../src/GCBinder.hpp:
|
||||
../src/Metric.hpp:
|
||||
../src/LoopChecker.hpp:
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -19,293 +19,316 @@
|
||||
"error_on_warn": "false",
|
||||
"force_dynamic_crt": 0,
|
||||
"host_arch": "x64",
|
||||
"icu_data_in": "../../deps/icu-tmp/icudt70l.dat",
|
||||
"icu_data_in": "../../deps/icu-tmp/icudt71l.dat",
|
||||
"icu_endianness": "l",
|
||||
"icu_gyp_path": "tools/icu/icu-generic.gyp",
|
||||
"icu_path": "deps/icu-small",
|
||||
"icu_small": "false",
|
||||
"icu_ver_major": "70",
|
||||
"icu_ver_major": "71",
|
||||
"is_debug": 0,
|
||||
"libdir": "lib",
|
||||
"llvm_version": "11.0",
|
||||
"napi_build_version": "8",
|
||||
"node_byteorder": "little",
|
||||
"node_debug_lib": "false",
|
||||
"node_enable_d8": "false",
|
||||
"node_fipsinstall": "false",
|
||||
"node_install_corepack": "true",
|
||||
"node_install_npm": "true",
|
||||
"node_library_files": [
|
||||
"lib/constants.js",
|
||||
"lib/net.js",
|
||||
"lib/trace_events.js",
|
||||
"lib/events.js",
|
||||
"lib/repl.js",
|
||||
"lib/util.js",
|
||||
"lib/dgram.js",
|
||||
"lib/vm.js",
|
||||
"lib/stream.js",
|
||||
"lib/child_process.js",
|
||||
"lib/assert.js",
|
||||
"lib/_tls_wrap.js",
|
||||
"lib/http2.js",
|
||||
"lib/inspector.js",
|
||||
"lib/os.js",
|
||||
"lib/_http_server.js",
|
||||
"lib/console.js",
|
||||
"lib/perf_hooks.js",
|
||||
"lib/readline.js",
|
||||
"lib/punycode.js",
|
||||
"lib/_http_incoming.js",
|
||||
"lib/https.js",
|
||||
"lib/_stream_wrap.js",
|
||||
"lib/domain.js",
|
||||
"lib/dns.js",
|
||||
"lib/_http_client.js",
|
||||
"lib/diagnostics_channel.js",
|
||||
"lib/tty.js",
|
||||
"lib/_http_agent.js",
|
||||
"lib/timers.js",
|
||||
"lib/_http_outgoing.js",
|
||||
"lib/querystring.js",
|
||||
"lib/_tls_common.js",
|
||||
"lib/module.js",
|
||||
"lib/_stream_passthrough.js",
|
||||
"lib/_stream_transform.js",
|
||||
"lib/worker_threads.js",
|
||||
"lib/sys.js",
|
||||
"lib/_stream_duplex.js",
|
||||
"lib/path.js",
|
||||
"lib/_http_client.js",
|
||||
"lib/_http_common.js",
|
||||
"lib/string_decoder.js",
|
||||
"lib/cluster.js",
|
||||
"lib/v8.js",
|
||||
"lib/crypto.js",
|
||||
"lib/wasi.js",
|
||||
"lib/_http_incoming.js",
|
||||
"lib/_http_outgoing.js",
|
||||
"lib/_http_server.js",
|
||||
"lib/_stream_duplex.js",
|
||||
"lib/_stream_passthrough.js",
|
||||
"lib/_stream_readable.js",
|
||||
"lib/zlib.js",
|
||||
"lib/url.js",
|
||||
"lib/tls.js",
|
||||
"lib/_stream_transform.js",
|
||||
"lib/_stream_wrap.js",
|
||||
"lib/_stream_writable.js",
|
||||
"lib/_tls_common.js",
|
||||
"lib/_tls_wrap.js",
|
||||
"lib/assert.js",
|
||||
"lib/assert/strict.js",
|
||||
"lib/async_hooks.js",
|
||||
"lib/process.js",
|
||||
"lib/http.js",
|
||||
"lib/buffer.js",
|
||||
"lib/child_process.js",
|
||||
"lib/cluster.js",
|
||||
"lib/console.js",
|
||||
"lib/constants.js",
|
||||
"lib/crypto.js",
|
||||
"lib/dgram.js",
|
||||
"lib/diagnostics_channel.js",
|
||||
"lib/dns.js",
|
||||
"lib/dns/promises.js",
|
||||
"lib/domain.js",
|
||||
"lib/events.js",
|
||||
"lib/fs.js",
|
||||
"lib/util/types.js",
|
||||
"lib/timers/promises.js",
|
||||
"lib/path/win32.js",
|
||||
"lib/path/posix.js",
|
||||
"lib/stream/consumers.js",
|
||||
"lib/stream/promises.js",
|
||||
"lib/stream/web.js",
|
||||
"lib/internal/constants.js",
|
||||
"lib/fs/promises.js",
|
||||
"lib/http.js",
|
||||
"lib/http2.js",
|
||||
"lib/https.js",
|
||||
"lib/inspector.js",
|
||||
"lib/internal/abort_controller.js",
|
||||
"lib/internal/net.js",
|
||||
"lib/internal/v8_prof_processor.js",
|
||||
"lib/internal/event_target.js",
|
||||
"lib/internal/inspector_async_hook.js",
|
||||
"lib/internal/validators.js",
|
||||
"lib/internal/linkedlist.js",
|
||||
"lib/internal/cli_table.js",
|
||||
"lib/internal/repl.js",
|
||||
"lib/internal/util.js",
|
||||
"lib/internal/histogram.js",
|
||||
"lib/internal/error_serdes.js",
|
||||
"lib/internal/dgram.js",
|
||||
"lib/internal/child_process.js",
|
||||
"lib/internal/assert.js",
|
||||
"lib/internal/fixed_queue.js",
|
||||
"lib/internal/blocklist.js",
|
||||
"lib/internal/v8_prof_polyfill.js",
|
||||
"lib/internal/options.js",
|
||||
"lib/internal/worker.js",
|
||||
"lib/internal/dtrace.js",
|
||||
"lib/internal/idna.js",
|
||||
"lib/internal/watchdog.js",
|
||||
"lib/internal/encoding.js",
|
||||
"lib/internal/tty.js",
|
||||
"lib/internal/freeze_intrinsics.js",
|
||||
"lib/internal/timers.js",
|
||||
"lib/internal/heap_utils.js",
|
||||
"lib/internal/querystring.js",
|
||||
"lib/internal/js_stream_socket.js",
|
||||
"lib/internal/errors.js",
|
||||
"lib/internal/priority_queue.js",
|
||||
"lib/internal/freelist.js",
|
||||
"lib/internal/blob.js",
|
||||
"lib/internal/socket_list.js",
|
||||
"lib/internal/socketaddress.js",
|
||||
"lib/internal/promise_hooks.js",
|
||||
"lib/internal/stream_base_commons.js",
|
||||
"lib/internal/url.js",
|
||||
"lib/internal/assert/assertion_error.js",
|
||||
"lib/internal/assert/calltracker.js",
|
||||
"lib/internal/assert/snapshot.js",
|
||||
"lib/internal/async_hooks.js",
|
||||
"lib/internal/http.js",
|
||||
"lib/internal/buffer.js",
|
||||
"lib/internal/trace_events_async_hooks.js",
|
||||
"lib/internal/crypto/sig.js",
|
||||
"lib/internal/crypto/rsa.js",
|
||||
"lib/internal/crypto/aes.js",
|
||||
"lib/internal/crypto/util.js",
|
||||
"lib/internal/crypto/scrypt.js",
|
||||
"lib/internal/crypto/random.js",
|
||||
"lib/internal/crypto/keys.js",
|
||||
"lib/internal/crypto/x509.js",
|
||||
"lib/internal/crypto/certificate.js",
|
||||
"lib/internal/crypto/ec.js",
|
||||
"lib/internal/crypto/keygen.js",
|
||||
"lib/internal/crypto/mac.js",
|
||||
"lib/internal/crypto/diffiehellman.js",
|
||||
"lib/internal/crypto/hkdf.js",
|
||||
"lib/internal/crypto/cipher.js",
|
||||
"lib/internal/crypto/hash.js",
|
||||
"lib/internal/crypto/pbkdf2.js",
|
||||
"lib/internal/crypto/webcrypto.js",
|
||||
"lib/internal/crypto/dsa.js",
|
||||
"lib/internal/crypto/hashnames.js",
|
||||
"lib/internal/cluster/shared_handle.js",
|
||||
"lib/internal/cluster/round_robin_handle.js",
|
||||
"lib/internal/cluster/worker.js",
|
||||
"lib/internal/cluster/primary.js",
|
||||
"lib/internal/cluster/utils.js",
|
||||
"lib/internal/cluster/child.js",
|
||||
"lib/internal/webstreams/compression.js",
|
||||
"lib/internal/webstreams/util.js",
|
||||
"lib/internal/webstreams/writablestream.js",
|
||||
"lib/internal/webstreams/readablestream.js",
|
||||
"lib/internal/webstreams/queuingstrategies.js",
|
||||
"lib/internal/webstreams/encoding.js",
|
||||
"lib/internal/webstreams/transformstream.js",
|
||||
"lib/internal/webstreams/adapters.js",
|
||||
"lib/internal/webstreams/transfer.js",
|
||||
"lib/internal/blob.js",
|
||||
"lib/internal/blocklist.js",
|
||||
"lib/internal/bootstrap/browser.js",
|
||||
"lib/internal/bootstrap/loaders.js",
|
||||
"lib/internal/bootstrap/pre_execution.js",
|
||||
"lib/internal/bootstrap/node.js",
|
||||
"lib/internal/bootstrap/environment.js",
|
||||
"lib/internal/bootstrap/switches/does_not_own_process_state.js",
|
||||
"lib/internal/bootstrap/switches/is_not_main_thread.js",
|
||||
"lib/internal/bootstrap/switches/does_own_process_state.js",
|
||||
"lib/internal/bootstrap/switches/is_main_thread.js",
|
||||
"lib/internal/test/binding.js",
|
||||
"lib/internal/test/transfer.js",
|
||||
"lib/internal/util/types.js",
|
||||
"lib/internal/util/inspector.js",
|
||||
"lib/internal/util/comparisons.js",
|
||||
"lib/internal/util/debuglog.js",
|
||||
"lib/internal/util/inspect.js",
|
||||
"lib/internal/util/iterable_weak_map.js",
|
||||
"lib/internal/streams/add-abort-signal.js",
|
||||
"lib/internal/streams/compose.js",
|
||||
"lib/internal/streams/duplexify.js",
|
||||
"lib/internal/streams/destroy.js",
|
||||
"lib/internal/streams/legacy.js",
|
||||
"lib/internal/streams/passthrough.js",
|
||||
"lib/internal/streams/operators.js",
|
||||
"lib/internal/streams/readable.js",
|
||||
"lib/internal/streams/from.js",
|
||||
"lib/internal/streams/writable.js",
|
||||
"lib/internal/streams/state.js",
|
||||
"lib/internal/streams/buffer_list.js",
|
||||
"lib/internal/streams/end-of-stream.js",
|
||||
"lib/internal/streams/utils.js",
|
||||
"lib/internal/streams/transform.js",
|
||||
"lib/internal/streams/lazy_transform.js",
|
||||
"lib/internal/streams/duplex.js",
|
||||
"lib/internal/streams/pipeline.js",
|
||||
"lib/internal/readline/interface.js",
|
||||
"lib/internal/readline/utils.js",
|
||||
"lib/internal/readline/emitKeypressEvents.js",
|
||||
"lib/internal/readline/callbacks.js",
|
||||
"lib/internal/repl/history.js",
|
||||
"lib/internal/repl/utils.js",
|
||||
"lib/internal/repl/await.js",
|
||||
"lib/internal/legacy/processbinding.js",
|
||||
"lib/internal/assert/calltracker.js",
|
||||
"lib/internal/assert/assertion_error.js",
|
||||
"lib/internal/http2/util.js",
|
||||
"lib/internal/http2/core.js",
|
||||
"lib/internal/http2/compat.js",
|
||||
"lib/internal/per_context/messageport.js",
|
||||
"lib/internal/per_context/primordials.js",
|
||||
"lib/internal/per_context/domexception.js",
|
||||
"lib/internal/vm/module.js",
|
||||
"lib/internal/tls/secure-pair.js",
|
||||
"lib/internal/tls/parse-cert-string.js",
|
||||
"lib/internal/tls/secure-context.js",
|
||||
"lib/internal/bootstrap/switches/is_not_main_thread.js",
|
||||
"lib/internal/buffer.js",
|
||||
"lib/internal/child_process.js",
|
||||
"lib/internal/child_process/serialization.js",
|
||||
"lib/internal/debugger/inspect_repl.js",
|
||||
"lib/internal/debugger/inspect_client.js",
|
||||
"lib/internal/cli_table.js",
|
||||
"lib/internal/cluster/child.js",
|
||||
"lib/internal/cluster/primary.js",
|
||||
"lib/internal/cluster/round_robin_handle.js",
|
||||
"lib/internal/cluster/shared_handle.js",
|
||||
"lib/internal/cluster/utils.js",
|
||||
"lib/internal/cluster/worker.js",
|
||||
"lib/internal/console/constructor.js",
|
||||
"lib/internal/console/global.js",
|
||||
"lib/internal/constants.js",
|
||||
"lib/internal/crypto/aes.js",
|
||||
"lib/internal/crypto/certificate.js",
|
||||
"lib/internal/crypto/cfrg.js",
|
||||
"lib/internal/crypto/cipher.js",
|
||||
"lib/internal/crypto/diffiehellman.js",
|
||||
"lib/internal/crypto/ec.js",
|
||||
"lib/internal/crypto/hash.js",
|
||||
"lib/internal/crypto/hashnames.js",
|
||||
"lib/internal/crypto/hkdf.js",
|
||||
"lib/internal/crypto/keygen.js",
|
||||
"lib/internal/crypto/keys.js",
|
||||
"lib/internal/crypto/mac.js",
|
||||
"lib/internal/crypto/pbkdf2.js",
|
||||
"lib/internal/crypto/random.js",
|
||||
"lib/internal/crypto/rsa.js",
|
||||
"lib/internal/crypto/scrypt.js",
|
||||
"lib/internal/crypto/sig.js",
|
||||
"lib/internal/crypto/util.js",
|
||||
"lib/internal/crypto/webcrypto.js",
|
||||
"lib/internal/crypto/x509.js",
|
||||
"lib/internal/debugger/inspect.js",
|
||||
"lib/internal/worker/io.js",
|
||||
"lib/internal/worker/js_transferable.js",
|
||||
"lib/internal/main/repl.js",
|
||||
"lib/internal/main/print_help.js",
|
||||
"lib/internal/main/eval_string.js",
|
||||
"lib/internal/main/check_syntax.js",
|
||||
"lib/internal/main/prof_process.js",
|
||||
"lib/internal/main/worker_thread.js",
|
||||
"lib/internal/main/inspect.js",
|
||||
"lib/internal/main/eval_stdin.js",
|
||||
"lib/internal/main/run_main_module.js",
|
||||
"lib/internal/modules/run_main.js",
|
||||
"lib/internal/modules/package_json_reader.js",
|
||||
"lib/internal/modules/esm/module_job.js",
|
||||
"lib/internal/modules/esm/assert.js",
|
||||
"lib/internal/modules/esm/fetch_module.js",
|
||||
"lib/internal/modules/esm/get_source.js",
|
||||
"lib/internal/modules/esm/translators.js",
|
||||
"lib/internal/modules/esm/resolve.js",
|
||||
"lib/internal/modules/esm/create_dynamic_module.js",
|
||||
"lib/internal/modules/esm/load.js",
|
||||
"lib/internal/modules/esm/handle_process_exit.js",
|
||||
"lib/internal/modules/esm/initialize_import_meta.js",
|
||||
"lib/internal/modules/esm/module_map.js",
|
||||
"lib/internal/modules/esm/get_format.js",
|
||||
"lib/internal/modules/esm/formats.js",
|
||||
"lib/internal/modules/esm/loader.js",
|
||||
"lib/internal/modules/cjs/helpers.js",
|
||||
"lib/internal/modules/cjs/loader.js",
|
||||
"lib/internal/source_map/source_map.js",
|
||||
"lib/internal/source_map/prepare_stack_trace.js",
|
||||
"lib/internal/source_map/source_map_cache.js",
|
||||
"lib/internal/debugger/inspect_client.js",
|
||||
"lib/internal/debugger/inspect_repl.js",
|
||||
"lib/internal/dgram.js",
|
||||
"lib/internal/dns/callback_resolver.js",
|
||||
"lib/internal/dns/promises.js",
|
||||
"lib/internal/dns/utils.js",
|
||||
"lib/internal/fs/watchers.js",
|
||||
"lib/internal/dtrace.js",
|
||||
"lib/internal/encoding.js",
|
||||
"lib/internal/error_serdes.js",
|
||||
"lib/internal/errors.js",
|
||||
"lib/internal/event_target.js",
|
||||
"lib/internal/fixed_queue.js",
|
||||
"lib/internal/freelist.js",
|
||||
"lib/internal/freeze_intrinsics.js",
|
||||
"lib/internal/fs/cp/cp-sync.js",
|
||||
"lib/internal/fs/cp/cp.js",
|
||||
"lib/internal/fs/dir.js",
|
||||
"lib/internal/fs/promises.js",
|
||||
"lib/internal/fs/read_file_context.js",
|
||||
"lib/internal/fs/rimraf.js",
|
||||
"lib/internal/fs/sync_write_stream.js",
|
||||
"lib/internal/fs/dir.js",
|
||||
"lib/internal/fs/streams.js",
|
||||
"lib/internal/fs/sync_write_stream.js",
|
||||
"lib/internal/fs/utils.js",
|
||||
"lib/internal/fs/cp/cp.js",
|
||||
"lib/internal/fs/cp/cp-sync.js",
|
||||
"lib/internal/perf/nodetiming.js",
|
||||
"lib/internal/perf/usertiming.js",
|
||||
"lib/internal/perf/performance_entry.js",
|
||||
"lib/internal/perf/performance.js",
|
||||
"lib/internal/perf/timerify.js",
|
||||
"lib/internal/perf/utils.js",
|
||||
"lib/internal/perf/observe.js",
|
||||
"lib/internal/fs/watchers.js",
|
||||
"lib/internal/heap_utils.js",
|
||||
"lib/internal/histogram.js",
|
||||
"lib/internal/http.js",
|
||||
"lib/internal/http2/compat.js",
|
||||
"lib/internal/http2/core.js",
|
||||
"lib/internal/http2/util.js",
|
||||
"lib/internal/idna.js",
|
||||
"lib/internal/inspector_async_hook.js",
|
||||
"lib/internal/js_stream_socket.js",
|
||||
"lib/internal/legacy/processbinding.js",
|
||||
"lib/internal/linkedlist.js",
|
||||
"lib/internal/main/check_syntax.js",
|
||||
"lib/internal/main/environment.js",
|
||||
"lib/internal/main/eval_stdin.js",
|
||||
"lib/internal/main/eval_string.js",
|
||||
"lib/internal/main/inspect.js",
|
||||
"lib/internal/main/mksnapshot.js",
|
||||
"lib/internal/main/print_help.js",
|
||||
"lib/internal/main/prof_process.js",
|
||||
"lib/internal/main/repl.js",
|
||||
"lib/internal/main/run_main_module.js",
|
||||
"lib/internal/main/test_runner.js",
|
||||
"lib/internal/main/watch_mode.js",
|
||||
"lib/internal/main/worker_thread.js",
|
||||
"lib/internal/modules/cjs/helpers.js",
|
||||
"lib/internal/modules/cjs/loader.js",
|
||||
"lib/internal/modules/esm/assert.js",
|
||||
"lib/internal/modules/esm/create_dynamic_module.js",
|
||||
"lib/internal/modules/esm/fetch_module.js",
|
||||
"lib/internal/modules/esm/formats.js",
|
||||
"lib/internal/modules/esm/get_format.js",
|
||||
"lib/internal/modules/esm/handle_process_exit.js",
|
||||
"lib/internal/modules/esm/initialize_import_meta.js",
|
||||
"lib/internal/modules/esm/load.js",
|
||||
"lib/internal/modules/esm/loader.js",
|
||||
"lib/internal/modules/esm/module_job.js",
|
||||
"lib/internal/modules/esm/module_map.js",
|
||||
"lib/internal/modules/esm/package_config.js",
|
||||
"lib/internal/modules/esm/resolve.js",
|
||||
"lib/internal/modules/esm/translators.js",
|
||||
"lib/internal/modules/package_json_reader.js",
|
||||
"lib/internal/modules/run_main.js",
|
||||
"lib/internal/net.js",
|
||||
"lib/internal/options.js",
|
||||
"lib/internal/per_context/domexception.js",
|
||||
"lib/internal/per_context/messageport.js",
|
||||
"lib/internal/per_context/primordials.js",
|
||||
"lib/internal/perf/event_loop_delay.js",
|
||||
"lib/internal/perf/event_loop_utilization.js",
|
||||
"lib/internal/perf/nodetiming.js",
|
||||
"lib/internal/perf/observe.js",
|
||||
"lib/internal/perf/performance.js",
|
||||
"lib/internal/perf/performance_entry.js",
|
||||
"lib/internal/perf/resource_timing.js",
|
||||
"lib/internal/perf/timerify.js",
|
||||
"lib/internal/perf/usertiming.js",
|
||||
"lib/internal/perf/utils.js",
|
||||
"lib/internal/policy/manifest.js",
|
||||
"lib/internal/policy/sri.js",
|
||||
"lib/internal/process/task_queues.js",
|
||||
"lib/internal/process/per_thread.js",
|
||||
"lib/internal/process/warning.js",
|
||||
"lib/internal/process/policy.js",
|
||||
"lib/internal/process/promises.js",
|
||||
"lib/internal/process/signal.js",
|
||||
"lib/internal/process/execution.js",
|
||||
"lib/internal/priority_queue.js",
|
||||
"lib/internal/process/esm_loader.js",
|
||||
"lib/internal/process/execution.js",
|
||||
"lib/internal/process/per_thread.js",
|
||||
"lib/internal/process/policy.js",
|
||||
"lib/internal/process/pre_execution.js",
|
||||
"lib/internal/process/promises.js",
|
||||
"lib/internal/process/report.js",
|
||||
"lib/internal/process/signal.js",
|
||||
"lib/internal/process/task_queues.js",
|
||||
"lib/internal/process/warning.js",
|
||||
"lib/internal/process/worker_thread_only.js",
|
||||
"lib/internal/console/constructor.js",
|
||||
"lib/internal/console/global.js",
|
||||
"lib/assert/strict.js",
|
||||
"lib/dns/promises.js",
|
||||
"lib/fs/promises.js"
|
||||
"lib/internal/promise_hooks.js",
|
||||
"lib/internal/querystring.js",
|
||||
"lib/internal/readline/callbacks.js",
|
||||
"lib/internal/readline/emitKeypressEvents.js",
|
||||
"lib/internal/readline/interface.js",
|
||||
"lib/internal/readline/promises.js",
|
||||
"lib/internal/readline/utils.js",
|
||||
"lib/internal/repl.js",
|
||||
"lib/internal/repl/await.js",
|
||||
"lib/internal/repl/history.js",
|
||||
"lib/internal/repl/utils.js",
|
||||
"lib/internal/socket_list.js",
|
||||
"lib/internal/socketaddress.js",
|
||||
"lib/internal/source_map/prepare_stack_trace.js",
|
||||
"lib/internal/source_map/source_map.js",
|
||||
"lib/internal/source_map/source_map_cache.js",
|
||||
"lib/internal/stream_base_commons.js",
|
||||
"lib/internal/streams/add-abort-signal.js",
|
||||
"lib/internal/streams/buffer_list.js",
|
||||
"lib/internal/streams/compose.js",
|
||||
"lib/internal/streams/destroy.js",
|
||||
"lib/internal/streams/duplex.js",
|
||||
"lib/internal/streams/duplexify.js",
|
||||
"lib/internal/streams/end-of-stream.js",
|
||||
"lib/internal/streams/from.js",
|
||||
"lib/internal/streams/lazy_transform.js",
|
||||
"lib/internal/streams/legacy.js",
|
||||
"lib/internal/streams/operators.js",
|
||||
"lib/internal/streams/passthrough.js",
|
||||
"lib/internal/streams/pipeline.js",
|
||||
"lib/internal/streams/readable.js",
|
||||
"lib/internal/streams/state.js",
|
||||
"lib/internal/streams/transform.js",
|
||||
"lib/internal/streams/utils.js",
|
||||
"lib/internal/streams/writable.js",
|
||||
"lib/internal/structured_clone.js",
|
||||
"lib/internal/test/binding.js",
|
||||
"lib/internal/test/transfer.js",
|
||||
"lib/internal/test_runner/harness.js",
|
||||
"lib/internal/test_runner/runner.js",
|
||||
"lib/internal/test_runner/tap_stream.js",
|
||||
"lib/internal/test_runner/test.js",
|
||||
"lib/internal/test_runner/utils.js",
|
||||
"lib/internal/timers.js",
|
||||
"lib/internal/tls/secure-context.js",
|
||||
"lib/internal/tls/secure-pair.js",
|
||||
"lib/internal/trace_events_async_hooks.js",
|
||||
"lib/internal/tty.js",
|
||||
"lib/internal/url.js",
|
||||
"lib/internal/util.js",
|
||||
"lib/internal/util/colors.js",
|
||||
"lib/internal/util/comparisons.js",
|
||||
"lib/internal/util/debuglog.js",
|
||||
"lib/internal/util/inspect.js",
|
||||
"lib/internal/util/inspector.js",
|
||||
"lib/internal/util/iterable_weak_map.js",
|
||||
"lib/internal/util/parse_args/parse_args.js",
|
||||
"lib/internal/util/parse_args/utils.js",
|
||||
"lib/internal/util/types.js",
|
||||
"lib/internal/v8/startup_snapshot.js",
|
||||
"lib/internal/v8_prof_polyfill.js",
|
||||
"lib/internal/v8_prof_processor.js",
|
||||
"lib/internal/validators.js",
|
||||
"lib/internal/vm/module.js",
|
||||
"lib/internal/wasm_web_api.js",
|
||||
"lib/internal/watch_mode/files_watcher.js",
|
||||
"lib/internal/watchdog.js",
|
||||
"lib/internal/webstreams/adapters.js",
|
||||
"lib/internal/webstreams/compression.js",
|
||||
"lib/internal/webstreams/encoding.js",
|
||||
"lib/internal/webstreams/queuingstrategies.js",
|
||||
"lib/internal/webstreams/readablestream.js",
|
||||
"lib/internal/webstreams/transfer.js",
|
||||
"lib/internal/webstreams/transformstream.js",
|
||||
"lib/internal/webstreams/util.js",
|
||||
"lib/internal/webstreams/writablestream.js",
|
||||
"lib/internal/worker.js",
|
||||
"lib/internal/worker/io.js",
|
||||
"lib/internal/worker/js_transferable.js",
|
||||
"lib/module.js",
|
||||
"lib/net.js",
|
||||
"lib/os.js",
|
||||
"lib/path.js",
|
||||
"lib/path/posix.js",
|
||||
"lib/path/win32.js",
|
||||
"lib/perf_hooks.js",
|
||||
"lib/process.js",
|
||||
"lib/punycode.js",
|
||||
"lib/querystring.js",
|
||||
"lib/readline.js",
|
||||
"lib/readline/promises.js",
|
||||
"lib/repl.js",
|
||||
"lib/stream.js",
|
||||
"lib/stream/consumers.js",
|
||||
"lib/stream/promises.js",
|
||||
"lib/stream/web.js",
|
||||
"lib/string_decoder.js",
|
||||
"lib/sys.js",
|
||||
"lib/test.js",
|
||||
"lib/timers.js",
|
||||
"lib/timers/promises.js",
|
||||
"lib/tls.js",
|
||||
"lib/trace_events.js",
|
||||
"lib/tty.js",
|
||||
"lib/url.js",
|
||||
"lib/util.js",
|
||||
"lib/util/types.js",
|
||||
"lib/v8.js",
|
||||
"lib/vm.js",
|
||||
"lib/wasi.js",
|
||||
"lib/worker_threads.js",
|
||||
"lib/zlib.js"
|
||||
],
|
||||
"node_module_version": 93,
|
||||
"node_module_version": 108,
|
||||
"node_no_browser_globals": "false",
|
||||
"node_prefix": "/",
|
||||
"node_release_urlbase": "https://nodejs.org/download/release/",
|
||||
@@ -330,20 +353,22 @@
|
||||
"node_use_v8_platform": "true",
|
||||
"node_with_ltcg": "false",
|
||||
"node_without_node_options": "false",
|
||||
"openssl_fips": "",
|
||||
"openssl_is_fips": "false",
|
||||
"openssl_quic": "true",
|
||||
"ossfuzz": "false",
|
||||
"shlib_suffix": "93.dylib",
|
||||
"shlib_suffix": "108.dylib",
|
||||
"target_arch": "x64",
|
||||
"v8_enable_31bit_smis_on_64bit_arch": 0,
|
||||
"v8_enable_gdbjit": 0,
|
||||
"v8_enable_hugepage": 0,
|
||||
"v8_enable_i18n_support": 1,
|
||||
"v8_enable_inspector": 1,
|
||||
"v8_enable_javascript_promise_hooks": 1,
|
||||
"v8_enable_lite_mode": 0,
|
||||
"v8_enable_object_print": 1,
|
||||
"v8_enable_pointer_compression": 0,
|
||||
"v8_enable_shared_ro_heap": 1,
|
||||
"v8_enable_short_builtin_calls": 1,
|
||||
"v8_enable_webassembly": 1,
|
||||
"v8_no_strict_aliasing": 1,
|
||||
"v8_optimized_debug": 1,
|
||||
@@ -353,8 +378,8 @@
|
||||
"v8_use_siphash": 1,
|
||||
"want_separate_host_toolset": 0,
|
||||
"xcode_version": "11.0",
|
||||
"nodedir": "/Users/karolsojko/Library/Caches/node-gyp/16.15.1",
|
||||
"nodedir": "/Users/karolsojko/Library/Caches/node-gyp/18.12.1",
|
||||
"standalone_static_library": 1,
|
||||
"user_agent": "yarn/3.2.1 npm/? node/v16.15.1 darwin x64"
|
||||
"user_agent": "yarn/4.0.0-rc.25 npm/? node/v18.12.1 darwin x64"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ DEFS_Debug := \
|
||||
CFLAGS_Debug := \
|
||||
-O0 \
|
||||
-gdwarf-2 \
|
||||
-mmacosx-version-min=10.13 \
|
||||
-mmacosx-version-min=10.15 \
|
||||
-arch x86_64 \
|
||||
-Wall \
|
||||
-Wendif-labels \
|
||||
@@ -38,7 +38,7 @@ CFLAGS_C_Debug := \
|
||||
|
||||
# Flags passed to only C++ files.
|
||||
CFLAGS_CC_Debug := \
|
||||
-std=gnu++14 \
|
||||
-std=gnu++17 \
|
||||
-stdlib=libc++ \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
@@ -51,13 +51,13 @@ CFLAGS_OBJC_Debug :=
|
||||
CFLAGS_OBJCC_Debug :=
|
||||
|
||||
INCS_Debug := \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/src \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/config \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/openssl/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/uv/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/zlib \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/v8/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/include/node \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/src \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/openssl/config \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/openssl/openssl/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/uv/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/zlib \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/v8/include \
|
||||
-I$(srcdir)/src \
|
||||
-I$(srcdir)/../../../../nan-npm-2.16.0-cac314a230/node_modules/nan
|
||||
|
||||
@@ -81,7 +81,7 @@ DEFS_Release := \
|
||||
CFLAGS_Release := \
|
||||
-O3 \
|
||||
-gdwarf-2 \
|
||||
-mmacosx-version-min=10.13 \
|
||||
-mmacosx-version-min=10.15 \
|
||||
-arch x86_64 \
|
||||
-Wall \
|
||||
-Wendif-labels \
|
||||
@@ -94,7 +94,7 @@ CFLAGS_C_Release := \
|
||||
|
||||
# Flags passed to only C++ files.
|
||||
CFLAGS_CC_Release := \
|
||||
-std=gnu++14 \
|
||||
-std=gnu++17 \
|
||||
-stdlib=libc++ \
|
||||
-fno-rtti \
|
||||
-fno-exceptions \
|
||||
@@ -107,13 +107,13 @@ CFLAGS_OBJC_Release :=
|
||||
CFLAGS_OBJCC_Release :=
|
||||
|
||||
INCS_Release := \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/include/node \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/src \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/config \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/openssl/openssl/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/uv/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/zlib \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/16.15.1/deps/v8/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/include/node \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/src \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/openssl/config \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/openssl/openssl/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/uv/include \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/zlib \
|
||||
-I/Users/karolsojko/Library/Caches/node-gyp/18.12.1/deps/v8/include \
|
||||
-I$(srcdir)/src \
|
||||
-I$(srcdir)/../../../../nan-npm-2.16.0-cac314a230/node_modules/nan
|
||||
|
||||
@@ -151,7 +151,7 @@ $(obj).$(TOOLSET)/$(TARGET)/%.o: $(obj)/%.cpp FORCE_DO_CMD
|
||||
LDFLAGS_Debug := \
|
||||
-undefined dynamic_lookup \
|
||||
-Wl,-search_paths_first \
|
||||
-mmacosx-version-min=10.13 \
|
||||
-mmacosx-version-min=10.15 \
|
||||
-arch x86_64 \
|
||||
-L$(builddir) \
|
||||
-stdlib=libc++
|
||||
@@ -163,7 +163,7 @@ LIBTOOLFLAGS_Debug := \
|
||||
LDFLAGS_Release := \
|
||||
-undefined dynamic_lookup \
|
||||
-Wl,-search_paths_first \
|
||||
-mmacosx-version-min=10.13 \
|
||||
-mmacosx-version-min=10.15 \
|
||||
-arch x86_64 \
|
||||
-L$(builddir) \
|
||||
-stdlib=libc++
|
||||
|
||||
14
package.json
14
package.json
@@ -8,7 +8,7 @@
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "yarn workspaces foreach -p -j 10 --verbose run lint",
|
||||
@@ -20,6 +20,8 @@
|
||||
"lint:event-store": "yarn workspace @standardnotes/event-store lint",
|
||||
"lint:websockets": "yarn workspace @standardnotes/websockets-server lint",
|
||||
"lint:workspace": "yarn workspace @standardnotes/workspace-server lint",
|
||||
"lint:analytics": "yarn workspace @standardnotes/analytics lint",
|
||||
"lint:revisions": "yarn workspace @standardnotes/revisions-server lint",
|
||||
"clean": "yarn workspaces foreach -p --verbose run clean",
|
||||
"setup:env": "cp .env.sample .env && yarn workspaces foreach -p --verbose run setup:env",
|
||||
"start:auth": "yarn workspace @standardnotes/auth-server start",
|
||||
@@ -32,6 +34,8 @@
|
||||
"start:api-gateway": "yarn workspace @standardnotes/api-gateway start",
|
||||
"start:websockets": "yarn workspace @standardnotes/websockets-server start",
|
||||
"start:workspace": "yarn workspace @standardnotes/workspace-server start",
|
||||
"start:analytics": "yarn workspace @standardnotes/analytics worker",
|
||||
"start:revisions": "yarn workspace @standardnotes/revisions-server start",
|
||||
"release": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish new version\"",
|
||||
"publish": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
|
||||
"postversion": "./scripts/push-tags-one-by-one.sh",
|
||||
@@ -44,8 +48,8 @@
|
||||
"@lerna-lite/list": "^1.5.1",
|
||||
"@lerna-lite/run": "^1.5.1",
|
||||
"@types/jest": "^29.1.1",
|
||||
"@types/newrelic": "^7.0.3",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/newrelic": "^7.0.4",
|
||||
"@types/node": "^18.11.9",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@@ -57,7 +61,7 @@
|
||||
},
|
||||
"packageManager": "yarn@4.0.0-rc.25",
|
||||
"dependencies": {
|
||||
"@sentry/node": "^7.3.0",
|
||||
"newrelic": "^9.0.0"
|
||||
"@sentry/node": "^7.19.0",
|
||||
"newrelic": "^9.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
28
packages/analytics/.env.sample
Normal file
28
packages/analytics/.env.sample
Normal file
@@ -0,0 +1,28 @@
|
||||
LOG_LEVEL=debug
|
||||
NODE_ENV=development
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_REPLICA_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=analytics
|
||||
DB_PASSWORD=changeme123
|
||||
DB_DATABASE=analytics
|
||||
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
||||
DB_MIGRATIONS_PATH=dist/migrations/*.js
|
||||
|
||||
REDIS_URL=redis://cache
|
||||
REDIS_EVENTS_CHANNEL=events
|
||||
|
||||
SNS_TOPIC_ARN=
|
||||
SNS_AWS_REGION=
|
||||
SQS_QUEUE_URL=
|
||||
SQS_AWS_REGION=
|
||||
|
||||
# (Optional) New Relic Setup
|
||||
NEW_RELIC_ENABLED=false
|
||||
NEW_RELIC_APP_NAME=Analytics
|
||||
NEW_RELIC_LICENSE_KEY=
|
||||
NEW_RELIC_NO_CONFIG_FILE=true
|
||||
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
||||
NEW_RELIC_LOG_ENABLED=false
|
||||
NEW_RELIC_LOG_LEVEL=info
|
||||
@@ -3,6 +3,451 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.12.22](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.21...@standardnotes/analytics@2.12.22) (2022-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** report event publishing ([a605660](https://github.com/standardnotes/server/commit/a6056600eb96bf175189ad6d62870c9d736f331b))
|
||||
|
||||
## [2.12.21](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.20...@standardnotes/analytics@2.12.21) (2022-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** add debug logs for report ([48c0cb5](https://github.com/standardnotes/server/commit/48c0cb5e62dc8af930de191deaa1eb3ff6c5a29f))
|
||||
|
||||
## [2.12.20](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.19...@standardnotes/analytics@2.12.20) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.19](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.18...@standardnotes/analytics@2.12.19) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.18](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.17...@standardnotes/analytics@2.12.18) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.16...@standardnotes/analytics@2.12.17) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.15...@standardnotes/analytics@2.12.16) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.14...@standardnotes/analytics@2.12.15) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.13...@standardnotes/analytics@2.12.14) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.12...@standardnotes/analytics@2.12.13) (2022-12-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.11...@standardnotes/analytics@2.12.12) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.10...@standardnotes/analytics@2.12.11) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.9...@standardnotes/analytics@2.12.10) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.8...@standardnotes/analytics@2.12.9) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.7...@standardnotes/analytics@2.12.8) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.6...@standardnotes/analytics@2.12.7) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.5...@standardnotes/analytics@2.12.6) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.4...@standardnotes/analytics@2.12.5) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.3...@standardnotes/analytics@2.12.4) (2022-12-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.2...@standardnotes/analytics@2.12.3) (2022-12-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.1...@standardnotes/analytics@2.12.2) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.12.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.0...@standardnotes/analytics@2.12.1) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
# [2.12.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.17...@standardnotes/analytics@2.12.0) (2022-12-05)
|
||||
|
||||
### Features
|
||||
|
||||
* **domain-core:** distinguish between username and email ([06fd404](https://github.com/standardnotes/server/commit/06fd404d44b44a53733f889aabd4da63f21e2f36))
|
||||
|
||||
## [2.11.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.16...@standardnotes/analytics@2.11.17) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.15...@standardnotes/analytics@2.11.16) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.14...@standardnotes/analytics@2.11.15) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.13...@standardnotes/analytics@2.11.14) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.12...@standardnotes/analytics@2.11.13) (2022-11-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.11...@standardnotes/analytics@2.11.12) (2022-11-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.10...@standardnotes/analytics@2.11.11) (2022-11-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.9...@standardnotes/analytics@2.11.10) (2022-11-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.8...@standardnotes/analytics@2.11.9) (2022-11-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.7...@standardnotes/analytics@2.11.8) (2022-11-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.6...@standardnotes/analytics@2.11.7) (2022-11-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* binding of sns and sqs with additional config ([74bc791](https://github.com/standardnotes/server/commit/74bc79116bc50d9a5af1a558db1b7108dcda6d0e))
|
||||
|
||||
## [2.11.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.5...@standardnotes/analytics@2.11.6) (2022-11-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.4...@standardnotes/analytics@2.11.5) (2022-11-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.3...@standardnotes/analytics@2.11.4) (2022-11-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.2...@standardnotes/analytics@2.11.3) (2022-11-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.11.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.1...@standardnotes/analytics@2.11.2) (2022-11-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** specs ([507d43b](https://github.com/standardnotes/server/commit/507d43b3289d1e178644df6d3e15d1d55e56c7bb))
|
||||
|
||||
## [2.11.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.0...@standardnotes/analytics@2.11.1) (2022-11-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* mapper interface imports ([1ec0723](https://github.com/standardnotes/server/commit/1ec072373d640c4e2f24b9bb12fec0c678b48032))
|
||||
|
||||
# [2.11.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.10.3...@standardnotes/analytics@2.11.0) (2022-11-16)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add publishing churn calculation values in the report ([6c43a33](https://github.com/standardnotes/server/commit/6c43a331d09c2dcf1300742509da6a1d8ef2f5b7))
|
||||
|
||||
## [2.10.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.10.2...@standardnotes/analytics@2.10.3) (2022-11-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** exclude five year plans from mrr stats ([fe1b2a0](https://github.com/standardnotes/server/commit/fe1b2a0e0744417e592f3f61f42610765b416ce6))
|
||||
|
||||
## [2.10.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.10.1...@standardnotes/analytics@2.10.2) (2022-11-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** imports from domain-core ([15dfd6d](https://github.com/standardnotes/server/commit/15dfd6dcba75a772000eeb01b78a532067b01d5b))
|
||||
|
||||
## [2.10.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.10.0...@standardnotes/analytics@2.10.1) (2022-11-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** retrieving revisions ([50f7ae3](https://github.com/standardnotes/server/commit/50f7ae338ad66d3465fa16c31e7c47c57b1e0c3c))
|
||||
|
||||
# [2.10.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.9...@standardnotes/analytics@2.10.0) (2022-11-14)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** extract domain core into a separate package ([0f94e2a](https://github.com/standardnotes/server/commit/0f94e2ad0c8927733eac31f130cbe649dce765f9))
|
||||
|
||||
## [2.9.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.6...@standardnotes/analytics@2.9.9) (2022-11-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** bump version ([8715fe1](https://github.com/standardnotes/server/commit/8715fe182221f67f02b5f49e566366db3db581f4))
|
||||
|
||||
## [2.9.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.5...@standardnotes/analytics@2.9.6) (2022-11-14)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.9.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.4...@standardnotes/analytics@2.9.5) (2022-11-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.9.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.3...@standardnotes/analytics@2.9.4) (2022-11-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/analytics
|
||||
|
||||
## [2.9.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.2...@standardnotes/analytics@2.9.3) (2022-11-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** add five year plans mrr calculation ([a03c5bc](https://github.com/standardnotes/server/commit/a03c5bceea2a9b166b1d5ad75181021462a86627))
|
||||
|
||||
## [2.9.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.1...@standardnotes/analytics@2.9.2) (2022-11-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** add missing period for stats report ([9b593f2](https://github.com/standardnotes/server/commit/9b593f2a6b105ab8f9c7cef8bdda6892c42e20ef))
|
||||
|
||||
## [2.9.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.9.0...@standardnotes/analytics@2.9.1) (2022-11-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** generate mrr stats for last 30 days including Today ([b92c4ae](https://github.com/standardnotes/server/commit/b92c4ae650b53db5c0bb2a9cf9afb01caeb8d822))
|
||||
|
||||
# [2.9.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.8.3...@standardnotes/analytics@2.9.0) (2022-11-10)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add mrr for annual, monthly, pro and plus subscription plans ([ce3e259](https://github.com/standardnotes/server/commit/ce3e259bdedd10796fb4469f0eabd64bc326a115))
|
||||
|
||||
## [2.8.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.8.2...@standardnotes/analytics@2.8.3) (2022-11-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** add subscription id to error logs ([81be065](https://github.com/standardnotes/server/commit/81be06598c918279f98a8ba6b59ea1b3803c949c))
|
||||
|
||||
## [2.8.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.8.1...@standardnotes/analytics@2.8.2) (2022-11-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** add monthly mrr to the report ([fce47a0](https://github.com/standardnotes/server/commit/fce47a0a37a67b3edf3ea0b6ccda43c54dbd9870))
|
||||
|
||||
## [2.8.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.8.0...@standardnotes/analytics@2.8.1) (2022-11-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** add persisting mrr for this month and this year as well ([8df0482](https://github.com/standardnotes/server/commit/8df0482eb4bfd63b9639fd786c9b6952ad7f801d))
|
||||
|
||||
# [2.8.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.7.3...@standardnotes/analytics@2.8.0) (2022-11-10)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add calculating monthly recurring revenue ([77e5065](https://github.com/standardnotes/server/commit/77e50655f6fa7f9c28e13f8b8bc6de246c0454f0))
|
||||
|
||||
## [2.7.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.7.2...@standardnotes/analytics@2.7.3) (2022-11-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** arhcitecture arrangements for use case execution ([7393954](https://github.com/standardnotes/server/commit/7393954ff6ece6143f7661104299172548db90ee))
|
||||
|
||||
## [2.7.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.7.1...@standardnotes/analytics@2.7.2) (2022-11-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** mrr column types ([90aef90](https://github.com/standardnotes/server/commit/90aef905af05b8c1c86c7bd383df6b2b502f7c91))
|
||||
|
||||
## [2.7.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.7.0...@standardnotes/analytics@2.7.1) (2022-11-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** add missing created at column ([89502be](https://github.com/standardnotes/server/commit/89502bed638b17301e42e0d5916635b0a59f585d))
|
||||
|
||||
# [2.7.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.6.0...@standardnotes/analytics@2.7.0) (2022-11-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add saving revenue modifications upon subscription canceled ([52a257a](https://github.com/standardnotes/server/commit/52a257abb16034134a50474fbbb2493a00c58b99))
|
||||
|
||||
# [2.6.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.5.0...@standardnotes/analytics@2.6.0) (2022-11-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add saving revenue modifications upon subscription refunded ([0f65c05](https://github.com/standardnotes/server/commit/0f65c051abcff805e920f91d338e5fadda7905a9))
|
||||
|
||||
# [2.5.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.4.0...@standardnotes/analytics@2.5.0) (2022-11-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add saving revenue modifications upon subscription expired ([5c3db2c](https://github.com/standardnotes/server/commit/5c3db2cb29a929e44b63eb8226ce4ad1d14f8a99))
|
||||
|
||||
# [2.4.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.3.1...@standardnotes/analytics@2.4.0) (2022-11-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add saving revenue modifications upon subscription renewed ([cdb7fcf](https://github.com/standardnotes/server/commit/cdb7fcf8311fecfabe3ef9eb656cd6ec57b87de0))
|
||||
|
||||
## [2.3.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.3.0...@standardnotes/analytics@2.3.1) (2022-11-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** missing injectable annotation ([9d3ef24](https://github.com/standardnotes/server/commit/9d3ef24ba94ad28976a211d40f94f1bce8d0d305))
|
||||
|
||||
# [2.3.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.2.0...@standardnotes/analytics@2.3.0) (2022-11-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add saving revenue modifications upon subscription purchased ([5ea9941](https://github.com/standardnotes/server/commit/5ea9941519ffb3027527130ec869da14abc5e994))
|
||||
|
||||
# [2.2.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.1.0...@standardnotes/analytics@2.2.0) (2022-11-08)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add persistence for revenue modifications ([4928685](https://github.com/standardnotes/server/commit/49286851989f557d3b391b6b535a9aa307fbef50))
|
||||
* **analytics:** create new ddd architecture for persisting revenue modifications ([0103233](https://github.com/standardnotes/server/commit/0103233d4a1e222e7c9b059475c1cdc3b2617455))
|
||||
|
||||
# [2.1.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.52.0...@standardnotes/analytics@2.1.0) (2022-11-07)
|
||||
|
||||
### Features
|
||||
|
||||
* remove analytics scope from other services in favor of a separate service ([ff1d5db](https://github.com/standardnotes/server/commit/ff1d5db12c93f8e51c07c3aecb9fed4be48ea96a))
|
||||
|
||||
# [1.52.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.51.0...@standardnotes/analytics@1.52.0) (2022-11-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add handling subscription reactivated events ([6359030](https://github.com/standardnotes/server/commit/63590300308975097f4d092a4f140f479093a1ae))
|
||||
|
||||
# [1.51.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.50.0...@standardnotes/analytics@1.51.0) (2022-11-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add handling subscription expired events ([c0f5817](https://github.com/standardnotes/server/commit/c0f5817d4753410ee5d997c1b94e340b400cb5d9))
|
||||
|
||||
# [1.50.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.49.0...@standardnotes/analytics@1.50.0) (2022-11-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add handling subscription purchased events ([f1834d5](https://github.com/standardnotes/server/commit/f1834d58d2215c81322e82a0ec279617103b3260))
|
||||
|
||||
# [1.49.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.48.0...@standardnotes/analytics@1.49.0) (2022-11-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add handling subscription refunded event ([197c991](https://github.com/standardnotes/server/commit/197c9914caf8fb31ce3ee69d05381d2a6416b366))
|
||||
|
||||
# [1.48.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.47.0...@standardnotes/analytics@1.48.0) (2022-11-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add subscription renewed event handler ([2aa57f1](https://github.com/standardnotes/server/commit/2aa57f1f0ddace741b79e9375b87464eaaba6320))
|
||||
|
||||
# [1.47.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.46.0...@standardnotes/analytics@1.47.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add subscription cancelled event handler ([037fb23](https://github.com/standardnotes/server/commit/037fb2398ae9aaa11682e1a8576bab28c69e0f9b))
|
||||
|
||||
# [1.46.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.45.0...@standardnotes/analytics@1.46.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add payment success event handler ([5902cbb](https://github.com/standardnotes/server/commit/5902cbb6218a5b3982b4c56f8d6644f4f5bc6d32))
|
||||
|
||||
# [1.45.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.44.0...@standardnotes/analytics@1.45.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* add payment failed handler and email to analytics entity ([51b12d0](https://github.com/standardnotes/server/commit/51b12d05d49868db1d61313c4d8b3829994e7eb3))
|
||||
|
||||
# [1.44.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.43.0...@standardnotes/analytics@1.44.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** removing analytics entity upon account deletion ([2720a7c](https://github.com/standardnotes/server/commit/2720a7c827a6352cb5254e88d42d45c385921448))
|
||||
|
||||
# [1.43.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.42.0...@standardnotes/analytics@1.43.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add account deletion event handler ([5383e0c](https://github.com/standardnotes/server/commit/5383e0cf525ddd203beee451f1cfd5fd8600b522))
|
||||
|
||||
# [1.42.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.41.0...@standardnotes/analytics@1.42.0) (2022-11-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** imports ([35611fb](https://github.com/standardnotes/server/commit/35611fbc07e50efbba954d7c96db9917e0dddd7d))
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add user registered handler ([034aa38](https://github.com/standardnotes/server/commit/034aa381539cf4b46769cfd1646dec2051d836c3))
|
||||
|
||||
# [1.41.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.40.0...@standardnotes/analytics@1.41.0) (2022-11-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** linter setup with migrations ([262d295](https://github.com/standardnotes/server/commit/262d2951218753f6f14613c5d8ae20ade0e4ef06))
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add retrieving user analytics id ([4e5ac0a](https://github.com/standardnotes/server/commit/4e5ac0a47b469e5fa681f0131d04c9823cebedf3))
|
||||
|
||||
# [1.40.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.39.1...@standardnotes/analytics@1.40.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add analytics entities ([f315b1a](https://github.com/standardnotes/server/commit/f315b1ac5c9369d36fa616c6b4bb5492148564f8))
|
||||
|
||||
## [1.39.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.39.0...@standardnotes/analytics@1.39.1) (2022-11-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analytics:** linter setup ([5329f2a](https://github.com/standardnotes/server/commit/5329f2a2fb815f8691e37279fcadcf01beb716ad))
|
||||
|
||||
# [1.39.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.38.0...@standardnotes/analytics@1.39.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** move the analytics report from api-gateway to analytics ([34e11fd](https://github.com/standardnotes/server/commit/34e11fd5b0ef09a056c90127065c9dfae3e0172a))
|
||||
|
||||
# [1.38.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.37.0...@standardnotes/analytics@1.38.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* add analytics worker service ([d568432](https://github.com/standardnotes/server/commit/d5684326b1301855d0e07415195d4b246292f9a9))
|
||||
|
||||
# [1.37.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.36.0...@standardnotes/analytics@1.37.0) (2022-11-03)
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add analytics for subscription reactivating ([460d6a8](https://github.com/standardnotes/server/commit/460d6a8d0f17eee624feb5d2588086ae6f0996e4))
|
||||
|
||||
# [1.36.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.35.1...@standardnotes/analytics@1.36.0) (2022-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
17
packages/analytics/Dockerfile
Normal file
17
packages/analytics/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM node:18.12.1-alpine
|
||||
|
||||
RUN apk add --update \
|
||||
curl \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
COPY ./ /workspace
|
||||
|
||||
ENTRYPOINT [ "/workspace/packages/analytics/docker/entrypoint.sh" ]
|
||||
|
||||
CMD [ "start-worker" ]
|
||||
@@ -4,34 +4,45 @@ import 'newrelic'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
|
||||
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'
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
import {
|
||||
DomainEventPublisherInterface,
|
||||
DailyAnalyticsReportGeneratedEvent,
|
||||
DomainEventService,
|
||||
} from '@standardnotes/domain-events'
|
||||
import {
|
||||
AnalyticsActivity,
|
||||
AnalyticsStoreInterface,
|
||||
Period,
|
||||
PeriodKeyGeneratorInterface,
|
||||
StatisticsMeasure,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||
import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
||||
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
const requestReport = async (
|
||||
analyticsStore: AnalyticsStoreInterface,
|
||||
statisticsStore: StatisticsStoreInterface,
|
||||
domainEventFactory: DomainEventFactoryInterface,
|
||||
domainEventPublisher: DomainEventPublisherInterface,
|
||||
periodKeyGenerator: PeriodKeyGeneratorInterface,
|
||||
calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
|
||||
timer: TimerInterface,
|
||||
adminEmails: string[],
|
||||
): Promise<void> => {
|
||||
const analyticsOverTime = []
|
||||
await calculateMonthlyRecurringRevenue.execute({})
|
||||
|
||||
const analyticsOverTime: Array<{
|
||||
name: string
|
||||
period: number
|
||||
counts: Array<{
|
||||
periodKey: string
|
||||
totalCount: number
|
||||
}>
|
||||
totalCount: number
|
||||
}> = []
|
||||
|
||||
const thirtyDaysAnalyticsNames = [
|
||||
AnalyticsActivity.GeneralActivity,
|
||||
AnalyticsActivity.EditingItems,
|
||||
AnalyticsActivity.SubscriptionPurchased,
|
||||
AnalyticsActivity.Register,
|
||||
AnalyticsActivity.SubscriptionRenewed,
|
||||
@@ -40,6 +51,7 @@ const requestReport = async (
|
||||
AnalyticsActivity.SubscriptionRefunded,
|
||||
AnalyticsActivity.ExistingCustomersChurn,
|
||||
AnalyticsActivity.NewCustomersChurn,
|
||||
AnalyticsActivity.SubscriptionReactivated,
|
||||
]
|
||||
|
||||
for (const analyticsName of thirtyDaysAnalyticsNames) {
|
||||
@@ -68,12 +80,13 @@ const requestReport = async (
|
||||
}
|
||||
}
|
||||
|
||||
const yesterdayActivityStatistics = []
|
||||
const yesterdayActivityStatistics: Array<{
|
||||
name: string
|
||||
retention: number
|
||||
totalCount: number
|
||||
}> = []
|
||||
const yesterdayActivityNames = [
|
||||
AnalyticsActivity.LimitedDiscountOfferPurchased,
|
||||
AnalyticsActivity.GeneralActivity,
|
||||
AnalyticsActivity.GeneralActivityFreeUsers,
|
||||
AnalyticsActivity.GeneralActivityPaidUsers,
|
||||
AnalyticsActivity.PaymentFailed,
|
||||
AnalyticsActivity.PaymentSuccess,
|
||||
AnalyticsActivity.NewCustomersChurn,
|
||||
@@ -92,6 +105,40 @@ const requestReport = async (
|
||||
})
|
||||
}
|
||||
|
||||
const statisticsOverTime: Array<{
|
||||
name: string
|
||||
period: number
|
||||
counts: Array<{
|
||||
periodKey: string
|
||||
totalCount: number
|
||||
}>
|
||||
}> = []
|
||||
|
||||
const thirtyDaysStatisticsNames = [
|
||||
StatisticsMeasure.MRR,
|
||||
StatisticsMeasure.AnnualPlansMRR,
|
||||
StatisticsMeasure.MonthlyPlansMRR,
|
||||
StatisticsMeasure.FiveYearPlansMRR,
|
||||
StatisticsMeasure.PlusPlansMRR,
|
||||
StatisticsMeasure.ProPlansMRR,
|
||||
]
|
||||
for (const statisticName of thirtyDaysStatisticsNames) {
|
||||
statisticsOverTime.push({
|
||||
name: statisticName,
|
||||
period: Period.Last30DaysIncludingToday,
|
||||
counts: await statisticsStore.calculateTotalCountOverPeriod(statisticName, Period.Last30DaysIncludingToday),
|
||||
})
|
||||
}
|
||||
|
||||
const monthlyStatisticsNames = [StatisticsMeasure.MRR]
|
||||
for (const statisticName of monthlyStatisticsNames) {
|
||||
statisticsOverTime.push({
|
||||
name: statisticName,
|
||||
period: Period.ThisYear,
|
||||
counts: await statisticsStore.calculateTotalCountOverPeriod(statisticName, Period.ThisYear),
|
||||
})
|
||||
}
|
||||
|
||||
const statisticMeasureNames = [
|
||||
StatisticsMeasure.Income,
|
||||
StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome,
|
||||
@@ -107,13 +154,16 @@ const requestReport = async (
|
||||
StatisticsMeasure.SubscriptionLength,
|
||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
||||
StatisticsMeasure.NotesCountFreeUsers,
|
||||
StatisticsMeasure.NotesCountPaidUsers,
|
||||
StatisticsMeasure.FilesCount,
|
||||
StatisticsMeasure.NewCustomers,
|
||||
StatisticsMeasure.TotalCustomers,
|
||||
]
|
||||
const statisticMeasures = []
|
||||
const statisticMeasures: Array<{
|
||||
name: string
|
||||
totalValue: number
|
||||
average: number
|
||||
increments: number
|
||||
period: number
|
||||
}> = []
|
||||
for (const statisticMeasureName of statisticMeasureNames) {
|
||||
for (const period of [Period.Yesterday, Period.ThisMonth]) {
|
||||
statisticMeasures.push({
|
||||
@@ -126,27 +176,14 @@ const requestReport = async (
|
||||
}
|
||||
}
|
||||
|
||||
const periodKeys = periodKeyGenerator.getDiscretePeriodKeys(Period.Last7Days)
|
||||
const retentionOverDays = []
|
||||
for (let i = 0; i < periodKeys.length; i++) {
|
||||
for (let j = 0; j < periodKeys.length - i; j++) {
|
||||
const dailyRetention = await analyticsStore.calculateActivitiesRetention({
|
||||
firstActivity: AnalyticsActivity.Register,
|
||||
firstActivityPeriodKey: periodKeys[i],
|
||||
secondActivity: AnalyticsActivity.GeneralActivity,
|
||||
secondActivityPeriodKey: periodKeys[i + j],
|
||||
})
|
||||
|
||||
retentionOverDays.push({
|
||||
firstPeriodKey: periodKeys[i],
|
||||
secondPeriodKey: periodKeys[i + j],
|
||||
value: dailyRetention,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const monthlyPeriodKeys = periodKeyGenerator.getDiscretePeriodKeys(Period.ThisYear)
|
||||
const churnRates = []
|
||||
const churnRates: Array<{
|
||||
rate: number
|
||||
periodKey: string
|
||||
averageCustomersCount: number
|
||||
existingCustomersChurn: number
|
||||
newCustomersChurn: number
|
||||
}> = []
|
||||
for (const monthPeriodKey of monthlyPeriodKeys) {
|
||||
const monthPeriod = periodKeyGenerator.convertPeriodKeyToPeriod(monthPeriodKey)
|
||||
const dailyPeriodKeys = periodKeyGenerator.getDiscretePeriodKeys(monthPeriod)
|
||||
@@ -175,44 +212,35 @@ const requestReport = async (
|
||||
churnRates.push({
|
||||
periodKey: monthPeriodKey,
|
||||
rate: averageCustomersCount ? (totalChurn / averageCustomersCount) * 100 : 0,
|
||||
averageCustomersCount,
|
||||
existingCustomersChurn,
|
||||
newCustomersChurn,
|
||||
})
|
||||
}
|
||||
|
||||
const event: DailyAnalyticsReportGeneratedEvent = {
|
||||
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
|
||||
createdAt: new Date(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: '',
|
||||
userIdentifierType: 'uuid',
|
||||
},
|
||||
origin: DomainEventService.ApiGateway,
|
||||
},
|
||||
payload: {
|
||||
applicationStatistics: await statisticsStore.getYesterdayApplicationUsage(),
|
||||
snjsStatistics: await statisticsStore.getYesterdaySNJSUsage(),
|
||||
outOfSyncIncidents: await statisticsStore.getYesterdayOutOfSyncIncidents(),
|
||||
activityStatistics: yesterdayActivityStatistics,
|
||||
activityStatisticsOverTime: analyticsOverTime,
|
||||
statisticMeasures,
|
||||
retentionStatistics: [
|
||||
{
|
||||
firstActivity: AnalyticsActivity.Register,
|
||||
secondActivity: AnalyticsActivity.GeneralActivity,
|
||||
retention: {
|
||||
periodKeys,
|
||||
values: retentionOverDays,
|
||||
for (const adminEmail of adminEmails) {
|
||||
await domainEventPublisher.publish(
|
||||
domainEventFactory.createEmailRequestedEvent({
|
||||
messageIdentifier: 'VERSION_ADOPTION_REPORT',
|
||||
subject: getSubject(),
|
||||
body: getBody(
|
||||
{
|
||||
activityStatistics: yesterdayActivityStatistics,
|
||||
activityStatisticsOverTime: analyticsOverTime,
|
||||
statisticsOverTime,
|
||||
statisticMeasures,
|
||||
churn: {
|
||||
periodKeys: monthlyPeriodKeys,
|
||||
values: churnRates,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
churn: {
|
||||
periodKeys: monthlyPeriodKeys,
|
||||
values: churnRates,
|
||||
},
|
||||
},
|
||||
timer,
|
||||
),
|
||||
level: EmailLevel.LEVELS.System,
|
||||
userEmail: adminEmail,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
await domainEventPublisher.publish(event)
|
||||
}
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
@@ -226,10 +254,29 @@ void container.load().then((container) => {
|
||||
|
||||
const analyticsStore: AnalyticsStoreInterface = container.get(TYPES.AnalyticsStore)
|
||||
const statisticsStore: StatisticsStoreInterface = container.get(TYPES.StatisticsStore)
|
||||
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
|
||||
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
|
||||
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
|
||||
const timer: TimerInterface = container.get(TYPES.Timer)
|
||||
const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
|
||||
TYPES.CalculateMonthlyRecurringRevenue,
|
||||
)
|
||||
const adminEmails = container.get(TYPES.ADMIN_EMAILS) as string[]
|
||||
|
||||
Promise.resolve(requestReport(analyticsStore, statisticsStore, domainEventPublisher, periodKeyGenerator))
|
||||
logger.info('Sending report to following admins: %O', adminEmails)
|
||||
|
||||
Promise.resolve(
|
||||
requestReport(
|
||||
analyticsStore,
|
||||
statisticsStore,
|
||||
domainEventFactory,
|
||||
domainEventPublisher,
|
||||
periodKeyGenerator,
|
||||
calculateMonthlyRecurringRevenue,
|
||||
timer,
|
||||
adminEmails,
|
||||
),
|
||||
)
|
||||
.then(() => {
|
||||
logger.info('Usage report generation complete')
|
||||
|
||||
29
packages/analytics/bin/worker.ts
Normal file
29
packages/analytics/bin/worker.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import 'newrelic'
|
||||
|
||||
import { Logger } from 'winston'
|
||||
import { DomainEventSubscriberFactoryInterface } from '@standardnotes/domain-events'
|
||||
import * as dayjs from 'dayjs'
|
||||
import * as utc from 'dayjs/plugin/utc'
|
||||
|
||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||
import TYPES from '../src/Bootstrap/Types'
|
||||
import { Env } from '../src/Bootstrap/Env'
|
||||
|
||||
const container = new ContainerConfigLoader()
|
||||
void container.load().then((container) => {
|
||||
dayjs.extend(utc)
|
||||
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const logger: Logger = container.get(TYPES.Logger)
|
||||
|
||||
logger.info('Starting worker...')
|
||||
|
||||
const subscriberFactory: DomainEventSubscriberFactoryInterface = container.get(TYPES.DomainEventSubscriberFactory)
|
||||
subscriberFactory.create().start()
|
||||
|
||||
setInterval(() => logger.info('Alive and kicking!'), 20 * 60 * 1000)
|
||||
})
|
||||
22
packages/analytics/docker/entrypoint.sh
Executable file
22
packages/analytics/docker/entrypoint.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
COMMAND=$1 && shift 1
|
||||
|
||||
case "$COMMAND" in
|
||||
'start-worker' )
|
||||
echo "[Docker] Starting Worker..."
|
||||
yarn workspace @standardnotes/analytics worker
|
||||
;;
|
||||
|
||||
'report' )
|
||||
echo "[Docker] Starting Usage Report Generation..."
|
||||
yarn workspace @standardnotes/analytics report
|
||||
;;
|
||||
|
||||
* )
|
||||
echo "[Docker] Unknown command"
|
||||
;;
|
||||
esac
|
||||
|
||||
exec "$@"
|
||||
@@ -7,4 +7,5 @@ module.exports = {
|
||||
transform: {
|
||||
...tsjPreset.transform,
|
||||
},
|
||||
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/'],
|
||||
}
|
||||
|
||||
16
packages/analytics/migrations/1667555285111-init_database.ts
Normal file
16
packages/analytics/migrations/1667555285111-init_database.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class initDatabase1667555285111 implements MigrationInterface {
|
||||
name = 'initDatabase1667555285111'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE `analytics_entities` (`id` int NOT NULL AUTO_INCREMENT, `user_uuid` varchar(36) NOT NULL, INDEX `user_uuid` (`user_uuid`), PRIMARY KEY (`id`)) ENGINE=InnoDB',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `user_uuid` ON `analytics_entities`')
|
||||
await queryRunner.query('DROP TABLE `analytics_entities`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addEmailToAnalyticsEntity1667568051894 implements MigrationInterface {
|
||||
name = 'addEmailToAnalyticsEntity1667568051894'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `analytics_entities` ADD `user_email` varchar(255) NULL')
|
||||
await queryRunner.query('CREATE INDEX `email` ON `analytics_entities` (`user_email`)')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `email` ON `analytics_entities`')
|
||||
await queryRunner.query('ALTER TABLE `analytics_entities` DROP COLUMN `user_email`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addRevenueModifications1667912580964 implements MigrationInterface {
|
||||
name = 'addRevenueModifications1667912580964'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'CREATE TABLE `revenue_modifications` (`uuid` varchar(36) NOT NULL, `subscription_id` int NOT NULL, `user_email` varchar(255) NOT NULL, `user_uuid` varchar(36) NOT NULL, `event_type` varchar(255) NOT NULL, `subscription_plan` varchar(255) NOT NULL, `billing_frequency` int NOT NULL, `new_customer` tinyint NOT NULL, `previous_mrr` int NOT NULL, `new_mrr` int NOT NULL, INDEX `email` (`user_email`), INDEX `user_uuid` (`user_uuid`), PRIMARY KEY (`uuid`)) ENGINE=InnoDB',
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `user_uuid` ON `revenue_modifications`')
|
||||
await queryRunner.query('DROP INDEX `email` ON `revenue_modifications`')
|
||||
await queryRunner.query('DROP TABLE `revenue_modifications`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addMissingCreatedAt1667994036734 implements MigrationInterface {
|
||||
name = 'addMissingCreatedAt1667994036734'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` ADD `created_at` bigint NOT NULL')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` DROP COLUMN `created_at`')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class fixMrrFloatingColumns1667995681714 implements MigrationInterface {
|
||||
name = 'fixMrrFloatingColumns1667995681714'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` DROP COLUMN `previous_mrr`')
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` ADD `previous_mrr` float NOT NULL')
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` DROP COLUMN `new_mrr`')
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` ADD `new_mrr` float NOT NULL')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` DROP COLUMN `new_mrr`')
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` ADD `new_mrr` int NOT NULL')
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` DROP COLUMN `previous_mrr`')
|
||||
await queryRunner.query('ALTER TABLE `revenue_modifications` ADD `previous_mrr` int NOT NULL')
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "1.36.0",
|
||||
"version": "2.12.22",
|
||||
"engines": {
|
||||
"node": ">=14.0.0 <17.0.0"
|
||||
"node": ">=18.0.0 <19.0.0"
|
||||
},
|
||||
"private": true,
|
||||
"description": "Analytics tools for Standard Notes projects",
|
||||
"main": "dist/src/index.js",
|
||||
"author": "Standard Notes",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"files": [
|
||||
"dist/src/**/*.js",
|
||||
"dist/src/**/*.d.ts"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -20,19 +17,42 @@
|
||||
"clean": "rm -fr dist",
|
||||
"build": "tsc --build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "jest spec --coverage"
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"test": "jest --coverage --config=./jest.config.js --maxWorkers=50%",
|
||||
"worker": "yarn node dist/bin/worker.js",
|
||||
"report": "yarn node dist/bin/report.js",
|
||||
"setup:env": "cp .env.sample .env",
|
||||
"typeorm": "typeorm-ts-node-commonjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ioredis": "^4.28.10",
|
||||
"@types/ioredis": "^5.0.0",
|
||||
"@types/jest": "^29.1.1",
|
||||
"@types/newrelic": "^7.0.4",
|
||||
"@types/node": "^18.11.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^29.1.2",
|
||||
"ts-jest": "^29.0.3",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"ioredis": "^5.2.3",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
"@newrelic/winston-enricher": "^4.0.0",
|
||||
"@sentry/node": "^7.19.0",
|
||||
"@standardnotes/common": "workspace:*",
|
||||
"@standardnotes/domain-core": "workspace:^",
|
||||
"@standardnotes/domain-events": "workspace:*",
|
||||
"@standardnotes/domain-events-infra": "workspace:*",
|
||||
"@standardnotes/time": "workspace:*",
|
||||
"aws-sdk": "^2.1260.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"dotenv": "^16.0.1",
|
||||
"inversify": "^6.0.1",
|
||||
"ioredis": "^5.2.4",
|
||||
"mysql2": "^2.3.3",
|
||||
"newrelic": "^9.6.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"typeorm": "^0.3.10",
|
||||
"winston": "^3.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
261
packages/analytics/src/Bootstrap/Container.ts
Normal file
261
packages/analytics/src/Bootstrap/Container.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import * as winston from 'winston'
|
||||
import Redis from 'ioredis'
|
||||
import * as AWS from 'aws-sdk'
|
||||
import { Container } from 'inversify'
|
||||
import {
|
||||
DomainEventHandlerInterface,
|
||||
DomainEventMessageHandlerInterface,
|
||||
DomainEventSubscriberFactoryInterface,
|
||||
} from '@standardnotes/domain-events'
|
||||
import { MapperInterface } from '@standardnotes/domain-core'
|
||||
|
||||
import { Env } from './Env'
|
||||
import TYPES from './Types'
|
||||
import { AppDataSource } from './DataSource'
|
||||
import { DomainEventFactory } from '../Domain/Event/DomainEventFactory'
|
||||
import {
|
||||
RedisDomainEventPublisher,
|
||||
RedisDomainEventSubscriberFactory,
|
||||
RedisEventMessageHandler,
|
||||
SNSDomainEventPublisher,
|
||||
SQSDomainEventSubscriberFactory,
|
||||
SQSEventMessageHandler,
|
||||
SQSNewRelicEventMessageHandler,
|
||||
} from '@standardnotes/domain-events-infra'
|
||||
import { Timer, TimerInterface } from '@standardnotes/time'
|
||||
import { PeriodKeyGeneratorInterface } from '../Domain/Time/PeriodKeyGeneratorInterface'
|
||||
import { PeriodKeyGenerator } from '../Domain/Time/PeriodKeyGenerator'
|
||||
import { AnalyticsStoreInterface } from '../Domain/Analytics/AnalyticsStoreInterface'
|
||||
import { RedisAnalyticsStore } from '../Infra/Redis/RedisAnalyticsStore'
|
||||
import { StatisticsStoreInterface } from '../Domain/Statistics/StatisticsStoreInterface'
|
||||
import { RedisStatisticsStore } from '../Infra/Redis/RedisStatisticsStore'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../Domain/Entity/AnalyticsEntityRepositoryInterface'
|
||||
import { MySQLAnalyticsEntityRepository } from '../Infra/MySQL/MySQLAnalyticsEntityRepository'
|
||||
import { Repository } from 'typeorm'
|
||||
import { AnalyticsEntity } from '../Domain/Entity/AnalyticsEntity'
|
||||
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRegisteredEventHandler } from '../Domain/Handler/UserRegisteredEventHandler'
|
||||
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
|
||||
import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventHandler'
|
||||
import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEventHandler'
|
||||
import { SubscriptionCancelledEventHandler } from '../Domain/Handler/SubscriptionCancelledEventHandler'
|
||||
import { SubscriptionRenewedEventHandler } from '../Domain/Handler/SubscriptionRenewedEventHandler'
|
||||
import { SubscriptionRefundedEventHandler } from '../Domain/Handler/SubscriptionRefundedEventHandler'
|
||||
import { SubscriptionPurchasedEventHandler } from '../Domain/Handler/SubscriptionPurchasedEventHandler'
|
||||
import { SubscriptionExpiredEventHandler } from '../Domain/Handler/SubscriptionExpiredEventHandler'
|
||||
import { SubscriptionReactivatedEventHandler } from '../Domain/Handler/SubscriptionReactivatedEventHandler'
|
||||
import { RefundProcessedEventHandler } from '../Domain/Handler/RefundProcessedEventHandler'
|
||||
import { RevenueModificationRepositoryInterface } from '../Domain/Revenue/RevenueModificationRepositoryInterface'
|
||||
import { MySQLRevenueModificationRepository } from '../Infra/MySQL/MySQLRevenueModificationRepository'
|
||||
import { TypeORMRevenueModification } from '../Infra/TypeORM/TypeORMRevenueModification'
|
||||
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'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
|
||||
export class ContainerConfigLoader {
|
||||
async load(): Promise<Container> {
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const container = new Container()
|
||||
|
||||
await AppDataSource.initialize()
|
||||
|
||||
const redisUrl = env.get('REDIS_URL')
|
||||
const isRedisInClusterMode = redisUrl.indexOf(',') > 0
|
||||
let redis
|
||||
if (isRedisInClusterMode) {
|
||||
redis = new Redis.Cluster(redisUrl.split(','))
|
||||
} else {
|
||||
redis = new Redis(redisUrl)
|
||||
}
|
||||
|
||||
container.bind(TYPES.Redis).toConstantValue(redis)
|
||||
|
||||
const newrelicWinstonFormatter = newrelicFormatter(winston)
|
||||
const winstonFormatters = [winston.format.splat(), winston.format.json()]
|
||||
if (env.get('NEW_RELIC_ENABLED', true) === 'true') {
|
||||
winstonFormatters.push(newrelicWinstonFormatter())
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: env.get('LOG_LEVEL') || 'info',
|
||||
format: winston.format.combine(...winstonFormatters),
|
||||
transports: [new winston.transports.Console({ level: env.get('LOG_LEVEL') || 'info' })],
|
||||
})
|
||||
container.bind<winston.Logger>(TYPES.Logger).toConstantValue(logger)
|
||||
|
||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||
const snsConfig: AWS.SNS.Types.ClientConfiguration = {
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SNS_AWS_REGION', true),
|
||||
}
|
||||
if (env.get('SNS_ENDPOINT', true)) {
|
||||
snsConfig.endpoint = env.get('SNS_ENDPOINT', true)
|
||||
}
|
||||
if (env.get('SNS_DISABLE_SSL', true) === 'true') {
|
||||
snsConfig.sslEnabled = false
|
||||
}
|
||||
if (env.get('SNS_ACCESS_KEY_ID', true) && env.get('SNS_SECRET_ACCESS_KEY', true)) {
|
||||
snsConfig.credentials = {
|
||||
accessKeyId: env.get('SNS_ACCESS_KEY_ID', true),
|
||||
secretAccessKey: env.get('SNS_SECRET_ACCESS_KEY', true),
|
||||
}
|
||||
}
|
||||
container.bind<AWS.SNS>(TYPES.SNS).toConstantValue(new AWS.SNS(snsConfig))
|
||||
}
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
const sqsConfig: AWS.SQS.Types.ClientConfiguration = {
|
||||
apiVersion: 'latest',
|
||||
region: env.get('SQS_AWS_REGION', true),
|
||||
}
|
||||
if (env.get('SQS_ACCESS_KEY_ID', true) && env.get('SQS_SECRET_ACCESS_KEY', true)) {
|
||||
sqsConfig.credentials = {
|
||||
accessKeyId: env.get('SQS_ACCESS_KEY_ID', true),
|
||||
secretAccessKey: env.get('SQS_SECRET_ACCESS_KEY', true),
|
||||
}
|
||||
}
|
||||
container.bind<AWS.SQS>(TYPES.SQS).toConstantValue(new AWS.SQS(sqsConfig))
|
||||
}
|
||||
|
||||
// env vars
|
||||
container.bind(TYPES.REDIS_URL).toConstantValue(env.get('REDIS_URL'))
|
||||
container.bind(TYPES.SNS_TOPIC_ARN).toConstantValue(env.get('SNS_TOPIC_ARN', true))
|
||||
container.bind(TYPES.SNS_AWS_REGION).toConstantValue(env.get('SNS_AWS_REGION', true))
|
||||
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
|
||||
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(','))
|
||||
|
||||
// Repositories
|
||||
container
|
||||
.bind<AnalyticsEntityRepositoryInterface>(TYPES.AnalyticsEntityRepository)
|
||||
.to(MySQLAnalyticsEntityRepository)
|
||||
container
|
||||
.bind<RevenueModificationRepositoryInterface>(TYPES.RevenueModificationRepository)
|
||||
.to(MySQLRevenueModificationRepository)
|
||||
|
||||
// ORM
|
||||
container
|
||||
.bind<Repository<AnalyticsEntity>>(TYPES.ORMAnalyticsEntityRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(AnalyticsEntity))
|
||||
container
|
||||
.bind<Repository<TypeORMRevenueModification>>(TYPES.ORMRevenueModificationRepository)
|
||||
.toConstantValue(AppDataSource.getRepository(TypeORMRevenueModification))
|
||||
|
||||
// Use Case
|
||||
container.bind<GetUserAnalyticsId>(TYPES.GetUserAnalyticsId).to(GetUserAnalyticsId)
|
||||
container.bind<SaveRevenueModification>(TYPES.SaveRevenueModification).to(SaveRevenueModification)
|
||||
container
|
||||
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
|
||||
.to(CalculateMonthlyRecurringRevenue)
|
||||
|
||||
// Hanlders
|
||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
|
||||
.to(AccountDeletionRequestedEventHandler)
|
||||
container.bind<PaymentFailedEventHandler>(TYPES.PaymentFailedEventHandler).to(PaymentFailedEventHandler)
|
||||
container.bind<PaymentSuccessEventHandler>(TYPES.PaymentSuccessEventHandler).to(PaymentSuccessEventHandler)
|
||||
container
|
||||
.bind<SubscriptionCancelledEventHandler>(TYPES.SubscriptionCancelledEventHandler)
|
||||
.to(SubscriptionCancelledEventHandler)
|
||||
container
|
||||
.bind<SubscriptionRenewedEventHandler>(TYPES.SubscriptionRenewedEventHandler)
|
||||
.to(SubscriptionRenewedEventHandler)
|
||||
container
|
||||
.bind<SubscriptionRefundedEventHandler>(TYPES.SubscriptionRefundedEventHandler)
|
||||
.to(SubscriptionRefundedEventHandler)
|
||||
container
|
||||
.bind<SubscriptionPurchasedEventHandler>(TYPES.SubscriptionPurchasedEventHandler)
|
||||
.to(SubscriptionPurchasedEventHandler)
|
||||
container
|
||||
.bind<SubscriptionExpiredEventHandler>(TYPES.SubscriptionExpiredEventHandler)
|
||||
.to(SubscriptionExpiredEventHandler)
|
||||
container
|
||||
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
||||
.to(SubscriptionReactivatedEventHandler)
|
||||
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
||||
|
||||
// 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)],
|
||||
['PAYMENT_FAILED', container.get(TYPES.PaymentFailedEventHandler)],
|
||||
['PAYMENT_SUCCESS', container.get(TYPES.PaymentSuccessEventHandler)],
|
||||
['SUBSCRIPTION_CANCELLED', container.get(TYPES.SubscriptionCancelledEventHandler)],
|
||||
['SUBSCRIPTION_RENEWED', container.get(TYPES.SubscriptionRenewedEventHandler)],
|
||||
['SUBSCRIPTION_REFUNDED', container.get(TYPES.SubscriptionRefundedEventHandler)],
|
||||
['SUBSCRIPTION_PURCHASED', container.get(TYPES.SubscriptionPurchasedEventHandler)],
|
||||
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
|
||||
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
||||
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
||||
])
|
||||
|
||||
if (env.get('SQS_QUEUE_URL', true)) {
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
|
||||
.toConstantValue(
|
||||
env.get('NEW_RELIC_ENABLED', true) === 'true'
|
||||
? new SQSNewRelicEventMessageHandler(eventHandlers, container.get(TYPES.Logger))
|
||||
: new SQSEventMessageHandler(eventHandlers, container.get(TYPES.Logger)),
|
||||
)
|
||||
container
|
||||
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
|
||||
.toConstantValue(
|
||||
new SQSDomainEventSubscriberFactory(
|
||||
container.get(TYPES.SQS),
|
||||
container.get(TYPES.SQS_QUEUE_URL),
|
||||
container.get(TYPES.DomainEventMessageHandler),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
container
|
||||
.bind<DomainEventMessageHandlerInterface>(TYPES.DomainEventMessageHandler)
|
||||
.toConstantValue(new RedisEventMessageHandler(eventHandlers, container.get(TYPES.Logger)))
|
||||
container
|
||||
.bind<DomainEventSubscriberFactoryInterface>(TYPES.DomainEventSubscriberFactory)
|
||||
.toConstantValue(
|
||||
new RedisDomainEventSubscriberFactory(
|
||||
container.get(TYPES.Redis),
|
||||
container.get(TYPES.DomainEventMessageHandler),
|
||||
container.get(TYPES.REDIS_EVENTS_CHANNEL),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
}
|
||||
44
packages/analytics/src/Bootstrap/DataSource.ts
Normal file
44
packages/analytics/src/Bootstrap/DataSource.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { DataSource, LoggerOptions } from 'typeorm'
|
||||
|
||||
import { AnalyticsEntity } from '../Domain/Entity/AnalyticsEntity'
|
||||
import { TypeORMRevenueModification } from '../Infra/TypeORM/TypeORMRevenueModification'
|
||||
|
||||
import { Env } from './Env'
|
||||
|
||||
const env: Env = new Env()
|
||||
env.load()
|
||||
|
||||
const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||
: 45_000
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'mysql',
|
||||
charset: 'utf8mb4',
|
||||
supportBigNumbers: true,
|
||||
bigNumberStrings: false,
|
||||
maxQueryExecutionTime,
|
||||
replication: {
|
||||
master: {
|
||||
host: env.get('DB_HOST'),
|
||||
port: parseInt(env.get('DB_PORT')),
|
||||
username: env.get('DB_USERNAME'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
},
|
||||
slaves: [
|
||||
{
|
||||
host: env.get('DB_REPLICA_HOST'),
|
||||
port: parseInt(env.get('DB_PORT')),
|
||||
username: env.get('DB_USERNAME'),
|
||||
password: env.get('DB_PASSWORD'),
|
||||
database: env.get('DB_DATABASE'),
|
||||
},
|
||||
],
|
||||
removeNodeErrorCount: 10,
|
||||
},
|
||||
entities: [AnalyticsEntity, TypeORMRevenueModification],
|
||||
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
|
||||
migrationsRun: true,
|
||||
logging: <LoggerOptions>env.get('DB_DEBUG_LEVEL'),
|
||||
})
|
||||
24
packages/analytics/src/Bootstrap/Env.ts
Normal file
24
packages/analytics/src/Bootstrap/Env.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { config, DotenvParseOutput } from 'dotenv'
|
||||
import { injectable } from 'inversify'
|
||||
|
||||
@injectable()
|
||||
export class Env {
|
||||
private env?: DotenvParseOutput
|
||||
|
||||
public load(): void {
|
||||
const output = config()
|
||||
this.env = <DotenvParseOutput>output.parsed
|
||||
}
|
||||
|
||||
public get(key: string, optional = false): string {
|
||||
if (!this.env) {
|
||||
this.load()
|
||||
}
|
||||
|
||||
if (!process.env[key] && !optional) {
|
||||
throw new Error(`Environment variable ${key} not set`)
|
||||
}
|
||||
|
||||
return <string>process.env[key]
|
||||
}
|
||||
}
|
||||
50
packages/analytics/src/Bootstrap/Types.ts
Normal file
50
packages/analytics/src/Bootstrap/Types.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
const TYPES = {
|
||||
Logger: Symbol.for('Logger'),
|
||||
Redis: Symbol.for('Redis'),
|
||||
SNS: Symbol.for('SNS'),
|
||||
SQS: Symbol.for('SQS'),
|
||||
// env vars
|
||||
REDIS_URL: Symbol.for('REDIS_URL'),
|
||||
SNS_TOPIC_ARN: Symbol.for('SNS_TOPIC_ARN'),
|
||||
SNS_AWS_REGION: Symbol.for('SNS_AWS_REGION'),
|
||||
SQS_QUEUE_URL: Symbol.for('SQS_QUEUE_URL'),
|
||||
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||
ADMIN_EMAILS: Symbol.for('ADMIN_EMAILS'),
|
||||
// Repositories
|
||||
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
|
||||
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
|
||||
// ORM
|
||||
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
|
||||
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
|
||||
// Use Case
|
||||
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
||||
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
|
||||
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
|
||||
// Handlers
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
PaymentFailedEventHandler: Symbol.for('PaymentFailedEventHandler'),
|
||||
PaymentSuccessEventHandler: Symbol.for('PaymentSuccessEventHandler'),
|
||||
SubscriptionCancelledEventHandler: Symbol.for('SubscriptionCancelledEventHandler'),
|
||||
SubscriptionRenewedEventHandler: Symbol.for('SubscriptionRenewedEventHandler'),
|
||||
SubscriptionRefundedEventHandler: Symbol.for('SubscriptionRefundedEventHandler'),
|
||||
SubscriptionPurchasedEventHandler: Symbol.for('SubscriptionPurchasedEventHandler'),
|
||||
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
|
||||
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
||||
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
||||
// Maps
|
||||
RevenueModificationMap: Symbol.for('RevenueModificationMap'),
|
||||
// Services
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
DomainEventFactory: Symbol.for('DomainEventFactory'),
|
||||
DomainEventMessageHandler: Symbol.for('DomainEventMessageHandler'),
|
||||
AnalyticsStore: Symbol.for('AnalyticsStore'),
|
||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||
Timer: Symbol.for('Timer'),
|
||||
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
|
||||
}
|
||||
|
||||
export default TYPES
|
||||
@@ -1,10 +1,4 @@
|
||||
export enum AnalyticsActivity {
|
||||
GeneralActivity = 'general-activity',
|
||||
GeneralActivityFreeUsers = 'general-activity-free-users',
|
||||
GeneralActivityPaidUsers = 'general-activity-paid-users',
|
||||
EditingItems = 'editing-items',
|
||||
CheckingIntegrity = 'checking-integrity',
|
||||
Login = 'login',
|
||||
Register = 'register',
|
||||
DeleteAccount = 'DeleteAccount',
|
||||
SubscriptionPurchased = 'subscription-purchased',
|
||||
@@ -12,8 +6,7 @@ export enum AnalyticsActivity {
|
||||
SubscriptionRefunded = 'subscription-refunded',
|
||||
SubscriptionCancelled = 'subscription-cancelled',
|
||||
SubscriptionExpired = 'subscription-expired',
|
||||
EmailUnbackedUpData = 'email-unbacked-up-data',
|
||||
EmailBackup = 'email-backup',
|
||||
SubscriptionReactivated = 'subscription-reactivated',
|
||||
LimitedDiscountOfferPurchased = 'limited-discount-offer-purchased',
|
||||
PaymentFailed = 'payment-failed',
|
||||
PaymentSuccess = 'payment-success',
|
||||
|
||||
11
packages/analytics/src/Domain/Email/DailyAnalyticsReport.ts
Normal file
11
packages/analytics/src/Domain/Email/DailyAnalyticsReport.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { html } from './daily-analytics-report.html'
|
||||
|
||||
export function getSubject(): string {
|
||||
return `Daily analytics report ${new Date().toLocaleDateString('en-US')}`
|
||||
}
|
||||
|
||||
export function getBody(data: unknown, timer: TimerInterface): string {
|
||||
return html(data, timer)
|
||||
}
|
||||
@@ -0,0 +1,966 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
const getChartUrls = (
|
||||
data: any,
|
||||
): {
|
||||
subscriptions: string
|
||||
users: string
|
||||
quarterlyPerformance: string
|
||||
churn: string
|
||||
mrr: string
|
||||
mrrMonthly: string
|
||||
} => {
|
||||
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
|
||||
)
|
||||
const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
|
||||
)
|
||||
const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
|
||||
)
|
||||
const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
|
||||
)
|
||||
const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
|
||||
)
|
||||
|
||||
const subscriptionsLinerOverTimeConfig = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: subscriptionPurchasingOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Subscription Purchases',
|
||||
backgroundColor: 'rgb(25, 255, 140)',
|
||||
borderColor: 'rgb(25, 255, 140)',
|
||||
data: subscriptionPurchasingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||
fill: false,
|
||||
pointRadius: 2,
|
||||
},
|
||||
{
|
||||
label: 'Subscription Renewals',
|
||||
backgroundColor: 'rgb(54, 162, 235)',
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
data: subscriptionRenewingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||
fill: false,
|
||||
pointRadius: 2,
|
||||
},
|
||||
{
|
||||
label: 'Subscription Refunds',
|
||||
backgroundColor: 'rgb(255, 221, 51)',
|
||||
borderColor: 'rgb(255, 221, 51)',
|
||||
data: subscriptionRefundingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||
fill: false,
|
||||
pointRadius: 2,
|
||||
},
|
||||
{
|
||||
label: 'Subscription Cancels',
|
||||
backgroundColor: 'rgb(255, 99, 132)',
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
data: subscriptionCancelledOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||
fill: false,
|
||||
pointRadius: 2,
|
||||
},
|
||||
{
|
||||
label: 'Subscription Reactivations',
|
||||
backgroundColor: 'rgb(221, 51, 255)',
|
||||
borderColor: 'rgb(221, 51, 255)',
|
||||
data: subscriptionReactivatedOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||
fill: false,
|
||||
pointRadius: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const userRegistrationOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
|
||||
)
|
||||
const userDeletionOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
|
||||
)
|
||||
|
||||
const usersLinerOverTimeConfig = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: userRegistrationOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
|
||||
datasets: [
|
||||
{
|
||||
label: 'User Registrations',
|
||||
backgroundColor: 'rgb(25, 255, 140)',
|
||||
borderColor: 'rgb(25, 255, 140)',
|
||||
data: userRegistrationOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||
fill: false,
|
||||
pointRadius: 2,
|
||||
},
|
||||
{
|
||||
label: 'Account Deletions',
|
||||
backgroundColor: 'rgb(255, 99, 132)',
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
data: userDeletionOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||
fill: false,
|
||||
pointRadius: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const quarters = [Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear]
|
||||
const quarterlyUserRegistrations = []
|
||||
const quarterlySubscriptionPurchases = []
|
||||
const quarterlySubscriptionRenewals = []
|
||||
for (const quarter of quarters) {
|
||||
const registrations =
|
||||
data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.Register && a.period === quarter,
|
||||
)?.totalCount ?? 0
|
||||
const purchases =
|
||||
data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === quarter,
|
||||
)?.totalCount ?? 0
|
||||
const renewals =
|
||||
data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === quarter,
|
||||
)?.totalCount ?? 0
|
||||
quarterlyUserRegistrations.push(registrations)
|
||||
quarterlySubscriptionPurchases.push(purchases)
|
||||
quarterlySubscriptionRenewals.push(renewals)
|
||||
}
|
||||
|
||||
const quarterlyConfig = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'User Registrations',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.5)',
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
borderWidth: 1,
|
||||
data: quarterlyUserRegistrations,
|
||||
},
|
||||
{
|
||||
label: 'Subscription Purchases',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
||||
borderColor: 'rgb(54, 162, 235)',
|
||||
borderWidth: 1,
|
||||
data: quarterlySubscriptionPurchases,
|
||||
},
|
||||
{
|
||||
label: 'Subscription Renewals',
|
||||
backgroundColor: 'rgb(25, 255, 140, 0.5)',
|
||||
borderColor: 'rgb(25, 255, 140)',
|
||||
borderWidth: 1,
|
||||
data: quarterlySubscriptionRenewals,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Quarterly Performance',
|
||||
},
|
||||
plugins: {
|
||||
datalabels: {
|
||||
anchor: 'center',
|
||||
align: 'center',
|
||||
color: '#666',
|
||||
font: {
|
||||
weight: 'normal',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const monthlyChurnRates = data.churn.values.map((value: { rate: number }) => +value.rate.toFixed(2))
|
||||
|
||||
const churnConfig = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Churn Percent',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.5)',
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
borderWidth: 1,
|
||||
data: monthlyChurnRates,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Monthly Churn Rate',
|
||||
},
|
||||
plugins: {
|
||||
datalabels: {
|
||||
anchor: 'center',
|
||||
align: 'center',
|
||||
color: '#666',
|
||||
font: {
|
||||
weight: 'normal',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
const mrrMonthlyConfig = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: 'MRR',
|
||||
backgroundColor: 'rgba(25, 255, 140, 0.5)',
|
||||
borderColor: 'rgb(25, 255, 140)',
|
||||
borderWidth: 1,
|
||||
data: mrrMonthlyOverTime,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Monthly MRR',
|
||||
},
|
||||
plugins: {
|
||||
datalabels: {
|
||||
anchor: 'center',
|
||||
align: 'center',
|
||||
color: '#666',
|
||||
font: {
|
||||
weight: 'normal',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
subscriptions: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
|
||||
JSON.stringify(subscriptionsLinerOverTimeConfig),
|
||||
)}`,
|
||||
users: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(usersLinerOverTimeConfig))}`,
|
||||
quarterlyPerformance: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
|
||||
JSON.stringify(quarterlyConfig),
|
||||
)}`,
|
||||
churn: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(churnConfig))}`,
|
||||
mrr: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrOverTimeConfig))}`,
|
||||
mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
|
||||
}
|
||||
}
|
||||
|
||||
export const html = (data: any, timer: TimerInterface) => {
|
||||
const chartUrls = getChartUrls(event)
|
||||
|
||||
const successfullPaymentsActivity = data.activityStatistics.find(
|
||||
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentSuccess && Period.Yesterday,
|
||||
)
|
||||
const failedPaymentsActivity = data.activityStatistics.find(
|
||||
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentFailed && Period.Yesterday,
|
||||
)
|
||||
const limitedDiscountPurchasedActivity = data.activityStatistics.find(
|
||||
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.LimitedDiscountOfferPurchased && Period.Yesterday,
|
||||
)
|
||||
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
|
||||
)
|
||||
const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
|
||||
)
|
||||
const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
|
||||
)
|
||||
const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
|
||||
)
|
||||
const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
|
||||
)
|
||||
const userRegistrationOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
|
||||
)
|
||||
const userDeletionOverTime = data.activityStatisticsOverTime.find(
|
||||
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||
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,
|
||||
)
|
||||
const refundMeasureYesterday = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
const refundMeasureThisMonth = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
|
||||
)
|
||||
const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
|
||||
)
|
||||
const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
|
||||
)
|
||||
const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
|
||||
)
|
||||
const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.Yesterday,
|
||||
)
|
||||
const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.Yesterday,
|
||||
)
|
||||
const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.Yesterday,
|
||||
)
|
||||
const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
||||
)
|
||||
const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
|
||||
)
|
||||
const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
||||
)
|
||||
const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
|
||||
)
|
||||
const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
||||
)
|
||||
const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome && a.period === Period.ThisMonth,
|
||||
)
|
||||
const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
||||
)
|
||||
const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||
(a: { name: StatisticsMeasure; period: Period }) =>
|
||||
a.name === StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome && a.period === Period.ThisMonth,
|
||||
)
|
||||
|
||||
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 today = new Date()
|
||||
const thisMonthPeriodKey = `${today.getFullYear().toString()}-${(today.getMonth() + 1).toString()}`
|
||||
const thisMonthChurn = data.churn.values.find(
|
||||
(value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
|
||||
)
|
||||
|
||||
return ` <div>
|
||||
<p>Hello,</p>
|
||||
<p>
|
||||
<strong>Here are some statistics from yesterday:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Payments</b>
|
||||
<ul>
|
||||
<li>
|
||||
Revenue: <b>$${revenueYesterday.toLocaleString('en-US')}</b> (Income: $
|
||||
${incomeYesterday.toLocaleString('en-US')}, Refunds: $${refundsYesterday.toLocaleString('en-US')})
|
||||
</li>
|
||||
<li>
|
||||
Successfull payments: <b>${successfullPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Failed payments: <b>${failedPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>MRR Breakdown</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Total:</b> $${mrrOverTime?.counts[mrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
|
||||
</li>
|
||||
<li>
|
||||
<b>By Subscription Type:</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>PLUS:</b> $
|
||||
${plusPlansMrrOverTime?.counts[plusPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
|
||||
</li>
|
||||
<li>
|
||||
<b>PRO:</b> $
|
||||
${proPlansMrrOverTime?.counts[proPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>By Billing Frequency:</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Monthly:</b> $
|
||||
${monthlyPlansMrrOverTime?.counts[monthlyPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
|
||||
'en-US',
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<b>Annual:</b> $
|
||||
${annualPlansMrrOverTime?.counts[annualPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
|
||||
'en-US',
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
<b>5-year:</b> $
|
||||
${fiveYearPlansMrrOverTime?.counts[fiveYearPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
|
||||
'en-US',
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Income Breakdown</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Plus Subscription:</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>${plusSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
|
||||
<b>$${plusSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${plusSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
|
||||
<b>$${plusSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
|
||||
<b>$${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${plusSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
|
||||
<b>$${plusSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Pro Subscription:</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>${proSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
|
||||
<b>$${proSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${proSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
|
||||
<b>$${proSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${proSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
|
||||
<b>$${proSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${proSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
|
||||
<b>$${proSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Users</b>
|
||||
<ul>
|
||||
<li>
|
||||
Number of users registered:${' '}
|
||||
<b>
|
||||
${userRegistrationOverTime?.counts[userRegistrationOverTime?.counts.length - 1]?.totalCount.toLocaleString(
|
||||
'en-US',
|
||||
)}
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
Number of users unregistered:${' '}
|
||||
<b>
|
||||
${userDeletionOverTime?.counts[userDeletionOverTime?.counts.length - 1]?.totalCount.toLocaleString('en-US')}
|
||||
</b>${' '}
|
||||
(average account duration: ${registrationLengthDurationYesterday.days} days${' '}
|
||||
${registrationLengthDurationYesterday.hours} hours ${registrationLengthDurationYesterday.minutes} minutes)
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Subscriptions</b>
|
||||
<ul>
|
||||
<li>
|
||||
Number of subscriptions purchased:${' '}
|
||||
<b>
|
||||
${subscriptionPurchasingOverTime?.counts[
|
||||
subscriptionPurchasingOverTime?.counts.length - 1
|
||||
]?.totalCount.toLocaleString('en-US')}
|
||||
</b>${' '}
|
||||
(includes <b>${limitedDiscountPurchasedActivity?.totalCount.toLocaleString('en-US')}</b> limited time
|
||||
offer purchases)
|
||||
</li>
|
||||
<li>
|
||||
Number of subscriptions renewed:${' '}
|
||||
<b>
|
||||
${subscriptionRenewingOverTime?.counts[
|
||||
subscriptionRenewingOverTime?.counts.length - 1
|
||||
]?.totalCount.toLocaleString('en-US')}
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
Number of subscriptions refunded:${' '}
|
||||
<b>
|
||||
${subscriptionRefundingOverTime?.counts[
|
||||
subscriptionRefundingOverTime?.counts.length - 1
|
||||
]?.totalCount.toLocaleString('en-US')}
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
Number of subscriptions cancelled:${' '}
|
||||
<b>
|
||||
${subscriptionCancelledOverTime?.counts[
|
||||
subscriptionCancelledOverTime?.counts.length - 1
|
||||
]?.totalCount.toLocaleString('en-US')}
|
||||
</b>${' '}
|
||||
(average subscription duration: ${subscriptionLengthDurationYesterday.days} days${' '}
|
||||
${subscriptionLengthDurationYesterday.hours} hours ${subscriptionLengthDurationYesterday.minutes} minutes,
|
||||
average remaining subscription percentage: ${subscriptionRemainingTimePercentageYesterday}%)
|
||||
</li>
|
||||
<li>
|
||||
Number of subscriptions reactivated:${' '}
|
||||
<b>
|
||||
${subscriptionReactivatedOverTime?.counts[
|
||||
subscriptionReactivatedOverTime?.counts.length - 1
|
||||
]?.totalCount.toLocaleString('en-US')}
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
Average time from registration to subscription purchase:${' '}
|
||||
<b>
|
||||
${registrationToSubscriptionDurationYesterday.days} days${' '}
|
||||
${registrationToSubscriptionDurationYesterday.hours} hours${' '}
|
||||
${registrationToSubscriptionDurationYesterday.minutes} minutes
|
||||
</b>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<strong>Here are some statistics from last 30 days:</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Payments (This Month)</b>
|
||||
<ul>
|
||||
<li>
|
||||
Revenue: <b>$${revenueThisMonth.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Income: <b>$${incomeThisMonth.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Refunds: <b>$${refundsThisMonth.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Income Breakdown (This Month)</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>Plus Subscription:</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
|
||||
<b>$${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${plusSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
|
||||
<b>$${plusSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
|
||||
<b>$${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
|
||||
<b>$${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Pro Subscription:</b>
|
||||
<ul>
|
||||
<li>
|
||||
<b>${proSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
|
||||
<b>$${proSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${proSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
|
||||
<b>$${proSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
|
||||
<b>$${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>${proSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
|
||||
<b>$${proSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Users</b>
|
||||
<ul>
|
||||
<li>
|
||||
Number of users registered: <b>${userRegistrationOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Number of users unregistered: <b>${userDeletionOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Average account duration this month:${' '}
|
||||
<b>
|
||||
${registrationLengthDurationThisMonth.days} days ${registrationLengthDurationThisMonth.hours} hours${' '}
|
||||
${registrationLengthDurationThisMonth.minutes} minutes
|
||||
</b>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>Subscriptions</b>
|
||||
<ul>
|
||||
<li>
|
||||
Number of subscriptions purchased:${' '}
|
||||
<b>${subscriptionPurchasingOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Number of subscriptions renewed:${' '}
|
||||
<b>${subscriptionRenewingOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Number of subscriptions refunded:${' '}
|
||||
<b>${subscriptionRefundingOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Number of subscriptions cancelled:${' '}
|
||||
<b>${subscriptionCancelledOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Number of subscriptions reactivated:${' '}
|
||||
<b>${subscriptionReactivatedOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||
</li>
|
||||
<li>
|
||||
Average subscription duration this month:${' '}
|
||||
<b>
|
||||
${subscriptionLengthDurationThisMonth.days} days ${subscriptionLengthDurationThisMonth.hours} hours${' '}
|
||||
${subscriptionLengthDurationThisMonth.minutes} minutes
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
Average subscription remaining percentage this month:${' '}
|
||||
<b>${subscriptionRemainingTimePercentageThisMonth}%</b>
|
||||
</li>
|
||||
<li>
|
||||
Average time from registration to subscription purchase this month:${' '}
|
||||
<b>
|
||||
${registrationToSubscriptionDurationThisMonth.days} days${' '}
|
||||
${registrationToSubscriptionDurationThisMonth.hours} hours${' '}
|
||||
${registrationToSubscriptionDurationThisMonth.minutes} minutes
|
||||
</b>
|
||||
</li>
|
||||
</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>
|
||||
<img src=${chartUrls.mrrMonthly}></img>
|
||||
<p>
|
||||
<strong>Here is the subscription chart over 30 days:</strong>
|
||||
</p>
|
||||
<img src=${chartUrls.subscriptions}></img>
|
||||
<p>
|
||||
<strong>Here is the users chart over 30 days:</strong>
|
||||
</p>
|
||||
<img src=${chartUrls.users}></img>
|
||||
<p>
|
||||
<strong>Here is the monthly churn rate percentage:</strong>
|
||||
</p>
|
||||
<p>✅ GREAT! Up to 7% 🔶 OKAY: 8-10% 🩸 BAD: 11 -15 % 🚨 TERRIBLE! 16-20%</p>
|
||||
<p>Churn is calculated by the following formula:</p>
|
||||
<p>
|
||||
( Existing Customers Churn [${thisMonthChurn?.existingCustomersChurn}] + New Customers Churn [
|
||||
${thisMonthChurn?.newCustomersChurn}] ) * 100 / Average Customers Count This Month [
|
||||
${thisMonthChurn?.averageCustomersCount}]
|
||||
</p>
|
||||
<img src=${chartUrls.churn}></img>
|
||||
<p>
|
||||
<strong>Here is quarterly performance chart:</strong>
|
||||
</p>
|
||||
<img src=${chartUrls.quarterlyPerformance}></img>
|
||||
<p>Thanks,SN</p>
|
||||
</div>`
|
||||
}
|
||||
22
packages/analytics/src/Domain/Entity/AnalyticsEntity.ts
Normal file
22
packages/analytics/src/Domain/Entity/AnalyticsEntity.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
|
||||
|
||||
@Entity({ name: 'analytics_entities' })
|
||||
export class AnalyticsEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
declare id: number
|
||||
|
||||
@Column({
|
||||
name: 'user_uuid',
|
||||
length: 36,
|
||||
})
|
||||
@Index('user_uuid')
|
||||
declare userUuid: string
|
||||
|
||||
@Column({
|
||||
name: 'user_email',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
})
|
||||
@Index('email')
|
||||
declare username: string
|
||||
}
|
||||
@@ -3,5 +3,7 @@ import { AnalyticsEntity } from './AnalyticsEntity'
|
||||
|
||||
export interface AnalyticsEntityRepositoryInterface {
|
||||
save(analyticsEntity: AnalyticsEntity): Promise<AnalyticsEntity>
|
||||
remove(analyticsEntity: AnalyticsEntity): Promise<void>
|
||||
findOneByUserUuid(userUuid: Uuid): Promise<AnalyticsEntity | null>
|
||||
findOneByUserEmail(email: string): Promise<AnalyticsEntity | null>
|
||||
}
|
||||
32
packages/analytics/src/Domain/Event/DomainEventFactory.ts
Normal file
32
packages/analytics/src/Domain/Event/DomainEventFactory.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { DomainEventService, EmailRequestedEvent } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
messageIdentifier: string
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
}): EmailRequestedEvent {
|
||||
return {
|
||||
type: 'EMAIL_REQUESTED',
|
||||
createdAt: this.timer.getUTCDate(),
|
||||
meta: {
|
||||
correlation: {
|
||||
userIdentifier: dto.userEmail,
|
||||
userIdentifierType: 'email',
|
||||
},
|
||||
origin: DomainEventService.Analytics,
|
||||
},
|
||||
payload: dto,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { EmailRequestedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
export interface DomainEventFactoryInterface {
|
||||
createEmailRequestedEvent(dto: {
|
||||
userEmail: string
|
||||
messageIdentifier: string
|
||||
level: string
|
||||
body: string
|
||||
subject: string
|
||||
}): EmailRequestedEvent
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||
import { TimerInterface } from '@standardnotes/time'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
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 { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
@injectable()
|
||||
export class AccountDeletionRequestedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||
const analyticsEntity = await this.analyticsEntityRepository.findOneByUserUuid(event.payload.userUuid)
|
||||
|
||||
if (analyticsEntity === null) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.DeleteAccount], analyticsEntity.id, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
const registrationLength = this.timer.getTimestampInMicroseconds() - event.payload.userCreatedAtTimestamp
|
||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.RegistrationLength, registrationLength, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
await this.analyticsEntityRepository.remove(analyticsEntity)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,19 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { PaymentFailedEvent } from '@standardnotes/domain-events'
|
||||
import { AnalyticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
import { PaymentFailedEventHandler } from './PaymentFailedEventHandler'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
|
||||
describe('PaymentFailedEventHandler', () => {
|
||||
let userRepository: UserRepositoryInterface
|
||||
let event: PaymentFailedEvent
|
||||
let user: User
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
|
||||
const createHandler = () => new PaymentFailedEventHandler(userRepository, getUserAnalyticsId, analyticsStore)
|
||||
const createHandler = () => new PaymentFailedEventHandler(getUserAnalyticsId, analyticsStore)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
@@ -40,12 +31,4 @@ describe('PaymentFailedEventHandler', () => {
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not mark payment failed for analytics if user is not found', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,26 +1,21 @@
|
||||
import { AnalyticsActivity, AnalyticsStoreInterface, Period } from '@standardnotes/analytics'
|
||||
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentFailedEvent): Promise<void> {
|
||||
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
|
||||
if (user === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentFailed], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
@@ -1,32 +1,25 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { PaymentSuccessEvent } from '@standardnotes/domain-events'
|
||||
import { AnalyticsStoreInterface, Period, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
import { PaymentSuccessEventHandler } from './PaymentSuccessEventHandler'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
import { User } from '../User/User'
|
||||
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 userRepository: UserRepositoryInterface
|
||||
let event: PaymentSuccessEvent
|
||||
let user: User
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () =>
|
||||
new PaymentSuccessEventHandler(userRepository, getUserAnalyticsId, analyticsStore, statisticsStore, logger)
|
||||
new PaymentSuccessEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
user = {} as jest.Mocked<User>
|
||||
|
||||
userRepository = {} as jest.Mocked<UserRepositoryInterface>
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(user)
|
||||
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
@@ -79,12 +72,4 @@ describe('PaymentSuccessEventHandler', () => {
|
||||
Period.ThisMonth,
|
||||
])
|
||||
})
|
||||
|
||||
it('should not mark payment failed for analytics if user is not found', async () => {
|
||||
userRepository.findOneByEmail = jest.fn().mockReturnValue(null)
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,18 +1,15 @@
|
||||
import {
|
||||
AnalyticsActivity,
|
||||
AnalyticsStoreInterface,
|
||||
Period,
|
||||
StatisticsMeasure,
|
||||
StatisticsStoreInterface,
|
||||
} from '@standardnotes/analytics'
|
||||
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
|
||||
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
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 { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
|
||||
|
||||
@injectable()
|
||||
export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -58,7 +55,6 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
||||
])
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@@ -66,12 +62,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentSuccessEvent): Promise<void> {
|
||||
const user = await this.userRepository.findOneByEmail(event.payload.userEmail)
|
||||
if (user === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userUuid: user.uuid })
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentSuccess], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { RefundProcessedEvent } from '@standardnotes/domain-events'
|
||||
import { Period, StatisticsMeasure, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
|
||||
import { RefundProcessedEventHandler } from './RefundProcessedEventHandler'
|
||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
describe('RefundProcessedEventHandler', () => {
|
||||
let event: RefundProcessedEvent
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Period, StatisticsMeasure, StatisticsStoreInterface } from '@standardnotes/analytics'
|
||||
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
@injectable()
|
||||
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
|
||||
@@ -0,0 +1,104 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
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'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionCancelled], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
await this.trackSubscriptionStatistics(event)
|
||||
|
||||
const result = await this.saveRevenueModification.execute({
|
||||
billingFrequency: event.payload.billingFrequency,
|
||||
eventType: SubscriptionEventType.create(event.type).getValue(),
|
||||
newSubscriber: event.payload.userExistingSubscriptionsCount === 1,
|
||||
payedAmount: event.payload.payAmount,
|
||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
username: Username.create(event.payload.userEmail).getValue(),
|
||||
userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
|
||||
if (this.isLegacy5yearSubscriptionPlan(event.payload.subscriptionEndsAt, event.payload.subscriptionCreatedAt)) {
|
||||
return
|
||||
}
|
||||
|
||||
const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt
|
||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
const remainingSubscriptionTime = event.payload.subscriptionEndsAt - event.payload.timestamp
|
||||
const totalSubscriptionTime = event.payload.subscriptionEndsAt - event.payload.lastPayedAt
|
||||
|
||||
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
||||
|
||||
await this.statisticsStore.incrementMeasure(
|
||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
||||
remainingSubscriptionPercentage,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
}
|
||||
|
||||
private isLegacy5yearSubscriptionPlan(subscriptionEndsAt: number, subscriptionCreatedAt: number) {
|
||||
const fourYearsInMicroseconds = 126_230_400_000_000
|
||||
|
||||
return subscriptionEndsAt - subscriptionCreatedAt > fourYearsInMicroseconds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
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 { 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'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity(
|
||||
[AnalyticsActivity.SubscriptionExpired, AnalyticsActivity.ExistingCustomersChurn],
|
||||
analyticsId,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
|
||||
await this.statisticsStore.setMeasure(
|
||||
StatisticsMeasure.TotalCustomers,
|
||||
event.payload.totalActiveSubscriptionsCount,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||
)
|
||||
|
||||
const result = await this.saveRevenueModification.execute({
|
||||
billingFrequency: event.payload.billingFrequency,
|
||||
eventType: SubscriptionEventType.create(event.type).getValue(),
|
||||
newSubscriber: event.payload.userExistingSubscriptionsCount === 1,
|
||||
payedAmount: event.payload.payAmount,
|
||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
username: Username.create(event.payload.userEmail).getValue(),
|
||||
userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
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 { 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'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionPurchased], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
await this.analyticsStore.unmarkActivity(
|
||||
[AnalyticsActivity.ExistingCustomersChurn, AnalyticsActivity.NewCustomersChurn],
|
||||
analyticsId,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
|
||||
if (event.payload.limitedDiscountPurchased) {
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.LimitedDiscountOfferPurchased], analyticsId, [
|
||||
Period.Today,
|
||||
])
|
||||
}
|
||||
|
||||
if (event.payload.newSubscriber) {
|
||||
await this.statisticsStore.incrementMeasure(
|
||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
||||
event.payload.timestamp - event.payload.userRegisteredAt,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.NewCustomers, 1, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
Period.ThisYear,
|
||||
])
|
||||
await this.statisticsStore.setMeasure(
|
||||
StatisticsMeasure.TotalCustomers,
|
||||
event.payload.totalActiveSubscriptionsCount,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||
)
|
||||
}
|
||||
|
||||
const result = await this.saveRevenueModification.execute({
|
||||
billingFrequency: event.payload.billingFrequency,
|
||||
eventType: SubscriptionEventType.create(event.type).getValue(),
|
||||
newSubscriber: event.payload.newSubscriber,
|
||||
payedAmount: event.payload.payAmount,
|
||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
username: Username.create(event.payload.userEmail).getValue(),
|
||||
userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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,
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionReactivatedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionReactivated], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
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 { 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'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionRefundedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionRefunded], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
await this.markChurnActivity(analyticsId, event)
|
||||
|
||||
const result = await this.saveRevenueModification.execute({
|
||||
billingFrequency: event.payload.billingFrequency,
|
||||
eventType: SubscriptionEventType.create(event.type).getValue(),
|
||||
newSubscriber: event.payload.userExistingSubscriptionsCount === 1,
|
||||
payedAmount: event.payload.payAmount,
|
||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
username: Username.create(event.payload.userEmail).getValue(),
|
||||
userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async markChurnActivity(analyticsId: number, event: SubscriptionRefundedEvent): Promise<void> {
|
||||
const churnActivity =
|
||||
event.payload.userExistingSubscriptionsCount > 1
|
||||
? AnalyticsActivity.ExistingCustomersChurn
|
||||
: AnalyticsActivity.NewCustomersChurn
|
||||
|
||||
for (const period of [Period.ThisMonth, Period.ThisWeek, Period.Today]) {
|
||||
const customerPurchasedInPeriod = await this.analyticsStore.wasActivityDone(
|
||||
AnalyticsActivity.SubscriptionPurchased,
|
||||
analyticsId,
|
||||
period,
|
||||
)
|
||||
if (customerPurchasedInPeriod) {
|
||||
await this.analyticsStore.markActivity([churnActivity], analyticsId, [period])
|
||||
}
|
||||
}
|
||||
|
||||
await this.statisticsStore.setMeasure(
|
||||
StatisticsMeasure.TotalCustomers,
|
||||
event.payload.totalActiveSubscriptionsCount,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
||||
const { analyticsId, userUuid } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionRenewed], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
await this.analyticsStore.unmarkActivity(
|
||||
[AnalyticsActivity.ExistingCustomersChurn, AnalyticsActivity.NewCustomersChurn],
|
||||
analyticsId,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
|
||||
const result = await this.saveRevenueModification.execute({
|
||||
billingFrequency: event.payload.billingFrequency,
|
||||
eventType: SubscriptionEventType.create(event.type).getValue(),
|
||||
newSubscriber: false,
|
||||
payedAmount: event.payload.payAmount,
|
||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||
subscriptionId: event.payload.subscriptionId,
|
||||
username: Username.create(event.payload.userEmail).getValue(),
|
||||
userUuid,
|
||||
})
|
||||
|
||||
if (result.isFailed()) {
|
||||
this.logger.error(
|
||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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,
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
import { AnalyticsEntity } from '../Entity/AnalyticsEntity'
|
||||
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
@injectable()
|
||||
export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: UserRegisteredEvent): Promise<void> {
|
||||
let analyticsEntity = new AnalyticsEntity()
|
||||
analyticsEntity.userUuid = event.payload.userUuid
|
||||
analyticsEntity.username = event.payload.email
|
||||
analyticsEntity = await this.analyticsEntityRepository.save(analyticsEntity)
|
||||
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.Register], analyticsEntity.id, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
}
|
||||
79
packages/analytics/src/Domain/Map/RevenueModificationMap.ts
Normal file
79
packages/analytics/src/Domain/Map/RevenueModificationMap.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { injectable } from 'inversify'
|
||||
import { MapperInterface, UniqueEntityId, Username } from '@standardnotes/domain-core'
|
||||
|
||||
import { TypeORMRevenueModification } from '../../Infra/TypeORM/TypeORMRevenueModification'
|
||||
import { MonthlyRevenue } from '../Revenue/MonthlyRevenue'
|
||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
||||
import { Subscription } from '../Subscription/Subscription'
|
||||
import { User } from '../User/User'
|
||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||
|
||||
@injectable()
|
||||
export class RevenueModificationMap implements MapperInterface<RevenueModification, TypeORMRevenueModification> {
|
||||
toDomain(persistence: TypeORMRevenueModification): RevenueModification {
|
||||
const userOrError = User.create(
|
||||
{
|
||||
username: Username.create(persistence.username).getValue(),
|
||||
},
|
||||
new UniqueEntityId(persistence.userUuid),
|
||||
)
|
||||
if (userOrError.isFailed()) {
|
||||
throw new Error(`Could not create user: ${userOrError.getError()}`)
|
||||
}
|
||||
const user = userOrError.getValue()
|
||||
|
||||
const subscriptionOrError = Subscription.create(
|
||||
{
|
||||
billingFrequency: persistence.billingFrequency,
|
||||
isFirstSubscriptionForUser: persistence.isNewCustomer,
|
||||
payedAmount: persistence.billingFrequency * persistence.newMonthlyRevenue,
|
||||
planName: SubscriptionPlanName.create(persistence.subscriptionPlan).getValue(),
|
||||
},
|
||||
new UniqueEntityId(persistence.subscriptionId),
|
||||
)
|
||||
if (subscriptionOrError.isFailed()) {
|
||||
throw new Error(`Could not create subscription: ${subscriptionOrError.getError()}`)
|
||||
}
|
||||
const subscription = subscriptionOrError.getValue()
|
||||
|
||||
const previousMonthlyRevenueOrError = MonthlyRevenue.create(persistence.previousMonthlyRevenue)
|
||||
const newMonthlyRevenueOrError = MonthlyRevenue.create(persistence.newMonthlyRevenue)
|
||||
|
||||
const revenuModificationOrError = RevenueModification.create(
|
||||
{
|
||||
user,
|
||||
subscription,
|
||||
eventType: SubscriptionEventType.create(persistence.eventType).getValue(),
|
||||
previousMonthlyRevenue: previousMonthlyRevenueOrError.getValue(),
|
||||
newMonthlyRevenue: newMonthlyRevenueOrError.getValue(),
|
||||
createdAt: persistence.createdAt,
|
||||
},
|
||||
new UniqueEntityId(persistence.uuid),
|
||||
)
|
||||
|
||||
if (revenuModificationOrError.isFailed()) {
|
||||
throw new Error(`Could not map revenue modification to domain: ${revenuModificationOrError.getError()}`)
|
||||
}
|
||||
|
||||
return revenuModificationOrError.getValue()
|
||||
}
|
||||
|
||||
toProjection(domain: RevenueModification): TypeORMRevenueModification {
|
||||
const { subscription, user } = domain.props
|
||||
const persistence = new TypeORMRevenueModification()
|
||||
persistence.uuid = domain.id.toString()
|
||||
persistence.billingFrequency = subscription.props.billingFrequency
|
||||
persistence.eventType = domain.props.eventType.value
|
||||
persistence.isNewCustomer = subscription.props.isFirstSubscriptionForUser
|
||||
persistence.newMonthlyRevenue = domain.props.newMonthlyRevenue.value
|
||||
persistence.previousMonthlyRevenue = domain.props.previousMonthlyRevenue.value
|
||||
persistence.subscriptionId = subscription.id.toValue() as number
|
||||
persistence.subscriptionPlan = subscription.props.planName.value
|
||||
persistence.username = user.props.username.value
|
||||
persistence.userUuid = user.id.toString()
|
||||
persistence.createdAt = domain.props.createdAt
|
||||
|
||||
return persistence
|
||||
}
|
||||
}
|
||||
16
packages/analytics/src/Domain/Revenue/MonthlyRevenue.spec.ts
Normal file
16
packages/analytics/src/Domain/Revenue/MonthlyRevenue.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { MonthlyRevenue } from './MonthlyRevenue'
|
||||
|
||||
describe('MonthlyRevenue', () => {
|
||||
it('should create a value object', () => {
|
||||
const valueOrError = MonthlyRevenue.create(123)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeFalsy()
|
||||
expect(valueOrError.getValue().value).toEqual(123)
|
||||
})
|
||||
|
||||
it('should not create an invalid value object', () => {
|
||||
const valueOrError = MonthlyRevenue.create(-3)
|
||||
|
||||
expect(valueOrError.isFailed()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
21
packages/analytics/src/Domain/Revenue/MonthlyRevenue.ts
Normal file
21
packages/analytics/src/Domain/Revenue/MonthlyRevenue.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Result, ValueObject } from '@standardnotes/domain-core'
|
||||
|
||||
import { MonthlyRevenueProps } from './MonthlyRevenueProps'
|
||||
|
||||
export class MonthlyRevenue extends ValueObject<MonthlyRevenueProps> {
|
||||
get value(): number {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
private constructor(props: MonthlyRevenueProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
static create(revenue: number): Result<MonthlyRevenue> {
|
||||
if (isNaN(revenue) || revenue < 0) {
|
||||
return Result.fail<MonthlyRevenue>(`Monthly revenue must be a non-negative number. Supplied: ${revenue}`)
|
||||
} else {
|
||||
return Result.ok<MonthlyRevenue>(new MonthlyRevenue({ value: revenue }))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface MonthlyRevenueProps {
|
||||
value: number
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Username } from '@standardnotes/domain-core'
|
||||
|
||||
import { Subscription } from '../Subscription/Subscription'
|
||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||
import { User } from '../User/User'
|
||||
import { MonthlyRevenue } from './MonthlyRevenue'
|
||||
import { RevenueModification } from './RevenueModification'
|
||||
|
||||
describe('RevenueModification', () => {
|
||||
let user: User
|
||||
let subscription: Subscription
|
||||
|
||||
beforeEach(() => {
|
||||
subscription = Subscription.create({
|
||||
billingFrequency: 12,
|
||||
isFirstSubscriptionForUser: true,
|
||||
payedAmount: 123,
|
||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||
}).getValue()
|
||||
user = User.create({
|
||||
username: Username.create('test@test.te').getValue(),
|
||||
}).getValue()
|
||||
})
|
||||
|
||||
it('should create an aggregate for purchased subscription', () => {
|
||||
const revenueModification = RevenueModification.create({
|
||||
createdAt: 2,
|
||||
eventType: SubscriptionEventType.create('SUBSCRIPTION_PURCHASED').getValue(),
|
||||
previousMonthlyRevenue: MonthlyRevenue.create(123).getValue(),
|
||||
newMonthlyRevenue: MonthlyRevenue.create(45).getValue(),
|
||||
subscription,
|
||||
user,
|
||||
}).getValue()
|
||||
|
||||
expect(revenueModification.id.toString()).toHaveLength(36)
|
||||
expect(revenueModification.props.newMonthlyRevenue.value).toEqual(45)
|
||||
})
|
||||
})
|
||||
13
packages/analytics/src/Domain/Revenue/RevenueModification.ts
Normal file
13
packages/analytics/src/Domain/Revenue/RevenueModification.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Aggregate, UniqueEntityId, Result } from '@standardnotes/domain-core'
|
||||
|
||||
import { RevenueModificationProps } from './RevenueModificationProps'
|
||||
|
||||
export class RevenueModification extends Aggregate<RevenueModificationProps> {
|
||||
private constructor(props: RevenueModificationProps, id?: UniqueEntityId) {
|
||||
super(props, id)
|
||||
}
|
||||
|
||||
static create(props: RevenueModificationProps, id?: UniqueEntityId): Result<RevenueModification> {
|
||||
return Result.ok<RevenueModification>(new RevenueModification(props, id))
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user