mirror of
https://github.com/standardnotes/server
synced 2026-04-19 17:02:25 -04:00
Compare commits
199 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d266eada88 | |||
| 11b8b078b4 | |||
| 37912fa29a | |||
| b97dafe6f3 | |||
| 2a29151395 | |||
| 8b988d89c0 | |||
| c0908f1b58 | |||
| bb46044f7c | |||
| 60b3dd6138 | |||
| 22c1f936c3 | |||
| e899874b04 | |||
| 04c6888cf6 | |||
| 29c56c6919 | |||
| c98ed9cc85 | |||
| 88f7530c13 | |||
| bb820437af | |||
| d1a4bd38e0 | |||
| d18f6ccd32 | |||
| aa317c964e | |||
| 7ae8845ae9 | |||
| 123a6dbe0c | |||
| dda8d79526 | |||
| de5293955a | |||
| 96669bff5b | |||
| a99762f004 | |||
| 1fc3c9b83e | |||
| af86b6f664 | |||
| a0208dd5b3 | |||
| 1c5c8b81d5 | |||
| 79c3e33434 | |||
| 5ab8729a31 | |||
| db0baf92f1 | |||
| a8974094db | |||
| 13c5c97ba7 | |||
| 894ebb3edd | |||
| cac899a7e5 | |||
| 901e0dd93b | |||
| a360231fd0 | |||
| 6ccc6ee42f | |||
| 9c72ad85a0 | |||
| fa6d80a753 | |||
| f6ab2ca9ba | |||
| ba1e1ad5ad | |||
| 02705ea3ad | |||
| df6e3f06a6 | |||
| 1cb5ee9fd6 | |||
| 893d6176c3 | |||
| 2c1b512e40 | |||
| de50d76800 | |||
| 401b78e477 | |||
| 01837eaea9 | |||
| 7df699353c | |||
| 5455972be2 | |||
| 57488bcd16 | |||
| b6fda901ef | |||
| 14669df890 | |||
| 64525a65f2 | |||
| 61fc7efecb | |||
| 8c7c1e4745 | |||
| f64d30ec88 | |||
| 384dfc8da4 | |||
| 841784ae8c | |||
| f5683cfd94 | |||
| 0a420ce30e | |||
| a5e7132d3c | |||
| 6dfb2be4a2 | |||
| d81cbad550 | |||
| 51ad06b303 | |||
| 27048ad95c | |||
| fa9bf0b448 | |||
| 305190b64e | |||
| 98e3d18335 | |||
| 72e398956b | |||
| 1e69a13a97 | |||
| 7f9e6e2f44 | |||
| d3c6c0d48e | |||
| 6c83476fd2 | |||
| 9cdf7e2c51 | |||
| 599119e14e | |||
| a2c484e0f3 | |||
| 97ff4d5ac2 | |||
| 5255cfbb25 | |||
| 780358368b | |||
| cf0b918913 | |||
| 4ea690204e | |||
| 14eb775749 | |||
| bf4a3be6d9 | |||
| b9e1e47871 | |||
| ff532ecb22 | |||
| eb21872db1 | |||
| 8e3df184dc | |||
| b34bbcac8b | |||
| 226965a1d7 | |||
| 17b2ea126c | |||
| 59fc4a089c | |||
| ef26dc8cbb | |||
| 8a0fbb28b0 | |||
| 618d8d5b1a | |||
| 3a936dc9c1 | |||
| 031fcd75ee | |||
| c8cd23cb32 | |||
| a3049938a3 | |||
| b23488e862 | |||
| c8203cf04c | |||
| 4f2616ef0a | |||
| 04ffc69e00 | |||
| 5b4bb6e7a7 | |||
| 2e953ba998 | |||
| ed5a4eb960 | |||
| 31b2c05084 | |||
| 6e1662038c | |||
| df78d88f79 | |||
| addedb3091 | |||
| 2ea17b2dea | |||
| 85d2f42f47 | |||
| cdb655c1bd | |||
| 3064d03aa9 | |||
| 6af6417ca2 | |||
| a35271fbb3 | |||
| 63aef71f60 | |||
| 0f8457534c | |||
| 2984582e62 | |||
| 147d8fd9af | |||
| c12d354900 | |||
| 8bcb552783 | |||
| f504a8288c | |||
| 79f5b54228 | |||
| 669a9855e6 | |||
| e3b96c3a1f | |||
| 7e0d4bef20 | |||
| 0bd0f48df3 | |||
| ae56126585 | |||
| 6dcf0ac124 | |||
| 63e2ce43c2 | |||
| f27aa21eb5 | |||
| 42926c663b | |||
| d38116183c | |||
| 9ca373e208 | |||
| 4084f2f5ec | |||
| 684ffbadbc | |||
| 1c4d4c57de | |||
| d83111a199 | |||
| f10fa839fb | |||
| 1f20395ff3 | |||
| bfe6f4255a | |||
| b9032f3012 | |||
| ce53c459e6 | |||
| 6df42fb0d5 | |||
| 1e2b496f4f | |||
| 528c1b0d57 | |||
| 22fba8ba80 | |||
| 6f26261ebe | |||
| 4b1fe3ba91 | |||
| 9f95262bd4 | |||
| 2ec28e541e | |||
| 4764d4b19a | |||
| 9b27547dae | |||
| a96f2c9153 | |||
| 225e0aaf88 | |||
| f0c85910bc | |||
| 124c443528 | |||
| 37c7f8d39f | |||
| c419f1ce22 | |||
| 4949cdfe2f | |||
| cd101b96ea | |||
| 40d0e4631f | |||
| a55a995660 | |||
| 1d576d48ad | |||
| 4ff8030f87 | |||
| c15e2e2c8f | |||
| 41d31a8d75 | |||
| 10e2a26352 | |||
| 6e547f77d0 | |||
| 530a426601 | |||
| 642d6bab77 | |||
| 7980af3d82 | |||
| 2980c42e88 | |||
| b03994f9db | |||
| 41906ec2f9 | |||
| 4d1e7ff2a5 | |||
| 7f18fcfc13 | |||
| ff02ce0747 | |||
| a6056600eb | |||
| 24c94326d5 | |||
| 48c0cb5e62 | |||
| 9968efe1b2 | |||
| 6368342149 | |||
| b5f73db210 | |||
| 22d6a02d04 | |||
| 4e0bcfcccf | |||
| 104313c15d | |||
| 814289af46 | |||
| 3096cd98d5 | |||
| 45dfefbc7a | |||
| 20d92149a8 | |||
| 9c01fffca5 | |||
| 61c1cfff4b | |||
| 7e74261f62 | |||
| 32601f34f1 |
@@ -187,12 +187,12 @@ jobs:
|
|||||||
tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
|
tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
|
||||||
|
|
||||||
- name: Run E2E test suite
|
- name: Run E2E test suite
|
||||||
uses: convictional/trigger-workflow-and-wait@v1.6.3
|
uses: convictional/trigger-workflow-and-wait@master
|
||||||
with:
|
with:
|
||||||
owner: standardnotes
|
owner: standardnotes
|
||||||
repo: e2e
|
repo: self-hosted
|
||||||
github_token: ${{ secrets.CI_PAT_TOKEN }}
|
github_token: ${{ secrets.CI_PAT_TOKEN }}
|
||||||
workflow_file_name: testing-with-stable-client.yml
|
workflow_file_name: testing-with-updating-client-and-server.yml
|
||||||
wait_interval: 30
|
wait_interval: 30
|
||||||
client_payload: '{"${{ inputs.e2e_tag_parameter_name }}": "${{ github.sha }}"}'
|
client_payload: '{"${{ inputs.e2e_tag_parameter_name }}": "${{ github.sha }}"}'
|
||||||
propagate_failure: true
|
propagate_failure: true
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@lerna-lite/cli", "npm:1.6.0"],\
|
["@lerna-lite/cli", "npm:1.6.0"],\
|
||||||
["@lerna-lite/list", "npm:1.6.0"],\
|
["@lerna-lite/list", "npm:1.6.0"],\
|
||||||
["@lerna-lite/run", "npm:1.6.0"],\
|
["@lerna-lite/run", "npm:1.6.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/node", "npm:18.11.9"],\
|
["@types/node", "npm:18.11.9"],\
|
||||||
@@ -1968,6 +1968,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["@noble/ed25519", [\
|
||||||
|
["npm:1.7.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@noble-ed25519-npm-1.7.1-177d9beb01-b1aa4b9264.zip/node_modules/@noble/ed25519/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@noble/ed25519", "npm:1.7.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["@nodelib/fs.scandir", [\
|
["@nodelib/fs.scandir", [\
|
||||||
["npm:2.1.5", {\
|
["npm:2.1.5", {\
|
||||||
"packageLocation": "./.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-5f309a3b37.zip/node_modules/@nodelib/fs.scandir/",\
|
"packageLocation": "./.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-5f309a3b37.zip/node_modules/@nodelib/fs.scandir/",\
|
||||||
@@ -2324,6 +2333,44 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["@peculiar/asn1-android", [\
|
||||||
|
["npm:2.3.3", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@peculiar-asn1-android-npm-2.3.3-28df67d7a3-0c7cad544e.zip/node_modules/@peculiar/asn1-android/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@peculiar/asn1-android", "npm:2.3.3"],\
|
||||||
|
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||||
|
["asn1js", "npm:3.0.5"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["@peculiar/asn1-schema", [\
|
||||||
|
["npm:2.3.3", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@peculiar-asn1-schema-npm-2.3.3-7c2b9469c4-f584f79d5a.zip/node_modules/@peculiar/asn1-schema/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||||
|
["asn1js", "npm:3.0.5"],\
|
||||||
|
["pvtsutils", "npm:1.3.2"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["@peculiar/asn1-x509", [\
|
||||||
|
["npm:2.3.4", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@peculiar-asn1-x509-npm-2.3.4-a579005836-10a8659980.zip/node_modules/@peculiar/asn1-x509/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@peculiar/asn1-x509", "npm:2.3.4"],\
|
||||||
|
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||||
|
["asn1js", "npm:3.0.5"],\
|
||||||
|
["ipaddr.js", "npm:2.0.1"],\
|
||||||
|
["pvtsutils", "npm:1.3.2"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["@pnpm/network.ca-file", [\
|
["@pnpm/network.ca-file", [\
|
||||||
["npm:1.0.1", {\
|
["npm:1.0.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@pnpm-network.ca-file-npm-1.0.1-42bfe40bec-ed952a5574.zip/node_modules/@pnpm/network.ca-file/",\
|
"packageLocation": "./.yarn/cache/@pnpm-network.ca-file-npm-1.0.1-42bfe40bec-ed952a5574.zip/node_modules/@pnpm/network.ca-file/",\
|
||||||
@@ -2438,25 +2485,25 @@ const RAW_RUNTIME_STATE =
|
|||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@sentry/core", [\
|
["@sentry/core", [\
|
||||||
["npm:7.19.0", {\
|
["npm:7.28.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip/node_modules/@sentry/core/",\
|
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.28.1-a468033ea8-f29d747d3e.zip/node_modules/@sentry/core/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@sentry/core", "npm:7.19.0"],\
|
["@sentry/core", "npm:7.28.1"],\
|
||||||
["@sentry/types", "npm:7.19.0"],\
|
["@sentry/types", "npm:7.28.1"],\
|
||||||
["@sentry/utils", "npm:7.19.0"],\
|
["@sentry/utils", "npm:7.28.1"],\
|
||||||
["tslib", "npm:1.14.1"]\
|
["tslib", "npm:1.14.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@sentry/node", [\
|
["@sentry/node", [\
|
||||||
["npm:7.19.0", {\
|
["npm:7.28.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip/node_modules/@sentry/node/",\
|
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.28.1-b0e124fdfc-b4922d1f0a.zip/node_modules/@sentry/node/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@sentry/core", "npm:7.19.0"],\
|
["@sentry/core", "npm:7.28.1"],\
|
||||||
["@sentry/types", "npm:7.19.0"],\
|
["@sentry/types", "npm:7.28.1"],\
|
||||||
["@sentry/utils", "npm:7.19.0"],\
|
["@sentry/utils", "npm:7.28.1"],\
|
||||||
["cookie", "npm:0.4.2"],\
|
["cookie", "npm:0.4.2"],\
|
||||||
["https-proxy-agent", "npm:5.0.1"],\
|
["https-proxy-agent", "npm:5.0.1"],\
|
||||||
["lru_map", "npm:0.3.3"],\
|
["lru_map", "npm:0.3.3"],\
|
||||||
@@ -2465,26 +2512,67 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@sentry/types", [\
|
["@sentry/tracing", [\
|
||||||
["npm:7.19.0", {\
|
["npm:7.28.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip/node_modules/@sentry/types/",\
|
"packageLocation": "./.yarn/cache/@sentry-tracing-npm-7.28.1-e15d453d8e-be501ca9d7.zip/node_modules/@sentry/tracing/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@sentry/types", "npm:7.19.0"]\
|
["@sentry/tracing", "npm:7.28.1"],\
|
||||||
|
["@sentry/core", "npm:7.28.1"],\
|
||||||
|
["@sentry/types", "npm:7.28.1"],\
|
||||||
|
["@sentry/utils", "npm:7.28.1"],\
|
||||||
|
["tslib", "npm:1.14.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["@sentry/types", [\
|
||||||
|
["npm:7.28.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.28.1-42d9a8574c-7dc6639cb7.zip/node_modules/@sentry/types/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@sentry/types", "npm:7.28.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@sentry/utils", [\
|
["@sentry/utils", [\
|
||||||
["npm:7.19.0", {\
|
["npm:7.28.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip/node_modules/@sentry/utils/",\
|
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.28.1-71eaeb767f-a4b5f73db0.zip/node_modules/@sentry/utils/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@sentry/utils", "npm:7.19.0"],\
|
["@sentry/utils", "npm:7.28.1"],\
|
||||||
["@sentry/types", "npm:7.19.0"],\
|
["@sentry/types", "npm:7.28.1"],\
|
||||||
["tslib", "npm:1.14.1"]\
|
["tslib", "npm:1.14.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["@simplewebauthn/server", [\
|
||||||
|
["npm:6.2.2", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-6.2.2-ca870b05c2-5ffb9b1c15.zip/node_modules/@simplewebauthn/server/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@simplewebauthn/server", "npm:6.2.2"],\
|
||||||
|
["@noble/ed25519", "npm:1.7.1"],\
|
||||||
|
["@peculiar/asn1-android", "npm:2.3.3"],\
|
||||||
|
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||||
|
["@peculiar/asn1-x509", "npm:2.3.4"],\
|
||||||
|
["base64url", "npm:3.0.1"],\
|
||||||
|
["cbor", "npm:5.2.0"],\
|
||||||
|
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
|
||||||
|
["jsrsasign", "npm:10.6.1"],\
|
||||||
|
["jwk-to-pem", "npm:2.0.5"],\
|
||||||
|
["node-fetch", "virtual:25a5f5382d53dbf298bf7a1191760bc2e0a523a619eeb0e667b99a8649e8ad183f9e2e0b45f6fb831b92f4078b61622aa567cf79565f6aa5af9597e3c84864f6#npm:2.6.7"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["@simplewebauthn/typescript-types", [\
|
||||||
|
["npm:6.3.0-alpha.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@simplewebauthn-typescript-types-npm-6.3.0-alpha.1-629da05c10-5667c214e9.zip/node_modules/@simplewebauthn/typescript-types/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@simplewebauthn/typescript-types", "npm:6.3.0-alpha.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["@sinclair/typebox", [\
|
["@sinclair/typebox", [\
|
||||||
["npm:0.24.44", {\
|
["npm:0.24.44", {\
|
||||||
"packageLocation": "./.yarn/cache/@sinclair-typebox-npm-0.24.44-38506ddef6-f37b9d28bf.zip/node_modules/@sinclair/typebox/",\
|
"packageLocation": "./.yarn/cache/@sinclair-typebox-npm-0.24.44-38506ddef6-f37b9d28bf.zip/node_modules/@sinclair/typebox/",\
|
||||||
@@ -2538,7 +2626,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/analytics", "workspace:packages/analytics"],\
|
["@standardnotes/analytics", "workspace:packages/analytics"],\
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
@@ -2546,6 +2634,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@standardnotes/time", "workspace:packages/time"],\
|
["@standardnotes/time", "workspace:packages/time"],\
|
||||||
["@types/ioredis", "npm:5.0.0"],\
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
|
["@types/mixpanel", "npm:2.14.4"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/node", "npm:18.11.9"],\
|
["@types/node", "npm:18.11.9"],\
|
||||||
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.30.5"],\
|
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.30.5"],\
|
||||||
@@ -2557,6 +2646,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["inversify", "npm:6.0.1"],\
|
["inversify", "npm:6.0.1"],\
|
||||||
["ioredis", "npm:5.2.4"],\
|
["ioredis", "npm:5.2.4"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
|
["mixpanel", "npm:0.17.0"],\
|
||||||
["mysql2", "npm:2.3.3"],\
|
["mysql2", "npm:2.3.3"],\
|
||||||
["newrelic", "npm:9.6.0"],\
|
["newrelic", "npm:9.6.0"],\
|
||||||
["reflect-metadata", "npm:0.1.13"],\
|
["reflect-metadata", "npm:0.1.13"],\
|
||||||
@@ -2590,7 +2680,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
|
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||||
@@ -2600,7 +2690,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@types/express", "npm:4.17.14"],\
|
["@types/express", "npm:4.17.14"],\
|
||||||
["@types/ioredis", "npm:5.0.0"],\
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/prettyjson", "npm:0.0.30"],\
|
["@types/prettyjson", "npm:0.0.30"],\
|
||||||
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
|
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
|
||||||
@@ -2611,12 +2701,13 @@ const RAW_RUNTIME_STATE =
|
|||||||
["eslint", "npm:8.25.0"],\
|
["eslint", "npm:8.25.0"],\
|
||||||
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
|
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
|
||||||
["express", "npm:4.18.2"],\
|
["express", "npm:4.18.2"],\
|
||||||
|
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
|
||||||
["helmet", "npm:6.0.0"],\
|
["helmet", "npm:6.0.0"],\
|
||||||
["inversify", "npm:6.0.1"],\
|
["inversify", "npm:6.0.1"],\
|
||||||
["inversify-express-utils", "npm:6.4.3"],\
|
["inversify-express-utils", "npm:6.4.3"],\
|
||||||
["ioredis", "npm:5.2.4"],\
|
["ioredis", "npm:5.2.4"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
["jsonwebtoken", "npm:8.5.1"],\
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
["newrelic", "npm:9.6.0"],\
|
["newrelic", "npm:9.6.0"],\
|
||||||
["nodemon", "npm:2.0.20"],\
|
["nodemon", "npm:2.0.20"],\
|
||||||
["npm-check-updates", "npm:16.0.1"],\
|
["npm-check-updates", "npm:16.0.1"],\
|
||||||
@@ -2646,7 +2737,10 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/auth-server", "workspace:packages/auth"],\
|
["@standardnotes/auth-server", "workspace:packages/auth"],\
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
|
["@sentry/tracing", "npm:7.28.1"],\
|
||||||
|
["@simplewebauthn/server", "npm:6.2.2"],\
|
||||||
|
["@simplewebauthn/typescript-types", "npm:6.3.0-alpha.1"],\
|
||||||
["@standardnotes/api", "npm:1.19.0"],\
|
["@standardnotes/api", "npm:1.19.0"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
@@ -2737,10 +2831,6 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/domain-core/",\
|
"packageLocation": "./packages/domain-core/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
|
||||||
["@standardnotes/features", "npm:1.53.1"],\
|
|
||||||
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
|
||||||
["@standardnotes/security", "workspace:packages/security"],\
|
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/uuid", "npm:8.3.4"],\
|
["@types/uuid", "npm:8.3.4"],\
|
||||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
||||||
@@ -2874,7 +2964,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/files/",\
|
"packageLocation": "./packages/files/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/files-server", "workspace:packages/files"],\
|
["@standardnotes/files-server", "workspace:packages/files"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/config", "npm:2.4.3"],\
|
["@standardnotes/config", "npm:2.4.3"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
@@ -2888,7 +2978,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@types/express", "npm:4.17.14"],\
|
["@types/express", "npm:4.17.14"],\
|
||||||
["@types/ioredis", "npm:5.0.0"],\
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/prettyjson", "npm:0.0.30"],\
|
["@types/prettyjson", "npm:0.0.30"],\
|
||||||
["@types/uuid", "npm:8.3.4"],\
|
["@types/uuid", "npm:8.3.4"],\
|
||||||
@@ -2901,13 +2991,14 @@ const RAW_RUNTIME_STATE =
|
|||||||
["eslint", "npm:8.25.0"],\
|
["eslint", "npm:8.25.0"],\
|
||||||
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
|
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
|
||||||
["express", "npm:4.18.2"],\
|
["express", "npm:4.18.2"],\
|
||||||
|
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
|
||||||
["express-winston", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0"],\
|
["express-winston", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0"],\
|
||||||
["helmet", "npm:6.0.0"],\
|
["helmet", "npm:6.0.0"],\
|
||||||
["inversify", "npm:6.0.1"],\
|
["inversify", "npm:6.0.1"],\
|
||||||
["inversify-express-utils", "npm:6.4.3"],\
|
["inversify-express-utils", "npm:6.4.3"],\
|
||||||
["ioredis", "npm:5.2.4"],\
|
["ioredis", "npm:5.2.4"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
["jsonwebtoken", "npm:8.5.1"],\
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
["newrelic", "npm:9.6.0"],\
|
["newrelic", "npm:9.6.0"],\
|
||||||
["nodemon", "npm:2.0.20"],\
|
["nodemon", "npm:2.0.20"],\
|
||||||
["npm-check-updates", "npm:16.0.1"],\
|
["npm-check-updates", "npm:16.0.1"],\
|
||||||
@@ -3009,7 +3100,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
|
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/api", "npm:1.19.0"],\
|
["@standardnotes/api", "npm:1.19.0"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
@@ -3054,7 +3145,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
|
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
@@ -3093,11 +3184,11 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@standardnotes/security", "workspace:packages/security"],\
|
["@standardnotes/security", "workspace:packages/security"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
||||||
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
|
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
["jsonwebtoken", "npm:8.5.1"],\
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
["reflect-metadata", "npm:0.1.13"],\
|
["reflect-metadata", "npm:0.1.13"],\
|
||||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
|
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
|
||||||
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"]\
|
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"]\
|
||||||
@@ -3115,7 +3206,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@lerna-lite/cli", "npm:1.6.0"],\
|
["@lerna-lite/cli", "npm:1.6.0"],\
|
||||||
["@lerna-lite/list", "npm:1.6.0"],\
|
["@lerna-lite/list", "npm:1.6.0"],\
|
||||||
["@lerna-lite/run", "npm:1.6.0"],\
|
["@lerna-lite/run", "npm:1.6.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/node", "npm:18.11.9"],\
|
["@types/node", "npm:18.11.9"],\
|
||||||
@@ -3181,7 +3272,8 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
|
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
|
["@sentry/tracing", "npm:7.28.1"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
@@ -3197,7 +3289,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@types/inversify-express-utils", "npm:2.0.0"],\
|
["@types/inversify-express-utils", "npm:2.0.0"],\
|
||||||
["@types/ioredis", "npm:5.0.0"],\
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/prettyjson", "npm:0.0.30"],\
|
["@types/prettyjson", "npm:0.0.30"],\
|
||||||
["@types/ua-parser-js", "npm:0.7.36"],\
|
["@types/ua-parser-js", "npm:0.7.36"],\
|
||||||
@@ -3215,7 +3307,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["inversify-express-utils", "npm:6.4.3"],\
|
["inversify-express-utils", "npm:6.4.3"],\
|
||||||
["ioredis", "npm:5.2.4"],\
|
["ioredis", "npm:5.2.4"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
["jsonwebtoken", "npm:8.5.1"],\
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
["mysql2", "npm:2.3.3"],\
|
["mysql2", "npm:2.3.3"],\
|
||||||
["newrelic", "npm:9.6.0"],\
|
["newrelic", "npm:9.6.0"],\
|
||||||
["nodemon", "npm:2.0.20"],\
|
["nodemon", "npm:2.0.20"],\
|
||||||
@@ -3281,7 +3373,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
|
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/api", "npm:1.19.0"],\
|
["@standardnotes/api", "npm:1.19.0"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
@@ -3321,9 +3413,10 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
|
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/api", "npm:1.19.0"],\
|
["@standardnotes/api", "npm:1.19.0"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||||
["@standardnotes/models", "npm:1.28.0"],\
|
["@standardnotes/models", "npm:1.28.0"],\
|
||||||
@@ -3663,10 +3756,10 @@ const RAW_RUNTIME_STATE =
|
|||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@types/jsonwebtoken", [\
|
["@types/jsonwebtoken", [\
|
||||||
["npm:8.5.9", {\
|
["npm:9.0.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@types-jsonwebtoken-npm-8.5.9-79c2843a81-3f15a76cd5.zip/node_modules/@types/jsonwebtoken/",\
|
"packageLocation": "./.yarn/cache/@types-jsonwebtoken-npm-9.0.1-5f660fdf38-44d3fccc6b.zip/node_modules/@types/jsonwebtoken/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@types/node", "npm:18.0.3"]\
|
["@types/node", "npm:18.0.3"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
@@ -3727,6 +3820,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["@types/mixpanel", [\
|
||||||
|
["npm:2.14.4", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@types-mixpanel-npm-2.14.4-34bd98306f-a2bf6e633e.zip/node_modules/@types/mixpanel/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@types/mixpanel", "npm:2.14.4"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["@types/newrelic", [\
|
["@types/newrelic", [\
|
||||||
["npm:7.0.4", {\
|
["npm:7.0.4", {\
|
||||||
"packageLocation": "./.yarn/cache/@types-newrelic-npm-7.0.4-4f0b179b51-b44215b3ab.zip/node_modules/@types/newrelic/",\
|
"packageLocation": "./.yarn/cache/@types-newrelic-npm-7.0.4-4f0b179b51-b44215b3ab.zip/node_modules/@types/newrelic/",\
|
||||||
@@ -4774,6 +4876,31 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["asn1.js", [\
|
||||||
|
["npm:5.4.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/asn1.js-npm-5.4.1-37c7edbcb0-5c36f81388.zip/node_modules/asn1.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["asn1.js", "npm:5.4.1"],\
|
||||||
|
["bn.js", "npm:4.12.0"],\
|
||||||
|
["inherits", "npm:2.0.4"],\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"],\
|
||||||
|
["safer-buffer", "npm:2.1.2"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["asn1js", [\
|
||||||
|
["npm:3.0.5", {\
|
||||||
|
"packageLocation": "./.yarn/cache/asn1js-npm-3.0.5-cf5558af33-d0bc57da97.zip/node_modules/asn1js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["asn1js", "npm:3.0.5"],\
|
||||||
|
["pvtsutils", "npm:1.3.2"],\
|
||||||
|
["pvutils", "npm:1.1.3"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["async", [\
|
["async", [\
|
||||||
["npm:3.2.4", {\
|
["npm:3.2.4", {\
|
||||||
"packageLocation": "./.yarn/cache/async-npm-3.2.4-aba13508f9-9719e38d24.zip/node_modules/async/",\
|
"packageLocation": "./.yarn/cache/async-npm-3.2.4-aba13508f9-9719e38d24.zip/node_modules/async/",\
|
||||||
@@ -4987,6 +5114,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["base64url", [\
|
||||||
|
["npm:3.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/base64url-npm-3.0.1-4c171c4917-72e1401ffe.zip/node_modules/base64url/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["base64url", "npm:3.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["bcryptjs", [\
|
["bcryptjs", [\
|
||||||
["npm:2.4.3", {\
|
["npm:2.4.3", {\
|
||||||
"packageLocation": "./.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-bf6a43e9c4.zip/node_modules/bcryptjs/",\
|
"packageLocation": "./.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-bf6a43e9c4.zip/node_modules/bcryptjs/",\
|
||||||
@@ -5005,6 +5141,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["bignumber.js", [\
|
||||||
|
["npm:9.1.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-e44d008049.zip/node_modules/bignumber.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["bignumber.js", "npm:9.1.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["binary-extensions", [\
|
["binary-extensions", [\
|
||||||
["npm:2.2.0", {\
|
["npm:2.2.0", {\
|
||||||
"packageLocation": "./.yarn/cache/binary-extensions-npm-2.2.0-180c33fec7-16cf7c0cfd.zip/node_modules/binary-extensions/",\
|
"packageLocation": "./.yarn/cache/binary-extensions-npm-2.2.0-180c33fec7-16cf7c0cfd.zip/node_modules/binary-extensions/",\
|
||||||
@@ -5026,6 +5171,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["bn.js", [\
|
||||||
|
["npm:4.12.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/bn.js-npm-4.12.0-3ec6c884f6-bfb4590775.zip/node_modules/bn.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["bn.js", "npm:4.12.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["body-parser", [\
|
["body-parser", [\
|
||||||
["npm:1.20.1", {\
|
["npm:1.20.1", {\
|
||||||
"packageLocation": "./.yarn/cache/body-parser-npm-1.20.1-759fd14db9-33f202c9d5.zip/node_modules/body-parser/",\
|
"packageLocation": "./.yarn/cache/body-parser-npm-1.20.1-759fd14db9-33f202c9d5.zip/node_modules/body-parser/",\
|
||||||
@@ -5093,6 +5247,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["brorand", [\
|
||||||
|
["npm:1.1.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/brorand-npm-1.1.0-ea86634c4b-f736e127fb.zip/node_modules/brorand/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["brorand", "npm:1.1.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["browserslist", [\
|
["browserslist", [\
|
||||||
["npm:4.21.1", {\
|
["npm:4.21.1", {\
|
||||||
"packageLocation": "./.yarn/cache/browserslist-npm-4.21.1-930e90b93a-617d624493.zip/node_modules/browserslist/",\
|
"packageLocation": "./.yarn/cache/browserslist-npm-4.21.1-930e90b93a-617d624493.zip/node_modules/browserslist/",\
|
||||||
@@ -5328,6 +5491,17 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["cbor", [\
|
||||||
|
["npm:5.2.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/cbor-npm-5.2.0-4f6440587f-d60986b9d0.zip/node_modules/cbor/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["cbor", "npm:5.2.0"],\
|
||||||
|
["bignumber.js", "npm:9.1.1"],\
|
||||||
|
["nofilter", "npm:1.0.4"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["chalk", [\
|
["chalk", [\
|
||||||
["npm:2.4.2", {\
|
["npm:2.4.2", {\
|
||||||
"packageLocation": "./.yarn/cache/chalk-npm-2.4.2-3ea16dd91e-befd2fe888.zip/node_modules/chalk/",\
|
"packageLocation": "./.yarn/cache/chalk-npm-2.4.2-3ea16dd91e-befd2fe888.zip/node_modules/chalk/",\
|
||||||
@@ -6452,6 +6626,22 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["elliptic", [\
|
||||||
|
["npm:6.5.4", {\
|
||||||
|
"packageLocation": "./.yarn/cache/elliptic-npm-6.5.4-0ca8204a86-4453b008cf.zip/node_modules/elliptic/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["elliptic", "npm:6.5.4"],\
|
||||||
|
["bn.js", "npm:4.12.0"],\
|
||||||
|
["brorand", "npm:1.1.0"],\
|
||||||
|
["hash.js", "npm:1.1.7"],\
|
||||||
|
["hmac-drbg", "npm:1.0.1"],\
|
||||||
|
["inherits", "npm:2.0.4"],\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"],\
|
||||||
|
["minimalistic-crypto-utils", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["emittery", [\
|
["emittery", [\
|
||||||
["npm:0.10.2", {\
|
["npm:0.10.2", {\
|
||||||
"packageLocation": "./.yarn/cache/emittery-npm-0.10.2-aac10498b5-c55b286714.zip/node_modules/emittery/",\
|
"packageLocation": "./.yarn/cache/emittery-npm-0.10.2-aac10498b5-c55b286714.zip/node_modules/emittery/",\
|
||||||
@@ -7128,6 +7318,28 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["express-robots-txt", [\
|
||||||
|
["npm:1.0.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-54f066f6c3.zip/node_modules/express-robots-txt/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["express-robots-txt", "npm:1.0.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "SOFT"\
|
||||||
|
}],\
|
||||||
|
["virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0", {\
|
||||||
|
"packageLocation": "./.yarn/__virtual__/express-robots-txt-virtual-0a3eb9f2f5/0/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-54f066f6c3.zip/node_modules/express-robots-txt/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
|
||||||
|
["@types/express", "npm:4.17.14"],\
|
||||||
|
["express", "npm:4.18.2"]\
|
||||||
|
],\
|
||||||
|
"packagePeers": [\
|
||||||
|
"@types/express",\
|
||||||
|
"express"\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["express-winston", [\
|
["express-winston", [\
|
||||||
["npm:4.2.0", {\
|
["npm:4.2.0", {\
|
||||||
"packageLocation": "./.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-2d4b37671d.zip/node_modules/express-winston/",\
|
"packageLocation": "./.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-2d4b37671d.zip/node_modules/express-winston/",\
|
||||||
@@ -7958,6 +8170,17 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["hash.js", [\
|
||||||
|
["npm:1.1.7", {\
|
||||||
|
"packageLocation": "./.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e4266370d1.zip/node_modules/hash.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["hash.js", "npm:1.1.7"],\
|
||||||
|
["inherits", "npm:2.0.4"],\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["helmet", [\
|
["helmet", [\
|
||||||
["npm:6.0.0", {\
|
["npm:6.0.0", {\
|
||||||
"packageLocation": "./.yarn/cache/helmet-npm-6.0.0-2285459f57-73b6ba802d.zip/node_modules/helmet/",\
|
"packageLocation": "./.yarn/cache/helmet-npm-6.0.0-2285459f57-73b6ba802d.zip/node_modules/helmet/",\
|
||||||
@@ -7976,6 +8199,18 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["hmac-drbg", [\
|
||||||
|
["npm:1.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/hmac-drbg-npm-1.0.1-3499ad31cd-4e88d58ffc.zip/node_modules/hmac-drbg/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["hmac-drbg", "npm:1.0.1"],\
|
||||||
|
["hash.js", "npm:1.1.7"],\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"],\
|
||||||
|
["minimalistic-crypto-utils", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["hosted-git-info", [\
|
["hosted-git-info", [\
|
||||||
["npm:2.8.9", {\
|
["npm:2.8.9", {\
|
||||||
"packageLocation": "./.yarn/cache/hosted-git-info-npm-2.8.9-62c44fa93f-c24da52f98.zip/node_modules/hosted-git-info/",\
|
"packageLocation": "./.yarn/cache/hosted-git-info-npm-2.8.9-62c44fa93f-c24da52f98.zip/node_modules/hosted-git-info/",\
|
||||||
@@ -8066,6 +8301,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["https-proxy-agent", [\
|
["https-proxy-agent", [\
|
||||||
|
["npm:5.0.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.0-bb777903c3-77d11b0e2c.zip/node_modules/https-proxy-agent/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["https-proxy-agent", "npm:5.0.0"],\
|
||||||
|
["agent-base", "npm:6.0.2"],\
|
||||||
|
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
["npm:5.0.1", {\
|
["npm:5.0.1", {\
|
||||||
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.1-42d65f358e-8e767faec9.zip/node_modules/https-proxy-agent/",\
|
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.1-42d65f358e-8e767faec9.zip/node_modules/https-proxy-agent/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
@@ -8356,6 +8600,13 @@ const RAW_RUNTIME_STATE =
|
|||||||
["ipaddr.js", "npm:1.9.1"]\
|
["ipaddr.js", "npm:1.9.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
|
["npm:2.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/ipaddr.js-npm-2.0.1-04e97280d7-04ce6c896c.zip/node_modules/ipaddr.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["ipaddr.js", "npm:2.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["is-arguments", [\
|
["is-arguments", [\
|
||||||
@@ -9556,6 +9807,26 @@ const RAW_RUNTIME_STATE =
|
|||||||
["semver", "npm:5.7.1"]\
|
["semver", "npm:5.7.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
|
["npm:9.0.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/jsonwebtoken-npm-9.0.0-36fd1594c0-7ccbd0b7bf.zip/node_modules/jsonwebtoken/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
|
["jws", "npm:3.2.2"],\
|
||||||
|
["lodash", "npm:4.17.21"],\
|
||||||
|
["ms", "npm:2.1.3"],\
|
||||||
|
["semver", "npm:7.3.8"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["jsrsasign", [\
|
||||||
|
["npm:10.6.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/jsrsasign-npm-10.6.1-a8fa295369-e8e9c1b24f.zip/node_modules/jsrsasign/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["jsrsasign", "npm:10.6.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["jwa", [\
|
["jwa", [\
|
||||||
@@ -9570,6 +9841,18 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["jwk-to-pem", [\
|
||||||
|
["npm:2.0.5", {\
|
||||||
|
"packageLocation": "./.yarn/cache/jwk-to-pem-npm-2.0.5-aff7d9f125-fced3a75b0.zip/node_modules/jwk-to-pem/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["jwk-to-pem", "npm:2.0.5"],\
|
||||||
|
["asn1.js", "npm:5.4.1"],\
|
||||||
|
["elliptic", "npm:6.5.4"],\
|
||||||
|
["safe-buffer", "npm:5.2.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["jws", [\
|
["jws", [\
|
||||||
["npm:3.2.2", {\
|
["npm:3.2.2", {\
|
||||||
"packageLocation": "./.yarn/cache/jws-npm-3.2.2-c1ae59c7af-347ed7c334.zip/node_modules/jws/",\
|
"packageLocation": "./.yarn/cache/jws-npm-3.2.2-c1ae59c7af-347ed7c334.zip/node_modules/jws/",\
|
||||||
@@ -10183,6 +10466,24 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["minimalistic-assert", [\
|
||||||
|
["npm:1.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/minimalistic-assert-npm-1.0.1-dc8bb23d29-e2310081d8.zip/node_modules/minimalistic-assert/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["minimalistic-crypto-utils", [\
|
||||||
|
["npm:1.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/minimalistic-crypto-utils-npm-1.0.1-e66b10822e-7d909decd2.zip/node_modules/minimalistic-crypto-utils/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["minimalistic-crypto-utils", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["minimatch", [\
|
["minimatch", [\
|
||||||
["npm:3.1.2", {\
|
["npm:3.1.2", {\
|
||||||
"packageLocation": "./.yarn/cache/minimatch-npm-3.1.2-9405269906-97f5615ee8.zip/node_modules/minimatch/",\
|
"packageLocation": "./.yarn/cache/minimatch-npm-3.1.2-9405269906-97f5615ee8.zip/node_modules/minimatch/",\
|
||||||
@@ -10307,6 +10608,16 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["mixpanel", [\
|
||||||
|
["npm:0.17.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/mixpanel-npm-0.17.0-3073ce9949-5a945bdbdd.zip/node_modules/mixpanel/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["mixpanel", "npm:0.17.0"],\
|
||||||
|
["https-proxy-agent", "npm:5.0.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["mkdirp", [\
|
["mkdirp", [\
|
||||||
["npm:1.0.4", {\
|
["npm:1.0.4", {\
|
||||||
"packageLocation": "./.yarn/cache/mkdirp-npm-1.0.4-37f6ef56b9-1233611198.zip/node_modules/mkdirp/",\
|
"packageLocation": "./.yarn/cache/mkdirp-npm-1.0.4-37f6ef56b9-1233611198.zip/node_modules/mkdirp/",\
|
||||||
@@ -10567,6 +10878,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["nofilter", [\
|
||||||
|
["npm:1.0.4", {\
|
||||||
|
"packageLocation": "./.yarn/cache/nofilter-npm-1.0.4-1cbdc6c03a-9a26874e7d.zip/node_modules/nofilter/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["nofilter", "npm:1.0.4"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["nopt", [\
|
["nopt", [\
|
||||||
["npm:1.0.10", {\
|
["npm:1.0.10", {\
|
||||||
"packageLocation": "./.yarn/cache/nopt-npm-1.0.10-f3db192976-efa5a9c2c1.zip/node_modules/nopt/",\
|
"packageLocation": "./.yarn/cache/nopt-npm-1.0.10-f3db192976-efa5a9c2c1.zip/node_modules/nopt/",\
|
||||||
@@ -11615,6 +11935,25 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["pvtsutils", [\
|
||||||
|
["npm:1.3.2", {\
|
||||||
|
"packageLocation": "./.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-eb22d3df60.zip/node_modules/pvtsutils/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["pvtsutils", "npm:1.3.2"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["pvutils", [\
|
||||||
|
["npm:1.1.3", {\
|
||||||
|
"packageLocation": "./.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-0cb4f4878f.zip/node_modules/pvutils/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["pvutils", "npm:1.1.3"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["q", [\
|
["q", [\
|
||||||
["npm:1.5.1", {\
|
["npm:1.5.1", {\
|
||||||
"packageLocation": "./.yarn/cache/q-npm-1.5.1-a28b3cfeaf-276b7e93fc.zip/node_modules/q/",\
|
"packageLocation": "./.yarn/cache/q-npm-1.5.1-a28b3cfeaf-276b7e93fc.zip/node_modules/q/",\
|
||||||
@@ -12193,6 +12532,14 @@ const RAW_RUNTIME_STATE =
|
|||||||
["lru-cache", "npm:6.0.0"]\
|
["lru-cache", "npm:6.0.0"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
|
["npm:7.3.8", {\
|
||||||
|
"packageLocation": "./.yarn/cache/semver-npm-7.3.8-25a996cb4f-94ad80ee14.zip/node_modules/semver/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["semver", "npm:7.3.8"],\
|
||||||
|
["lru-cache", "npm:6.0.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["semver-diff", [\
|
["semver-diff", [\
|
||||||
@@ -13299,6 +13646,13 @@ const RAW_RUNTIME_STATE =
|
|||||||
["tslib", "npm:2.4.0"]\
|
["tslib", "npm:2.4.0"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
|
["npm:2.4.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/tslib-npm-2.4.1-36f0ed04db-a739a21e3f.zip/node_modules/tslib/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["tsutils", [\
|
["tsutils", [\
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -61,7 +61,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "yarn@4.0.0-rc.25",
|
"packageManager": "yarn@4.0.0-rc.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/node": "^7.19.0",
|
"@sentry/node": "^7.28.1",
|
||||||
"newrelic": "^9.6.0"
|
"newrelic": "^9.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ DB_DATABASE=analytics
|
|||||||
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
||||||
DB_MIGRATIONS_PATH=dist/migrations/*.js
|
DB_MIGRATIONS_PATH=dist/migrations/*.js
|
||||||
|
|
||||||
|
ADMIN_EMAILS=test@standardnotes.com
|
||||||
|
|
||||||
REDIS_URL=redis://cache
|
REDIS_URL=redis://cache
|
||||||
REDIS_EVENTS_CHANNEL=events
|
REDIS_EVENTS_CHANNEL=events
|
||||||
|
|
||||||
@@ -26,3 +28,6 @@ NEW_RELIC_NO_CONFIG_FILE=true
|
|||||||
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
||||||
NEW_RELIC_LOG_ENABLED=false
|
NEW_RELIC_LOG_ENABLED=false
|
||||||
NEW_RELIC_LOG_LEVEL=info
|
NEW_RELIC_LOG_LEVEL=info
|
||||||
|
|
||||||
|
# (Optional) Mixpanel
|
||||||
|
MIXPANEL_TOKEN=
|
||||||
|
|||||||
@@ -3,6 +3,159 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.19.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.1...@standardnotes/analytics@2.19.2) (2023-01-13)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.19.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.0...@standardnotes/analytics@2.19.1) (2022-12-30)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** remove unnecesary context from mixpanel events ([ba1e1ad](https://github.com/standardnotes/server/commit/ba1e1ad5ad82b052be4cc2d1cc2abdaf3b72cf4c))
|
||||||
|
|
||||||
|
# [2.19.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.18.0...@standardnotes/analytics@2.19.0) (2022-12-30)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add mixpanel events tracking ([df6e3f0](https://github.com/standardnotes/server/commit/df6e3f06a6868e30e60dd98431122983724644b4))
|
||||||
|
|
||||||
|
# [2.18.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.8...@standardnotes/analytics@2.18.0) (2022-12-30)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add mixpanel ([893d617](https://github.com/standardnotes/server/commit/893d6176c3b0b56c45e5188fe982232db2ceedc4))
|
||||||
|
|
||||||
|
## [2.17.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.7...@standardnotes/analytics@2.17.8) (2022-12-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.17.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.6...@standardnotes/analytics@2.17.7) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** monthly numbers of active users ([b34bbca](https://github.com/standardnotes/server/commit/b34bbcac8b9604283b3a5959ab3218c468ce8a00))
|
||||||
|
|
||||||
|
## [2.17.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.5...@standardnotes/analytics@2.17.6) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** filtered counts for user activity check ([17b2ea1](https://github.com/standardnotes/server/commit/17b2ea126c5ad2d7cf07657def63f9977f239a3c))
|
||||||
|
|
||||||
|
## [2.17.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.4...@standardnotes/analytics@2.17.5) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** accessing analytics in report ([ef26dc8](https://github.com/standardnotes/server/commit/ef26dc8cbb967e088ae7387ff6dbec1e60dc3ee4))
|
||||||
|
|
||||||
|
## [2.17.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.3...@standardnotes/analytics@2.17.4) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.17.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.2...@standardnotes/analytics@2.17.3) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** add debug logs for the report ([031fcd7](https://github.com/standardnotes/server/commit/031fcd75eecdcf4c2f17257754a0ba3f24ba6d6e))
|
||||||
|
|
||||||
|
## [2.17.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.1...@standardnotes/analytics@2.17.2) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** calculating active users ([a304993](https://github.com/standardnotes/server/commit/a3049938a31e21a5867a314ac62bee6aa4990d57))
|
||||||
|
|
||||||
|
## [2.17.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.0...@standardnotes/analytics@2.17.1) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** container binding ([04ffc69](https://github.com/standardnotes/server/commit/04ffc69e000803107d8834c286de97b3d213a842))
|
||||||
|
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
|
||||||
|
|
||||||
|
# [2.17.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.16.0...@standardnotes/analytics@2.17.0) (2022-12-20)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add users activit to the report email ([ed5a4eb](https://github.com/standardnotes/server/commit/ed5a4eb960a6c8fe9d0c77331f29dc3c7ffb9100))
|
||||||
|
|
||||||
|
# [2.16.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.1...@standardnotes/analytics@2.16.0) (2022-12-20)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add active users stats to report ([6e16620](https://github.com/standardnotes/server/commit/6e1662038c3340fb60939464616789bab7639160))
|
||||||
|
|
||||||
|
## [2.15.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.0...@standardnotes/analytics@2.15.1) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
|
||||||
|
|
||||||
|
# [2.15.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.14.0...@standardnotes/analytics@2.15.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
|
||||||
|
|
||||||
|
# [2.14.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.13.0...@standardnotes/analytics@2.14.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add persisting statistics on demand ([0f84575](https://github.com/standardnotes/server/commit/0f8457534c1829c58f3c036749d262307ddeb779))
|
||||||
|
|
||||||
|
# [2.13.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.27...@standardnotes/analytics@2.13.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add session traces ([8bcb552](https://github.com/standardnotes/server/commit/8bcb552783b2d12f3296b3195752168482790bc8))
|
||||||
|
|
||||||
|
## [2.12.27](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.26...@standardnotes/analytics@2.12.27) (2022-12-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.26](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.25...@standardnotes/analytics@2.12.26) (2022-12-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.25](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.24...@standardnotes/analytics@2.12.25) (2022-12-12)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.24](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.23...@standardnotes/analytics@2.12.24) (2022-12-12)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** daily analytics report template ([41906ec](https://github.com/standardnotes/server/commit/41906ec2f9fd4d605b1c002826173e14fb534e00))
|
||||||
|
|
||||||
|
## [2.12.23](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.22...@standardnotes/analytics@2.12.23) (2022-12-12)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [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)
|
## [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
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import 'newrelic'
|
|||||||
|
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
import { EmailLevel } from '@standardnotes/domain-core'
|
||||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||||
import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
|
||||||
import { Period } from '../src/Domain/Time/Period'
|
import { Period } from '../src/Domain/Time/Period'
|
||||||
import { StatisticsMeasure } from '../src/Domain/Statistics/StatisticsMeasure'
|
|
||||||
import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface'
|
||||||
import { PeriodKeyGeneratorInterface } from '../src/Domain/Time/PeriodKeyGeneratorInterface'
|
import { PeriodKeyGeneratorInterface } from '../src/Domain/Time/PeriodKeyGeneratorInterface'
|
||||||
@@ -16,6 +16,9 @@ import TYPES from '../src/Bootstrap/Types'
|
|||||||
import { Env } from '../src/Bootstrap/Env'
|
import { Env } from '../src/Bootstrap/Env'
|
||||||
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||||
import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
||||||
|
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { StatisticMeasureName } from '../src/Domain/Statistics/StatisticMeasureName'
|
||||||
|
|
||||||
const requestReport = async (
|
const requestReport = async (
|
||||||
analyticsStore: AnalyticsStoreInterface,
|
analyticsStore: AnalyticsStoreInterface,
|
||||||
@@ -24,6 +27,8 @@ const requestReport = async (
|
|||||||
domainEventPublisher: DomainEventPublisherInterface,
|
domainEventPublisher: DomainEventPublisherInterface,
|
||||||
periodKeyGenerator: PeriodKeyGeneratorInterface,
|
periodKeyGenerator: PeriodKeyGeneratorInterface,
|
||||||
calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
|
calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
|
||||||
|
timer: TimerInterface,
|
||||||
|
adminEmails: string[],
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await calculateMonthlyRecurringRevenue.execute({})
|
await calculateMonthlyRecurringRevenue.execute({})
|
||||||
|
|
||||||
@@ -110,12 +115,16 @@ const requestReport = async (
|
|||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
const thirtyDaysStatisticsNames = [
|
const thirtyDaysStatisticsNames = [
|
||||||
StatisticsMeasure.MRR,
|
StatisticMeasureName.NAMES.MRR,
|
||||||
StatisticsMeasure.AnnualPlansMRR,
|
StatisticMeasureName.NAMES.AnnualPlansMRR,
|
||||||
StatisticsMeasure.MonthlyPlansMRR,
|
StatisticMeasureName.NAMES.MonthlyPlansMRR,
|
||||||
StatisticsMeasure.FiveYearPlansMRR,
|
StatisticMeasureName.NAMES.FiveYearPlansMRR,
|
||||||
StatisticsMeasure.PlusPlansMRR,
|
StatisticMeasureName.NAMES.PlusPlansMRR,
|
||||||
StatisticsMeasure.ProPlansMRR,
|
StatisticMeasureName.NAMES.ProPlansMRR,
|
||||||
|
StatisticMeasureName.NAMES.ActiveUsers,
|
||||||
|
StatisticMeasureName.NAMES.ActiveFreeUsers,
|
||||||
|
StatisticMeasureName.NAMES.ActivePlusUsers,
|
||||||
|
StatisticMeasureName.NAMES.ActiveProUsers,
|
||||||
]
|
]
|
||||||
for (const statisticName of thirtyDaysStatisticsNames) {
|
for (const statisticName of thirtyDaysStatisticsNames) {
|
||||||
statisticsOverTime.push({
|
statisticsOverTime.push({
|
||||||
@@ -125,7 +134,7 @@ const requestReport = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthlyStatisticsNames = [StatisticsMeasure.MRR]
|
const monthlyStatisticsNames = [StatisticMeasureName.NAMES.MRR]
|
||||||
for (const statisticName of monthlyStatisticsNames) {
|
for (const statisticName of monthlyStatisticsNames) {
|
||||||
statisticsOverTime.push({
|
statisticsOverTime.push({
|
||||||
name: statisticName,
|
name: statisticName,
|
||||||
@@ -135,22 +144,22 @@ const requestReport = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statisticMeasureNames = [
|
const statisticMeasureNames = [
|
||||||
StatisticsMeasure.Income,
|
StatisticMeasureName.NAMES.Income,
|
||||||
StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.Refunds,
|
StatisticMeasureName.NAMES.Refunds,
|
||||||
StatisticsMeasure.RegistrationLength,
|
StatisticMeasureName.NAMES.RegistrationLength,
|
||||||
StatisticsMeasure.SubscriptionLength,
|
StatisticMeasureName.NAMES.SubscriptionLength,
|
||||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
|
||||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
|
||||||
StatisticsMeasure.NewCustomers,
|
StatisticMeasureName.NAMES.NewCustomers,
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
]
|
]
|
||||||
const statisticMeasures: Array<{
|
const statisticMeasures: Array<{
|
||||||
name: string
|
name: string
|
||||||
@@ -185,7 +194,10 @@ const requestReport = async (
|
|||||||
|
|
||||||
const totalCustomerCounts: Array<number> = []
|
const totalCustomerCounts: Array<number> = []
|
||||||
for (const dailyPeriodKey of dailyPeriodKeys) {
|
for (const dailyPeriodKey of dailyPeriodKeys) {
|
||||||
const customersCount = await statisticsStore.getMeasureTotal(StatisticsMeasure.TotalCustomers, dailyPeriodKey)
|
const customersCount = await statisticsStore.getMeasureTotal(
|
||||||
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
|
dailyPeriodKey,
|
||||||
|
)
|
||||||
totalCustomerCounts.push(customersCount)
|
totalCustomerCounts.push(customersCount)
|
||||||
}
|
}
|
||||||
const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count)
|
const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count)
|
||||||
@@ -213,18 +225,29 @@ const requestReport = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = domainEventFactory.createDailyAnalyticsReportGeneratedEvent({
|
for (const adminEmail of adminEmails) {
|
||||||
activityStatistics: yesterdayActivityStatistics,
|
await domainEventPublisher.publish(
|
||||||
activityStatisticsOverTime: analyticsOverTime,
|
domainEventFactory.createEmailRequestedEvent({
|
||||||
statisticsOverTime,
|
messageIdentifier: 'VERSION_ADOPTION_REPORT',
|
||||||
statisticMeasures,
|
subject: getSubject(),
|
||||||
churn: {
|
body: getBody(
|
||||||
periodKeys: monthlyPeriodKeys,
|
{
|
||||||
values: churnRates,
|
activityStatistics: yesterdayActivityStatistics,
|
||||||
},
|
activityStatisticsOverTime: analyticsOverTime,
|
||||||
})
|
statisticsOverTime,
|
||||||
|
statisticMeasures,
|
||||||
await domainEventPublisher.publish(event)
|
churn: {
|
||||||
|
periodKeys: monthlyPeriodKeys,
|
||||||
|
values: churnRates,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timer,
|
||||||
|
),
|
||||||
|
level: EmailLevel.LEVELS.System,
|
||||||
|
userEmail: adminEmail,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = new ContainerConfigLoader()
|
const container = new ContainerConfigLoader()
|
||||||
@@ -241,9 +264,13 @@ void container.load().then((container) => {
|
|||||||
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
|
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
|
||||||
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
|
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
|
||||||
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
|
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
|
||||||
|
const timer: TimerInterface = container.get(TYPES.Timer)
|
||||||
const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
|
const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
|
||||||
TYPES.CalculateMonthlyRecurringRevenue,
|
TYPES.CalculateMonthlyRecurringRevenue,
|
||||||
)
|
)
|
||||||
|
const adminEmails = container.get(TYPES.ADMIN_EMAILS) as string[]
|
||||||
|
|
||||||
|
logger.info(`Sending report to following admins: ${adminEmails}`)
|
||||||
|
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
requestReport(
|
requestReport(
|
||||||
@@ -253,6 +280,8 @@ void container.load().then((container) => {
|
|||||||
domainEventPublisher,
|
domainEventPublisher,
|
||||||
periodKeyGenerator,
|
periodKeyGenerator,
|
||||||
calculateMonthlyRecurringRevenue,
|
calculateMonthlyRecurringRevenue,
|
||||||
|
timer,
|
||||||
|
adminEmails,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ COMMAND=$1 && shift 1
|
|||||||
|
|
||||||
case "$COMMAND" in
|
case "$COMMAND" in
|
||||||
'start-worker' )
|
'start-worker' )
|
||||||
echo "Starting Worker..."
|
echo "[Docker] Starting Worker..."
|
||||||
yarn workspace @standardnotes/analytics worker
|
yarn workspace @standardnotes/analytics worker
|
||||||
;;
|
;;
|
||||||
|
|
||||||
'report' )
|
'report' )
|
||||||
echo "Starting Usage Report Generation..."
|
echo "[Docker] Starting Usage Report Generation..."
|
||||||
yarn workspace @standardnotes/analytics report
|
yarn workspace @standardnotes/analytics report
|
||||||
;;
|
;;
|
||||||
|
|
||||||
* )
|
* )
|
||||||
echo "Unknown command"
|
echo "[Docker] Unknown command"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ module.exports = {
|
|||||||
transform: {
|
transform: {
|
||||||
...tsjPreset.transform,
|
...tsjPreset.transform,
|
||||||
},
|
},
|
||||||
coveragePathIgnorePatterns: ['/Infra/'],
|
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/', '/Handler/'],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/analytics",
|
"name": "@standardnotes/analytics",
|
||||||
"version": "2.12.16",
|
"version": "2.19.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ioredis": "^5.0.0",
|
"@types/ioredis": "^5.0.0",
|
||||||
"@types/jest": "^29.1.1",
|
"@types/jest": "^29.1.1",
|
||||||
|
"@types/mixpanel": "^2.14.4",
|
||||||
"@types/newrelic": "^7.0.4",
|
"@types/newrelic": "^7.0.4",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||||
@@ -38,9 +39,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@newrelic/winston-enricher": "^4.0.0",
|
"@newrelic/winston-enricher": "^4.0.0",
|
||||||
"@sentry/node": "^7.19.0",
|
"@sentry/node": "^7.28.1",
|
||||||
"@standardnotes/common": "workspace:*",
|
"@standardnotes/common": "workspace:*",
|
||||||
"@standardnotes/domain-core": "workspace:*",
|
"@standardnotes/domain-core": "workspace:^",
|
||||||
"@standardnotes/domain-events": "workspace:*",
|
"@standardnotes/domain-events": "workspace:*",
|
||||||
"@standardnotes/domain-events-infra": "workspace:*",
|
"@standardnotes/domain-events-infra": "workspace:*",
|
||||||
"@standardnotes/time": "workspace:*",
|
"@standardnotes/time": "workspace:*",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"inversify": "^6.0.1",
|
"inversify": "^6.0.1",
|
||||||
"ioredis": "^5.2.4",
|
"ioredis": "^5.2.4",
|
||||||
|
"mixpanel": "^0.17.0",
|
||||||
"mysql2": "^2.3.3",
|
"mysql2": "^2.3.3",
|
||||||
"newrelic": "^9.6.0",
|
"newrelic": "^9.6.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
DomainEventSubscriberFactoryInterface,
|
DomainEventSubscriberFactoryInterface,
|
||||||
} from '@standardnotes/domain-events'
|
} from '@standardnotes/domain-events'
|
||||||
import { MapperInterface } from '@standardnotes/domain-core'
|
import { MapperInterface } from '@standardnotes/domain-core'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const Mixpanel = require('mixpanel')
|
||||||
|
|
||||||
import { Env } from './Env'
|
import { Env } from './Env'
|
||||||
import TYPES from './Types'
|
import TYPES from './Types'
|
||||||
@@ -52,6 +54,9 @@ import { RevenueModification } from '../Domain/Revenue/RevenueModification'
|
|||||||
import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap'
|
import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap'
|
||||||
import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification'
|
import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification'
|
||||||
import { CalculateMonthlyRecurringRevenue } from '../Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
import { CalculateMonthlyRecurringRevenue } from '../Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
||||||
|
import { PersistStatistic } from '../Domain/UseCase/PersistStatistic/PersistStatistic'
|
||||||
|
import { StatisticMeasureRepositoryInterface } from '../Domain/Statistics/StatisticMeasureRepositoryInterface'
|
||||||
|
import { StatisticPersistenceRequestedEventHandler } from '../Domain/Handler/StatisticPersistenceRequestedEventHandler'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||||
@@ -130,6 +135,34 @@ export class ContainerConfigLoader {
|
|||||||
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', 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.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.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||||
|
container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
|
||||||
|
container.bind(TYPES.MIXPANEL_TOKEN).toConstantValue(env.get('MIXPANEL_TOKEN', true))
|
||||||
|
|
||||||
|
// Services
|
||||||
|
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||||
|
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
|
||||||
|
container
|
||||||
|
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
|
||||||
|
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||||
|
container
|
||||||
|
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
||||||
|
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||||
|
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||||
|
|
||||||
|
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||||
|
container
|
||||||
|
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||||
|
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
||||||
|
} else {
|
||||||
|
container
|
||||||
|
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||||
|
.toConstantValue(
|
||||||
|
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (env.get('MIXPANEL_TOKEN', true)) {
|
||||||
|
container.bind<Mixpanel>(TYPES.MixpanelClient).toConstantValue(Mixpanel.init(env.get('MIXPANEL_TOKEN', true)))
|
||||||
|
}
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
container
|
container
|
||||||
@@ -138,6 +171,9 @@ export class ContainerConfigLoader {
|
|||||||
container
|
container
|
||||||
.bind<RevenueModificationRepositoryInterface>(TYPES.RevenueModificationRepository)
|
.bind<RevenueModificationRepositoryInterface>(TYPES.RevenueModificationRepository)
|
||||||
.to(MySQLRevenueModificationRepository)
|
.to(MySQLRevenueModificationRepository)
|
||||||
|
container
|
||||||
|
.bind<StatisticMeasureRepositoryInterface>(TYPES.StatisticMeasureRepository)
|
||||||
|
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||||
|
|
||||||
// ORM
|
// ORM
|
||||||
container
|
container
|
||||||
@@ -153,6 +189,9 @@ export class ContainerConfigLoader {
|
|||||||
container
|
container
|
||||||
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
|
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
|
||||||
.to(CalculateMonthlyRecurringRevenue)
|
.to(CalculateMonthlyRecurringRevenue)
|
||||||
|
container
|
||||||
|
.bind<PersistStatistic>(TYPES.PersistStatistic)
|
||||||
|
.toConstantValue(new PersistStatistic(container.get(TYPES.StatisticMeasureRepository)))
|
||||||
|
|
||||||
// Hanlders
|
// Hanlders
|
||||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||||
@@ -180,35 +219,22 @@ export class ContainerConfigLoader {
|
|||||||
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
||||||
.to(SubscriptionReactivatedEventHandler)
|
.to(SubscriptionReactivatedEventHandler)
|
||||||
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
||||||
|
container
|
||||||
|
.bind<StatisticPersistenceRequestedEventHandler>(TYPES.StatisticPersistenceRequestedEventHandler)
|
||||||
|
.toConstantValue(
|
||||||
|
new StatisticPersistenceRequestedEventHandler(
|
||||||
|
container.get(TYPES.PersistStatistic),
|
||||||
|
container.get(TYPES.Timer),
|
||||||
|
container.get(TYPES.Logger),
|
||||||
|
env.get('MIXPANEL_TOKEN', true) ? container.get(TYPES.MixpanelClient) : null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// Maps
|
// Maps
|
||||||
container
|
container
|
||||||
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
|
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
|
||||||
.to(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([
|
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||||
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
|
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
|
||||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
||||||
@@ -221,6 +247,7 @@ export class ContainerConfigLoader {
|
|||||||
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
|
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
|
||||||
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
||||||
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
||||||
|
['STATISTIC_PERSISTENCE_REQUESTED', container.get(TYPES.StatisticPersistenceRequestedEventHandler)],
|
||||||
])
|
])
|
||||||
|
|
||||||
if (env.get('SQS_QUEUE_URL', true)) {
|
if (env.get('SQS_QUEUE_URL', true)) {
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ const TYPES = {
|
|||||||
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
||||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||||
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||||
|
ADMIN_EMAILS: Symbol.for('ADMIN_EMAILS'),
|
||||||
|
MIXPANEL_TOKEN: Symbol.for('MIXPANEL_TOKEN'),
|
||||||
// Repositories
|
// Repositories
|
||||||
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
|
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
|
||||||
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
|
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
|
||||||
|
StatisticMeasureRepository: Symbol.for('StatisticMeasureRepository'),
|
||||||
// ORM
|
// ORM
|
||||||
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
|
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
|
||||||
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
|
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
|
||||||
@@ -21,6 +24,7 @@ const TYPES = {
|
|||||||
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
||||||
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
|
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
|
||||||
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
|
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
|
||||||
|
PersistStatistic: Symbol.for('PersistStatistic'),
|
||||||
// Handlers
|
// Handlers
|
||||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||||
@@ -33,6 +37,7 @@ const TYPES = {
|
|||||||
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
|
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
|
||||||
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
||||||
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
||||||
|
StatisticPersistenceRequestedEventHandler: Symbol.for('StatisticPersistenceRequestedEventHandler'),
|
||||||
// Maps
|
// Maps
|
||||||
RevenueModificationMap: Symbol.for('RevenueModificationMap'),
|
RevenueModificationMap: Symbol.for('RevenueModificationMap'),
|
||||||
// Services
|
// Services
|
||||||
@@ -44,6 +49,7 @@ const TYPES = {
|
|||||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||||
Timer: Symbol.for('Timer'),
|
Timer: Symbol.for('Timer'),
|
||||||
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
|
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
|
||||||
|
MixpanelClient: Symbol.for('MixpanelClient'),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TYPES
|
export default TYPES
|
||||||
|
|||||||
@@ -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,979 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
|
import { Period } from '../Time/Period'
|
||||||
|
|
||||||
|
const countActiveUsers = (measureName: string, data: any): { yesterday: number; last30Days: number } => {
|
||||||
|
const totalActiveUsersLast30DaysIncludingToday = data.statisticsOverTime.find(
|
||||||
|
(a: { name: string; period: number }) => a.name === measureName && a.period === 27,
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalActiveUsersYesterday =
|
||||||
|
totalActiveUsersLast30DaysIncludingToday.counts[totalActiveUsersLast30DaysIncludingToday.counts.length - 2]
|
||||||
|
.totalCount
|
||||||
|
|
||||||
|
const filteredCounts = totalActiveUsersLast30DaysIncludingToday.counts.filter(
|
||||||
|
(count: { totalCount: number }) => count.totalCount !== 0,
|
||||||
|
)
|
||||||
|
if (filteredCounts.length === 0) {
|
||||||
|
return {
|
||||||
|
yesterday: 0,
|
||||||
|
last30Days: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const last30DaysNumbers = filteredCounts.map((count: { totalCount: number }) => count.totalCount)
|
||||||
|
const last30DaysCount = last30DaysNumbers.reduce((previousValue: number, currentValue: number) => {
|
||||||
|
return previousValue + currentValue
|
||||||
|
})
|
||||||
|
|
||||||
|
const averageActiveUsersLast30Days = Math.floor(last30DaysCount / last30DaysNumbers.length)
|
||||||
|
|
||||||
|
return {
|
||||||
|
yesterday: totalActiveUsersYesterday,
|
||||||
|
last30Days: averageActiveUsersLast30Days,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChartUrls = (
|
||||||
|
data: any,
|
||||||
|
): {
|
||||||
|
subscriptions: string
|
||||||
|
users: string
|
||||||
|
quarterlyPerformance: string
|
||||||
|
churn: 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 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))}`,
|
||||||
|
mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const html = (data: any, timer: TimerInterface) => {
|
||||||
|
const chartUrls = getChartUrls(data)
|
||||||
|
|
||||||
|
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: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const refundMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0
|
||||||
|
const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0
|
||||||
|
const revenueYesterday = incomeYesterday - refundsYesterday
|
||||||
|
|
||||||
|
const subscriptionLengthMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const subscriptionRemainingTimePercentageYesterday = Math.floor(
|
||||||
|
subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const registrationLengthMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(registrationLengthMeasureYesterday?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const incomeMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const refundMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0
|
||||||
|
const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0
|
||||||
|
const revenueThisMonth = incomeThisMonth - refundsThisMonth
|
||||||
|
|
||||||
|
const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const subscriptionRemainingTimePercentageThisMonth = Math.floor(
|
||||||
|
subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const registrationLengthMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(registrationLengthMeasureThisMonth?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.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,
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalActiveUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveUsers, data)
|
||||||
|
const totalActiveFreeUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveFreeUsers, data)
|
||||||
|
const totalActivePlusUsers = countActiveUsers(StatisticMeasureName.NAMES.ActivePlusUsers, data)
|
||||||
|
const totalActiveProUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveProUsers, data)
|
||||||
|
|
||||||
|
return ` <div>
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>
|
||||||
|
<strong>Here are some statistics from yesterday:</strong>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Active Users</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Total:</b> ${totalActiveUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>By Subscription Type:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>FREE:</b> ${totalActiveFreeUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PLUS:</b> ${totalActivePlusUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PRO:</b> ${totalActiveProUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Payments</b>
|
||||||
|
<ul>
|
||||||
|
<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>Active Users (Average)</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Total:</b> ${totalActiveUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>By Subscription Type:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>FREE:</b> ${totalActiveFreeUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PLUS:</b> ${totalActivePlusUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PRO:</b> ${totalActiveProUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Payments (This Month)</b>
|
||||||
|
<ul>
|
||||||
|
<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 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>`
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
|
|
||||||
import { DomainEventService, DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
|
import { DomainEventService, EmailRequestedEvent } from '@standardnotes/domain-events'
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable } from 'inversify'
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
@@ -9,55 +9,20 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
|||||||
@injectable()
|
@injectable()
|
||||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||||
|
createEmailRequestedEvent(dto: {
|
||||||
createDailyAnalyticsReportGeneratedEvent(dto: {
|
userEmail: string
|
||||||
activityStatistics: Array<{
|
messageIdentifier: string
|
||||||
name: string
|
level: string
|
||||||
retention: number
|
body: string
|
||||||
totalCount: number
|
subject: string
|
||||||
}>
|
}): EmailRequestedEvent {
|
||||||
statisticMeasures: Array<{
|
|
||||||
name: string
|
|
||||||
totalValue: number
|
|
||||||
average: number
|
|
||||||
increments: number
|
|
||||||
period: number
|
|
||||||
}>
|
|
||||||
activityStatisticsOverTime: Array<{
|
|
||||||
name: string
|
|
||||||
period: number
|
|
||||||
counts: Array<{
|
|
||||||
periodKey: string
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
statisticsOverTime: Array<{
|
|
||||||
name: string
|
|
||||||
period: number
|
|
||||||
counts: Array<{
|
|
||||||
periodKey: string
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
}>
|
|
||||||
churn: {
|
|
||||||
periodKeys: Array<string>
|
|
||||||
values: Array<{
|
|
||||||
rate: number
|
|
||||||
periodKey: string
|
|
||||||
averageCustomersCount: number
|
|
||||||
existingCustomersChurn: number
|
|
||||||
newCustomersChurn: number
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
}): DailyAnalyticsReportGeneratedEvent {
|
|
||||||
return {
|
return {
|
||||||
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
|
type: 'EMAIL_REQUESTED',
|
||||||
createdAt: this.timer.getUTCDate(),
|
createdAt: this.timer.getUTCDate(),
|
||||||
meta: {
|
meta: {
|
||||||
correlation: {
|
correlation: {
|
||||||
userIdentifier: '',
|
userIdentifier: dto.userEmail,
|
||||||
userIdentifierType: 'uuid',
|
userIdentifierType: 'email',
|
||||||
},
|
},
|
||||||
origin: DomainEventService.Analytics,
|
origin: DomainEventService.Analytics,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,45 +1,11 @@
|
|||||||
import { DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
|
import { EmailRequestedEvent } from '@standardnotes/domain-events'
|
||||||
|
|
||||||
export interface DomainEventFactoryInterface {
|
export interface DomainEventFactoryInterface {
|
||||||
createDailyAnalyticsReportGeneratedEvent(dto: {
|
createEmailRequestedEvent(dto: {
|
||||||
activityStatistics: Array<{
|
userEmail: string
|
||||||
name: string
|
messageIdentifier: string
|
||||||
retention: number
|
level: string
|
||||||
totalCount: number
|
body: string
|
||||||
}>
|
subject: string
|
||||||
statisticMeasures: Array<{
|
}): EmailRequestedEvent
|
||||||
name: string
|
|
||||||
totalValue: number
|
|
||||||
average: number
|
|
||||||
increments: number
|
|
||||||
period: number
|
|
||||||
}>
|
|
||||||
activityStatisticsOverTime: Array<{
|
|
||||||
name: string
|
|
||||||
period: number
|
|
||||||
counts: Array<{
|
|
||||||
periodKey: string
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
statisticsOverTime: Array<{
|
|
||||||
name: string
|
|
||||||
period: number
|
|
||||||
counts: Array<{
|
|
||||||
periodKey: string
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
}>
|
|
||||||
churn: {
|
|
||||||
periodKeys: Array<string>
|
|
||||||
values: Array<{
|
|
||||||
rate: number
|
|
||||||
periodKey: string
|
|
||||||
averageCustomersCount: number
|
|
||||||
existingCustomersChurn: number
|
|
||||||
newCustomersChurn: number
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
}): DailyAnalyticsReportGeneratedEvent
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events'
|
|
||||||
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
|
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
|
||||||
|
|
||||||
describe('AccountDeletionRequestedEventHandler', () => {
|
|
||||||
let event: AccountDeletionRequestedEvent
|
|
||||||
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let timer: TimerInterface
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new AccountDeletionRequestedEventHandler(analyticsEntityRepository, analyticsStore, statisticsStore, timer)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.payload = {
|
|
||||||
userUuid: '1-2-3',
|
|
||||||
userCreatedAtTimestamp: 1,
|
|
||||||
regularSubscriptionUuid: '2-3-4',
|
|
||||||
}
|
|
||||||
|
|
||||||
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
|
|
||||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue({ id: 3 })
|
|
||||||
analyticsEntityRepository.remove = jest.fn()
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
timer = {} as jest.Mocked<TimerInterface>
|
|
||||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark account deletion and registration length', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['DeleteAccount'], 3, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith('registration-length', 122, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
expect(analyticsEntityRepository.remove).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not mark anything if entity is not found', async () => {
|
|
||||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
|
||||||
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
|
|
||||||
expect(analyticsEntityRepository.remove).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
|||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||||
@@ -33,12 +35,19 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
|||||||
])
|
])
|
||||||
|
|
||||||
const registrationLength = this.timer.getTimestampInMicroseconds() - event.payload.userCreatedAtTimestamp
|
const registrationLength = this.timer.getTimestampInMicroseconds() - event.payload.userCreatedAtTimestamp
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.RegistrationLength, registrationLength, [
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.RegistrationLength, registrationLength, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
await this.analyticsEntityRepository.remove(analyticsEntity)
|
await this.analyticsEntityRepository.remove(analyticsEntity)
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsEntity.id.toString(),
|
||||||
|
user_created_at: this.timer.convertMicrosecondsToDate(event.payload.userCreatedAtTimestamp),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { PaymentFailedEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { PaymentFailedEventHandler } from './PaymentFailedEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
|
|
||||||
describe('PaymentFailedEventHandler', () => {
|
|
||||||
let event: PaymentFailedEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
|
|
||||||
const createHandler = () => new PaymentFailedEventHandler(getUserAnalyticsId, analyticsStore)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<PaymentFailedEvent>
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark payment failed for analytics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
@@ -12,6 +13,7 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
|||||||
constructor(
|
constructor(
|
||||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: PaymentFailedEvent): Promise<void> {
|
async handle(event: PaymentFailedEvent): Promise<void> {
|
||||||
@@ -21,5 +23,11 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
|||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { PaymentSuccessEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { PaymentSuccessEventHandler } from './PaymentSuccessEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
|
|
||||||
describe('PaymentSuccessEventHandler', () => {
|
|
||||||
let event: PaymentSuccessEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new PaymentSuccessEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, logger)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<PaymentSuccessEvent>
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
amount: 12.45,
|
|
||||||
billingFrequency: 12,
|
|
||||||
paymentType: 'initial',
|
|
||||||
subscriptionName: 'PRO_PLAN',
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.warn = jest.fn()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark payment success for analytics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
'pro-subscription-initial-annual-payments-income',
|
|
||||||
12.45,
|
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark non-detailed payment success statistics for analytics', async () => {
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
amount: 12.45,
|
|
||||||
billingFrequency: 13,
|
|
||||||
paymentType: 'initial',
|
|
||||||
subscriptionName: 'PRO_PLAN',
|
|
||||||
}
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).toBeCalledTimes(1)
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(1, 'income', 12.45, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
|
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
|
||||||
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
@@ -20,15 +21,27 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
[
|
[
|
||||||
PaymentType.Initial,
|
PaymentType.Initial,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
PaymentType.Renewal,
|
PaymentType.Renewal,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
@@ -39,15 +52,27 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
[
|
[
|
||||||
PaymentType.Initial,
|
PaymentType.Initial,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
PaymentType.Renewal,
|
PaymentType.Renewal,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
@@ -59,6 +84,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: PaymentSuccessEvent): Promise<void> {
|
async handle(event: PaymentSuccessEvent): Promise<void> {
|
||||||
@@ -69,7 +95,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
const statisticMeasures = [StatisticsMeasure.Income]
|
const statisticMeasures = [StatisticMeasureName.NAMES.Income]
|
||||||
|
|
||||||
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
|
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
|
||||||
?.get(event.payload.paymentType as PaymentType)
|
?.get(event.payload.paymentType as PaymentType)
|
||||||
@@ -89,5 +115,19 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
amount: event.payload.amount,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
payment_type: event.payload.paymentType,
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.track_charge(analyticsId.toString(), event.payload.amount)
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { RefundProcessedEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { RefundProcessedEventHandler } from './RefundProcessedEventHandler'
|
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
|
|
||||||
describe('RefundProcessedEventHandler', () => {
|
|
||||||
let event: RefundProcessedEvent
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
|
|
||||||
const createHandler = () => new RefundProcessedEventHandler(statisticsStore)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<RefundProcessedEvent>
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
amount: 12.45,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark refunds for statistics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.Refunds, 12.45, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,20 +1,36 @@
|
|||||||
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
|
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
|
||||||
constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {}
|
constructor(
|
||||||
|
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||||
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
) {}
|
||||||
|
|
||||||
async handle(event: RefundProcessedEvent): Promise<void> {
|
async handle(event: RefundProcessedEvent): Promise<void> {
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.Refunds, event.payload.amount, [
|
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||||
|
|
||||||
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
amount: event.payload.amount,
|
||||||
|
})
|
||||||
|
this.mixpanelClient.people.track_charge(analyticsId.toString(), -event.payload.amount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { DomainEventHandlerInterface, StatisticPersistenceRequestedEvent } from '@standardnotes/domain-events'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
import { PersistStatistic } from '../UseCase/PersistStatistic/PersistStatistic'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
|
export class StatisticPersistenceRequestedEventHandler implements DomainEventHandlerInterface {
|
||||||
|
constructor(
|
||||||
|
private persistStatistic: PersistStatistic,
|
||||||
|
private timer: TimerInterface,
|
||||||
|
private logger: Logger,
|
||||||
|
private mixpanelClient: Mixpanel | null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handle(event: StatisticPersistenceRequestedEvent): Promise<void> {
|
||||||
|
const result = await this.persistStatistic.execute({
|
||||||
|
date: this.timer.convertMicrosecondsToDate(event.payload.date),
|
||||||
|
statisticMeasureName: event.payload.statisticMeasureName,
|
||||||
|
value: event.payload.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.isFailed()) {
|
||||||
|
this.logger.error(result.getError())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: 'global-stats',
|
||||||
|
statistic: event.payload.statisticMeasureName,
|
||||||
|
value: event.payload.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
import { SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { SubscriptionCancelledEventHandler } from './SubscriptionCancelledEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionCancelledEventHandler', () => {
|
|
||||||
let event: SubscriptionCancelledEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionCancelledEventHandler(
|
|
||||||
getUserAnalyticsId,
|
|
||||||
analyticsStore,
|
|
||||||
statisticsStore,
|
|
||||||
saveRevenueModification,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionCancelledEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_CANCELLED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.ProPlan,
|
|
||||||
subscriptionCreatedAt: 1642395451515000,
|
|
||||||
subscriptionUpdatedAt: 1642395451515001,
|
|
||||||
lastPayedAt: 1642395451515001,
|
|
||||||
subscriptionEndsAt: 1642395451515000 + 10,
|
|
||||||
timestamp: 1,
|
|
||||||
offline: false,
|
|
||||||
replaced: false,
|
|
||||||
userExistingSubscriptionsCount: 1,
|
|
||||||
billingFrequency: 1,
|
|
||||||
payAmount: 12.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should track subscription cancelled statistics', async () => {
|
|
||||||
event.payload.timestamp = 1642395451516000
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.SubscriptionLength, 1000, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not track statistics for subscriptions that are in a legacy 5 year plan', async () => {
|
|
||||||
event.payload.timestamp = 1642395451516000
|
|
||||||
event.payload.subscriptionEndsAt = 1642395451515000 + 126_230_400_000_001
|
|
||||||
event.payload.subscriptionCreatedAt = 1642395451515000
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
event.payload.timestamp = 1642395451516000
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
import { Username } from '@standardnotes/domain-core'
|
import { Username } from '@standardnotes/domain-core'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
||||||
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||||
@@ -22,6 +24,8 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||||
@@ -50,6 +54,22 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
subscription_created_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionCreatedAt),
|
||||||
|
subscription_updated_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionUpdatedAt),
|
||||||
|
last_payed_at: this.timer.convertMicrosecondsToDate(event.payload.lastPayedAt),
|
||||||
|
subscription_ends_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionEndsAt),
|
||||||
|
offline: event.payload.offline,
|
||||||
|
replaced: event.payload.replaced,
|
||||||
|
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
|
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
|
||||||
@@ -58,7 +78,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt
|
const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.SubscriptionLength, subscriptionLength, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
@@ -70,7 +90,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
||||||
|
|
||||||
await this.statisticsStore.incrementMeasure(
|
await this.statisticsStore.incrementMeasure(
|
||||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
|
||||||
remainingSubscriptionPercentage,
|
remainingSubscriptionPercentage,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionExpiredEvent } from '@standardnotes/domain-events'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
|
|
||||||
import { SubscriptionExpiredEventHandler } from './SubscriptionExpiredEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionExpiredEventHandler', () => {
|
|
||||||
let event: SubscriptionExpiredEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionExpiredEventHandler(
|
|
||||||
getUserAnalyticsId,
|
|
||||||
analyticsStore,
|
|
||||||
statisticsStore,
|
|
||||||
saveRevenueModification,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionExpiredEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_EXPIRED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.PlusPlan,
|
|
||||||
timestamp: 1,
|
|
||||||
offline: false,
|
|
||||||
totalActiveSubscriptionsCount: 123,
|
|
||||||
userExistingSubscriptionsCount: 2,
|
|
||||||
billingFrequency: 1,
|
|
||||||
payAmount: 12.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.setMeasure = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update analytics and statistics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
expect(statisticsStore.setMeasure).toHaveBeenCalled()
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Username } from '@standardnotes/domain-core'
|
import { Username } from '@standardnotes/domain-core'
|
||||||
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
@@ -22,6 +23,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
|||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
||||||
@@ -33,7 +35,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
|||||||
)
|
)
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(
|
await this.statisticsStore.setMeasure(
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
event.payload.totalActiveSubscriptionsCount,
|
event.payload.totalActiveSubscriptionsCount,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||||
)
|
)
|
||||||
@@ -54,5 +56,18 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
offline: event.payload.offline,
|
||||||
|
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
|
|
||||||
import { SubscriptionPurchasedEventHandler } from './SubscriptionPurchasedEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionPurchasedEventHandler', () => {
|
|
||||||
let event: SubscriptionPurchasedEvent
|
|
||||||
let subscriptionExpiresAt: number
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionPurchasedEventHandler(
|
|
||||||
getUserAnalyticsId,
|
|
||||||
analyticsStore,
|
|
||||||
statisticsStore,
|
|
||||||
saveRevenueModification,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
statisticsStore.setMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionPurchasedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_PURCHASED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.ProPlan,
|
|
||||||
subscriptionExpiresAt,
|
|
||||||
timestamp: 60,
|
|
||||||
offline: false,
|
|
||||||
discountCode: null,
|
|
||||||
limitedDiscountPurchased: false,
|
|
||||||
newSubscriber: true,
|
|
||||||
totalActiveSubscriptionsCount: 123,
|
|
||||||
userRegisteredAt: 23,
|
|
||||||
billingFrequency: 12,
|
|
||||||
payAmount: 29.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
analyticsStore.unmarkActivity = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark subscription creation statistics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalled()
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should not measure registration to subscription time if this is not user's first subscription", async () => {
|
|
||||||
event.payload.newSubscriber = false
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update analytics on limited discount offer purchasing', async () => {
|
|
||||||
event.payload.limitedDiscountPurchased = true
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['limited-discount-offer-purchased'], 3, [Period.Today])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Username } from '@standardnotes/domain-core'
|
import { Username } from '@standardnotes/domain-core'
|
||||||
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
@@ -22,6 +24,8 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
|||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
||||||
@@ -45,18 +49,18 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
|||||||
|
|
||||||
if (event.payload.newSubscriber) {
|
if (event.payload.newSubscriber) {
|
||||||
await this.statisticsStore.incrementMeasure(
|
await this.statisticsStore.incrementMeasure(
|
||||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
|
||||||
event.payload.timestamp - event.payload.userRegisteredAt,
|
event.payload.timestamp - event.payload.userRegisteredAt,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||||
)
|
)
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.NewCustomers, 1, [
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.NewCustomers, 1, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
])
|
])
|
||||||
await this.statisticsStore.setMeasure(
|
await this.statisticsStore.setMeasure(
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
event.payload.totalActiveSubscriptionsCount,
|
event.payload.totalActiveSubscriptionsCount,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||||
)
|
)
|
||||||
@@ -78,5 +82,22 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
|
||||||
|
offline: event.payload.offline,
|
||||||
|
discount_code: event.payload.discountCode,
|
||||||
|
limited_discount_purchased: event.payload.limitedDiscountPurchased,
|
||||||
|
new_subscriber: event.payload.newSubscriber,
|
||||||
|
user_registered_at: this.timer.convertMicrosecondsToDate(event.payload.userRegisteredAt),
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { SubscriptionReactivatedEventHandler } from './SubscriptionReactivatedEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
|
|
||||||
describe('SubscriptionReactivatedEventHandler', () => {
|
|
||||||
let event: SubscriptionReactivatedEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
|
|
||||||
const createHandler = () => new SubscriptionReactivatedEventHandler(analyticsStore, getUserAnalyticsId)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
event = {} as jest.Mocked<SubscriptionReactivatedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.payload = {
|
|
||||||
previousSubscriptionId: 1,
|
|
||||||
currentSubscriptionId: 2,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.PlusPlan,
|
|
||||||
subscriptionExpiresAt: 5,
|
|
||||||
discountCode: 'exit-20',
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark subscription reactivated activity for analytics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['subscription-reactivated'], 3, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
@@ -12,6 +14,8 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
|
|||||||
constructor(
|
constructor(
|
||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
|
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
|
||||||
@@ -21,5 +25,16 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
|
|||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
|
||||||
|
discount_code: event.payload.discountCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
|
|
||||||
import { SubscriptionRefundedEventHandler } from './SubscriptionRefundedEventHandler'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionRefundedEventHandler', () => {
|
|
||||||
let event: SubscriptionRefundedEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionRefundedEventHandler(
|
|
||||||
getUserAnalyticsId,
|
|
||||||
analyticsStore,
|
|
||||||
statisticsStore,
|
|
||||||
saveRevenueModification,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionRefundedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_REFUNDED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.PlusPlan,
|
|
||||||
timestamp: 1,
|
|
||||||
offline: false,
|
|
||||||
userExistingSubscriptionsCount: 3,
|
|
||||||
totalActiveSubscriptionsCount: 1,
|
|
||||||
billingFrequency: 1,
|
|
||||||
payAmount: 12.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(true)
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.setMeasure = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark churn for new customer', async () => {
|
|
||||||
event.payload.userExistingSubscriptionsCount = 1
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.SubscriptionRefunded], 3, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.NewCustomersChurn], 3, [
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark churn for existing customer', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not mark churn if customer did not purchase subscription in defined analytic periods', async () => {
|
|
||||||
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(false)
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).not.toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Username } from '@standardnotes/domain-core'
|
import { Username } from '@standardnotes/domain-core'
|
||||||
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
@@ -22,6 +23,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
|||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
||||||
@@ -50,6 +52,18 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
|
||||||
|
offline: event.payload.offline,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async markChurnActivity(analyticsId: number, event: SubscriptionRefundedEvent): Promise<void> {
|
private async markChurnActivity(analyticsId: number, event: SubscriptionRefundedEvent): Promise<void> {
|
||||||
@@ -70,7 +84,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(
|
await this.statisticsStore.setMeasure(
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
event.payload.totalActiveSubscriptionsCount,
|
event.payload.totalActiveSubscriptionsCount,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
|
|
||||||
import { SubscriptionRenewedEventHandler } from './SubscriptionRenewedEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionRenewedEventHandler', () => {
|
|
||||||
let event: SubscriptionRenewedEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionRenewedEventHandler(getUserAnalyticsId, analyticsStore, saveRevenueModification, logger)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionRenewedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_RENEWED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.ProPlan,
|
|
||||||
subscriptionExpiresAt: 2,
|
|
||||||
timestamp: 1,
|
|
||||||
offline: false,
|
|
||||||
billingFrequency: 1,
|
|
||||||
payAmount: 12.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
analyticsStore.unmarkActivity = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should track subscription renewed statistics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
expect(analyticsStore.unmarkActivity).toHaveBeenCalled()
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Username } from '@standardnotes/domain-core'
|
import { Username } from '@standardnotes/domain-core'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
@@ -11,6 +12,7 @@ import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/Save
|
|||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
|
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
|
||||||
@@ -19,6 +21,8 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
|||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
||||||
@@ -50,5 +54,17 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
|
||||||
|
offline: event.payload.offline,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { UserRegisteredEvent } from '@standardnotes/domain-events'
|
|
||||||
import { ProtocolVersion } from '@standardnotes/common'
|
|
||||||
|
|
||||||
import { UserRegisteredEventHandler } from './UserRegisteredEventHandler'
|
|
||||||
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
|
|
||||||
describe('UserRegisteredEventHandler', () => {
|
|
||||||
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
|
|
||||||
let event: UserRegisteredEvent
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
|
|
||||||
const createHandler = () => new UserRegisteredEventHandler(analyticsEntityRepository, analyticsStore)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
event = {} as jest.Mocked<UserRegisteredEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.payload = {
|
|
||||||
userUuid: '1-2-3',
|
|
||||||
email: 'test@test.te',
|
|
||||||
protocolVersion: ProtocolVersion.V004,
|
|
||||||
}
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
|
|
||||||
analyticsEntityRepository.save = jest.fn().mockImplementation((entity) => ({
|
|
||||||
...entity,
|
|
||||||
id: 1,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should save analytics entity upon user registration', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsEntityRepository.save).toHaveBeenCalled()
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['register'], 1, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
@@ -13,6 +14,7 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
|||||||
constructor(
|
constructor(
|
||||||
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: UserRegisteredEvent): Promise<void> {
|
async handle(event: UserRegisteredEvent): Promise<void> {
|
||||||
@@ -26,5 +28,17 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
|||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsEntity.id.toString(),
|
||||||
|
protocol_version: event.payload.protocolVersion,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsEntity.id.toString(), {
|
||||||
|
subscription: 'free',
|
||||||
|
protocol_version: event.payload.protocolVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Result, Entity, UniqueEntityId } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { StatisticMeasureProps } from './StatisticMeasureProps'
|
||||||
|
|
||||||
|
export class StatisticMeasure extends Entity<StatisticMeasureProps> {
|
||||||
|
get id(): UniqueEntityId {
|
||||||
|
return this._id
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this.props.name.value
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): number {
|
||||||
|
return this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(props: StatisticMeasureProps, id?: UniqueEntityId) {
|
||||||
|
super(props, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(props: StatisticMeasureProps, id?: UniqueEntityId): Result<StatisticMeasure> {
|
||||||
|
return Result.ok<StatisticMeasure>(new StatisticMeasure(props, id))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { StatisticMeasureName } from './StatisticMeasureName'
|
||||||
|
|
||||||
|
describe('StatisticMeasureName', () => {
|
||||||
|
it('should create a value object', () => {
|
||||||
|
const valueOrError = StatisticMeasureName.create('pro-subscription-initial-monthly-payments-income')
|
||||||
|
|
||||||
|
expect(valueOrError.isFailed()).toBeFalsy()
|
||||||
|
expect(valueOrError.getValue().value).toEqual('pro-subscription-initial-monthly-payments-income')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create an invalid value object', () => {
|
||||||
|
for (const value of ['', undefined, null, 0, 'foobar']) {
|
||||||
|
const valueOrError = StatisticMeasureName.create(value as string)
|
||||||
|
|
||||||
|
expect(valueOrError.isFailed()).toBeTruthy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ValueObject, Result } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { StatisticMeasureNameProps } from './StatisticMeasureNameProps'
|
||||||
|
|
||||||
|
export class StatisticMeasureName extends ValueObject<StatisticMeasureNameProps> {
|
||||||
|
static readonly NAMES = {
|
||||||
|
Income: 'income',
|
||||||
|
PlusSubscriptionInitialMonthlyPaymentsIncome: 'plus-subscription-initial-monthly-payments-income',
|
||||||
|
ProSubscriptionInitialMonthlyPaymentsIncome: 'pro-subscription-initial-monthly-payments-income',
|
||||||
|
PlusSubscriptionInitialAnnualPaymentsIncome: 'plus-subscription-initial-annual-payments-income',
|
||||||
|
ProSubscriptionInitialAnnualPaymentsIncome: 'pro-subscription-initial-annual-payments-income',
|
||||||
|
PlusSubscriptionRenewingMonthlyPaymentsIncome: 'plus-subscription-renewing-monthly-payments-income',
|
||||||
|
ProSubscriptionRenewingMonthlyPaymentsIncome: 'pro-subscription-renewing-monthly-payments-income',
|
||||||
|
PlusSubscriptionRenewingAnnualPaymentsIncome: 'plus-subscription-renewing-annual-payments-income',
|
||||||
|
ProSubscriptionRenewingAnnualPaymentsIncome: 'pro-subscription-renewing-annual-payments-income',
|
||||||
|
SubscriptionLength: 'subscription-length',
|
||||||
|
RegistrationLength: 'registration-length',
|
||||||
|
RegistrationToSubscriptionTime: 'registration-to-subscription-time',
|
||||||
|
RemainingSubscriptionTimePercentage: 'remaining-subscription-time-percentage',
|
||||||
|
Refunds: 'refunds',
|
||||||
|
NewCustomers: 'new-customers',
|
||||||
|
TotalCustomers: 'total-customers',
|
||||||
|
MRR: 'mrr',
|
||||||
|
MonthlyPlansMRR: 'monthly-plans-mrr',
|
||||||
|
AnnualPlansMRR: 'annual-plans-mrr',
|
||||||
|
FiveYearPlansMRR: 'five-year-plans-mrr',
|
||||||
|
ProPlansMRR: 'pro-plans-mrr',
|
||||||
|
PlusPlansMRR: 'plus-plans-mrr',
|
||||||
|
ActiveUsers: 'active-users',
|
||||||
|
ActiveProUsers: 'active-pro-users',
|
||||||
|
ActivePlusUsers: 'active-plus-users',
|
||||||
|
ActiveFreeUsers: 'active-free-users',
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): string {
|
||||||
|
return this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(props: StatisticMeasureNameProps) {
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(name: string): Result<StatisticMeasureName> {
|
||||||
|
const isValidName = Object.values(this.NAMES).includes(name)
|
||||||
|
if (!isValidName) {
|
||||||
|
return Result.fail<StatisticMeasureName>(`Invalid statistics measure name: ${name}`)
|
||||||
|
} else {
|
||||||
|
return Result.ok<StatisticMeasureName>(new StatisticMeasureName({ value: name }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface StatisticMeasureNameProps {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { StatisticMeasureName } from './StatisticMeasureName'
|
||||||
|
|
||||||
|
export interface StatisticMeasureProps {
|
||||||
|
name: StatisticMeasureName
|
||||||
|
value: number
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { StatisticMeasure } from './StatisticMeasure'
|
||||||
|
|
||||||
|
export interface StatisticMeasureRepositoryInterface {
|
||||||
|
save(statisticMeasure: StatisticMeasure): Promise<void>
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
export enum StatisticsMeasure {
|
|
||||||
Income = 'income',
|
|
||||||
PlusSubscriptionInitialMonthlyPaymentsIncome = 'plus-subscription-initial-monthly-payments-income',
|
|
||||||
ProSubscriptionInitialMonthlyPaymentsIncome = 'pro-subscription-initial-monthly-payments-income',
|
|
||||||
PlusSubscriptionInitialAnnualPaymentsIncome = 'plus-subscription-initial-annual-payments-income',
|
|
||||||
ProSubscriptionInitialAnnualPaymentsIncome = 'pro-subscription-initial-annual-payments-income',
|
|
||||||
PlusSubscriptionRenewingMonthlyPaymentsIncome = 'plus-subscription-renewing-monthly-payments-income',
|
|
||||||
ProSubscriptionRenewingMonthlyPaymentsIncome = 'pro-subscription-renewing-monthly-payments-income',
|
|
||||||
PlusSubscriptionRenewingAnnualPaymentsIncome = 'plus-subscription-renewing-annual-payments-income',
|
|
||||||
ProSubscriptionRenewingAnnualPaymentsIncome = 'pro-subscription-renewing-annual-payments-income',
|
|
||||||
SubscriptionLength = 'subscription-length',
|
|
||||||
RegistrationLength = 'registration-length',
|
|
||||||
RegistrationToSubscriptionTime = 'registration-to-subscription-time',
|
|
||||||
RemainingSubscriptionTimePercentage = 'remaining-subscription-time-percentage',
|
|
||||||
Refunds = 'refunds',
|
|
||||||
NewCustomers = 'new-customers',
|
|
||||||
TotalCustomers = 'total-customers',
|
|
||||||
MRR = 'mrr',
|
|
||||||
MonthlyPlansMRR = 'monthly-plans-mrr',
|
|
||||||
AnnualPlansMRR = 'annual-plans-mrr',
|
|
||||||
FiveYearPlansMRR = 'five-year-plans-mrr',
|
|
||||||
ProPlansMRR = 'pro-plans-mrr',
|
|
||||||
PlusPlansMRR = 'plus-plans-mrr',
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
import { StatisticsMeasure } from './StatisticsMeasure'
|
|
||||||
|
|
||||||
export interface StatisticsStoreInterface {
|
export interface StatisticsStoreInterface {
|
||||||
incrementSNJSVersionUsage(snjsVersion: string): Promise<void>
|
incrementSNJSVersionUsage(snjsVersion: string): Promise<void>
|
||||||
@@ -8,13 +7,13 @@ export interface StatisticsStoreInterface {
|
|||||||
getYesterdaySNJSUsage(): Promise<Array<{ version: string; count: number }>>
|
getYesterdaySNJSUsage(): Promise<Array<{ version: string; count: number }>>
|
||||||
getYesterdayApplicationUsage(): Promise<Array<{ version: string; count: number }>>
|
getYesterdayApplicationUsage(): Promise<Array<{ version: string; count: number }>>
|
||||||
getYesterdayOutOfSyncIncidents(): Promise<number>
|
getYesterdayOutOfSyncIncidents(): Promise<number>
|
||||||
incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
|
incrementMeasure(measure: string, value: number, periods: Period[]): Promise<void>
|
||||||
setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
|
setMeasure(measure: string, value: number, periods: Period[]): Promise<void>
|
||||||
getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number>
|
getMeasureAverage(measure: string, period: Period): Promise<number>
|
||||||
getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise<number>
|
getMeasureTotal(measure: string, periodOrPeriodKey: Period | string): Promise<number>
|
||||||
getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise<number>
|
getMeasureIncrementCounts(measure: string, period: Period): Promise<number>
|
||||||
calculateTotalCountOverPeriod(
|
calculateTotalCountOverPeriod(
|
||||||
measure: StatisticsMeasure,
|
measure: string,
|
||||||
period: Period,
|
period: Period,
|
||||||
): Promise<Array<{ periodKey: string; totalCount: number }>>
|
): Promise<Array<{ periodKey: string; totalCount: number }>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export class PeriodKeyGenerator implements PeriodKeyGeneratorInterface {
|
|||||||
return `${this.getYear(date)}-${this.getMonth(date)}`
|
return `${this.getYear(date)}-${this.getMonth(date)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDailyKey(date?: Date): string {
|
getDailyKey(date?: Date): string {
|
||||||
date = date ?? new Date()
|
date = date ?? new Date()
|
||||||
|
|
||||||
return `${this.getYear(date)}-${this.getMonth(date)}-${this.getDayOfTheMonth(date)}`
|
return `${this.getYear(date)}-${this.getMonth(date)}-${this.getDayOfTheMonth(date)}`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Period } from './Period'
|
|||||||
|
|
||||||
export interface PeriodKeyGeneratorInterface {
|
export interface PeriodKeyGeneratorInterface {
|
||||||
getPeriodKey(period: Period): string
|
getPeriodKey(period: Period): string
|
||||||
|
getDailyKey(date?: Date): string
|
||||||
convertPeriodKeyToPeriod(periodKey: string): Period
|
convertPeriodKeyToPeriod(periodKey: string): Period
|
||||||
getDiscretePeriodKeys(period: Period): string[]
|
getDiscretePeriodKeys(period: Period): string[]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import 'reflect-metadata'
|
import 'reflect-metadata'
|
||||||
|
|
||||||
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
||||||
import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../../Time/Period'
|
import { Period } from '../../Time/Period'
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ describe('CalculateMonthlyRecurringRevenue', () => {
|
|||||||
it('should calculate the MRR diff and persist it as a statistic', async () => {
|
it('should calculate the MRR diff and persist it as a statistic', async () => {
|
||||||
await createUseCase().execute({})
|
await createUseCase().execute({})
|
||||||
|
|
||||||
expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticsMeasure.MRR, 123.45, [
|
expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticMeasureName.NAMES.MRR, 123.45, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
|
|||||||
+7
-7
@@ -5,11 +5,11 @@ import { Result } from '@standardnotes/domain-core'
|
|||||||
import TYPES from '../../../Bootstrap/Types'
|
import TYPES from '../../../Bootstrap/Types'
|
||||||
import { MonthlyRevenue } from '../../Revenue/MonthlyRevenue'
|
import { MonthlyRevenue } from '../../Revenue/MonthlyRevenue'
|
||||||
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
||||||
import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure'
|
|
||||||
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../../Time/Period'
|
import { Period } from '../../Time/Period'
|
||||||
import { DomainUseCaseInterface } from '../DomainUseCaseInterface'
|
import { DomainUseCaseInterface } from '../DomainUseCaseInterface'
|
||||||
import { CalculateMonthlyRecurringRevenueDTO } from './CalculateMonthlyRecurringRevenueDTO'
|
import { CalculateMonthlyRecurringRevenueDTO } from './CalculateMonthlyRecurringRevenueDTO'
|
||||||
|
import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<MonthlyRevenue> {
|
export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<MonthlyRevenue> {
|
||||||
@@ -24,7 +24,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.MRR, mrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.MRR, mrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -34,7 +34,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Monthly],
|
billingFrequencies: [SubscriptionBillingFrequency.Monthly],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.MonthlyPlansMRR, monthlyPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.MonthlyPlansMRR, monthlyPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -44,7 +44,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Annual],
|
billingFrequencies: [SubscriptionBillingFrequency.Annual],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.AnnualPlansMRR, annualPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.AnnualPlansMRR, annualPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -54,7 +54,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.FiveYear],
|
billingFrequencies: [SubscriptionBillingFrequency.FiveYear],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.FiveYearPlansMRR, fiveYearPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.FiveYearPlansMRR, fiveYearPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -65,7 +65,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.ProPlansMRR, proPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.ProPlansMRR, proPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
@@ -76,7 +76,7 @@ export class CalculateMonthlyRecurringRevenue implements DomainUseCaseInterface<
|
|||||||
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
billingFrequencies: [SubscriptionBillingFrequency.Annual, SubscriptionBillingFrequency.Monthly],
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(StatisticsMeasure.PlusPlansMRR, plusPlansMrrDiff, [
|
await this.statisticsStore.setMeasure(StatisticMeasureName.NAMES.PlusPlansMRR, plusPlansMrrDiff, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { StatisticMeasure } from '../../Statistics/StatisticMeasure'
|
||||||
|
import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName'
|
||||||
|
import { StatisticMeasureRepositoryInterface } from '../../Statistics/StatisticMeasureRepositoryInterface'
|
||||||
|
import { PersistStatisticDTO } from './PersistStatisticDTO'
|
||||||
|
|
||||||
|
export class PersistStatistic implements UseCaseInterface<StatisticMeasure> {
|
||||||
|
constructor(private statisticMeasureRepository: StatisticMeasureRepositoryInterface) {}
|
||||||
|
|
||||||
|
async execute(dto: PersistStatisticDTO): Promise<Result<StatisticMeasure>> {
|
||||||
|
const statisticMeasureNameOrError = StatisticMeasureName.create(dto.statisticMeasureName)
|
||||||
|
if (statisticMeasureNameOrError.isFailed()) {
|
||||||
|
return Result.fail(`Could not persist statistic measure: ${statisticMeasureNameOrError.getError()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statisticMeasureOrError = StatisticMeasure.create({
|
||||||
|
date: dto.date,
|
||||||
|
name: statisticMeasureNameOrError.getValue(),
|
||||||
|
value: dto.value,
|
||||||
|
})
|
||||||
|
if (statisticMeasureOrError.isFailed()) {
|
||||||
|
return Result.fail(`Could not persist statistic measure: ${statisticMeasureOrError.getError()}`)
|
||||||
|
}
|
||||||
|
const statisticMeasure = statisticMeasureOrError.getValue()
|
||||||
|
|
||||||
|
await this.statisticMeasureRepository.save(statisticMeasure)
|
||||||
|
|
||||||
|
return Result.ok(statisticMeasure)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface PersistStatisticDTO {
|
||||||
|
statisticMeasureName: string
|
||||||
|
value: number
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
@@ -1,16 +1,23 @@
|
|||||||
import * as IORedis from 'ioredis'
|
import * as IORedis from 'ioredis'
|
||||||
|
|
||||||
import { StatisticsMeasure } from '../../Domain/Statistics/StatisticsMeasure'
|
import { StatisticMeasure } from '../../Domain/Statistics/StatisticMeasure'
|
||||||
|
import { StatisticMeasureRepositoryInterface } from '../../Domain/Statistics/StatisticMeasureRepositoryInterface'
|
||||||
|
|
||||||
import { StatisticsStoreInterface } from '../../Domain/Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../../Domain/Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../../Domain/Time/Period'
|
import { Period } from '../../Domain/Time/Period'
|
||||||
import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
|
import { PeriodKeyGeneratorInterface } from '../../Domain/Time/PeriodKeyGeneratorInterface'
|
||||||
|
|
||||||
export class RedisStatisticsStore implements StatisticsStoreInterface {
|
export class RedisStatisticsStore implements StatisticsStoreInterface, StatisticMeasureRepositoryInterface {
|
||||||
constructor(private periodKeyGenerator: PeriodKeyGeneratorInterface, private redisClient: IORedis.Redis) {}
|
constructor(private periodKeyGenerator: PeriodKeyGeneratorInterface, private redisClient: IORedis.Redis) {}
|
||||||
|
|
||||||
|
async save(statisticMeasure: StatisticMeasure): Promise<void> {
|
||||||
|
const periodKey = this.periodKeyGenerator.getDailyKey(statisticMeasure.props.date)
|
||||||
|
|
||||||
|
await this.setMeasure(statisticMeasure.name, statisticMeasure.value, [periodKey])
|
||||||
|
}
|
||||||
|
|
||||||
async calculateTotalCountOverPeriod(
|
async calculateTotalCountOverPeriod(
|
||||||
measure: StatisticsMeasure,
|
measure: string,
|
||||||
period: Period,
|
period: Period,
|
||||||
): Promise<{ periodKey: string; totalCount: number }[]> {
|
): Promise<{ periodKey: string; totalCount: number }[]> {
|
||||||
if (
|
if (
|
||||||
@@ -38,7 +45,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
|
|||||||
return counts
|
return counts
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise<number> {
|
async getMeasureIncrementCounts(measure: string, period: Period): Promise<number> {
|
||||||
const increments = await this.redisClient.get(
|
const increments = await this.redisClient.get(
|
||||||
`count:increments:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
|
`count:increments:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`,
|
||||||
)
|
)
|
||||||
@@ -49,17 +56,22 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
|
|||||||
return +increments
|
return +increments
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void> {
|
async setMeasure(measure: string, value: number, periodsOrPeriodKeys: Period[] | string[]): Promise<void> {
|
||||||
const pipeline = this.redisClient.pipeline()
|
const pipeline = this.redisClient.pipeline()
|
||||||
|
|
||||||
for (const period of periods) {
|
for (const periodOrPeriodKey of periodsOrPeriodKeys) {
|
||||||
pipeline.set(`count:measure:${measure}:timespan:${this.periodKeyGenerator.getPeriodKey(period)}`, value)
|
let periodKey = periodOrPeriodKey
|
||||||
|
if (!isNaN(+periodOrPeriodKey)) {
|
||||||
|
periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period)
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline.set(`count:measure:${measure}:timespan:${periodKey}`, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
await pipeline.exec()
|
await pipeline.exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise<number> {
|
async getMeasureTotal(measure: string, periodOrPeriodKey: Period | string): Promise<number> {
|
||||||
let periodKey = periodOrPeriodKey
|
let periodKey = periodOrPeriodKey
|
||||||
if (!isNaN(+periodOrPeriodKey)) {
|
if (!isNaN(+periodOrPeriodKey)) {
|
||||||
periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period)
|
periodKey = this.periodKeyGenerator.getPeriodKey(periodOrPeriodKey as Period)
|
||||||
@@ -74,7 +86,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
|
|||||||
return +totalValue
|
return +totalValue
|
||||||
}
|
}
|
||||||
|
|
||||||
async incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void> {
|
async incrementMeasure(measure: string, value: number, periods: Period[]): Promise<void> {
|
||||||
const pipeline = this.redisClient.pipeline()
|
const pipeline = this.redisClient.pipeline()
|
||||||
|
|
||||||
for (const period of periods) {
|
for (const period of periods) {
|
||||||
@@ -85,7 +97,7 @@ export class RedisStatisticsStore implements StatisticsStoreInterface {
|
|||||||
await pipeline.exec()
|
await pipeline.exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number> {
|
async getMeasureAverage(measure: string, period: Period): Promise<number> {
|
||||||
const increments = await this.getMeasureIncrementCounts(measure, period)
|
const increments = await this.getMeasureIncrementCounts(measure, period)
|
||||||
|
|
||||||
if (increments === 0) {
|
if (increments === 0) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ WEB_SOCKET_SERVER_URL=http://websockets:3000
|
|||||||
PAYMENTS_SERVER_URL=http://payments:3000
|
PAYMENTS_SERVER_URL=http://payments:3000
|
||||||
FILES_SERVER_URL=http://files:3000
|
FILES_SERVER_URL=http://files:3000
|
||||||
REVISIONS_SERVER_URL=http://revisions:3000
|
REVISIONS_SERVER_URL=http://revisions:3000
|
||||||
|
EMAIL_SERVER_URL=http://email:3000
|
||||||
|
|
||||||
HTTP_CALL_TIMEOUT=60000
|
HTTP_CALL_TIMEOUT=60000
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,101 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.46.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.3...@standardnotes/api-gateway@1.46.0) (2023-01-16)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api-gateway:** add all revisions endpoints on v2 ([60b3dd6](https://github.com/standardnotes/api-gateway/commit/60b3dd6138ef9b8e9a717873548afc2d3924a0d7))
|
||||||
|
* **api-gateway:** switch to fetching revisions from reivsions server ([22c1f93](https://github.com/standardnotes/api-gateway/commit/22c1f936c3a770a82dc1a1e6aa136e183d308aa6))
|
||||||
|
|
||||||
|
## [1.45.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.2...@standardnotes/api-gateway@1.45.3) (2023-01-16)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **api-gateway:** add noindex robots meta tag to api gateway homepage ([04c6888](https://github.com/standardnotes/api-gateway/commit/04c6888cf65f9f1315fc2fb8af069d26bfbc31b1))
|
||||||
|
|
||||||
|
## [1.45.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.1...@standardnotes/api-gateway@1.45.2) (2023-01-13)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
## [1.45.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.45.0...@standardnotes/api-gateway@1.45.1) (2023-01-13)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add robots.txt setup for api-gateway and files server to disallow indexing ([bb82043](https://github.com/standardnotes/api-gateway/commit/bb820437af2b9644d7597de045b5840037b81db3))
|
||||||
|
|
||||||
|
# [1.45.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.44.0...@standardnotes/api-gateway@1.45.0) (2023-01-05)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add recovery sign in with recovery codes ([cac899a](https://github.com/standardnotes/api-gateway/commit/cac899a7e558d066895dfb3ba28418d94072f2b7))
|
||||||
|
|
||||||
|
# [1.44.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.43.0...@standardnotes/api-gateway@1.44.0) (2022-12-29)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add removing authenticator ([de50d76](https://github.com/standardnotes/api-gateway/commit/de50d76800a4240729763b2df11c4a1718951670))
|
||||||
|
|
||||||
|
# [1.43.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.42.0...@standardnotes/api-gateway@1.43.0) (2022-12-29)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add listing authenticators ([01837ea](https://github.com/standardnotes/api-gateway/commit/01837eaea9b1f219e7ad3be4d28cd0df099fe423))
|
||||||
|
|
||||||
|
# [1.42.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.3...@standardnotes/api-gateway@1.42.0) (2022-12-29)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add http endpoints for authenticators ([b6fda90](https://github.com/standardnotes/api-gateway/commit/b6fda901ef66a3e66541bd1e3f041b8268a1c3f5))
|
||||||
|
|
||||||
|
## [1.41.3](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.2...@standardnotes/api-gateway@1.41.3) (2022-12-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
## [1.41.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.1...@standardnotes/api-gateway@1.41.2) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
## [1.41.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.41.0...@standardnotes/api-gateway@1.41.1) (2022-12-19)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
# [1.41.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.2...@standardnotes/api-gateway@1.41.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add session traces ([8bcb552](https://github.com/standardnotes/api-gateway/commit/8bcb552783b2d12f3296b3195752168482790bc8))
|
||||||
|
|
||||||
|
## [1.40.2](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.1...@standardnotes/api-gateway@1.40.2) (2022-12-12)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
## [1.40.1](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.40.0...@standardnotes/api-gateway@1.40.1) (2022-12-12)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
# [1.40.0](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.24...@standardnotes/api-gateway@1.40.0) (2022-12-12)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **api-gateway:** add unsubscribe from emails endpoint ([22d6a02](https://github.com/standardnotes/api-gateway/commit/22d6a02d049ba3bde890c7def91e19f013ba3e22))
|
||||||
|
|
||||||
|
## [1.39.24](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.23...@standardnotes/api-gateway@1.39.24) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
## [1.39.23](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.22...@standardnotes/api-gateway@1.39.23) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
## [1.39.22](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.21...@standardnotes/api-gateway@1.39.22) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
|
## [1.39.21](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.20...@standardnotes/api-gateway@1.39.21) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|
||||||
## [1.39.20](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.19...@standardnotes/api-gateway@1.39.20) (2022-12-09)
|
## [1.39.20](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.19...@standardnotes/api-gateway@1.39.20) (2022-12-09)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import '../src/Controller/v1/FilesController'
|
|||||||
import '../src/Controller/v1/SubscriptionInvitesController'
|
import '../src/Controller/v1/SubscriptionInvitesController'
|
||||||
import '../src/Controller/v1/WorkspacesController'
|
import '../src/Controller/v1/WorkspacesController'
|
||||||
import '../src/Controller/v1/InvitesController'
|
import '../src/Controller/v1/InvitesController'
|
||||||
|
import '../src/Controller/v1/AuthenticatorsController'
|
||||||
|
|
||||||
import '../src/Controller/v2/PaymentsControllerV2'
|
import '../src/Controller/v2/PaymentsControllerV2'
|
||||||
import '../src/Controller/v2/ActionsControllerV2'
|
import '../src/Controller/v2/ActionsControllerV2'
|
||||||
@@ -30,6 +31,8 @@ import helmet from 'helmet'
|
|||||||
import * as cors from 'cors'
|
import * as cors from 'cors'
|
||||||
import { text, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
|
import { text, json, Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'
|
||||||
import * as winston from 'winston'
|
import * as winston from 'winston'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const robots = require('express-robots-txt')
|
||||||
|
|
||||||
import { InversifyExpressServer } from 'inversify-express-utils'
|
import { InversifyExpressServer } from 'inversify-express-utils'
|
||||||
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
|
||||||
@@ -77,6 +80,12 @@ void container.load().then((container) => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
app.use(cors())
|
app.use(cors())
|
||||||
|
app.use(
|
||||||
|
robots({
|
||||||
|
UserAgent: '*',
|
||||||
|
Disallow: '/',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
if (env.get('SENTRY_DSN', true)) {
|
if (env.get('SENTRY_DSN', true)) {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/api-gateway",
|
"name": "@standardnotes/api-gateway",
|
||||||
"version": "1.39.20",
|
"version": "1.46.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@newrelic/winston-enricher": "^4.0.0",
|
"@newrelic/winston-enricher": "^4.0.0",
|
||||||
"@sentry/node": "^7.19.0",
|
"@sentry/node": "^7.28.1",
|
||||||
"@standardnotes/common": "workspace:^",
|
"@standardnotes/common": "workspace:^",
|
||||||
"@standardnotes/domain-events": "workspace:*",
|
"@standardnotes/domain-events": "workspace:*",
|
||||||
"@standardnotes/domain-events-infra": "workspace:*",
|
"@standardnotes/domain-events-infra": "workspace:*",
|
||||||
@@ -32,11 +32,12 @@
|
|||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"express-robots-txt": "^1.0.0",
|
||||||
"helmet": "^6.0.0",
|
"helmet": "^6.0.0",
|
||||||
"inversify": "^6.0.1",
|
"inversify": "^6.0.1",
|
||||||
"inversify-express-utils": "^6.4.3",
|
"inversify-express-utils": "^6.4.3",
|
||||||
"ioredis": "^5.2.4",
|
"ioredis": "^5.2.4",
|
||||||
"jsonwebtoken": "8.5.1",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"newrelic": "^9.6.0",
|
"newrelic": "^9.6.0",
|
||||||
"prettyjson": "^1.2.5",
|
"prettyjson": "^1.2.5",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
@@ -47,7 +48,7 @@
|
|||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/ioredis": "^5.0.0",
|
"@types/ioredis": "^5.0.0",
|
||||||
"@types/jest": "^29.1.1",
|
"@types/jest": "^29.1.1",
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/jsonwebtoken": "^9.0.1",
|
||||||
"@types/newrelic": "^7.0.4",
|
"@types/newrelic": "^7.0.4",
|
||||||
"@types/prettyjson": "^0.0.30",
|
"@types/prettyjson": "^0.0.30",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
"@typescript-eslint/eslint-plugin": "^5.29.0",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export class ContainerConfigLoader {
|
|||||||
container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL'))
|
container.bind(TYPES.SYNCING_SERVER_JS_URL).toConstantValue(env.get('SYNCING_SERVER_JS_URL'))
|
||||||
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
|
container.bind(TYPES.AUTH_SERVER_URL).toConstantValue(env.get('AUTH_SERVER_URL'))
|
||||||
container.bind(TYPES.REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL', true))
|
container.bind(TYPES.REVISIONS_SERVER_URL).toConstantValue(env.get('REVISIONS_SERVER_URL', true))
|
||||||
|
container.bind(TYPES.EMAIL_SERVER_URL).toConstantValue(env.get('EMAIL_SERVER_URL', true))
|
||||||
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
|
container.bind(TYPES.PAYMENTS_SERVER_URL).toConstantValue(env.get('PAYMENTS_SERVER_URL', true))
|
||||||
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
|
container.bind(TYPES.FILES_SERVER_URL).toConstantValue(env.get('FILES_SERVER_URL', true))
|
||||||
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
container.bind(TYPES.AUTH_JWT_SECRET).toConstantValue(env.get('AUTH_JWT_SECRET'))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const TYPES = {
|
|||||||
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
|
PAYMENTS_SERVER_URL: Symbol.for('PAYMENTS_SERVER_URL'),
|
||||||
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
|
FILES_SERVER_URL: Symbol.for('FILES_SERVER_URL'),
|
||||||
REVISIONS_SERVER_URL: Symbol.for('REVISIONS_SERVER_URL'),
|
REVISIONS_SERVER_URL: Symbol.for('REVISIONS_SERVER_URL'),
|
||||||
|
EMAIL_SERVER_URL: Symbol.for('EMAIL_SERVER_URL'),
|
||||||
WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'),
|
WORKSPACE_SERVER_URL: Symbol.for('WORKSPACE_SERVER_URL'),
|
||||||
WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'),
|
WEB_SOCKET_SERVER_URL: Symbol.for('WEB_SOCKET_SERVER_URL'),
|
||||||
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
|
AUTH_JWT_SECRET: Symbol.for('AUTH_JWT_SECRET'),
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ export class LegacyController extends BaseHttpController {
|
|||||||
@all('*')
|
@all('*')
|
||||||
async legacyProxyToSyncingServer(request: Request, response: Response): Promise<void> {
|
async legacyProxyToSyncingServer(request: Request, response: Response): Promise<void> {
|
||||||
if (request.path === '/') {
|
if (request.path === '/') {
|
||||||
response.send('Welcome to the Standard Notes server infrastructure. Learn more at https://docs.standardnotes.com')
|
response.send(
|
||||||
|
'<!DOCTYPE html><html lang="en"><head><meta name="robots" content="noindex"></head><body>Welcome to the Standard Notes server infrastructure. Learn more at https://docs.standardnotes.com</body></html>',
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user