Compare commits

..

7 Commits

Author SHA1 Message Date
Karol Sójko
55f8f65c3f wip 2022-12-13 13:27:19 +01:00
Karol Sójko
3953dbc6b4 feat(settings): add unsubscribe token for muting emails 2022-12-13 13:18:15 +01:00
Karol Sójko
0b205287d1 fix(settings): binding for controller 2022-12-13 13:18:15 +01:00
Karol Sójko
4f0bc57b1a feat(settings): add controller for muting all emails 2022-12-13 13:18:15 +01:00
Karol Sójko
7d43316597 feat(settings): add mutting all emails use case 2022-12-13 13:18:15 +01:00
Karol Sójko
65d31f011b chore: remove settings dependency from all packages 2022-12-13 13:18:13 +01:00
Karol Sójko
80dd6efae3 feat(settings): replace setting with a domain entity 2022-12-13 13:13:28 +01:00
361 changed files with 3315 additions and 6914 deletions

View File

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

505
.pnp.cjs generated
View File

@@ -66,7 +66,7 @@ const RAW_RUNTIME_STATE =
"reference": "workspace:packages/security"\
},\
{\
"name": "@standardnotes/settings",\
"name": "@standardnotes/settings-server",\
"reference": "workspace:packages/settings"\
},\
{\
@@ -107,7 +107,7 @@ const RAW_RUNTIME_STATE =
["@standardnotes/scheduler-server", ["workspace:packages/scheduler"]],\
["@standardnotes/security", ["workspace:packages/security"]],\
["@standardnotes/server-monorepo", ["workspace:."]],\
["@standardnotes/settings", ["workspace:packages/settings"]],\
["@standardnotes/settings-server", ["workspace:packages/settings"]],\
["@standardnotes/sncrypto-node", ["workspace:packages/sncrypto-node"]],\
["@standardnotes/syncing-server", ["workspace:packages/syncing-server"]],\
["@standardnotes/time", ["workspace:packages/time"]],\
@@ -126,7 +126,7 @@ const RAW_RUNTIME_STATE =
["@lerna-lite/cli", "npm:1.6.0"],\
["@lerna-lite/list", "npm:1.6.0"],\
["@lerna-lite/run", "npm:1.6.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
@@ -1968,15 +1968,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@noble/ed25519", [\
["npm:1.7.1", {\
"packageLocation": "./.yarn/cache/@noble-ed25519-npm-1.7.1-177d9beb01-b1aa4b9264.zip/node_modules/@noble/ed25519/",\
"packageDependencies": [\
["@noble/ed25519", "npm:1.7.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@nodelib/fs.scandir", [\
["npm:2.1.5", {\
"packageLocation": "./.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-5f309a3b37.zip/node_modules/@nodelib/fs.scandir/",\
@@ -2333,44 +2324,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@peculiar/asn1-android", [\
["npm:2.3.3", {\
"packageLocation": "./.yarn/cache/@peculiar-asn1-android-npm-2.3.3-28df67d7a3-0c7cad544e.zip/node_modules/@peculiar/asn1-android/",\
"packageDependencies": [\
["@peculiar/asn1-android", "npm:2.3.3"],\
["@peculiar/asn1-schema", "npm:2.3.3"],\
["asn1js", "npm:3.0.5"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@peculiar/asn1-schema", [\
["npm:2.3.3", {\
"packageLocation": "./.yarn/cache/@peculiar-asn1-schema-npm-2.3.3-7c2b9469c4-f584f79d5a.zip/node_modules/@peculiar/asn1-schema/",\
"packageDependencies": [\
["@peculiar/asn1-schema", "npm:2.3.3"],\
["asn1js", "npm:3.0.5"],\
["pvtsutils", "npm:1.3.2"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@peculiar/asn1-x509", [\
["npm:2.3.4", {\
"packageLocation": "./.yarn/cache/@peculiar-asn1-x509-npm-2.3.4-a579005836-10a8659980.zip/node_modules/@peculiar/asn1-x509/",\
"packageDependencies": [\
["@peculiar/asn1-x509", "npm:2.3.4"],\
["@peculiar/asn1-schema", "npm:2.3.3"],\
["asn1js", "npm:3.0.5"],\
["ipaddr.js", "npm:2.0.1"],\
["pvtsutils", "npm:1.3.2"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@pnpm/network.ca-file", [\
["npm:1.0.1", {\
"packageLocation": "./.yarn/cache/@pnpm-network.ca-file-npm-1.0.1-42bfe40bec-ed952a5574.zip/node_modules/@pnpm/network.ca-file/",\
@@ -2485,25 +2438,25 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["@sentry/core", [\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.28.1-a468033ea8-f29d747d3e.zip/node_modules/@sentry/core/",\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip/node_modules/@sentry/core/",\
"packageDependencies": [\
["@sentry/core", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.28.1"],\
["@sentry/core", "npm:7.19.0"],\
["@sentry/types", "npm:7.19.0"],\
["@sentry/utils", "npm:7.19.0"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/node", [\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.28.1-b0e124fdfc-b4922d1f0a.zip/node_modules/@sentry/node/",\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip/node_modules/@sentry/node/",\
"packageDependencies": [\
["@sentry/node", "npm:7.28.1"],\
["@sentry/core", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@sentry/core", "npm:7.19.0"],\
["@sentry/types", "npm:7.19.0"],\
["@sentry/utils", "npm:7.19.0"],\
["cookie", "npm:0.4.2"],\
["https-proxy-agent", "npm:5.0.1"],\
["lru_map", "npm:0.3.3"],\
@@ -2512,67 +2465,26 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@sentry/tracing", [\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-tracing-npm-7.28.1-e15d453d8e-be501ca9d7.zip/node_modules/@sentry/tracing/",\
"packageDependencies": [\
["@sentry/tracing", "npm:7.28.1"],\
["@sentry/core", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.28.1"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/types", [\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.28.1-42d9a8574c-7dc6639cb7.zip/node_modules/@sentry/types/",\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip/node_modules/@sentry/types/",\
"packageDependencies": [\
["@sentry/types", "npm:7.28.1"]\
["@sentry/types", "npm:7.19.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sentry/utils", [\
["npm:7.28.1", {\
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.28.1-71eaeb767f-a4b5f73db0.zip/node_modules/@sentry/utils/",\
["npm:7.19.0", {\
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip/node_modules/@sentry/utils/",\
"packageDependencies": [\
["@sentry/utils", "npm:7.28.1"],\
["@sentry/types", "npm:7.28.1"],\
["@sentry/utils", "npm:7.19.0"],\
["@sentry/types", "npm:7.19.0"],\
["tslib", "npm:1.14.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@simplewebauthn/server", [\
["npm:6.2.2", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-6.2.2-ca870b05c2-5ffb9b1c15.zip/node_modules/@simplewebauthn/server/",\
"packageDependencies": [\
["@simplewebauthn/server", "npm:6.2.2"],\
["@noble/ed25519", "npm:1.7.1"],\
["@peculiar/asn1-android", "npm:2.3.3"],\
["@peculiar/asn1-schema", "npm:2.3.3"],\
["@peculiar/asn1-x509", "npm:2.3.4"],\
["base64url", "npm:3.0.1"],\
["cbor", "npm:5.2.0"],\
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
["jsrsasign", "npm:10.6.1"],\
["jwk-to-pem", "npm:2.0.5"],\
["node-fetch", "virtual:25a5f5382d53dbf298bf7a1191760bc2e0a523a619eeb0e667b99a8649e8ad183f9e2e0b45f6fb831b92f4078b61622aa567cf79565f6aa5af9597e3c84864f6#npm:2.6.7"]\
],\
"linkType": "HARD"\
}]\
]],\
["@simplewebauthn/typescript-types", [\
["npm:6.3.0-alpha.1", {\
"packageLocation": "./.yarn/cache/@simplewebauthn-typescript-types-npm-6.3.0-alpha.1-629da05c10-5667c214e9.zip/node_modules/@simplewebauthn/typescript-types/",\
"packageDependencies": [\
["@simplewebauthn/typescript-types", "npm:6.3.0-alpha.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@sinclair/typebox", [\
["npm:0.24.44", {\
"packageLocation": "./.yarn/cache/@sinclair-typebox-npm-0.24.44-38506ddef6-f37b9d28bf.zip/node_modules/@sinclair/typebox/",\
@@ -2626,7 +2538,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/analytics", "workspace:packages/analytics"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -2634,7 +2546,6 @@ const RAW_RUNTIME_STATE =
["@standardnotes/time", "workspace:packages/time"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/mixpanel", "npm:2.14.4"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.30.5"],\
@@ -2646,7 +2557,6 @@ const RAW_RUNTIME_STATE =
["inversify", "npm:6.0.1"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["mixpanel", "npm:0.17.0"],\
["mysql2", "npm:2.3.3"],\
["newrelic", "npm:9.6.0"],\
["reflect-metadata", "npm:0.1.13"],\
@@ -2672,6 +2582,20 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.20.13", {\
"packageLocation": "./.yarn/cache/@standardnotes-api-npm-1.20.13-3efe52d749-67bdb982ec.zip/node_modules/@standardnotes/api/",\
"packageDependencies": [\
["@standardnotes/api", "npm:1.20.13"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/encryption", "npm:1.19.21"],\
["@standardnotes/models", "npm:1.38.0"],\
["@standardnotes/responses", "npm:1.12.9"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/utils", "npm:1.13.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/api-gateway", [\
@@ -2680,7 +2604,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
@@ -2736,10 +2660,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/auth-server", "workspace:packages/auth"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.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"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -2749,7 +2670,6 @@ const RAW_RUNTIME_STATE =
["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/responses", "npm:1.11.1"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/settings", "workspace:packages/settings"],\
["@standardnotes/sncrypto-common", "npm:1.13.0"],\
["@standardnotes/sncrypto-node", "workspace:packages/sncrypto-node"],\
["@standardnotes/time", "workspace:packages/time"],\
@@ -2830,6 +2750,10 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/domain-core/",\
"packageDependencies": [\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.53.1"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\
["@standardnotes/security", "workspace:packages/security"],\
["@types/jest", "npm:29.1.1"],\
["@types/uuid", "npm:8.3.4"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
@@ -2901,6 +2825,19 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.19.21", {\
"packageLocation": "./.yarn/cache/@standardnotes-encryption-npm-1.19.21-dfa10f00e6-c8c2c27bfe.zip/node_modules/@standardnotes/encryption/",\
"packageDependencies": [\
["@standardnotes/encryption", "npm:1.19.21"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/models", "npm:1.38.0"],\
["@standardnotes/responses", "npm:1.12.9"],\
["@standardnotes/sncrypto-common", "npm:1.13.3"],\
["@standardnotes/utils", "npm:1.13.0"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/event-store", [\
@@ -2956,6 +2893,17 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.55.3", {\
"packageLocation": "./.yarn/cache/@standardnotes-features-npm-1.55.3-c124505183-b39fe2d49b.zip/node_modules/@standardnotes/features/",\
"packageDependencies": [\
["@standardnotes/features", "npm:1.55.3"],\
["@standardnotes/auth", "npm:3.19.4"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/files-server", [\
@@ -2963,7 +2911,7 @@ const RAW_RUNTIME_STATE =
"packageLocation": "./packages/files/",\
"packageDependencies": [\
["@standardnotes/files-server", "workspace:packages/files"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/config", "npm:2.4.3"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3037,6 +2985,13 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.38.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-models-npm-1.38.0-108f602f56-2dc2ac957e.zip/node_modules/@standardnotes/models/",\
"packageDependencies": [\
["@standardnotes/models", "npm:1.38.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/payloads", [\
@@ -3090,6 +3045,17 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.12.9", {\
"packageLocation": "./.yarn/cache/@standardnotes-responses-npm-1.12.9-280dc75972-353fe1ca6d.zip/node_modules/@standardnotes/responses/",\
"packageDependencies": [\
["@standardnotes/responses", "npm:1.12.9"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/features", "npm:1.55.3"],\
["@standardnotes/security", "workspace:packages/security"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/revisions-server", [\
@@ -3098,7 +3064,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -3143,7 +3109,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3204,7 +3170,7 @@ const RAW_RUNTIME_STATE =
["@lerna-lite/cli", "npm:1.6.0"],\
["@lerna-lite/list", "npm:1.6.0"],\
["@lerna-lite/run", "npm:1.6.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@types/node", "npm:18.11.9"],\
@@ -3221,15 +3187,46 @@ const RAW_RUNTIME_STATE =
"linkType": "SOFT"\
}]\
]],\
["@standardnotes/settings", [\
["@standardnotes/settings-server", [\
["workspace:packages/settings", {\
"packageLocation": "./packages/settings/",\
"packageDependencies": [\
["@standardnotes/settings", "workspace:packages/settings"],\
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
["@standardnotes/settings-server", "workspace:packages/settings"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/api", "npm:1.20.13"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/time", "workspace:packages/time"],\
["@types/cors", "npm:2.8.12"],\
["@types/dotenv", "npm:8.2.0"],\
["@types/express", "npm:4.17.14"],\
["@types/inversify-express-utils", "npm:2.0.0"],\
["@types/ioredis", "npm:5.0.0"],\
["@types/jest", "npm:29.1.1"],\
["@types/newrelic", "npm:7.0.4"],\
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
["aws-sdk", "npm:2.1260.0"],\
["cors", "npm:2.8.5"],\
["dotenv", "npm:16.0.1"],\
["eslint", "npm:8.25.0"],\
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
["express", "npm:4.18.2"],\
["helmet", "npm:6.0.0"],\
["inversify", "npm:6.0.1"],\
["inversify-express-utils", "npm:6.4.3"],\
["ioredis", "npm:5.2.4"],\
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
["mysql2", "npm:2.3.3"],\
["newrelic", "npm:9.6.0"],\
["npm-check-updates", "npm:16.0.1"],\
["reflect-metadata", "npm:0.1.13"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"]\
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
["typeorm", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:0.3.10"],\
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"],\
["winston", "npm:3.8.2"]\
],\
"linkType": "SOFT"\
}]\
@@ -3242,6 +3239,14 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.13.3", {\
"packageLocation": "./.yarn/cache/@standardnotes-sncrypto-common-npm-1.13.3-97ef3850ce-a73af90962.zip/node_modules/@standardnotes/sncrypto-common/",\
"packageDependencies": [\
["@standardnotes/sncrypto-common", "npm:1.13.3"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/sncrypto-node", [\
@@ -3270,8 +3275,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/tracing", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3279,7 +3283,6 @@ const RAW_RUNTIME_STATE =
["@standardnotes/payloads", "npm:1.5.1"],\
["@standardnotes/responses", "npm:1.11.1"],\
["@standardnotes/security", "workspace:packages/security"],\
["@standardnotes/settings", "workspace:packages/settings"],\
["@standardnotes/time", "workspace:packages/time"],\
["@types/cors", "npm:2.8.12"],\
["@types/dotenv", "npm:8.2.0"],\
@@ -3363,6 +3366,17 @@ const RAW_RUNTIME_STATE =
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}],\
["npm:1.13.0", {\
"packageLocation": "./.yarn/cache/@standardnotes-utils-npm-1.13.0-28780a59f0-1578e8adb7.zip/node_modules/@standardnotes/utils/",\
"packageDependencies": [\
["@standardnotes/utils", "npm:1.13.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["dompurify", "npm:2.4.1"],\
["lodash", "npm:4.17.21"],\
["reflect-metadata", "npm:0.1.13"]\
],\
"linkType": "HARD"\
}]\
]],\
["@standardnotes/websockets-server", [\
@@ -3371,7 +3385,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
@@ -3411,7 +3425,7 @@ const RAW_RUNTIME_STATE =
"packageDependencies": [\
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.28.1"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
@@ -3818,15 +3832,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["@types/mixpanel", [\
["npm:2.14.4", {\
"packageLocation": "./.yarn/cache/@types-mixpanel-npm-2.14.4-34bd98306f-a2bf6e633e.zip/node_modules/@types/mixpanel/",\
"packageDependencies": [\
["@types/mixpanel", "npm:2.14.4"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/newrelic", [\
["npm:7.0.4", {\
"packageLocation": "./.yarn/cache/@types-newrelic-npm-7.0.4-4f0b179b51-b44215b3ab.zip/node_modules/@types/newrelic/",\
@@ -4874,31 +4879,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["asn1.js", [\
["npm:5.4.1", {\
"packageLocation": "./.yarn/cache/asn1.js-npm-5.4.1-37c7edbcb0-5c36f81388.zip/node_modules/asn1.js/",\
"packageDependencies": [\
["asn1.js", "npm:5.4.1"],\
["bn.js", "npm:4.12.0"],\
["inherits", "npm:2.0.4"],\
["minimalistic-assert", "npm:1.0.1"],\
["safer-buffer", "npm:2.1.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["asn1js", [\
["npm:3.0.5", {\
"packageLocation": "./.yarn/cache/asn1js-npm-3.0.5-cf5558af33-d0bc57da97.zip/node_modules/asn1js/",\
"packageDependencies": [\
["asn1js", "npm:3.0.5"],\
["pvtsutils", "npm:1.3.2"],\
["pvutils", "npm:1.1.3"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["async", [\
["npm:3.2.4", {\
"packageLocation": "./.yarn/cache/async-npm-3.2.4-aba13508f9-9719e38d24.zip/node_modules/async/",\
@@ -5112,15 +5092,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["base64url", [\
["npm:3.0.1", {\
"packageLocation": "./.yarn/cache/base64url-npm-3.0.1-4c171c4917-72e1401ffe.zip/node_modules/base64url/",\
"packageDependencies": [\
["base64url", "npm:3.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["bcryptjs", [\
["npm:2.4.3", {\
"packageLocation": "./.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-bf6a43e9c4.zip/node_modules/bcryptjs/",\
@@ -5139,15 +5110,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["bignumber.js", [\
["npm:9.1.1", {\
"packageLocation": "./.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-e44d008049.zip/node_modules/bignumber.js/",\
"packageDependencies": [\
["bignumber.js", "npm:9.1.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["binary-extensions", [\
["npm:2.2.0", {\
"packageLocation": "./.yarn/cache/binary-extensions-npm-2.2.0-180c33fec7-16cf7c0cfd.zip/node_modules/binary-extensions/",\
@@ -5169,15 +5131,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["bn.js", [\
["npm:4.12.0", {\
"packageLocation": "./.yarn/cache/bn.js-npm-4.12.0-3ec6c884f6-bfb4590775.zip/node_modules/bn.js/",\
"packageDependencies": [\
["bn.js", "npm:4.12.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["body-parser", [\
["npm:1.20.1", {\
"packageLocation": "./.yarn/cache/body-parser-npm-1.20.1-759fd14db9-33f202c9d5.zip/node_modules/body-parser/",\
@@ -5245,15 +5198,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["brorand", [\
["npm:1.1.0", {\
"packageLocation": "./.yarn/cache/brorand-npm-1.1.0-ea86634c4b-f736e127fb.zip/node_modules/brorand/",\
"packageDependencies": [\
["brorand", "npm:1.1.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["browserslist", [\
["npm:4.21.1", {\
"packageLocation": "./.yarn/cache/browserslist-npm-4.21.1-930e90b93a-617d624493.zip/node_modules/browserslist/",\
@@ -5489,17 +5433,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["cbor", [\
["npm:5.2.0", {\
"packageLocation": "./.yarn/cache/cbor-npm-5.2.0-4f6440587f-d60986b9d0.zip/node_modules/cbor/",\
"packageDependencies": [\
["cbor", "npm:5.2.0"],\
["bignumber.js", "npm:9.1.1"],\
["nofilter", "npm:1.0.4"]\
],\
"linkType": "HARD"\
}]\
]],\
["chalk", [\
["npm:2.4.2", {\
"packageLocation": "./.yarn/cache/chalk-npm-2.4.2-3ea16dd91e-befd2fe888.zip/node_modules/chalk/",\
@@ -6542,6 +6475,13 @@ const RAW_RUNTIME_STATE =
["dompurify", "npm:2.4.0"]\
],\
"linkType": "HARD"\
}],\
["npm:2.4.1", {\
"packageLocation": "./.yarn/cache/dompurify-npm-2.4.1-1c79f22057-ddc0633356.zip/node_modules/dompurify/",\
"packageDependencies": [\
["dompurify", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["dot-prop", [\
@@ -6624,22 +6564,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["elliptic", [\
["npm:6.5.4", {\
"packageLocation": "./.yarn/cache/elliptic-npm-6.5.4-0ca8204a86-4453b008cf.zip/node_modules/elliptic/",\
"packageDependencies": [\
["elliptic", "npm:6.5.4"],\
["bn.js", "npm:4.12.0"],\
["brorand", "npm:1.1.0"],\
["hash.js", "npm:1.1.7"],\
["hmac-drbg", "npm:1.0.1"],\
["inherits", "npm:2.0.4"],\
["minimalistic-assert", "npm:1.0.1"],\
["minimalistic-crypto-utils", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["emittery", [\
["npm:0.10.2", {\
"packageLocation": "./.yarn/cache/emittery-npm-0.10.2-aac10498b5-c55b286714.zip/node_modules/emittery/",\
@@ -8146,17 +8070,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["hash.js", [\
["npm:1.1.7", {\
"packageLocation": "./.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e4266370d1.zip/node_modules/hash.js/",\
"packageDependencies": [\
["hash.js", "npm:1.1.7"],\
["inherits", "npm:2.0.4"],\
["minimalistic-assert", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["helmet", [\
["npm:6.0.0", {\
"packageLocation": "./.yarn/cache/helmet-npm-6.0.0-2285459f57-73b6ba802d.zip/node_modules/helmet/",\
@@ -8175,18 +8088,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["hmac-drbg", [\
["npm:1.0.1", {\
"packageLocation": "./.yarn/cache/hmac-drbg-npm-1.0.1-3499ad31cd-4e88d58ffc.zip/node_modules/hmac-drbg/",\
"packageDependencies": [\
["hmac-drbg", "npm:1.0.1"],\
["hash.js", "npm:1.1.7"],\
["minimalistic-assert", "npm:1.0.1"],\
["minimalistic-crypto-utils", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["hosted-git-info", [\
["npm:2.8.9", {\
"packageLocation": "./.yarn/cache/hosted-git-info-npm-2.8.9-62c44fa93f-c24da52f98.zip/node_modules/hosted-git-info/",\
@@ -8277,15 +8178,6 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["https-proxy-agent", [\
["npm:5.0.0", {\
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.0-bb777903c3-77d11b0e2c.zip/node_modules/https-proxy-agent/",\
"packageDependencies": [\
["https-proxy-agent", "npm:5.0.0"],\
["agent-base", "npm:6.0.2"],\
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"]\
],\
"linkType": "HARD"\
}],\
["npm:5.0.1", {\
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.1-42d65f358e-8e767faec9.zip/node_modules/https-proxy-agent/",\
"packageDependencies": [\
@@ -8576,13 +8468,6 @@ const RAW_RUNTIME_STATE =
["ipaddr.js", "npm:1.9.1"]\
],\
"linkType": "HARD"\
}],\
["npm:2.0.1", {\
"packageLocation": "./.yarn/cache/ipaddr.js-npm-2.0.1-04e97280d7-04ce6c896c.zip/node_modules/ipaddr.js/",\
"packageDependencies": [\
["ipaddr.js", "npm:2.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["is-arguments", [\
@@ -9785,15 +9670,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["jsrsasign", [\
["npm:10.6.1", {\
"packageLocation": "./.yarn/cache/jsrsasign-npm-10.6.1-a8fa295369-e8e9c1b24f.zip/node_modules/jsrsasign/",\
"packageDependencies": [\
["jsrsasign", "npm:10.6.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["jwa", [\
["npm:1.4.1", {\
"packageLocation": "./.yarn/cache/jwa-npm-1.4.1-4f19d6572c-0cc3e68b68.zip/node_modules/jwa/",\
@@ -9806,18 +9682,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["jwk-to-pem", [\
["npm:2.0.5", {\
"packageLocation": "./.yarn/cache/jwk-to-pem-npm-2.0.5-aff7d9f125-fced3a75b0.zip/node_modules/jwk-to-pem/",\
"packageDependencies": [\
["jwk-to-pem", "npm:2.0.5"],\
["asn1.js", "npm:5.4.1"],\
["elliptic", "npm:6.5.4"],\
["safe-buffer", "npm:5.2.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["jws", [\
["npm:3.2.2", {\
"packageLocation": "./.yarn/cache/jws-npm-3.2.2-c1ae59c7af-347ed7c334.zip/node_modules/jws/",\
@@ -10431,24 +10295,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["minimalistic-assert", [\
["npm:1.0.1", {\
"packageLocation": "./.yarn/cache/minimalistic-assert-npm-1.0.1-dc8bb23d29-e2310081d8.zip/node_modules/minimalistic-assert/",\
"packageDependencies": [\
["minimalistic-assert", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["minimalistic-crypto-utils", [\
["npm:1.0.1", {\
"packageLocation": "./.yarn/cache/minimalistic-crypto-utils-npm-1.0.1-e66b10822e-7d909decd2.zip/node_modules/minimalistic-crypto-utils/",\
"packageDependencies": [\
["minimalistic-crypto-utils", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["minimatch", [\
["npm:3.1.2", {\
"packageLocation": "./.yarn/cache/minimatch-npm-3.1.2-9405269906-97f5615ee8.zip/node_modules/minimatch/",\
@@ -10573,16 +10419,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["mixpanel", [\
["npm:0.17.0", {\
"packageLocation": "./.yarn/cache/mixpanel-npm-0.17.0-3073ce9949-5a945bdbdd.zip/node_modules/mixpanel/",\
"packageDependencies": [\
["mixpanel", "npm:0.17.0"],\
["https-proxy-agent", "npm:5.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["mkdirp", [\
["npm:1.0.4", {\
"packageLocation": "./.yarn/cache/mkdirp-npm-1.0.4-37f6ef56b9-1233611198.zip/node_modules/mkdirp/",\
@@ -10843,15 +10679,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["nofilter", [\
["npm:1.0.4", {\
"packageLocation": "./.yarn/cache/nofilter-npm-1.0.4-1cbdc6c03a-9a26874e7d.zip/node_modules/nofilter/",\
"packageDependencies": [\
["nofilter", "npm:1.0.4"]\
],\
"linkType": "HARD"\
}]\
]],\
["nopt", [\
["npm:1.0.10", {\
"packageLocation": "./.yarn/cache/nopt-npm-1.0.10-f3db192976-efa5a9c2c1.zip/node_modules/nopt/",\
@@ -11900,25 +11727,6 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
["pvtsutils", [\
["npm:1.3.2", {\
"packageLocation": "./.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-eb22d3df60.zip/node_modules/pvtsutils/",\
"packageDependencies": [\
["pvtsutils", "npm:1.3.2"],\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["pvutils", [\
["npm:1.1.3", {\
"packageLocation": "./.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-0cb4f4878f.zip/node_modules/pvutils/",\
"packageDependencies": [\
["pvutils", "npm:1.1.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["q", [\
["npm:1.5.1", {\
"packageLocation": "./.yarn/cache/q-npm-1.5.1-a28b3cfeaf-276b7e93fc.zip/node_modules/q/",\
@@ -13603,13 +13411,6 @@ const RAW_RUNTIME_STATE =
["tslib", "npm:2.4.0"]\
],\
"linkType": "HARD"\
}],\
["npm:2.4.1", {\
"packageLocation": "./.yarn/cache/tslib-npm-2.4.1-36f0ed04db-a739a21e3f.zip/node_modules/tslib/",\
"packageDependencies": [\
["tslib", "npm:2.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["tsutils", [\

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -10,8 +10,6 @@ DB_DATABASE=analytics
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
DB_MIGRATIONS_PATH=dist/migrations/*.js
ADMIN_EMAILS=test@standardnotes.com
REDIS_URL=redis://cache
REDIS_EVENTS_CHANNEL=events
@@ -28,6 +26,3 @@ NEW_RELIC_NO_CONFIG_FILE=true
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
NEW_RELIC_LOG_ENABLED=false
NEW_RELIC_LOG_LEVEL=info
# (Optional) Mixpanel
MIXPANEL_TOKEN=

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.19.1",
"version": "2.12.25",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -27,7 +27,6 @@
"devDependencies": {
"@types/ioredis": "^5.0.0",
"@types/jest": "^29.1.1",
"@types/mixpanel": "^2.14.4",
"@types/newrelic": "^7.0.4",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.30.0",
@@ -39,7 +38,7 @@
},
"dependencies": {
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.28.1",
"@sentry/node": "^7.19.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
@@ -50,7 +49,6 @@
"dotenv": "^16.0.1",
"inversify": "^6.0.1",
"ioredis": "^5.2.4",
"mixpanel": "^0.17.0",
"mysql2": "^2.3.3",
"newrelic": "^9.6.0",
"reflect-metadata": "^0.1.13",

View File

@@ -8,8 +8,6 @@ import {
DomainEventSubscriberFactoryInterface,
} from '@standardnotes/domain-events'
import { MapperInterface } from '@standardnotes/domain-core'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Mixpanel = require('mixpanel')
import { Env } from './Env'
import TYPES from './Types'
@@ -54,9 +52,6 @@ import { RevenueModification } from '../Domain/Revenue/RevenueModification'
import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap'
import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification'
import { CalculateMonthlyRecurringRevenue } from '../Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
import { PersistStatistic } from '../Domain/UseCase/PersistStatistic/PersistStatistic'
import { StatisticMeasureRepositoryInterface } from '../Domain/Statistics/StatisticMeasureRepositoryInterface'
import { StatisticPersistenceRequestedEventHandler } from '../Domain/Handler/StatisticPersistenceRequestedEventHandler'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const newrelicFormatter = require('@newrelic/winston-enricher')
@@ -136,33 +131,6 @@ export class ContainerConfigLoader {
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
container.bind(TYPES.MIXPANEL_TOKEN).toConstantValue(env.get('MIXPANEL_TOKEN', true))
// Services
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
container
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
if (env.get('SNS_TOPIC_ARN', true)) {
container
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
} else {
container
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
)
}
if (env.get('MIXPANEL_TOKEN', true)) {
container.bind<Mixpanel>(TYPES.MixpanelClient).toConstantValue(Mixpanel.init(env.get('MIXPANEL_TOKEN', true)))
}
// Repositories
container
@@ -171,9 +139,6 @@ export class ContainerConfigLoader {
container
.bind<RevenueModificationRepositoryInterface>(TYPES.RevenueModificationRepository)
.to(MySQLRevenueModificationRepository)
container
.bind<StatisticMeasureRepositoryInterface>(TYPES.StatisticMeasureRepository)
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
// ORM
container
@@ -189,9 +154,6 @@ export class ContainerConfigLoader {
container
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
.to(CalculateMonthlyRecurringRevenue)
container
.bind<PersistStatistic>(TYPES.PersistStatistic)
.toConstantValue(new PersistStatistic(container.get(TYPES.StatisticMeasureRepository)))
// Hanlders
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
@@ -219,22 +181,35 @@ export class ContainerConfigLoader {
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
.to(SubscriptionReactivatedEventHandler)
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
container
.bind<StatisticPersistenceRequestedEventHandler>(TYPES.StatisticPersistenceRequestedEventHandler)
.toConstantValue(
new StatisticPersistenceRequestedEventHandler(
container.get(TYPES.PersistStatistic),
container.get(TYPES.Timer),
container.get(TYPES.Logger),
env.get('MIXPANEL_TOKEN', true) ? container.get(TYPES.MixpanelClient) : null,
),
)
// Maps
container
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
.to(RevenueModificationMap)
// Services
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
container
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
if (env.get('SNS_TOPIC_ARN', true)) {
container
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
} else {
container
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
.toConstantValue(
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
)
}
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
@@ -247,7 +222,6 @@ export class ContainerConfigLoader {
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
['STATISTIC_PERSISTENCE_REQUESTED', container.get(TYPES.StatisticPersistenceRequestedEventHandler)],
])
if (env.get('SQS_QUEUE_URL', true)) {

View File

@@ -12,11 +12,9 @@ const TYPES = {
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
ADMIN_EMAILS: Symbol.for('ADMIN_EMAILS'),
MIXPANEL_TOKEN: Symbol.for('MIXPANEL_TOKEN'),
// Repositories
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
StatisticMeasureRepository: Symbol.for('StatisticMeasureRepository'),
// ORM
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
@@ -24,7 +22,6 @@ const TYPES = {
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
PersistStatistic: Symbol.for('PersistStatistic'),
// Handlers
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
@@ -37,7 +34,6 @@ const TYPES = {
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
StatisticPersistenceRequestedEventHandler: Symbol.for('StatisticPersistenceRequestedEventHandler'),
// Maps
RevenueModificationMap: Symbol.for('RevenueModificationMap'),
// Services
@@ -49,7 +45,6 @@ const TYPES = {
StatisticsStore: Symbol.for('StatisticsStore'),
Timer: Symbol.for('Timer'),
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
MixpanelClient: Symbol.for('MixpanelClient'),
}
export default TYPES

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
@@ -18,7 +17,6 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
@@ -35,19 +33,12 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
])
const registrationLength = this.timer.getTimestampInMicroseconds() - event.payload.userCreatedAtTimestamp
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.RegistrationLength, registrationLength, [
await this.statisticsStore.incrementMeasure(StatisticsMeasure.RegistrationLength, registrationLength, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
await this.analyticsEntityRepository.remove(analyticsEntity)
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsEntity.id.toString(),
user_created_at: this.timer.convertMicrosecondsToDate(event.payload.userCreatedAtTimestamp),
})
}
}
}

View File

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

View File

@@ -1,6 +1,5 @@
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -13,7 +12,6 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: PaymentFailedEvent): Promise<void> {
@@ -23,11 +21,5 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
})
}
}
}

View File

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

View File

@@ -1,13 +1,12 @@
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@@ -21,27 +20,15 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
[
PaymentType.Initial,
new Map([
[
SubscriptionBillingFrequency.Monthly,
StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
],
[
SubscriptionBillingFrequency.Annual,
StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
],
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome],
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome],
]),
],
[
PaymentType.Renewal,
new Map([
[
SubscriptionBillingFrequency.Monthly,
StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome,
],
[
SubscriptionBillingFrequency.Annual,
StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome,
],
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome],
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome],
]),
],
]),
@@ -52,27 +39,15 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
[
PaymentType.Initial,
new Map([
[
SubscriptionBillingFrequency.Monthly,
StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
],
[
SubscriptionBillingFrequency.Annual,
StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
],
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome],
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome],
]),
],
[
PaymentType.Renewal,
new Map([
[
SubscriptionBillingFrequency.Monthly,
StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome,
],
[
SubscriptionBillingFrequency.Annual,
StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome,
],
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome],
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome],
]),
],
]),
@@ -84,7 +59,6 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: PaymentSuccessEvent): Promise<void> {
@@ -95,7 +69,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
Period.ThisMonth,
])
const statisticMeasures = [StatisticMeasureName.NAMES.Income]
const statisticMeasures = [StatisticsMeasure.Income]
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
?.get(event.payload.paymentType as PaymentType)
@@ -115,19 +89,5 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
Period.ThisMonth,
])
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
amount: event.payload.amount,
billing_frequency: event.payload.billingFrequency,
payment_type: event.payload.paymentType,
subscription_name: event.payload.subscriptionName,
})
this.mixpanelClient.people.track_charge(analyticsId.toString(), event.payload.amount)
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}

View File

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

View File

@@ -1,36 +1,20 @@
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@injectable()
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {}
async handle(event: RefundProcessedEvent): Promise<void> {
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [
await this.statisticsStore.incrementMeasure(StatisticsMeasure.Refunds, event.payload.amount, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
amount: event.payload.amount,
})
this.mixpanelClient.people.track_charge(analyticsId.toString(), -event.payload.amount)
}
}
}

View File

@@ -1,34 +0,0 @@
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,
})
}
}
}

View File

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

View File

@@ -1,20 +1,18 @@
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
import { inject, injectable, optional } from 'inversify'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Username } from '@standardnotes/domain-core'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
import { Period } from '../Time/Period'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { TimerInterface } from '@standardnotes/time'
@injectable()
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
@@ -24,8 +22,6 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionCancelledEvent): Promise<void> {
@@ -54,22 +50,6 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_created_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionCreatedAt),
subscription_updated_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionUpdatedAt),
last_payed_at: this.timer.convertMicrosecondsToDate(event.payload.lastPayedAt),
subscription_ends_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionEndsAt),
offline: event.payload.offline,
replaced: event.payload.replaced,
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
}
}
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
@@ -78,7 +58,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
}
const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.SubscriptionLength, subscriptionLength, [
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
@@ -90,7 +70,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
await this.statisticsStore.incrementMeasure(
StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
StatisticsMeasure.RemainingSubscriptionTimePercentage,
remainingSubscriptionPercentage,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)

View File

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

View File

@@ -1,13 +1,12 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
import { inject, injectable, optional } from 'inversify'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
@@ -23,7 +22,6 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: SubscriptionExpiredEvent): Promise<void> {
@@ -35,7 +33,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
)
await this.statisticsStore.setMeasure(
StatisticMeasureName.NAMES.TotalCustomers,
StatisticsMeasure.TotalCustomers,
event.payload.totalActiveSubscriptionsCount,
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
)
@@ -56,18 +54,5 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
offline: event.payload.offline,
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
}
}
}

View File

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

View File

@@ -1,14 +1,12 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable, optional } from 'inversify'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
@@ -24,8 +22,6 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
@@ -49,18 +45,18 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
if (event.payload.newSubscriber) {
await this.statisticsStore.incrementMeasure(
StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
StatisticsMeasure.RegistrationToSubscriptionTime,
event.payload.timestamp - event.payload.userRegisteredAt,
[Period.Today, Period.ThisWeek, Period.ThisMonth],
)
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.NewCustomers, 1, [
await this.statisticsStore.incrementMeasure(StatisticsMeasure.NewCustomers, 1, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
Period.ThisYear,
])
await this.statisticsStore.setMeasure(
StatisticMeasureName.NAMES.TotalCustomers,
StatisticsMeasure.TotalCustomers,
event.payload.totalActiveSubscriptionsCount,
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
)
@@ -82,22 +78,5 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
offline: event.payload.offline,
discount_code: event.payload.discountCode,
limited_discount_purchased: event.payload.limitedDiscountPurchased,
new_subscriber: event.payload.newSubscriber,
user_registered_at: this.timer.convertMicrosecondsToDate(event.payload.userRegisteredAt),
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}

View File

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

View File

@@ -1,7 +1,5 @@
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
import { TimerInterface } from '@standardnotes/time'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -14,8 +12,6 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
constructor(
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
@@ -25,16 +21,5 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
discount_code: event.payload.discountCode,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}

View File

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

View File

@@ -1,13 +1,12 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
import { inject, injectable, optional } from 'inversify'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
@@ -23,7 +22,6 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: SubscriptionRefundedEvent): Promise<void> {
@@ -52,18 +50,6 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
offline: event.payload.offline,
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
}
}
private async markChurnActivity(analyticsId: number, event: SubscriptionRefundedEvent): Promise<void> {
@@ -84,7 +70,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
}
await this.statisticsStore.setMeasure(
StatisticMeasureName.NAMES.TotalCustomers,
StatisticsMeasure.TotalCustomers,
event.payload.totalActiveSubscriptionsCount,
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
)

View File

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

View File

@@ -1,7 +1,6 @@
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
import { inject, injectable, optional } from 'inversify'
import { inject, injectable } from 'inversify'
import { Username } from '@standardnotes/domain-core'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@@ -12,7 +11,6 @@ import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/Save
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
import { Logger } from 'winston'
import { TimerInterface } from '@standardnotes/time'
@injectable()
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
@@ -21,8 +19,6 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionRenewedEvent): Promise<void> {
@@ -54,17 +50,5 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
offline: event.payload.offline,
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}

View File

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

View File

@@ -1,6 +1,5 @@
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
import { inject, injectable, optional } from 'inversify'
import { Mixpanel } from 'mixpanel'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -14,7 +13,6 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
constructor(
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: UserRegisteredEvent): Promise<void> {
@@ -28,17 +26,5 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsEntity.id.toString(),
protocol_version: event.payload.protocolVersion,
})
this.mixpanelClient.people.set(analyticsEntity.id.toString(), {
subscription: 'free',
protocol_version: event.payload.protocolVersion,
})
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,6 @@ import '../src/Controller/v1/FilesController'
import '../src/Controller/v1/SubscriptionInvitesController'
import '../src/Controller/v1/WorkspacesController'
import '../src/Controller/v1/InvitesController'
import '../src/Controller/v1/AuthenticatorsController'
import '../src/Controller/v2/PaymentsControllerV2'
import '../src/Controller/v2/ActionsControllerV2'

View File

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

View File

@@ -39,19 +39,4 @@ export class ActionsController extends BaseHttpController {
request.body,
)
}
@httpPost('/recovery/codes', TYPES.AuthMiddleware)
async recoveryCodes(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'auth/recovery/codes', request.body)
}
@httpPost('/recovery/login')
async recoveryLogin(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'auth/recovery/login', request.body)
}
@httpPost('/recovery/login-params')
async recoveryParams(request: Request, response: Response): Promise<void> {
await this.httpService.callAuthServer(request, response, 'auth/recovery/params', request.body)
}
}

View File

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

View File

@@ -3,203 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.81.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.2...@standardnotes/auth-server@1.81.3) (2023-01-09)
### Bug Fixes
* **auth:** remove mfa settings after recovery sign in ([a0208dd](https://github.com/standardnotes/server/commit/a0208dd5b3ce54ccfa96b3497cb36e16ccb4cf89))
## [1.81.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.1...@standardnotes/auth-server@1.81.2) (2023-01-05)
### Bug Fixes
* **auth:** return type to include user ([db0baf9](https://github.com/standardnotes/server/commit/db0baf92f1336071a8602a1a20ba6439f10028e6))
## [1.81.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.81.0...@standardnotes/auth-server@1.81.1) (2023-01-05)
### Bug Fixes
* **auth:** allow retrieval of recovery codes setting ([13c5c97](https://github.com/standardnotes/server/commit/13c5c97ba73fcf1f4bd2905a1d8668ef5b468016))
# [1.81.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.80.0...@standardnotes/auth-server@1.81.0) (2023-01-05)
### Features
* **auth:** add recovery sign in with recovery codes ([cac899a](https://github.com/standardnotes/server/commit/cac899a7e558d066895dfb3ba28418d94072f2b7))
# [1.80.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.79.1...@standardnotes/auth-server@1.80.0) (2023-01-04)
### Features
* **auth:** add generating recovery codes ([a360231](https://github.com/standardnotes/server/commit/a360231fd0b04afbc8cfa0ed74c8e21c4edadb4e))
## [1.79.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.79.0...@standardnotes/auth-server@1.79.1) (2023-01-02)
### Bug Fixes
* remove @sentry/profiling-node as it is not compatible with ARM - fixes [#383](https://github.com/standardnotes/server/issues/383) ([fa6d80a](https://github.com/standardnotes/server/commit/fa6d80a753d4999818bb32a7fcb124f23c15f574))
* remove @sentry/profiling-node integration as it is not compatible with ARM - fixes [#383](https://github.com/standardnotes/server/issues/383) ([9c72ad8](https://github.com/standardnotes/server/commit/9c72ad85a04040b3fdfce4f769e5e717ce81a3ce))
# [1.79.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.78.0...@standardnotes/auth-server@1.79.0) (2022-12-29)
### Features
* **auth:** add removing authenticator ([de50d76](https://github.com/standardnotes/server/commit/de50d76800a4240729763b2df11c4a1718951670))
# [1.78.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.77.1...@standardnotes/auth-server@1.78.0) (2022-12-29)
### Features
* **auth:** add listing authenticators ([01837ea](https://github.com/standardnotes/server/commit/01837eaea9b1f219e7ad3be4d28cd0df099fe423))
## [1.77.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.77.0...@standardnotes/auth-server@1.77.1) (2022-12-29)
### Bug Fixes
* **auth:** specs for verifying authenticator authentication response ([5455972](https://github.com/standardnotes/server/commit/5455972be2c62d7862c351b1328beacf4bd5c3da))
# [1.77.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.76.0...@standardnotes/auth-server@1.77.0) (2022-12-29)
### Features
* **auth:** add http endpoints for authenticators ([b6fda90](https://github.com/standardnotes/server/commit/b6fda901ef66a3e66541bd1e3f041b8268a1c3f5))
# [1.76.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.75.0...@standardnotes/auth-server@1.76.0) (2022-12-29)
### Features
* **auth:** add verifying authenticator authentication response ([64525a6](https://github.com/standardnotes/server/commit/64525a65f2e1677f942868903f318d6700c34c74))
# [1.75.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.74.1...@standardnotes/auth-server@1.75.0) (2022-12-29)
### Features
* **auth:** add generating authenticator authentication options ([8c7c1e4](https://github.com/standardnotes/server/commit/8c7c1e4745647004f3dc361ec374014390952486))
## [1.74.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.74.0...@standardnotes/auth-server@1.74.1) (2022-12-28)
### Bug Fixes
* **auth:** migrations to not include unique index for credentials id ([384dfc8](https://github.com/standardnotes/server/commit/384dfc8da4b1b640964fa6da207a67fcd68dc7ec))
# [1.74.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.73.1...@standardnotes/auth-server@1.74.0) (2022-12-28)
### Features
* **auth:** add verifying authenticator registration response ([f5683cf](https://github.com/standardnotes/server/commit/f5683cfd9494db8e25010e9c4ef5fd4d8fcd6bc7))
## [1.73.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.73.0...@standardnotes/auth-server@1.73.1) (2022-12-28)
### Bug Fixes
* **auth:** temporarily remove credential id index due to mysql 5.6 limitations ([a5e7132](https://github.com/standardnotes/server/commit/a5e7132d3c4b74ed13877d7d437062c509201874))
# [1.73.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.72.0...@standardnotes/auth-server@1.73.0) (2022-12-28)
### Features
* **auth:** add generating authencator registration options ([51ad06b](https://github.com/standardnotes/server/commit/51ad06b303d7dc994920818872fdf8bd37fc445c))
# [1.72.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.71.1...@standardnotes/auth-server@1.72.0) (2022-12-28)
### Features
* **auth:** add authenticator challenges model ([fa9bf0b](https://github.com/standardnotes/server/commit/fa9bf0b448acb3f19ab44c4b431ce367dab37b76))
## [1.71.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.71.0...@standardnotes/auth-server@1.71.1) (2022-12-28)
### Bug Fixes
* **auth:** credential id field type ([98e3d18](https://github.com/standardnotes/server/commit/98e3d1833530dcd9e3e34a4c4a6b14a2a01afea1))
# [1.71.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.9...@standardnotes/auth-server@1.71.0) (2022-12-28)
### Features
* **auth:** add authenticators model ([1e69a13](https://github.com/standardnotes/server/commit/1e69a13a97c4d9022aa96397cce1b349d3cede89))
## [1.70.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.8...@standardnotes/auth-server@1.70.9) (2022-12-28)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.70.8](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.7...@standardnotes/auth-server@1.70.8) (2022-12-20)
### Bug Fixes
* **auth:** move tracing sessions to session creation instead of cross service token creation ([5255cfb](https://github.com/standardnotes/server/commit/5255cfbb257cc9e6ac437fe0c5b28d938e3e599b))
## [1.70.7](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.6...@standardnotes/auth-server@1.70.7) (2022-12-20)
### Bug Fixes
* **auth:** change severity on tracing session errors - most probably hazardous reads ([cf0b918](https://github.com/standardnotes/server/commit/cf0b91891370e1c1799ad80c10ee9f6b98087a94))
## [1.70.6](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.5...@standardnotes/auth-server@1.70.6) (2022-12-20)
### Bug Fixes
* **auth:** query for session traces ([14eb775](https://github.com/standardnotes/server/commit/14eb775749bfa9972dc3c07049505f3d15f0b556))
## [1.70.5](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.4...@standardnotes/auth-server@1.70.5) (2022-12-20)
### Bug Fixes
* **auth:** add session traces index ([b9e1e47](https://github.com/standardnotes/server/commit/b9e1e4787129f00fab8f98cb721141f2e7d75600))
## [1.70.4](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.3...@standardnotes/auth-server@1.70.4) (2022-12-20)
### Bug Fixes
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
## [1.70.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.2...@standardnotes/auth-server@1.70.3) (2022-12-20)
### Bug Fixes
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
## [1.70.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.1...@standardnotes/auth-server@1.70.2) (2022-12-20)
### Bug Fixes
* **auth:** docker command ([85d2f42](https://github.com/standardnotes/server/commit/85d2f42f473110e8dfca975bfecc7a56823bdef4))
## [1.70.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.70.0...@standardnotes/auth-server@1.70.1) (2022-12-20)
### Bug Fixes
* **auth:** saving subscription plan name in session traces ([3064d03](https://github.com/standardnotes/server/commit/3064d03aa9a2ac9ca3acfff30480ea8629faeb14))
# [1.70.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.1...@standardnotes/auth-server@1.70.0) (2022-12-19)
### Features
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
## [1.69.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.69.0...@standardnotes/auth-server@1.69.1) (2022-12-19)
**Note:** Version bump only for package @standardnotes/auth-server
# [1.69.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.68.0...@standardnotes/auth-server@1.69.0) (2022-12-19)
### Features
* **auth:** add session traces cleanup procedure ([147d8fd](https://github.com/standardnotes/server/commit/147d8fd9af89d2b97cd68eefac36e53d14d511bf))
# [1.68.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.3...@standardnotes/auth-server@1.68.0) (2022-12-19)
### Features
* **auth:** add session traces ([8bcb552](https://github.com/standardnotes/server/commit/8bcb552783b2d12f3296b3195752168482790bc8))
## [1.67.3](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.2...@standardnotes/auth-server@1.67.3) (2022-12-15)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.67.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.1...@standardnotes/auth-server@1.67.2) (2022-12-15)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.67.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.67.0...@standardnotes/auth-server@1.67.1) (2022-12-12)
### Bug Fixes

View File

@@ -3,20 +3,19 @@ import 'reflect-metadata'
import 'newrelic'
import { Stream } from 'stream'
import { Logger } from 'winston'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { SettingName } from '@standardnotes/domain-core'
import { PermissionName } from '@standardnotes/features'
import { ContainerConfigLoader } from '../src/Bootstrap/Container'
import TYPES from '../src/Bootstrap/Types'
import { Env } from '../src/Bootstrap/Env'
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
import { SettingRepositoryInterface } from '../src/Domain/Setting/SettingRepositoryInterface'
import { MuteFailedBackupsEmailsOption, MuteFailedCloudBackupsEmailsOption, SettingName } from '@standardnotes/settings'
import { RoleServiceInterface } from '../src/Domain/Role/RoleServiceInterface'
import { PermissionName } from '@standardnotes/features'
import { SettingServiceInterface } from '../src/Domain/Setting/SettingServiceInterface'
const inputArgs = process.argv.slice(2)
@@ -30,38 +29,38 @@ const requestBackups = async (
domainEventFactory: DomainEventFactoryInterface,
domainEventPublisher: DomainEventPublisherInterface,
): Promise<void> => {
let settingName: SettingName,
let settingName: string,
permissionName: PermissionName,
muteEmailsSettingName: SettingName,
muteEmailsSettingName: string,
muteEmailsSettingValue: string,
providerTokenSettingName: SettingName
providerTokenSettingName: string
switch (backupProvider) {
case 'email':
settingName = SettingName.EmailBackupFrequency
settingName = SettingName.NAMES.EmailBackupFrequency
permissionName = PermissionName.DailyEmailBackup
muteEmailsSettingName = SettingName.MuteFailedBackupsEmails
muteEmailsSettingValue = MuteFailedBackupsEmailsOption.Muted
muteEmailsSettingName = SettingName.NAMES.MuteFailedBackupsEmails
muteEmailsSettingValue = 'muted'
break
case 'dropbox':
settingName = SettingName.DropboxBackupFrequency
settingName = SettingName.NAMES.DropboxBackupFrequency
permissionName = PermissionName.DailyDropboxBackup
muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
providerTokenSettingName = SettingName.DropboxBackupToken
muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = 'muted'
providerTokenSettingName = SettingName.NAMES.DropboxBackupToken
break
case 'one_drive':
settingName = SettingName.OneDriveBackupFrequency
settingName = SettingName.NAMES.OneDriveBackupFrequency
permissionName = PermissionName.DailyOneDriveBackup
muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
providerTokenSettingName = SettingName.OneDriveBackupToken
muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = 'muted'
providerTokenSettingName = SettingName.NAMES.OneDriveBackupToken
break
case 'google_drive':
settingName = SettingName.GoogleDriveBackupFrequency
settingName = SettingName.NAMES.GoogleDriveBackupFrequency
permissionName = PermissionName.DailyGDriveBackup
muteEmailsSettingName = SettingName.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = MuteFailedCloudBackupsEmailsOption.Muted
providerTokenSettingName = SettingName.GoogleDriveBackupToken
muteEmailsSettingName = SettingName.NAMES.MuteFailedCloudBackupsEmails
muteEmailsSettingValue = 'muted'
providerTokenSettingName = SettingName.NAMES.GoogleDriveBackupToken
break
default:
throw new Error(`Not handled backup provider: ${backupProvider}`)

View File

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

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