mirror of
https://github.com/standardnotes/server
synced 2026-05-08 12:57:34 -04:00
Compare commits
307 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd4924c925 | |||
| f73129cd7e | |||
| 4983c8741e | |||
| c5798640ff | |||
| 5803a8018a | |||
| e2aae8ac8a | |||
| 2917aeeb32 | |||
| 9377c03c3f | |||
| 9b926fbad6 | |||
| 8db19c3e2b | |||
| ca970781c7 | |||
| e7beee2788 | |||
| d266eada88 | |||
| 11b8b078b4 | |||
| 37912fa29a | |||
| b97dafe6f3 | |||
| 2a29151395 | |||
| 8b988d89c0 | |||
| c0908f1b58 | |||
| bb46044f7c | |||
| 60b3dd6138 | |||
| 22c1f936c3 | |||
| e899874b04 | |||
| 04c6888cf6 | |||
| 29c56c6919 | |||
| c98ed9cc85 | |||
| 88f7530c13 | |||
| bb820437af | |||
| d1a4bd38e0 | |||
| d18f6ccd32 | |||
| aa317c964e | |||
| 7ae8845ae9 | |||
| 123a6dbe0c | |||
| dda8d79526 | |||
| de5293955a | |||
| 96669bff5b | |||
| a99762f004 | |||
| 1fc3c9b83e | |||
| af86b6f664 | |||
| a0208dd5b3 | |||
| 1c5c8b81d5 | |||
| 79c3e33434 | |||
| 5ab8729a31 | |||
| db0baf92f1 | |||
| a8974094db | |||
| 13c5c97ba7 | |||
| 894ebb3edd | |||
| cac899a7e5 | |||
| 901e0dd93b | |||
| a360231fd0 | |||
| 6ccc6ee42f | |||
| 9c72ad85a0 | |||
| fa6d80a753 | |||
| f6ab2ca9ba | |||
| ba1e1ad5ad | |||
| 02705ea3ad | |||
| df6e3f06a6 | |||
| 1cb5ee9fd6 | |||
| 893d6176c3 | |||
| 2c1b512e40 | |||
| de50d76800 | |||
| 401b78e477 | |||
| 01837eaea9 | |||
| 7df699353c | |||
| 5455972be2 | |||
| 57488bcd16 | |||
| b6fda901ef | |||
| 14669df890 | |||
| 64525a65f2 | |||
| 61fc7efecb | |||
| 8c7c1e4745 | |||
| f64d30ec88 | |||
| 384dfc8da4 | |||
| 841784ae8c | |||
| f5683cfd94 | |||
| 0a420ce30e | |||
| a5e7132d3c | |||
| 6dfb2be4a2 | |||
| d81cbad550 | |||
| 51ad06b303 | |||
| 27048ad95c | |||
| fa9bf0b448 | |||
| 305190b64e | |||
| 98e3d18335 | |||
| 72e398956b | |||
| 1e69a13a97 | |||
| 7f9e6e2f44 | |||
| d3c6c0d48e | |||
| 6c83476fd2 | |||
| 9cdf7e2c51 | |||
| 599119e14e | |||
| a2c484e0f3 | |||
| 97ff4d5ac2 | |||
| 5255cfbb25 | |||
| 780358368b | |||
| cf0b918913 | |||
| 4ea690204e | |||
| 14eb775749 | |||
| bf4a3be6d9 | |||
| b9e1e47871 | |||
| ff532ecb22 | |||
| eb21872db1 | |||
| 8e3df184dc | |||
| b34bbcac8b | |||
| 226965a1d7 | |||
| 17b2ea126c | |||
| 59fc4a089c | |||
| ef26dc8cbb | |||
| 8a0fbb28b0 | |||
| 618d8d5b1a | |||
| 3a936dc9c1 | |||
| 031fcd75ee | |||
| c8cd23cb32 | |||
| a3049938a3 | |||
| b23488e862 | |||
| c8203cf04c | |||
| 4f2616ef0a | |||
| 04ffc69e00 | |||
| 5b4bb6e7a7 | |||
| 2e953ba998 | |||
| ed5a4eb960 | |||
| 31b2c05084 | |||
| 6e1662038c | |||
| df78d88f79 | |||
| addedb3091 | |||
| 2ea17b2dea | |||
| 85d2f42f47 | |||
| cdb655c1bd | |||
| 3064d03aa9 | |||
| 6af6417ca2 | |||
| a35271fbb3 | |||
| 63aef71f60 | |||
| 0f8457534c | |||
| 2984582e62 | |||
| 147d8fd9af | |||
| c12d354900 | |||
| 8bcb552783 | |||
| f504a8288c | |||
| 79f5b54228 | |||
| 669a9855e6 | |||
| e3b96c3a1f | |||
| 7e0d4bef20 | |||
| 0bd0f48df3 | |||
| ae56126585 | |||
| 6dcf0ac124 | |||
| 63e2ce43c2 | |||
| f27aa21eb5 | |||
| 42926c663b | |||
| d38116183c | |||
| 9ca373e208 | |||
| 4084f2f5ec | |||
| 684ffbadbc | |||
| 1c4d4c57de | |||
| d83111a199 | |||
| f10fa839fb | |||
| 1f20395ff3 | |||
| bfe6f4255a | |||
| b9032f3012 | |||
| ce53c459e6 | |||
| 6df42fb0d5 | |||
| 1e2b496f4f | |||
| 528c1b0d57 | |||
| 22fba8ba80 | |||
| 6f26261ebe | |||
| 4b1fe3ba91 | |||
| 9f95262bd4 | |||
| 2ec28e541e | |||
| 4764d4b19a | |||
| 9b27547dae | |||
| a96f2c9153 | |||
| 225e0aaf88 | |||
| f0c85910bc | |||
| 124c443528 | |||
| 37c7f8d39f | |||
| c419f1ce22 | |||
| 4949cdfe2f | |||
| cd101b96ea | |||
| 40d0e4631f | |||
| a55a995660 | |||
| 1d576d48ad | |||
| 4ff8030f87 | |||
| c15e2e2c8f | |||
| 41d31a8d75 | |||
| 10e2a26352 | |||
| 6e547f77d0 | |||
| 530a426601 | |||
| 642d6bab77 | |||
| 7980af3d82 | |||
| 2980c42e88 | |||
| b03994f9db | |||
| 41906ec2f9 | |||
| 4d1e7ff2a5 | |||
| 7f18fcfc13 | |||
| ff02ce0747 | |||
| a6056600eb | |||
| 24c94326d5 | |||
| 48c0cb5e62 | |||
| 9968efe1b2 | |||
| 6368342149 | |||
| b5f73db210 | |||
| 22d6a02d04 | |||
| 4e0bcfcccf | |||
| 104313c15d | |||
| 814289af46 | |||
| 3096cd98d5 | |||
| 45dfefbc7a | |||
| 20d92149a8 | |||
| 9c01fffca5 | |||
| 61c1cfff4b | |||
| 7e74261f62 | |||
| 32601f34f1 | |||
| aef69a1a96 | |||
| 130f90bdb6 | |||
| 851c7de87f | |||
| 118156c62d | |||
| cdad3143c9 | |||
| 00fe32136e | |||
| 52f56eeb68 | |||
| b595264e31 | |||
| bf04262170 | |||
| fd589922bb | |||
| fb7029f5c1 | |||
| cc4b4f9bf8 | |||
| b048d6d7e3 | |||
| cffc1f442f | |||
| 91fe710741 | |||
| 5a1eb9fdac | |||
| a64ef6e750 | |||
| 47d2012b3d | |||
| 2c99cd2e21 | |||
| 435cd2f66a | |||
| 372b12dfc2 | |||
| 3a12f5c1c4 | |||
| 781de224b6 | |||
| eff09454c3 | |||
| 473feba6a8 | |||
| e9f0704fb0 | |||
| 8c99469d88 | |||
| 8ec1311dfc | |||
| e48cca6b45 | |||
| d660721f95 | |||
| c52bb93d79 | |||
| ffb6bfd0c9 | |||
| 6e0855f9b3 | |||
| ec9e9ec387 | |||
| fa75aa40f0 | |||
| b865953c22 | |||
| 2542cf6f9a | |||
| cb9499b87f | |||
| c351f01f67 | |||
| c87561fca7 | |||
| a363c143fa | |||
| fb81d2b926 | |||
| 05b1b8f079 | |||
| 7848dc06d4 | |||
| 3a005719b7 | |||
| 6928988f78 | |||
| a521894d7c | |||
| b7fb1d9c08 | |||
| 5f67e45911 | |||
| fddf9fccbd | |||
| 2bedbd7bd2 | |||
| 02f3c85796 | |||
| 3b5bd6a47f | |||
| 06fd404d44 | |||
| d931c52508 | |||
| 800fe9e4c8 | |||
| 8b3d78678f | |||
| 2351cd3ad6 | |||
| dd86c5bcdf | |||
| d0c00e306e | |||
| 6cd68ddd6a | |||
| 02639cddb2 | |||
| 0f67aa4058 | |||
| 78c3403d5f | |||
| fc8f8c574d | |||
| 3972ee580d | |||
| b0a994d5be | |||
| 80df28a0c4 | |||
| 1c6c6a9296 | |||
| 7bb698e442 | |||
| 784728cd54 | |||
| 4b883b68de | |||
| dec2cc2aaf | |||
| b4e8971ad2 | |||
| 84e436265e | |||
| ac8a69f8d4 | |||
| b912e050ea | |||
| 284561d093 | |||
| efc355982c | |||
| 8907879a19 | |||
| 86f6057207 | |||
| 4c92698c73 | |||
| 8407c3b649 | |||
| ed8f82617d | |||
| 31d040d1b6 | |||
| 25a6796e63 | |||
| ff091918aa | |||
| 91b76edce1 | |||
| 5ae5c83bf5 | |||
| 9d90f276de | |||
| 245f091e22 | |||
| ae2f8f086b | |||
| 5e5eb7f937 | |||
| 748630e1f1 | |||
| 43064c8c55 | |||
| 4559a3047c |
@@ -11,19 +11,18 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call_server_application_workflow:
|
call_server_utility_workflow:
|
||||||
name: Server Application
|
name: Server Utility
|
||||||
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
|
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
|
||||||
with:
|
with:
|
||||||
service_name: analytics
|
service_name: analytics
|
||||||
workspace_name: "@standardnotes/analytics"
|
workspace_name: "@standardnotes/analytics"
|
||||||
e2e_tag_parameter_name: analytics_image_tag
|
|
||||||
deploy_web: false
|
deploy_web: false
|
||||||
package_path: packages/analytics
|
package_path: packages/analytics
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
newrelic:
|
newrelic:
|
||||||
needs: call_server_application_workflow
|
needs: call_server_utility_workflow
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -187,12 +187,12 @@ jobs:
|
|||||||
tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
|
tags: standardnotes/${{ inputs.service_name }}:${{ github.sha }}
|
||||||
|
|
||||||
- name: Run E2E test suite
|
- name: Run E2E test suite
|
||||||
uses: convictional/trigger-workflow-and-wait@v1.6.3
|
uses: convictional/trigger-workflow-and-wait@master
|
||||||
with:
|
with:
|
||||||
owner: standardnotes
|
owner: standardnotes
|
||||||
repo: e2e
|
repo: self-hosted
|
||||||
github_token: ${{ secrets.CI_PAT_TOKEN }}
|
github_token: ${{ secrets.CI_PAT_TOKEN }}
|
||||||
workflow_file_name: testing-with-stable-client.yml
|
workflow_file_name: testing-with-updating-client-and-server.yml
|
||||||
wait_interval: 30
|
wait_interval: 30
|
||||||
client_payload: '{"${{ inputs.e2e_tag_parameter_name }}": "${{ github.sha }}"}'
|
client_payload: '{"${{ inputs.e2e_tag_parameter_name }}": "${{ github.sha }}"}'
|
||||||
propagate_failure: true
|
propagate_failure: true
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
name: Reusable Server Utility Workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
service_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
workspace_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
deploy_web:
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
deploy_worker:
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
package_path:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
DOCKER_USERNAME:
|
||||||
|
required: true
|
||||||
|
DOCKER_PASSWORD:
|
||||||
|
required: true
|
||||||
|
CI_PAT_TOKEN:
|
||||||
|
required: true
|
||||||
|
AWS_ACCESS_KEY_ID:
|
||||||
|
required: true
|
||||||
|
AWS_SECRET_ACCESS_KEY:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
temp_dir: ${{ steps.bundle-dir.outputs.temp_dir }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Create Bundle Dir
|
||||||
|
id: bundle-dir
|
||||||
|
run: echo "temp_dir=$(mktemp -d -t ${{ inputs.service_name }}-${{ github.sha }}-XXXXXXX)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache build
|
||||||
|
id: cache-build
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
packages/**/dist
|
||||||
|
${{ steps.bundle-dir.outputs.temp_dir }}
|
||||||
|
key: ${{ runner.os }}-${{ inputs.service_name }}-build-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: yarn build ${{ inputs.package_path }}
|
||||||
|
|
||||||
|
- name: Bundle
|
||||||
|
run: yarn workspace ${{ inputs.workspace_name }} bundle --no-compress --output-directory ${{ steps.bundle-dir.outputs.temp_dir }}
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Cache build
|
||||||
|
id: cache-build
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
packages/**/dist
|
||||||
|
${{ needs.build.outputs.temp_dir }}
|
||||||
|
key: ${{ runner.os }}-${{ inputs.service_name }}-build-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
run: yarn build ${{ inputs.package_path }}
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: yarn lint:${{ inputs.service_name }}
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Cache build
|
||||||
|
id: cache-build
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
packages/**/dist
|
||||||
|
${{ needs.build.outputs.temp_dir }}
|
||||||
|
key: ${{ runner.os }}-${{ inputs.service_name }}-build-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
run: yarn build ${{ inputs.package_path }}
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: yarn test ${{ inputs.package_path }}
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [ build, test, lint ]
|
||||||
|
|
||||||
|
name: Publish Docker Image
|
||||||
|
uses: standardnotes/server/.github/workflows/common-docker-image.yml@main
|
||||||
|
with:
|
||||||
|
service_name: ${{ inputs.service_name }}
|
||||||
|
bundle_dir: ${{ needs.build.outputs.temp_dir }}
|
||||||
|
package_path: ${{ inputs.package_path }}
|
||||||
|
workspace_name: ${{ inputs.workspace_name }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
deploy-web:
|
||||||
|
if: ${{ inputs.deploy_web }}
|
||||||
|
|
||||||
|
needs: publish
|
||||||
|
|
||||||
|
name: Deploy Web
|
||||||
|
uses: standardnotes/server/.github/workflows/common-deploy.yml@main
|
||||||
|
with:
|
||||||
|
service_name: ${{ inputs.service_name }}
|
||||||
|
docker_image: ${{ inputs.service_name }}:${{ github.sha }}
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
deploy-worker:
|
||||||
|
if: ${{ inputs.deploy_worker }}
|
||||||
|
|
||||||
|
needs: publish
|
||||||
|
|
||||||
|
name: Deploy Worker
|
||||||
|
uses: standardnotes/server/.github/workflows/common-deploy.yml@main
|
||||||
|
with:
|
||||||
|
service_name: ${{ inputs.service_name }}-worker
|
||||||
|
docker_image: ${{ inputs.service_name }}:${{ github.sha }}
|
||||||
|
secrets: inherit
|
||||||
@@ -11,19 +11,18 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call_server_application_workflow:
|
call_server_utility_workflow:
|
||||||
name: Server Application
|
name: Server Utility
|
||||||
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
|
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
|
||||||
with:
|
with:
|
||||||
service_name: event-store
|
service_name: event-store
|
||||||
workspace_name: "@standardnotes/event-store"
|
workspace_name: "@standardnotes/event-store"
|
||||||
e2e_tag_parameter_name: event_store_image_tag
|
|
||||||
deploy_web: false
|
deploy_web: false
|
||||||
package_path: packages/event-store
|
package_path: packages/event-store
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
newrelic:
|
newrelic:
|
||||||
needs: call_server_application_workflow
|
needs: call_server_utility_workflow
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -11,19 +11,18 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call_server_application_workflow:
|
call_server_utility_workflow:
|
||||||
name: Server Application
|
name: Server Utility
|
||||||
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
|
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
|
||||||
with:
|
with:
|
||||||
service_name: scheduler
|
service_name: scheduler
|
||||||
workspace_name: "@standardnotes/scheduler-server"
|
workspace_name: "@standardnotes/scheduler-server"
|
||||||
e2e_tag_parameter_name: scheduler_image_tag
|
|
||||||
deploy_web: false
|
deploy_web: false
|
||||||
package_path: packages/scheduler
|
package_path: packages/scheduler
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
newrelic:
|
newrelic:
|
||||||
needs: call_server_application_workflow
|
needs: call_server_utility_workflow
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
|||||||
@@ -11,18 +11,17 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call_server_application_workflow:
|
call_server_utility_workflow:
|
||||||
name: Server Application
|
name: Server Utility
|
||||||
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
|
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
|
||||||
with:
|
with:
|
||||||
service_name: websockets
|
service_name: websockets
|
||||||
workspace_name: "@standardnotes/websockets-server"
|
workspace_name: "@standardnotes/websockets-server"
|
||||||
e2e_tag_parameter_name: websockets_image_tag
|
|
||||||
package_path: packages/websockets
|
package_path: packages/websockets
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
newrelic:
|
newrelic:
|
||||||
needs: call_server_application_workflow
|
needs: call_server_utility_workflow
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -11,18 +11,17 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
call_server_application_workflow:
|
call_server_utility_workflow:
|
||||||
name: Server Application
|
name: Server Utility
|
||||||
uses: standardnotes/server/.github/workflows/common-server-application.yml@main
|
uses: standardnotes/server/.github/workflows/common-server-utility.yml@main
|
||||||
with:
|
with:
|
||||||
service_name: workspace
|
service_name: workspace
|
||||||
workspace_name: "@standardnotes/workspace-server"
|
workspace_name: "@standardnotes/workspace-server"
|
||||||
e2e_tag_parameter_name: workspace_image_tag
|
|
||||||
package_path: packages/workspace
|
package_path: packages/workspace
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
newrelic:
|
newrelic:
|
||||||
needs: call_server_application_workflow
|
needs: call_server_utility_workflow
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -126,8 +126,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@lerna-lite/cli", "npm:1.6.0"],\
|
["@lerna-lite/cli", "npm:1.6.0"],\
|
||||||
["@lerna-lite/list", "npm:1.6.0"],\
|
["@lerna-lite/list", "npm:1.6.0"],\
|
||||||
["@lerna-lite/run", "npm:1.6.0"],\
|
["@lerna-lite/run", "npm:1.6.0"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/node", "npm:18.11.9"],\
|
["@types/node", "npm:18.11.9"],\
|
||||||
@@ -1969,6 +1968,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["@noble/ed25519", [\
|
||||||
|
["npm:1.7.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@noble-ed25519-npm-1.7.1-177d9beb01-b1aa4b9264.zip/node_modules/@noble/ed25519/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@noble/ed25519", "npm:1.7.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["@nodelib/fs.scandir", [\
|
["@nodelib/fs.scandir", [\
|
||||||
["npm:2.1.5", {\
|
["npm:2.1.5", {\
|
||||||
"packageLocation": "./.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-5f309a3b37.zip/node_modules/@nodelib/fs.scandir/",\
|
"packageLocation": "./.yarn/cache/@nodelib-fs.scandir-npm-2.1.5-89c67370dd-5f309a3b37.zip/node_modules/@nodelib/fs.scandir/",\
|
||||||
@@ -2325,6 +2333,44 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["@peculiar/asn1-android", [\
|
||||||
|
["npm:2.3.3", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@peculiar-asn1-android-npm-2.3.3-28df67d7a3-0c7cad544e.zip/node_modules/@peculiar/asn1-android/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@peculiar/asn1-android", "npm:2.3.3"],\
|
||||||
|
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||||
|
["asn1js", "npm:3.0.5"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["@peculiar/asn1-schema", [\
|
||||||
|
["npm:2.3.3", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@peculiar-asn1-schema-npm-2.3.3-7c2b9469c4-f584f79d5a.zip/node_modules/@peculiar/asn1-schema/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||||
|
["asn1js", "npm:3.0.5"],\
|
||||||
|
["pvtsutils", "npm:1.3.2"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["@peculiar/asn1-x509", [\
|
||||||
|
["npm:2.3.4", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@peculiar-asn1-x509-npm-2.3.4-a579005836-10a8659980.zip/node_modules/@peculiar/asn1-x509/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@peculiar/asn1-x509", "npm:2.3.4"],\
|
||||||
|
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||||
|
["asn1js", "npm:3.0.5"],\
|
||||||
|
["ipaddr.js", "npm:2.0.1"],\
|
||||||
|
["pvtsutils", "npm:1.3.2"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["@pnpm/network.ca-file", [\
|
["@pnpm/network.ca-file", [\
|
||||||
["npm:1.0.1", {\
|
["npm:1.0.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@pnpm-network.ca-file-npm-1.0.1-42bfe40bec-ed952a5574.zip/node_modules/@pnpm/network.ca-file/",\
|
"packageLocation": "./.yarn/cache/@pnpm-network.ca-file-npm-1.0.1-42bfe40bec-ed952a5574.zip/node_modules/@pnpm/network.ca-file/",\
|
||||||
@@ -2439,25 +2485,25 @@ const RAW_RUNTIME_STATE =
|
|||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@sentry/core", [\
|
["@sentry/core", [\
|
||||||
["npm:7.19.0", {\
|
["npm:7.28.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.19.0-151e6173ac-cabd7852ff.zip/node_modules/@sentry/core/",\
|
"packageLocation": "./.yarn/cache/@sentry-core-npm-7.28.1-a468033ea8-f29d747d3e.zip/node_modules/@sentry/core/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@sentry/core", "npm:7.19.0"],\
|
["@sentry/core", "npm:7.28.1"],\
|
||||||
["@sentry/types", "npm:7.19.0"],\
|
["@sentry/types", "npm:7.28.1"],\
|
||||||
["@sentry/utils", "npm:7.19.0"],\
|
["@sentry/utils", "npm:7.28.1"],\
|
||||||
["tslib", "npm:1.14.1"]\
|
["tslib", "npm:1.14.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@sentry/node", [\
|
["@sentry/node", [\
|
||||||
["npm:7.19.0", {\
|
["npm:7.28.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.19.0-fd3d8dbde1-3a69647d2e.zip/node_modules/@sentry/node/",\
|
"packageLocation": "./.yarn/cache/@sentry-node-npm-7.28.1-b0e124fdfc-b4922d1f0a.zip/node_modules/@sentry/node/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@sentry/core", "npm:7.19.0"],\
|
["@sentry/core", "npm:7.28.1"],\
|
||||||
["@sentry/types", "npm:7.19.0"],\
|
["@sentry/types", "npm:7.28.1"],\
|
||||||
["@sentry/utils", "npm:7.19.0"],\
|
["@sentry/utils", "npm:7.28.1"],\
|
||||||
["cookie", "npm:0.4.2"],\
|
["cookie", "npm:0.4.2"],\
|
||||||
["https-proxy-agent", "npm:5.0.1"],\
|
["https-proxy-agent", "npm:5.0.1"],\
|
||||||
["lru_map", "npm:0.3.3"],\
|
["lru_map", "npm:0.3.3"],\
|
||||||
@@ -2466,26 +2512,67 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@sentry/types", [\
|
["@sentry/tracing", [\
|
||||||
["npm:7.19.0", {\
|
["npm:7.28.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.19.0-d6ed1960f2-541e1ef49a.zip/node_modules/@sentry/types/",\
|
"packageLocation": "./.yarn/cache/@sentry-tracing-npm-7.28.1-e15d453d8e-be501ca9d7.zip/node_modules/@sentry/tracing/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@sentry/types", "npm:7.19.0"]\
|
["@sentry/tracing", "npm:7.28.1"],\
|
||||||
|
["@sentry/core", "npm:7.28.1"],\
|
||||||
|
["@sentry/types", "npm:7.28.1"],\
|
||||||
|
["@sentry/utils", "npm:7.28.1"],\
|
||||||
|
["tslib", "npm:1.14.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["@sentry/types", [\
|
||||||
|
["npm:7.28.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@sentry-types-npm-7.28.1-42d9a8574c-7dc6639cb7.zip/node_modules/@sentry/types/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@sentry/types", "npm:7.28.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@sentry/utils", [\
|
["@sentry/utils", [\
|
||||||
["npm:7.19.0", {\
|
["npm:7.28.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.19.0-79844d4d90-50e4f391fe.zip/node_modules/@sentry/utils/",\
|
"packageLocation": "./.yarn/cache/@sentry-utils-npm-7.28.1-71eaeb767f-a4b5f73db0.zip/node_modules/@sentry/utils/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@sentry/utils", "npm:7.19.0"],\
|
["@sentry/utils", "npm:7.28.1"],\
|
||||||
["@sentry/types", "npm:7.19.0"],\
|
["@sentry/types", "npm:7.28.1"],\
|
||||||
["tslib", "npm:1.14.1"]\
|
["tslib", "npm:1.14.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["@simplewebauthn/server", [\
|
||||||
|
["npm:6.2.2", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@simplewebauthn-server-npm-6.2.2-ca870b05c2-5ffb9b1c15.zip/node_modules/@simplewebauthn/server/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@simplewebauthn/server", "npm:6.2.2"],\
|
||||||
|
["@noble/ed25519", "npm:1.7.1"],\
|
||||||
|
["@peculiar/asn1-android", "npm:2.3.3"],\
|
||||||
|
["@peculiar/asn1-schema", "npm:2.3.3"],\
|
||||||
|
["@peculiar/asn1-x509", "npm:2.3.4"],\
|
||||||
|
["base64url", "npm:3.0.1"],\
|
||||||
|
["cbor", "npm:5.2.0"],\
|
||||||
|
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
|
||||||
|
["jsrsasign", "npm:10.6.1"],\
|
||||||
|
["jwk-to-pem", "npm:2.0.5"],\
|
||||||
|
["node-fetch", "virtual:25a5f5382d53dbf298bf7a1191760bc2e0a523a619eeb0e667b99a8649e8ad183f9e2e0b45f6fb831b92f4078b61622aa567cf79565f6aa5af9597e3c84864f6#npm:2.6.7"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["@simplewebauthn/typescript-types", [\
|
||||||
|
["npm:6.3.0-alpha.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@simplewebauthn-typescript-types-npm-6.3.0-alpha.1-629da05c10-5667c214e9.zip/node_modules/@simplewebauthn/typescript-types/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@simplewebauthn/typescript-types", "npm:6.3.0-alpha.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["@sinclair/typebox", [\
|
["@sinclair/typebox", [\
|
||||||
["npm:0.24.44", {\
|
["npm:0.24.44", {\
|
||||||
"packageLocation": "./.yarn/cache/@sinclair-typebox-npm-0.24.44-38506ddef6-f37b9d28bf.zip/node_modules/@sinclair/typebox/",\
|
"packageLocation": "./.yarn/cache/@sinclair-typebox-npm-0.24.44-38506ddef6-f37b9d28bf.zip/node_modules/@sinclair/typebox/",\
|
||||||
@@ -2538,9 +2625,8 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/analytics/",\
|
"packageLocation": "./packages/analytics/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/analytics", "workspace:packages/analytics"],\
|
["@standardnotes/analytics", "workspace:packages/analytics"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
@@ -2548,6 +2634,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@standardnotes/time", "workspace:packages/time"],\
|
["@standardnotes/time", "workspace:packages/time"],\
|
||||||
["@types/ioredis", "npm:5.0.0"],\
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
|
["@types/mixpanel", "npm:2.14.4"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/node", "npm:18.11.9"],\
|
["@types/node", "npm:18.11.9"],\
|
||||||
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.30.5"],\
|
["@typescript-eslint/eslint-plugin", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:5.30.5"],\
|
||||||
@@ -2559,6 +2646,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["inversify", "npm:6.0.1"],\
|
["inversify", "npm:6.0.1"],\
|
||||||
["ioredis", "npm:5.2.4"],\
|
["ioredis", "npm:5.2.4"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
|
["mixpanel", "npm:0.17.0"],\
|
||||||
["mysql2", "npm:2.3.3"],\
|
["mysql2", "npm:2.3.3"],\
|
||||||
["newrelic", "npm:9.6.0"],\
|
["newrelic", "npm:9.6.0"],\
|
||||||
["reflect-metadata", "npm:0.1.13"],\
|
["reflect-metadata", "npm:0.1.13"],\
|
||||||
@@ -2591,9 +2679,8 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/api-gateway/",\
|
"packageLocation": "./packages/api-gateway/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
|
["@standardnotes/api-gateway", "workspace:packages/api-gateway"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||||
@@ -2603,7 +2690,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@types/express", "npm:4.17.14"],\
|
["@types/express", "npm:4.17.14"],\
|
||||||
["@types/ioredis", "npm:5.0.0"],\
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/prettyjson", "npm:0.0.30"],\
|
["@types/prettyjson", "npm:0.0.30"],\
|
||||||
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
|
["@typescript-eslint/eslint-plugin", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:5.40.1"],\
|
||||||
@@ -2614,12 +2701,13 @@ const RAW_RUNTIME_STATE =
|
|||||||
["eslint", "npm:8.25.0"],\
|
["eslint", "npm:8.25.0"],\
|
||||||
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
|
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
|
||||||
["express", "npm:4.18.2"],\
|
["express", "npm:4.18.2"],\
|
||||||
|
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
|
||||||
["helmet", "npm:6.0.0"],\
|
["helmet", "npm:6.0.0"],\
|
||||||
["inversify", "npm:6.0.1"],\
|
["inversify", "npm:6.0.1"],\
|
||||||
["inversify-express-utils", "npm:6.4.3"],\
|
["inversify-express-utils", "npm:6.4.3"],\
|
||||||
["ioredis", "npm:5.2.4"],\
|
["ioredis", "npm:5.2.4"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
["jsonwebtoken", "npm:8.5.1"],\
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
["newrelic", "npm:9.6.0"],\
|
["newrelic", "npm:9.6.0"],\
|
||||||
["nodemon", "npm:2.0.20"],\
|
["nodemon", "npm:2.0.20"],\
|
||||||
["npm-check-updates", "npm:16.0.1"],\
|
["npm-check-updates", "npm:16.0.1"],\
|
||||||
@@ -2648,11 +2736,14 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/auth/",\
|
"packageLocation": "./packages/auth/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/auth-server", "workspace:packages/auth"],\
|
["@standardnotes/auth-server", "workspace:packages/auth"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
|
["@sentry/tracing", "npm:7.28.1"],\
|
||||||
|
["@simplewebauthn/server", "npm:6.2.2"],\
|
||||||
|
["@simplewebauthn/typescript-types", "npm:6.3.0-alpha.1"],\
|
||||||
["@standardnotes/api", "npm:1.19.0"],\
|
["@standardnotes/api", "npm:1.19.0"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||||
["@standardnotes/features", "npm:1.53.1"],\
|
["@standardnotes/features", "npm:1.53.1"],\
|
||||||
@@ -2740,10 +2831,6 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/domain-core/",\
|
"packageLocation": "./packages/domain-core/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
|
||||||
["@standardnotes/features", "npm:1.53.1"],\
|
|
||||||
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
|
||||||
["@standardnotes/security", "workspace:packages/security"],\
|
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/uuid", "npm:8.3.4"],\
|
["@types/uuid", "npm:8.3.4"],\
|
||||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
||||||
@@ -2783,7 +2870,6 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/domain-events-infra/",\
|
"packageLocation": "./packages/domain-events-infra/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
["@types/ioredis", "npm:5.0.0"],\
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
@@ -2823,7 +2909,6 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/event-store/",\
|
"packageLocation": "./packages/event-store/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/event-store", "workspace:packages/event-store"],\
|
["@standardnotes/event-store", "workspace:packages/event-store"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||||
["@standardnotes/time", "workspace:packages/time"],\
|
["@standardnotes/time", "workspace:packages/time"],\
|
||||||
@@ -2879,8 +2964,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/files/",\
|
"packageLocation": "./packages/files/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/files-server", "workspace:packages/files"],\
|
["@standardnotes/files-server", "workspace:packages/files"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/config", "npm:2.4.3"],\
|
["@standardnotes/config", "npm:2.4.3"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
@@ -2894,7 +2978,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@types/express", "npm:4.17.14"],\
|
["@types/express", "npm:4.17.14"],\
|
||||||
["@types/ioredis", "npm:5.0.0"],\
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/prettyjson", "npm:0.0.30"],\
|
["@types/prettyjson", "npm:0.0.30"],\
|
||||||
["@types/uuid", "npm:8.3.4"],\
|
["@types/uuid", "npm:8.3.4"],\
|
||||||
@@ -2907,13 +2991,14 @@ const RAW_RUNTIME_STATE =
|
|||||||
["eslint", "npm:8.25.0"],\
|
["eslint", "npm:8.25.0"],\
|
||||||
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
|
["eslint-plugin-prettier", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.2.1"],\
|
||||||
["express", "npm:4.18.2"],\
|
["express", "npm:4.18.2"],\
|
||||||
|
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
|
||||||
["express-winston", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0"],\
|
["express-winston", "virtual:b442cf0427cc365d1c137f7340f9b81f9b204561afe791a8564ae9590c3a7fc4b5f793aaf8817b946f75a3cb64d03ef8790eb847f8b576b41e700da7b00c240c#npm:4.2.0"],\
|
||||||
["helmet", "npm:6.0.0"],\
|
["helmet", "npm:6.0.0"],\
|
||||||
["inversify", "npm:6.0.1"],\
|
["inversify", "npm:6.0.1"],\
|
||||||
["inversify-express-utils", "npm:6.4.3"],\
|
["inversify-express-utils", "npm:6.4.3"],\
|
||||||
["ioredis", "npm:5.2.4"],\
|
["ioredis", "npm:5.2.4"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
["jsonwebtoken", "npm:8.5.1"],\
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
["newrelic", "npm:9.6.0"],\
|
["newrelic", "npm:9.6.0"],\
|
||||||
["nodemon", "npm:2.0.20"],\
|
["nodemon", "npm:2.0.20"],\
|
||||||
["npm-check-updates", "npm:16.0.1"],\
|
["npm-check-updates", "npm:16.0.1"],\
|
||||||
@@ -3014,9 +3099,8 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/revisions/",\
|
"packageLocation": "./packages/revisions/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
|
["@standardnotes/revisions-server", "workspace:packages/revisions"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/api", "npm:1.19.0"],\
|
["@standardnotes/api", "npm:1.19.0"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
@@ -3060,10 +3144,10 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/scheduler/",\
|
"packageLocation": "./packages/scheduler/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
|
["@standardnotes/scheduler-server", "workspace:packages/scheduler"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||||
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
["@standardnotes/predicates", "workspace:packages/predicates"],\
|
||||||
@@ -3100,11 +3184,11 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@standardnotes/security", "workspace:packages/security"],\
|
["@standardnotes/security", "workspace:packages/security"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
||||||
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
|
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
["jsonwebtoken", "npm:8.5.1"],\
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
["reflect-metadata", "npm:0.1.13"],\
|
["reflect-metadata", "npm:0.1.13"],\
|
||||||
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
|
["ts-jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.0.3"],\
|
||||||
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"]\
|
["typescript", "patch:typescript@npm%3A4.8.4#optional!builtin<compat/typescript>::version=4.8.4&hash=701156"]\
|
||||||
@@ -3122,8 +3206,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@lerna-lite/cli", "npm:1.6.0"],\
|
["@lerna-lite/cli", "npm:1.6.0"],\
|
||||||
["@lerna-lite/list", "npm:1.6.0"],\
|
["@lerna-lite/list", "npm:1.6.0"],\
|
||||||
["@lerna-lite/run", "npm:1.6.0"],\
|
["@lerna-lite/run", "npm:1.6.0"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/node", "npm:18.11.9"],\
|
["@types/node", "npm:18.11.9"],\
|
||||||
@@ -3145,7 +3228,6 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/settings/",\
|
"packageLocation": "./packages/settings/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/settings", "workspace:packages/settings"],\
|
["@standardnotes/settings", "workspace:packages/settings"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
["@typescript-eslint/eslint-plugin", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:5.30.5"],\
|
||||||
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
|
["eslint-plugin-prettier", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:4.2.1"],\
|
||||||
["reflect-metadata", "npm:0.1.13"],\
|
["reflect-metadata", "npm:0.1.13"],\
|
||||||
@@ -3189,9 +3271,9 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/syncing-server/",\
|
"packageLocation": "./packages/syncing-server/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
|
["@standardnotes/syncing-server", "workspace:packages/syncing-server"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
|
["@sentry/tracing", "npm:7.28.1"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
@@ -3207,7 +3289,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["@types/inversify-express-utils", "npm:2.0.0"],\
|
["@types/inversify-express-utils", "npm:2.0.0"],\
|
||||||
["@types/ioredis", "npm:5.0.0"],\
|
["@types/ioredis", "npm:5.0.0"],\
|
||||||
["@types/jest", "npm:29.1.1"],\
|
["@types/jest", "npm:29.1.1"],\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@types/newrelic", "npm:7.0.4"],\
|
["@types/newrelic", "npm:7.0.4"],\
|
||||||
["@types/prettyjson", "npm:0.0.30"],\
|
["@types/prettyjson", "npm:0.0.30"],\
|
||||||
["@types/ua-parser-js", "npm:0.7.36"],\
|
["@types/ua-parser-js", "npm:0.7.36"],\
|
||||||
@@ -3225,7 +3307,7 @@ const RAW_RUNTIME_STATE =
|
|||||||
["inversify-express-utils", "npm:6.4.3"],\
|
["inversify-express-utils", "npm:6.4.3"],\
|
||||||
["ioredis", "npm:5.2.4"],\
|
["ioredis", "npm:5.2.4"],\
|
||||||
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
["jest", "virtual:fd909b174d079e30b336c4ce72c38a88c1e447767b1a8dd7655e07719a1e31b97807f0931368724fc78897ff15e6a6d00b83316c0f76d11f85111f342e08bb79#npm:29.1.2"],\
|
||||||
["jsonwebtoken", "npm:8.5.1"],\
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
["mysql2", "npm:2.3.3"],\
|
["mysql2", "npm:2.3.3"],\
|
||||||
["newrelic", "npm:9.6.0"],\
|
["newrelic", "npm:9.6.0"],\
|
||||||
["nodemon", "npm:2.0.20"],\
|
["nodemon", "npm:2.0.20"],\
|
||||||
@@ -3290,9 +3372,8 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/websockets/",\
|
"packageLocation": "./packages/websockets/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
|
["@standardnotes/websockets-server", "workspace:packages/websockets"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/api", "npm:1.19.0"],\
|
["@standardnotes/api", "npm:1.19.0"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
@@ -3331,11 +3412,11 @@ const RAW_RUNTIME_STATE =
|
|||||||
"packageLocation": "./packages/workspace/",\
|
"packageLocation": "./packages/workspace/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
|
["@standardnotes/workspace-server", "workspace:packages/workspace"],\
|
||||||
["@newrelic/native-metrics", "npm:9.0.0"],\
|
|
||||||
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
|
||||||
["@sentry/node", "npm:7.19.0"],\
|
["@sentry/node", "npm:7.28.1"],\
|
||||||
["@standardnotes/api", "npm:1.19.0"],\
|
["@standardnotes/api", "npm:1.19.0"],\
|
||||||
["@standardnotes/common", "workspace:packages/common"],\
|
["@standardnotes/common", "workspace:packages/common"],\
|
||||||
|
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
|
||||||
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
|
||||||
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
|
||||||
["@standardnotes/models", "npm:1.28.0"],\
|
["@standardnotes/models", "npm:1.28.0"],\
|
||||||
@@ -3675,10 +3756,10 @@ const RAW_RUNTIME_STATE =
|
|||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["@types/jsonwebtoken", [\
|
["@types/jsonwebtoken", [\
|
||||||
["npm:8.5.9", {\
|
["npm:9.0.1", {\
|
||||||
"packageLocation": "./.yarn/cache/@types-jsonwebtoken-npm-8.5.9-79c2843a81-3f15a76cd5.zip/node_modules/@types/jsonwebtoken/",\
|
"packageLocation": "./.yarn/cache/@types-jsonwebtoken-npm-9.0.1-5f660fdf38-44d3fccc6b.zip/node_modules/@types/jsonwebtoken/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
["@types/jsonwebtoken", "npm:8.5.9"],\
|
["@types/jsonwebtoken", "npm:9.0.1"],\
|
||||||
["@types/node", "npm:18.0.3"]\
|
["@types/node", "npm:18.0.3"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
@@ -3739,6 +3820,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["@types/mixpanel", [\
|
||||||
|
["npm:2.14.4", {\
|
||||||
|
"packageLocation": "./.yarn/cache/@types-mixpanel-npm-2.14.4-34bd98306f-a2bf6e633e.zip/node_modules/@types/mixpanel/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["@types/mixpanel", "npm:2.14.4"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["@types/newrelic", [\
|
["@types/newrelic", [\
|
||||||
["npm:7.0.4", {\
|
["npm:7.0.4", {\
|
||||||
"packageLocation": "./.yarn/cache/@types-newrelic-npm-7.0.4-4f0b179b51-b44215b3ab.zip/node_modules/@types/newrelic/",\
|
"packageLocation": "./.yarn/cache/@types-newrelic-npm-7.0.4-4f0b179b51-b44215b3ab.zip/node_modules/@types/newrelic/",\
|
||||||
@@ -4786,6 +4876,31 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["asn1.js", [\
|
||||||
|
["npm:5.4.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/asn1.js-npm-5.4.1-37c7edbcb0-5c36f81388.zip/node_modules/asn1.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["asn1.js", "npm:5.4.1"],\
|
||||||
|
["bn.js", "npm:4.12.0"],\
|
||||||
|
["inherits", "npm:2.0.4"],\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"],\
|
||||||
|
["safer-buffer", "npm:2.1.2"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["asn1js", [\
|
||||||
|
["npm:3.0.5", {\
|
||||||
|
"packageLocation": "./.yarn/cache/asn1js-npm-3.0.5-cf5558af33-d0bc57da97.zip/node_modules/asn1js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["asn1js", "npm:3.0.5"],\
|
||||||
|
["pvtsutils", "npm:1.3.2"],\
|
||||||
|
["pvutils", "npm:1.1.3"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["async", [\
|
["async", [\
|
||||||
["npm:3.2.4", {\
|
["npm:3.2.4", {\
|
||||||
"packageLocation": "./.yarn/cache/async-npm-3.2.4-aba13508f9-9719e38d24.zip/node_modules/async/",\
|
"packageLocation": "./.yarn/cache/async-npm-3.2.4-aba13508f9-9719e38d24.zip/node_modules/async/",\
|
||||||
@@ -4999,6 +5114,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["base64url", [\
|
||||||
|
["npm:3.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/base64url-npm-3.0.1-4c171c4917-72e1401ffe.zip/node_modules/base64url/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["base64url", "npm:3.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["bcryptjs", [\
|
["bcryptjs", [\
|
||||||
["npm:2.4.3", {\
|
["npm:2.4.3", {\
|
||||||
"packageLocation": "./.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-bf6a43e9c4.zip/node_modules/bcryptjs/",\
|
"packageLocation": "./.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-bf6a43e9c4.zip/node_modules/bcryptjs/",\
|
||||||
@@ -5017,6 +5141,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["bignumber.js", [\
|
||||||
|
["npm:9.1.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-e44d008049.zip/node_modules/bignumber.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["bignumber.js", "npm:9.1.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["binary-extensions", [\
|
["binary-extensions", [\
|
||||||
["npm:2.2.0", {\
|
["npm:2.2.0", {\
|
||||||
"packageLocation": "./.yarn/cache/binary-extensions-npm-2.2.0-180c33fec7-16cf7c0cfd.zip/node_modules/binary-extensions/",\
|
"packageLocation": "./.yarn/cache/binary-extensions-npm-2.2.0-180c33fec7-16cf7c0cfd.zip/node_modules/binary-extensions/",\
|
||||||
@@ -5038,6 +5171,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["bn.js", [\
|
||||||
|
["npm:4.12.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/bn.js-npm-4.12.0-3ec6c884f6-bfb4590775.zip/node_modules/bn.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["bn.js", "npm:4.12.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["body-parser", [\
|
["body-parser", [\
|
||||||
["npm:1.20.1", {\
|
["npm:1.20.1", {\
|
||||||
"packageLocation": "./.yarn/cache/body-parser-npm-1.20.1-759fd14db9-33f202c9d5.zip/node_modules/body-parser/",\
|
"packageLocation": "./.yarn/cache/body-parser-npm-1.20.1-759fd14db9-33f202c9d5.zip/node_modules/body-parser/",\
|
||||||
@@ -5105,6 +5247,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["brorand", [\
|
||||||
|
["npm:1.1.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/brorand-npm-1.1.0-ea86634c4b-f736e127fb.zip/node_modules/brorand/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["brorand", "npm:1.1.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["browserslist", [\
|
["browserslist", [\
|
||||||
["npm:4.21.1", {\
|
["npm:4.21.1", {\
|
||||||
"packageLocation": "./.yarn/cache/browserslist-npm-4.21.1-930e90b93a-617d624493.zip/node_modules/browserslist/",\
|
"packageLocation": "./.yarn/cache/browserslist-npm-4.21.1-930e90b93a-617d624493.zip/node_modules/browserslist/",\
|
||||||
@@ -5340,6 +5491,17 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["cbor", [\
|
||||||
|
["npm:5.2.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/cbor-npm-5.2.0-4f6440587f-d60986b9d0.zip/node_modules/cbor/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["cbor", "npm:5.2.0"],\
|
||||||
|
["bignumber.js", "npm:9.1.1"],\
|
||||||
|
["nofilter", "npm:1.0.4"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["chalk", [\
|
["chalk", [\
|
||||||
["npm:2.4.2", {\
|
["npm:2.4.2", {\
|
||||||
"packageLocation": "./.yarn/cache/chalk-npm-2.4.2-3ea16dd91e-befd2fe888.zip/node_modules/chalk/",\
|
"packageLocation": "./.yarn/cache/chalk-npm-2.4.2-3ea16dd91e-befd2fe888.zip/node_modules/chalk/",\
|
||||||
@@ -6464,6 +6626,22 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["elliptic", [\
|
||||||
|
["npm:6.5.4", {\
|
||||||
|
"packageLocation": "./.yarn/cache/elliptic-npm-6.5.4-0ca8204a86-4453b008cf.zip/node_modules/elliptic/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["elliptic", "npm:6.5.4"],\
|
||||||
|
["bn.js", "npm:4.12.0"],\
|
||||||
|
["brorand", "npm:1.1.0"],\
|
||||||
|
["hash.js", "npm:1.1.7"],\
|
||||||
|
["hmac-drbg", "npm:1.0.1"],\
|
||||||
|
["inherits", "npm:2.0.4"],\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"],\
|
||||||
|
["minimalistic-crypto-utils", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["emittery", [\
|
["emittery", [\
|
||||||
["npm:0.10.2", {\
|
["npm:0.10.2", {\
|
||||||
"packageLocation": "./.yarn/cache/emittery-npm-0.10.2-aac10498b5-c55b286714.zip/node_modules/emittery/",\
|
"packageLocation": "./.yarn/cache/emittery-npm-0.10.2-aac10498b5-c55b286714.zip/node_modules/emittery/",\
|
||||||
@@ -7140,6 +7318,28 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["express-robots-txt", [\
|
||||||
|
["npm:1.0.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-54f066f6c3.zip/node_modules/express-robots-txt/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["express-robots-txt", "npm:1.0.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "SOFT"\
|
||||||
|
}],\
|
||||||
|
["virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0", {\
|
||||||
|
"packageLocation": "./.yarn/__virtual__/express-robots-txt-virtual-0a3eb9f2f5/0/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-54f066f6c3.zip/node_modules/express-robots-txt/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["express-robots-txt", "virtual:04783e12400851b8a3d76e71495851cc94959db6e62f04cb0a31190080629440b182d8c8eb4d7f2b04e281912f2783a5fd4d2c3c6ab68d38b7097246c93f4c19#npm:1.0.0"],\
|
||||||
|
["@types/express", "npm:4.17.14"],\
|
||||||
|
["express", "npm:4.18.2"]\
|
||||||
|
],\
|
||||||
|
"packagePeers": [\
|
||||||
|
"@types/express",\
|
||||||
|
"express"\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["express-winston", [\
|
["express-winston", [\
|
||||||
["npm:4.2.0", {\
|
["npm:4.2.0", {\
|
||||||
"packageLocation": "./.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-2d4b37671d.zip/node_modules/express-winston/",\
|
"packageLocation": "./.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-2d4b37671d.zip/node_modules/express-winston/",\
|
||||||
@@ -7970,6 +8170,17 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["hash.js", [\
|
||||||
|
["npm:1.1.7", {\
|
||||||
|
"packageLocation": "./.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e4266370d1.zip/node_modules/hash.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["hash.js", "npm:1.1.7"],\
|
||||||
|
["inherits", "npm:2.0.4"],\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["helmet", [\
|
["helmet", [\
|
||||||
["npm:6.0.0", {\
|
["npm:6.0.0", {\
|
||||||
"packageLocation": "./.yarn/cache/helmet-npm-6.0.0-2285459f57-73b6ba802d.zip/node_modules/helmet/",\
|
"packageLocation": "./.yarn/cache/helmet-npm-6.0.0-2285459f57-73b6ba802d.zip/node_modules/helmet/",\
|
||||||
@@ -7988,6 +8199,18 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["hmac-drbg", [\
|
||||||
|
["npm:1.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/hmac-drbg-npm-1.0.1-3499ad31cd-4e88d58ffc.zip/node_modules/hmac-drbg/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["hmac-drbg", "npm:1.0.1"],\
|
||||||
|
["hash.js", "npm:1.1.7"],\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"],\
|
||||||
|
["minimalistic-crypto-utils", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["hosted-git-info", [\
|
["hosted-git-info", [\
|
||||||
["npm:2.8.9", {\
|
["npm:2.8.9", {\
|
||||||
"packageLocation": "./.yarn/cache/hosted-git-info-npm-2.8.9-62c44fa93f-c24da52f98.zip/node_modules/hosted-git-info/",\
|
"packageLocation": "./.yarn/cache/hosted-git-info-npm-2.8.9-62c44fa93f-c24da52f98.zip/node_modules/hosted-git-info/",\
|
||||||
@@ -8078,6 +8301,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["https-proxy-agent", [\
|
["https-proxy-agent", [\
|
||||||
|
["npm:5.0.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.0-bb777903c3-77d11b0e2c.zip/node_modules/https-proxy-agent/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["https-proxy-agent", "npm:5.0.0"],\
|
||||||
|
["agent-base", "npm:6.0.2"],\
|
||||||
|
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
["npm:5.0.1", {\
|
["npm:5.0.1", {\
|
||||||
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.1-42d65f358e-8e767faec9.zip/node_modules/https-proxy-agent/",\
|
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-5.0.1-42d65f358e-8e767faec9.zip/node_modules/https-proxy-agent/",\
|
||||||
"packageDependencies": [\
|
"packageDependencies": [\
|
||||||
@@ -8368,6 +8600,13 @@ const RAW_RUNTIME_STATE =
|
|||||||
["ipaddr.js", "npm:1.9.1"]\
|
["ipaddr.js", "npm:1.9.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
|
["npm:2.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/ipaddr.js-npm-2.0.1-04e97280d7-04ce6c896c.zip/node_modules/ipaddr.js/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["ipaddr.js", "npm:2.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["is-arguments", [\
|
["is-arguments", [\
|
||||||
@@ -9568,6 +9807,26 @@ const RAW_RUNTIME_STATE =
|
|||||||
["semver", "npm:5.7.1"]\
|
["semver", "npm:5.7.1"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
|
["npm:9.0.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/jsonwebtoken-npm-9.0.0-36fd1594c0-7ccbd0b7bf.zip/node_modules/jsonwebtoken/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["jsonwebtoken", "npm:9.0.0"],\
|
||||||
|
["jws", "npm:3.2.2"],\
|
||||||
|
["lodash", "npm:4.17.21"],\
|
||||||
|
["ms", "npm:2.1.3"],\
|
||||||
|
["semver", "npm:7.3.8"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["jsrsasign", [\
|
||||||
|
["npm:10.6.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/jsrsasign-npm-10.6.1-a8fa295369-e8e9c1b24f.zip/node_modules/jsrsasign/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["jsrsasign", "npm:10.6.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["jwa", [\
|
["jwa", [\
|
||||||
@@ -9582,6 +9841,18 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["jwk-to-pem", [\
|
||||||
|
["npm:2.0.5", {\
|
||||||
|
"packageLocation": "./.yarn/cache/jwk-to-pem-npm-2.0.5-aff7d9f125-fced3a75b0.zip/node_modules/jwk-to-pem/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["jwk-to-pem", "npm:2.0.5"],\
|
||||||
|
["asn1.js", "npm:5.4.1"],\
|
||||||
|
["elliptic", "npm:6.5.4"],\
|
||||||
|
["safe-buffer", "npm:5.2.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["jws", [\
|
["jws", [\
|
||||||
["npm:3.2.2", {\
|
["npm:3.2.2", {\
|
||||||
"packageLocation": "./.yarn/cache/jws-npm-3.2.2-c1ae59c7af-347ed7c334.zip/node_modules/jws/",\
|
"packageLocation": "./.yarn/cache/jws-npm-3.2.2-c1ae59c7af-347ed7c334.zip/node_modules/jws/",\
|
||||||
@@ -10195,6 +10466,24 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["minimalistic-assert", [\
|
||||||
|
["npm:1.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/minimalistic-assert-npm-1.0.1-dc8bb23d29-e2310081d8.zip/node_modules/minimalistic-assert/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["minimalistic-assert", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["minimalistic-crypto-utils", [\
|
||||||
|
["npm:1.0.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/minimalistic-crypto-utils-npm-1.0.1-e66b10822e-7d909decd2.zip/node_modules/minimalistic-crypto-utils/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["minimalistic-crypto-utils", "npm:1.0.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["minimatch", [\
|
["minimatch", [\
|
||||||
["npm:3.1.2", {\
|
["npm:3.1.2", {\
|
||||||
"packageLocation": "./.yarn/cache/minimatch-npm-3.1.2-9405269906-97f5615ee8.zip/node_modules/minimatch/",\
|
"packageLocation": "./.yarn/cache/minimatch-npm-3.1.2-9405269906-97f5615ee8.zip/node_modules/minimatch/",\
|
||||||
@@ -10319,6 +10608,16 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["mixpanel", [\
|
||||||
|
["npm:0.17.0", {\
|
||||||
|
"packageLocation": "./.yarn/cache/mixpanel-npm-0.17.0-3073ce9949-5a945bdbdd.zip/node_modules/mixpanel/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["mixpanel", "npm:0.17.0"],\
|
||||||
|
["https-proxy-agent", "npm:5.0.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["mkdirp", [\
|
["mkdirp", [\
|
||||||
["npm:1.0.4", {\
|
["npm:1.0.4", {\
|
||||||
"packageLocation": "./.yarn/cache/mkdirp-npm-1.0.4-37f6ef56b9-1233611198.zip/node_modules/mkdirp/",\
|
"packageLocation": "./.yarn/cache/mkdirp-npm-1.0.4-37f6ef56b9-1233611198.zip/node_modules/mkdirp/",\
|
||||||
@@ -10579,6 +10878,15 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["nofilter", [\
|
||||||
|
["npm:1.0.4", {\
|
||||||
|
"packageLocation": "./.yarn/cache/nofilter-npm-1.0.4-1cbdc6c03a-9a26874e7d.zip/node_modules/nofilter/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["nofilter", "npm:1.0.4"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["nopt", [\
|
["nopt", [\
|
||||||
["npm:1.0.10", {\
|
["npm:1.0.10", {\
|
||||||
"packageLocation": "./.yarn/cache/nopt-npm-1.0.10-f3db192976-efa5a9c2c1.zip/node_modules/nopt/",\
|
"packageLocation": "./.yarn/cache/nopt-npm-1.0.10-f3db192976-efa5a9c2c1.zip/node_modules/nopt/",\
|
||||||
@@ -11627,6 +11935,25 @@ const RAW_RUNTIME_STATE =
|
|||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
|
["pvtsutils", [\
|
||||||
|
["npm:1.3.2", {\
|
||||||
|
"packageLocation": "./.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-eb22d3df60.zip/node_modules/pvtsutils/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["pvtsutils", "npm:1.3.2"],\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
|
["pvutils", [\
|
||||||
|
["npm:1.1.3", {\
|
||||||
|
"packageLocation": "./.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-0cb4f4878f.zip/node_modules/pvutils/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["pvutils", "npm:1.1.3"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
|
}]\
|
||||||
|
]],\
|
||||||
["q", [\
|
["q", [\
|
||||||
["npm:1.5.1", {\
|
["npm:1.5.1", {\
|
||||||
"packageLocation": "./.yarn/cache/q-npm-1.5.1-a28b3cfeaf-276b7e93fc.zip/node_modules/q/",\
|
"packageLocation": "./.yarn/cache/q-npm-1.5.1-a28b3cfeaf-276b7e93fc.zip/node_modules/q/",\
|
||||||
@@ -12205,6 +12532,14 @@ const RAW_RUNTIME_STATE =
|
|||||||
["lru-cache", "npm:6.0.0"]\
|
["lru-cache", "npm:6.0.0"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
|
["npm:7.3.8", {\
|
||||||
|
"packageLocation": "./.yarn/cache/semver-npm-7.3.8-25a996cb4f-94ad80ee14.zip/node_modules/semver/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["semver", "npm:7.3.8"],\
|
||||||
|
["lru-cache", "npm:6.0.0"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["semver-diff", [\
|
["semver-diff", [\
|
||||||
@@ -13311,6 +13646,13 @@ const RAW_RUNTIME_STATE =
|
|||||||
["tslib", "npm:2.4.0"]\
|
["tslib", "npm:2.4.0"]\
|
||||||
],\
|
],\
|
||||||
"linkType": "HARD"\
|
"linkType": "HARD"\
|
||||||
|
}],\
|
||||||
|
["npm:2.4.1", {\
|
||||||
|
"packageLocation": "./.yarn/cache/tslib-npm-2.4.1-36f0ed04db-a739a21e3f.zip/node_modules/tslib/",\
|
||||||
|
"packageDependencies": [\
|
||||||
|
["tslib", "npm:2.4.1"]\
|
||||||
|
],\
|
||||||
|
"linkType": "HARD"\
|
||||||
}]\
|
}]\
|
||||||
]],\
|
]],\
|
||||||
["tsutils", [\
|
["tsutils", [\
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": false
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-2
@@ -61,8 +61,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "yarn@4.0.0-rc.25",
|
"packageManager": "yarn@4.0.0-rc.25",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@newrelic/native-metrics": "^9.0.0",
|
"@sentry/node": "^7.28.1",
|
||||||
"@sentry/node": "^7.19.0",
|
|
||||||
"newrelic": "^9.6.0"
|
"newrelic": "^9.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ DB_DATABASE=analytics
|
|||||||
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
DB_DEBUG_LEVEL=all # "all" | "query" | "schema" | "error" | "warn" | "info" | "log" | "migration"
|
||||||
DB_MIGRATIONS_PATH=dist/migrations/*.js
|
DB_MIGRATIONS_PATH=dist/migrations/*.js
|
||||||
|
|
||||||
|
ADMIN_EMAILS=test@standardnotes.com
|
||||||
|
|
||||||
REDIS_URL=redis://cache
|
REDIS_URL=redis://cache
|
||||||
REDIS_EVENTS_CHANNEL=events
|
REDIS_EVENTS_CHANNEL=events
|
||||||
|
|
||||||
@@ -26,3 +28,6 @@ NEW_RELIC_NO_CONFIG_FILE=true
|
|||||||
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=false
|
||||||
NEW_RELIC_LOG_ENABLED=false
|
NEW_RELIC_LOG_ENABLED=false
|
||||||
NEW_RELIC_LOG_LEVEL=info
|
NEW_RELIC_LOG_LEVEL=info
|
||||||
|
|
||||||
|
# (Optional) Mixpanel
|
||||||
|
MIXPANEL_TOKEN=
|
||||||
|
|||||||
@@ -3,6 +3,275 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.19.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.3...@standardnotes/analytics@2.19.4) (2023-01-17)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow to run typeorm in non-replica mode ([f73129c](https://github.com/standardnotes/server/commit/f73129cd7e7d6a9b8a63e5c80284467597557982))
|
||||||
|
|
||||||
|
## [2.19.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.2...@standardnotes/analytics@2.19.3) (2023-01-16)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.19.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.1...@standardnotes/analytics@2.19.2) (2023-01-13)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.19.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.19.0...@standardnotes/analytics@2.19.1) (2022-12-30)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** remove unnecesary context from mixpanel events ([ba1e1ad](https://github.com/standardnotes/server/commit/ba1e1ad5ad82b052be4cc2d1cc2abdaf3b72cf4c))
|
||||||
|
|
||||||
|
# [2.19.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.18.0...@standardnotes/analytics@2.19.0) (2022-12-30)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add mixpanel events tracking ([df6e3f0](https://github.com/standardnotes/server/commit/df6e3f06a6868e30e60dd98431122983724644b4))
|
||||||
|
|
||||||
|
# [2.18.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.8...@standardnotes/analytics@2.18.0) (2022-12-30)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add mixpanel ([893d617](https://github.com/standardnotes/server/commit/893d6176c3b0b56c45e5188fe982232db2ceedc4))
|
||||||
|
|
||||||
|
## [2.17.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.7...@standardnotes/analytics@2.17.8) (2022-12-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.17.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.6...@standardnotes/analytics@2.17.7) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** monthly numbers of active users ([b34bbca](https://github.com/standardnotes/server/commit/b34bbcac8b9604283b3a5959ab3218c468ce8a00))
|
||||||
|
|
||||||
|
## [2.17.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.5...@standardnotes/analytics@2.17.6) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** filtered counts for user activity check ([17b2ea1](https://github.com/standardnotes/server/commit/17b2ea126c5ad2d7cf07657def63f9977f239a3c))
|
||||||
|
|
||||||
|
## [2.17.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.4...@standardnotes/analytics@2.17.5) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** accessing analytics in report ([ef26dc8](https://github.com/standardnotes/server/commit/ef26dc8cbb967e088ae7387ff6dbec1e60dc3ee4))
|
||||||
|
|
||||||
|
## [2.17.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.3...@standardnotes/analytics@2.17.4) (2022-12-20)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.17.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.2...@standardnotes/analytics@2.17.3) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** add debug logs for the report ([031fcd7](https://github.com/standardnotes/server/commit/031fcd75eecdcf4c2f17257754a0ba3f24ba6d6e))
|
||||||
|
|
||||||
|
## [2.17.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.1...@standardnotes/analytics@2.17.2) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** calculating active users ([a304993](https://github.com/standardnotes/server/commit/a3049938a31e21a5867a314ac62bee6aa4990d57))
|
||||||
|
|
||||||
|
## [2.17.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.17.0...@standardnotes/analytics@2.17.1) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** container binding ([04ffc69](https://github.com/standardnotes/server/commit/04ffc69e000803107d8834c286de97b3d213a842))
|
||||||
|
* **auth:** replace date object with number timestamp ([5b4bb6e](https://github.com/standardnotes/server/commit/5b4bb6e7a78a1b0f4e663bb990619f65f6a5c757))
|
||||||
|
|
||||||
|
# [2.17.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.16.0...@standardnotes/analytics@2.17.0) (2022-12-20)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add users activit to the report email ([ed5a4eb](https://github.com/standardnotes/server/commit/ed5a4eb960a6c8fe9d0c77331f29dc3c7ffb9100))
|
||||||
|
|
||||||
|
# [2.16.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.1...@standardnotes/analytics@2.16.0) (2022-12-20)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add active users stats to report ([6e16620](https://github.com/standardnotes/server/commit/6e1662038c3340fb60939464616789bab7639160))
|
||||||
|
|
||||||
|
## [2.15.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.15.0...@standardnotes/analytics@2.15.1) (2022-12-20)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **auth:** add persisting statistics for all subscription plans ([addedb3](https://github.com/standardnotes/server/commit/addedb3091ddae81618d56663e18f2ae76a43c4e))
|
||||||
|
|
||||||
|
# [2.15.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.14.0...@standardnotes/analytics@2.15.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add requesting persisting statistics ([a35271f](https://github.com/standardnotes/server/commit/a35271fbb399b68a3ac7021395d8063707fba222))
|
||||||
|
|
||||||
|
# [2.14.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.13.0...@standardnotes/analytics@2.14.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **analytics:** add persisting statistics on demand ([0f84575](https://github.com/standardnotes/server/commit/0f8457534c1829c58f3c036749d262307ddeb779))
|
||||||
|
|
||||||
|
# [2.13.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.27...@standardnotes/analytics@2.13.0) (2022-12-19)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **auth:** add session traces ([8bcb552](https://github.com/standardnotes/server/commit/8bcb552783b2d12f3296b3195752168482790bc8))
|
||||||
|
|
||||||
|
## [2.12.27](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.26...@standardnotes/analytics@2.12.27) (2022-12-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.26](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.25...@standardnotes/analytics@2.12.26) (2022-12-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.25](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.24...@standardnotes/analytics@2.12.25) (2022-12-12)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.24](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.23...@standardnotes/analytics@2.12.24) (2022-12-12)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** daily analytics report template ([41906ec](https://github.com/standardnotes/server/commit/41906ec2f9fd4d605b1c002826173e14fb534e00))
|
||||||
|
|
||||||
|
## [2.12.23](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.22...@standardnotes/analytics@2.12.23) (2022-12-12)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.22](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.21...@standardnotes/analytics@2.12.22) (2022-12-12)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** report event publishing ([a605660](https://github.com/standardnotes/server/commit/a6056600eb96bf175189ad6d62870c9d736f331b))
|
||||||
|
|
||||||
|
## [2.12.21](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.20...@standardnotes/analytics@2.12.21) (2022-12-12)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **analytics:** add debug logs for report ([48c0cb5](https://github.com/standardnotes/server/commit/48c0cb5e62dc8af930de191deaa1eb3ff6c5a29f))
|
||||||
|
|
||||||
|
## [2.12.20](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.19...@standardnotes/analytics@2.12.20) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.19](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.18...@standardnotes/analytics@2.12.19) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.18](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.17...@standardnotes/analytics@2.12.18) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.16...@standardnotes/analytics@2.12.17) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.15...@standardnotes/analytics@2.12.16) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.14...@standardnotes/analytics@2.12.15) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.13...@standardnotes/analytics@2.12.14) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.12...@standardnotes/analytics@2.12.13) (2022-12-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.11...@standardnotes/analytics@2.12.12) (2022-12-08)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.10...@standardnotes/analytics@2.12.11) (2022-12-08)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.9...@standardnotes/analytics@2.12.10) (2022-12-08)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.8...@standardnotes/analytics@2.12.9) (2022-12-08)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.7...@standardnotes/analytics@2.12.8) (2022-12-08)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.7](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.6...@standardnotes/analytics@2.12.7) (2022-12-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.5...@standardnotes/analytics@2.12.6) (2022-12-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.4...@standardnotes/analytics@2.12.5) (2022-12-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.3...@standardnotes/analytics@2.12.4) (2022-12-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.2...@standardnotes/analytics@2.12.3) (2022-12-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.1...@standardnotes/analytics@2.12.2) (2022-12-05)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.12.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.0...@standardnotes/analytics@2.12.1) (2022-12-05)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
# [2.12.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.17...@standardnotes/analytics@2.12.0) (2022-12-05)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **domain-core:** distinguish between username and email ([06fd404](https://github.com/standardnotes/server/commit/06fd404d44b44a53733f889aabd4da63f21e2f36))
|
||||||
|
|
||||||
|
## [2.11.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.16...@standardnotes/analytics@2.11.17) (2022-12-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.15...@standardnotes/analytics@2.11.16) (2022-12-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.15](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.14...@standardnotes/analytics@2.11.15) (2022-11-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.14](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.13...@standardnotes/analytics@2.11.14) (2022-11-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.13](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.12...@standardnotes/analytics@2.11.13) (2022-11-25)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.12](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.11...@standardnotes/analytics@2.11.12) (2022-11-24)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.11](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.10...@standardnotes/analytics@2.11.11) (2022-11-24)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.10](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.9...@standardnotes/analytics@2.11.10) (2022-11-24)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
|
## [2.11.9](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.8...@standardnotes/analytics@2.11.9) (2022-11-24)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|
||||||
## [2.11.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.7...@standardnotes/analytics@2.11.8) (2022-11-23)
|
## [2.11.8](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.7...@standardnotes/analytics@2.11.8) (2022-11-23)
|
||||||
|
|
||||||
**Note:** Version bump only for package @standardnotes/analytics
|
**Note:** Version bump only for package @standardnotes/analytics
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import 'newrelic'
|
|||||||
|
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
import { EmailLevel } from '@standardnotes/domain-core'
|
||||||
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
|
||||||
import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../src/Domain/Analytics/AnalyticsActivity'
|
||||||
import { Period } from '../src/Domain/Time/Period'
|
import { Period } from '../src/Domain/Time/Period'
|
||||||
import { StatisticsMeasure } from '../src/Domain/Statistics/StatisticsMeasure'
|
|
||||||
import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../src/Domain/Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../src/Domain/Statistics/StatisticsStoreInterface'
|
||||||
import { PeriodKeyGeneratorInterface } from '../src/Domain/Time/PeriodKeyGeneratorInterface'
|
import { PeriodKeyGeneratorInterface } from '../src/Domain/Time/PeriodKeyGeneratorInterface'
|
||||||
@@ -16,6 +16,9 @@ import TYPES from '../src/Bootstrap/Types'
|
|||||||
import { Env } from '../src/Bootstrap/Env'
|
import { Env } from '../src/Bootstrap/Env'
|
||||||
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
import { DomainEventFactoryInterface } from '../src/Domain/Event/DomainEventFactoryInterface'
|
||||||
import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
import { CalculateMonthlyRecurringRevenue } from '../src/Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
||||||
|
import { getBody, getSubject } from '../src/Domain/Email/DailyAnalyticsReport'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { StatisticMeasureName } from '../src/Domain/Statistics/StatisticMeasureName'
|
||||||
|
|
||||||
const requestReport = async (
|
const requestReport = async (
|
||||||
analyticsStore: AnalyticsStoreInterface,
|
analyticsStore: AnalyticsStoreInterface,
|
||||||
@@ -24,6 +27,8 @@ const requestReport = async (
|
|||||||
domainEventPublisher: DomainEventPublisherInterface,
|
domainEventPublisher: DomainEventPublisherInterface,
|
||||||
periodKeyGenerator: PeriodKeyGeneratorInterface,
|
periodKeyGenerator: PeriodKeyGeneratorInterface,
|
||||||
calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
|
calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue,
|
||||||
|
timer: TimerInterface,
|
||||||
|
adminEmails: string[],
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await calculateMonthlyRecurringRevenue.execute({})
|
await calculateMonthlyRecurringRevenue.execute({})
|
||||||
|
|
||||||
@@ -110,12 +115,16 @@ const requestReport = async (
|
|||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
const thirtyDaysStatisticsNames = [
|
const thirtyDaysStatisticsNames = [
|
||||||
StatisticsMeasure.MRR,
|
StatisticMeasureName.NAMES.MRR,
|
||||||
StatisticsMeasure.AnnualPlansMRR,
|
StatisticMeasureName.NAMES.AnnualPlansMRR,
|
||||||
StatisticsMeasure.MonthlyPlansMRR,
|
StatisticMeasureName.NAMES.MonthlyPlansMRR,
|
||||||
StatisticsMeasure.FiveYearPlansMRR,
|
StatisticMeasureName.NAMES.FiveYearPlansMRR,
|
||||||
StatisticsMeasure.PlusPlansMRR,
|
StatisticMeasureName.NAMES.PlusPlansMRR,
|
||||||
StatisticsMeasure.ProPlansMRR,
|
StatisticMeasureName.NAMES.ProPlansMRR,
|
||||||
|
StatisticMeasureName.NAMES.ActiveUsers,
|
||||||
|
StatisticMeasureName.NAMES.ActiveFreeUsers,
|
||||||
|
StatisticMeasureName.NAMES.ActivePlusUsers,
|
||||||
|
StatisticMeasureName.NAMES.ActiveProUsers,
|
||||||
]
|
]
|
||||||
for (const statisticName of thirtyDaysStatisticsNames) {
|
for (const statisticName of thirtyDaysStatisticsNames) {
|
||||||
statisticsOverTime.push({
|
statisticsOverTime.push({
|
||||||
@@ -125,7 +134,7 @@ const requestReport = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const monthlyStatisticsNames = [StatisticsMeasure.MRR]
|
const monthlyStatisticsNames = [StatisticMeasureName.NAMES.MRR]
|
||||||
for (const statisticName of monthlyStatisticsNames) {
|
for (const statisticName of monthlyStatisticsNames) {
|
||||||
statisticsOverTime.push({
|
statisticsOverTime.push({
|
||||||
name: statisticName,
|
name: statisticName,
|
||||||
@@ -135,22 +144,22 @@ const requestReport = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statisticMeasureNames = [
|
const statisticMeasureNames = [
|
||||||
StatisticsMeasure.Income,
|
StatisticMeasureName.NAMES.Income,
|
||||||
StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome,
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
StatisticsMeasure.Refunds,
|
StatisticMeasureName.NAMES.Refunds,
|
||||||
StatisticsMeasure.RegistrationLength,
|
StatisticMeasureName.NAMES.RegistrationLength,
|
||||||
StatisticsMeasure.SubscriptionLength,
|
StatisticMeasureName.NAMES.SubscriptionLength,
|
||||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
|
||||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
|
||||||
StatisticsMeasure.NewCustomers,
|
StatisticMeasureName.NAMES.NewCustomers,
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
]
|
]
|
||||||
const statisticMeasures: Array<{
|
const statisticMeasures: Array<{
|
||||||
name: string
|
name: string
|
||||||
@@ -185,7 +194,10 @@ const requestReport = async (
|
|||||||
|
|
||||||
const totalCustomerCounts: Array<number> = []
|
const totalCustomerCounts: Array<number> = []
|
||||||
for (const dailyPeriodKey of dailyPeriodKeys) {
|
for (const dailyPeriodKey of dailyPeriodKeys) {
|
||||||
const customersCount = await statisticsStore.getMeasureTotal(StatisticsMeasure.TotalCustomers, dailyPeriodKey)
|
const customersCount = await statisticsStore.getMeasureTotal(
|
||||||
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
|
dailyPeriodKey,
|
||||||
|
)
|
||||||
totalCustomerCounts.push(customersCount)
|
totalCustomerCounts.push(customersCount)
|
||||||
}
|
}
|
||||||
const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count)
|
const filteredTotalCustomerCounts = totalCustomerCounts.filter((count) => !!count)
|
||||||
@@ -213,18 +225,29 @@ const requestReport = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = domainEventFactory.createDailyAnalyticsReportGeneratedEvent({
|
for (const adminEmail of adminEmails) {
|
||||||
activityStatistics: yesterdayActivityStatistics,
|
await domainEventPublisher.publish(
|
||||||
activityStatisticsOverTime: analyticsOverTime,
|
domainEventFactory.createEmailRequestedEvent({
|
||||||
statisticsOverTime,
|
messageIdentifier: 'VERSION_ADOPTION_REPORT',
|
||||||
statisticMeasures,
|
subject: getSubject(),
|
||||||
churn: {
|
body: getBody(
|
||||||
periodKeys: monthlyPeriodKeys,
|
{
|
||||||
values: churnRates,
|
activityStatistics: yesterdayActivityStatistics,
|
||||||
},
|
activityStatisticsOverTime: analyticsOverTime,
|
||||||
})
|
statisticsOverTime,
|
||||||
|
statisticMeasures,
|
||||||
await domainEventPublisher.publish(event)
|
churn: {
|
||||||
|
periodKeys: monthlyPeriodKeys,
|
||||||
|
values: churnRates,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timer,
|
||||||
|
),
|
||||||
|
level: EmailLevel.LEVELS.System,
|
||||||
|
userEmail: adminEmail,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = new ContainerConfigLoader()
|
const container = new ContainerConfigLoader()
|
||||||
@@ -241,9 +264,13 @@ void container.load().then((container) => {
|
|||||||
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
|
const domainEventFactory: DomainEventFactoryInterface = container.get(TYPES.DomainEventFactory)
|
||||||
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
|
const domainEventPublisher: DomainEventPublisherInterface = container.get(TYPES.DomainEventPublisher)
|
||||||
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
|
const periodKeyGenerator: PeriodKeyGeneratorInterface = container.get(TYPES.PeriodKeyGenerator)
|
||||||
|
const timer: TimerInterface = container.get(TYPES.Timer)
|
||||||
const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
|
const calculateMonthlyRecurringRevenue: CalculateMonthlyRecurringRevenue = container.get(
|
||||||
TYPES.CalculateMonthlyRecurringRevenue,
|
TYPES.CalculateMonthlyRecurringRevenue,
|
||||||
)
|
)
|
||||||
|
const adminEmails = container.get(TYPES.ADMIN_EMAILS) as string[]
|
||||||
|
|
||||||
|
logger.info(`Sending report to following admins: ${adminEmails}`)
|
||||||
|
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
requestReport(
|
requestReport(
|
||||||
@@ -253,6 +280,8 @@ void container.load().then((container) => {
|
|||||||
domainEventPublisher,
|
domainEventPublisher,
|
||||||
periodKeyGenerator,
|
periodKeyGenerator,
|
||||||
calculateMonthlyRecurringRevenue,
|
calculateMonthlyRecurringRevenue,
|
||||||
|
timer,
|
||||||
|
adminEmails,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ COMMAND=$1 && shift 1
|
|||||||
|
|
||||||
case "$COMMAND" in
|
case "$COMMAND" in
|
||||||
'start-worker' )
|
'start-worker' )
|
||||||
echo "Starting Worker..."
|
echo "[Docker] Starting Worker..."
|
||||||
yarn workspace @standardnotes/analytics worker
|
yarn workspace @standardnotes/analytics worker
|
||||||
;;
|
;;
|
||||||
|
|
||||||
'report' )
|
'report' )
|
||||||
echo "Starting Usage Report Generation..."
|
echo "[Docker] Starting Usage Report Generation..."
|
||||||
yarn workspace @standardnotes/analytics report
|
yarn workspace @standardnotes/analytics report
|
||||||
;;
|
;;
|
||||||
|
|
||||||
* )
|
* )
|
||||||
echo "Unknown command"
|
echo "[Docker] Unknown command"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ module.exports = {
|
|||||||
transform: {
|
transform: {
|
||||||
...tsjPreset.transform,
|
...tsjPreset.transform,
|
||||||
},
|
},
|
||||||
coveragePathIgnorePatterns: ['/Infra/'],
|
coveragePathIgnorePatterns: ['/Infra/', '/Domain/Email/', '/Handler/'],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@standardnotes/analytics",
|
"name": "@standardnotes/analytics",
|
||||||
"version": "2.11.8",
|
"version": "2.19.4",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0 <19.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ioredis": "^5.0.0",
|
"@types/ioredis": "^5.0.0",
|
||||||
"@types/jest": "^29.1.1",
|
"@types/jest": "^29.1.1",
|
||||||
|
"@types/mixpanel": "^2.14.4",
|
||||||
"@types/newrelic": "^7.0.4",
|
"@types/newrelic": "^7.0.4",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||||
@@ -37,11 +38,10 @@
|
|||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@newrelic/native-metrics": "^9.0.0",
|
|
||||||
"@newrelic/winston-enricher": "^4.0.0",
|
"@newrelic/winston-enricher": "^4.0.0",
|
||||||
"@sentry/node": "^7.19.0",
|
"@sentry/node": "^7.28.1",
|
||||||
"@standardnotes/common": "workspace:*",
|
"@standardnotes/common": "workspace:*",
|
||||||
"@standardnotes/domain-core": "workspace:*",
|
"@standardnotes/domain-core": "workspace:^",
|
||||||
"@standardnotes/domain-events": "workspace:*",
|
"@standardnotes/domain-events": "workspace:*",
|
||||||
"@standardnotes/domain-events-infra": "workspace:*",
|
"@standardnotes/domain-events-infra": "workspace:*",
|
||||||
"@standardnotes/time": "workspace:*",
|
"@standardnotes/time": "workspace:*",
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"inversify": "^6.0.1",
|
"inversify": "^6.0.1",
|
||||||
"ioredis": "^5.2.4",
|
"ioredis": "^5.2.4",
|
||||||
|
"mixpanel": "^0.17.0",
|
||||||
"mysql2": "^2.3.3",
|
"mysql2": "^2.3.3",
|
||||||
"newrelic": "^9.6.0",
|
"newrelic": "^9.6.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
DomainEventSubscriberFactoryInterface,
|
DomainEventSubscriberFactoryInterface,
|
||||||
} from '@standardnotes/domain-events'
|
} from '@standardnotes/domain-events'
|
||||||
import { MapperInterface } from '@standardnotes/domain-core'
|
import { MapperInterface } from '@standardnotes/domain-core'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const Mixpanel = require('mixpanel')
|
||||||
|
|
||||||
import { Env } from './Env'
|
import { Env } from './Env'
|
||||||
import TYPES from './Types'
|
import TYPES from './Types'
|
||||||
@@ -52,6 +54,9 @@ import { RevenueModification } from '../Domain/Revenue/RevenueModification'
|
|||||||
import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap'
|
import { RevenueModificationMap } from '../Domain/Map/RevenueModificationMap'
|
||||||
import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification'
|
import { SaveRevenueModification } from '../Domain/UseCase/SaveRevenueModification/SaveRevenueModification'
|
||||||
import { CalculateMonthlyRecurringRevenue } from '../Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
import { CalculateMonthlyRecurringRevenue } from '../Domain/UseCase/CalculateMonthlyRecurringRevenue/CalculateMonthlyRecurringRevenue'
|
||||||
|
import { PersistStatistic } from '../Domain/UseCase/PersistStatistic/PersistStatistic'
|
||||||
|
import { StatisticMeasureRepositoryInterface } from '../Domain/Statistics/StatisticMeasureRepositoryInterface'
|
||||||
|
import { StatisticPersistenceRequestedEventHandler } from '../Domain/Handler/StatisticPersistenceRequestedEventHandler'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||||
@@ -130,6 +135,34 @@ export class ContainerConfigLoader {
|
|||||||
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
|
container.bind(TYPES.SQS_QUEUE_URL).toConstantValue(env.get('SQS_QUEUE_URL', true))
|
||||||
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
|
container.bind(TYPES.REDIS_EVENTS_CHANNEL).toConstantValue(env.get('REDIS_EVENTS_CHANNEL'))
|
||||||
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
container.bind(TYPES.NEW_RELIC_ENABLED).toConstantValue(env.get('NEW_RELIC_ENABLED', true))
|
||||||
|
container.bind(TYPES.ADMIN_EMAILS).toConstantValue(env.get('ADMIN_EMAILS').split(','))
|
||||||
|
container.bind(TYPES.MIXPANEL_TOKEN).toConstantValue(env.get('MIXPANEL_TOKEN', true))
|
||||||
|
|
||||||
|
// Services
|
||||||
|
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||||
|
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
|
||||||
|
container
|
||||||
|
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
|
||||||
|
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||||
|
container
|
||||||
|
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
||||||
|
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||||
|
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
||||||
|
|
||||||
|
if (env.get('SNS_TOPIC_ARN', true)) {
|
||||||
|
container
|
||||||
|
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||||
|
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
||||||
|
} else {
|
||||||
|
container
|
||||||
|
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
|
||||||
|
.toConstantValue(
|
||||||
|
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (env.get('MIXPANEL_TOKEN', true)) {
|
||||||
|
container.bind<Mixpanel>(TYPES.MixpanelClient).toConstantValue(Mixpanel.init(env.get('MIXPANEL_TOKEN', true)))
|
||||||
|
}
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
container
|
container
|
||||||
@@ -138,6 +171,9 @@ export class ContainerConfigLoader {
|
|||||||
container
|
container
|
||||||
.bind<RevenueModificationRepositoryInterface>(TYPES.RevenueModificationRepository)
|
.bind<RevenueModificationRepositoryInterface>(TYPES.RevenueModificationRepository)
|
||||||
.to(MySQLRevenueModificationRepository)
|
.to(MySQLRevenueModificationRepository)
|
||||||
|
container
|
||||||
|
.bind<StatisticMeasureRepositoryInterface>(TYPES.StatisticMeasureRepository)
|
||||||
|
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
||||||
|
|
||||||
// ORM
|
// ORM
|
||||||
container
|
container
|
||||||
@@ -153,6 +189,9 @@ export class ContainerConfigLoader {
|
|||||||
container
|
container
|
||||||
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
|
.bind<CalculateMonthlyRecurringRevenue>(TYPES.CalculateMonthlyRecurringRevenue)
|
||||||
.to(CalculateMonthlyRecurringRevenue)
|
.to(CalculateMonthlyRecurringRevenue)
|
||||||
|
container
|
||||||
|
.bind<PersistStatistic>(TYPES.PersistStatistic)
|
||||||
|
.toConstantValue(new PersistStatistic(container.get(TYPES.StatisticMeasureRepository)))
|
||||||
|
|
||||||
// Hanlders
|
// Hanlders
|
||||||
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
container.bind<UserRegisteredEventHandler>(TYPES.UserRegisteredEventHandler).to(UserRegisteredEventHandler)
|
||||||
@@ -180,35 +219,22 @@ export class ContainerConfigLoader {
|
|||||||
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
.bind<SubscriptionReactivatedEventHandler>(TYPES.SubscriptionReactivatedEventHandler)
|
||||||
.to(SubscriptionReactivatedEventHandler)
|
.to(SubscriptionReactivatedEventHandler)
|
||||||
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
container.bind<RefundProcessedEventHandler>(TYPES.RefundProcessedEventHandler).to(RefundProcessedEventHandler)
|
||||||
|
container
|
||||||
|
.bind<StatisticPersistenceRequestedEventHandler>(TYPES.StatisticPersistenceRequestedEventHandler)
|
||||||
|
.toConstantValue(
|
||||||
|
new StatisticPersistenceRequestedEventHandler(
|
||||||
|
container.get(TYPES.PersistStatistic),
|
||||||
|
container.get(TYPES.Timer),
|
||||||
|
container.get(TYPES.Logger),
|
||||||
|
env.get('MIXPANEL_TOKEN', true) ? container.get(TYPES.MixpanelClient) : null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// Maps
|
// Maps
|
||||||
container
|
container
|
||||||
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
|
.bind<MapperInterface<RevenueModification, TypeORMRevenueModification>>(TYPES.RevenueModificationMap)
|
||||||
.to(RevenueModificationMap)
|
.to(RevenueModificationMap)
|
||||||
|
|
||||||
// Services
|
|
||||||
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
|
||||||
container.bind<PeriodKeyGeneratorInterface>(TYPES.PeriodKeyGenerator).toConstantValue(new PeriodKeyGenerator())
|
|
||||||
container
|
|
||||||
.bind<AnalyticsStoreInterface>(TYPES.AnalyticsStore)
|
|
||||||
.toConstantValue(new RedisAnalyticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
|
||||||
container
|
|
||||||
.bind<StatisticsStoreInterface>(TYPES.StatisticsStore)
|
|
||||||
.toConstantValue(new RedisStatisticsStore(container.get(TYPES.PeriodKeyGenerator), container.get(TYPES.Redis)))
|
|
||||||
container.bind<TimerInterface>(TYPES.Timer).toConstantValue(new Timer())
|
|
||||||
|
|
||||||
if (env.get('SNS_TOPIC_ARN', true)) {
|
|
||||||
container
|
|
||||||
.bind<SNSDomainEventPublisher>(TYPES.DomainEventPublisher)
|
|
||||||
.toConstantValue(new SNSDomainEventPublisher(container.get(TYPES.SNS), container.get(TYPES.SNS_TOPIC_ARN)))
|
|
||||||
} else {
|
|
||||||
container
|
|
||||||
.bind<RedisDomainEventPublisher>(TYPES.DomainEventPublisher)
|
|
||||||
.toConstantValue(
|
|
||||||
new RedisDomainEventPublisher(container.get(TYPES.Redis), container.get(TYPES.REDIS_EVENTS_CHANNEL)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
const eventHandlers: Map<string, DomainEventHandlerInterface> = new Map([
|
||||||
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
|
['USER_REGISTERED', container.get(TYPES.UserRegisteredEventHandler)],
|
||||||
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
['ACCOUNT_DELETION_REQUESTED', container.get(TYPES.AccountDeletionRequestedEventHandler)],
|
||||||
@@ -221,6 +247,7 @@ export class ContainerConfigLoader {
|
|||||||
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
|
['SUBSCRIPTION_EXPIRED', container.get(TYPES.SubscriptionExpiredEventHandler)],
|
||||||
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
['SUBSCRIPTION_REACTIVATED', container.get(TYPES.SubscriptionReactivatedEventHandler)],
|
||||||
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
['REFUND_PROCESSED', container.get(TYPES.RefundProcessedEventHandler)],
|
||||||
|
['STATISTIC_PERSISTENCE_REQUESTED', container.get(TYPES.StatisticPersistenceRequestedEventHandler)],
|
||||||
])
|
])
|
||||||
|
|
||||||
if (env.get('SQS_QUEUE_URL', true)) {
|
if (env.get('SQS_QUEUE_URL', true)) {
|
||||||
|
|||||||
@@ -12,31 +12,41 @@ const maxQueryExecutionTime = env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
|||||||
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
? +env.get('DB_MAX_QUERY_EXECUTION_TIME', true)
|
||||||
: 45_000
|
: 45_000
|
||||||
|
|
||||||
|
const inReplicaMode = env.get('DB_REPLICA_HOST', true) ? true : false
|
||||||
|
|
||||||
|
const replicationConfig = {
|
||||||
|
master: {
|
||||||
|
host: env.get('DB_HOST'),
|
||||||
|
port: parseInt(env.get('DB_PORT')),
|
||||||
|
username: env.get('DB_USERNAME'),
|
||||||
|
password: env.get('DB_PASSWORD'),
|
||||||
|
database: env.get('DB_DATABASE'),
|
||||||
|
},
|
||||||
|
slaves: [
|
||||||
|
{
|
||||||
|
host: env.get('DB_REPLICA_HOST', true),
|
||||||
|
port: parseInt(env.get('DB_PORT')),
|
||||||
|
username: env.get('DB_USERNAME'),
|
||||||
|
password: env.get('DB_PASSWORD'),
|
||||||
|
database: env.get('DB_DATABASE'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
removeNodeErrorCount: 10,
|
||||||
|
restoreNodeTimeout: 5,
|
||||||
|
}
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
export const AppDataSource = new DataSource({
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
charset: 'utf8mb4',
|
charset: 'utf8mb4',
|
||||||
supportBigNumbers: true,
|
supportBigNumbers: true,
|
||||||
bigNumberStrings: false,
|
bigNumberStrings: false,
|
||||||
maxQueryExecutionTime,
|
maxQueryExecutionTime,
|
||||||
replication: {
|
replication: inReplicaMode ? replicationConfig : undefined,
|
||||||
master: {
|
host: inReplicaMode ? undefined : env.get('DB_HOST'),
|
||||||
host: env.get('DB_HOST'),
|
port: inReplicaMode ? undefined : parseInt(env.get('DB_PORT')),
|
||||||
port: parseInt(env.get('DB_PORT')),
|
username: inReplicaMode ? undefined : env.get('DB_USERNAME'),
|
||||||
username: env.get('DB_USERNAME'),
|
password: inReplicaMode ? undefined : env.get('DB_PASSWORD'),
|
||||||
password: env.get('DB_PASSWORD'),
|
database: inReplicaMode ? undefined : env.get('DB_DATABASE'),
|
||||||
database: env.get('DB_DATABASE'),
|
|
||||||
},
|
|
||||||
slaves: [
|
|
||||||
{
|
|
||||||
host: env.get('DB_REPLICA_HOST'),
|
|
||||||
port: parseInt(env.get('DB_PORT')),
|
|
||||||
username: env.get('DB_USERNAME'),
|
|
||||||
password: env.get('DB_PASSWORD'),
|
|
||||||
database: env.get('DB_DATABASE'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
removeNodeErrorCount: 10,
|
|
||||||
},
|
|
||||||
entities: [AnalyticsEntity, TypeORMRevenueModification],
|
entities: [AnalyticsEntity, TypeORMRevenueModification],
|
||||||
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
|
migrations: [env.get('DB_MIGRATIONS_PATH', true) ?? 'dist/migrations/*.js'],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ const TYPES = {
|
|||||||
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
SQS_AWS_REGION: Symbol.for('SQS_AWS_REGION'),
|
||||||
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
REDIS_EVENTS_CHANNEL: Symbol.for('REDIS_EVENTS_CHANNEL'),
|
||||||
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
NEW_RELIC_ENABLED: Symbol.for('NEW_RELIC_ENABLED'),
|
||||||
|
ADMIN_EMAILS: Symbol.for('ADMIN_EMAILS'),
|
||||||
|
MIXPANEL_TOKEN: Symbol.for('MIXPANEL_TOKEN'),
|
||||||
// Repositories
|
// Repositories
|
||||||
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
|
AnalyticsEntityRepository: Symbol.for('AnalyticsEntityRepository'),
|
||||||
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
|
RevenueModificationRepository: Symbol.for('RevenueModificationRepository'),
|
||||||
|
StatisticMeasureRepository: Symbol.for('StatisticMeasureRepository'),
|
||||||
// ORM
|
// ORM
|
||||||
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
|
ORMAnalyticsEntityRepository: Symbol.for('ORMAnalyticsEntityRepository'),
|
||||||
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
|
ORMRevenueModificationRepository: Symbol.for('ORMRevenueModificationRepository'),
|
||||||
@@ -21,6 +24,7 @@ const TYPES = {
|
|||||||
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
GetUserAnalyticsId: Symbol.for('GetUserAnalyticsId'),
|
||||||
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
|
SaveRevenueModification: Symbol.for('SaveRevenueModification'),
|
||||||
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
|
CalculateMonthlyRecurringRevenue: Symbol.for('CalculateMonthlyRecurringRevenue'),
|
||||||
|
PersistStatistic: Symbol.for('PersistStatistic'),
|
||||||
// Handlers
|
// Handlers
|
||||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||||
@@ -33,6 +37,7 @@ const TYPES = {
|
|||||||
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
|
SubscriptionExpiredEventHandler: Symbol.for('SubscriptionExpiredEventHandler'),
|
||||||
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
SubscriptionReactivatedEventHandler: Symbol.for('SubscriptionReactivatedEventHandler'),
|
||||||
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
RefundProcessedEventHandler: Symbol.for('RefundProcessedEventHandler'),
|
||||||
|
StatisticPersistenceRequestedEventHandler: Symbol.for('StatisticPersistenceRequestedEventHandler'),
|
||||||
// Maps
|
// Maps
|
||||||
RevenueModificationMap: Symbol.for('RevenueModificationMap'),
|
RevenueModificationMap: Symbol.for('RevenueModificationMap'),
|
||||||
// Services
|
// Services
|
||||||
@@ -44,6 +49,7 @@ const TYPES = {
|
|||||||
StatisticsStore: Symbol.for('StatisticsStore'),
|
StatisticsStore: Symbol.for('StatisticsStore'),
|
||||||
Timer: Symbol.for('Timer'),
|
Timer: Symbol.for('Timer'),
|
||||||
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
|
PeriodKeyGenerator: Symbol.for('PeriodKeyGenerator'),
|
||||||
|
MixpanelClient: Symbol.for('MixpanelClient'),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TYPES
|
export default TYPES
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
|
import { html } from './daily-analytics-report.html'
|
||||||
|
|
||||||
|
export function getSubject(): string {
|
||||||
|
return `Daily analytics report ${new Date().toLocaleDateString('en-US')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBody(data: unknown, timer: TimerInterface): string {
|
||||||
|
return html(data, timer)
|
||||||
|
}
|
||||||
@@ -0,0 +1,979 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
|
import { Period } from '../Time/Period'
|
||||||
|
|
||||||
|
const countActiveUsers = (measureName: string, data: any): { yesterday: number; last30Days: number } => {
|
||||||
|
const totalActiveUsersLast30DaysIncludingToday = data.statisticsOverTime.find(
|
||||||
|
(a: { name: string; period: number }) => a.name === measureName && a.period === 27,
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalActiveUsersYesterday =
|
||||||
|
totalActiveUsersLast30DaysIncludingToday.counts[totalActiveUsersLast30DaysIncludingToday.counts.length - 2]
|
||||||
|
.totalCount
|
||||||
|
|
||||||
|
const filteredCounts = totalActiveUsersLast30DaysIncludingToday.counts.filter(
|
||||||
|
(count: { totalCount: number }) => count.totalCount !== 0,
|
||||||
|
)
|
||||||
|
if (filteredCounts.length === 0) {
|
||||||
|
return {
|
||||||
|
yesterday: 0,
|
||||||
|
last30Days: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const last30DaysNumbers = filteredCounts.map((count: { totalCount: number }) => count.totalCount)
|
||||||
|
const last30DaysCount = last30DaysNumbers.reduce((previousValue: number, currentValue: number) => {
|
||||||
|
return previousValue + currentValue
|
||||||
|
})
|
||||||
|
|
||||||
|
const averageActiveUsersLast30Days = Math.floor(last30DaysCount / last30DaysNumbers.length)
|
||||||
|
|
||||||
|
return {
|
||||||
|
yesterday: totalActiveUsersYesterday,
|
||||||
|
last30Days: averageActiveUsersLast30Days,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChartUrls = (
|
||||||
|
data: any,
|
||||||
|
): {
|
||||||
|
subscriptions: string
|
||||||
|
users: string
|
||||||
|
quarterlyPerformance: string
|
||||||
|
churn: string
|
||||||
|
mrrMonthly: string
|
||||||
|
} => {
|
||||||
|
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscriptionsLinerOverTimeConfig = {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: subscriptionPurchasingOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Subscription Purchases',
|
||||||
|
backgroundColor: 'rgb(25, 255, 140)',
|
||||||
|
borderColor: 'rgb(25, 255, 140)',
|
||||||
|
data: subscriptionPurchasingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subscription Renewals',
|
||||||
|
backgroundColor: 'rgb(54, 162, 235)',
|
||||||
|
borderColor: 'rgb(54, 162, 235)',
|
||||||
|
data: subscriptionRenewingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subscription Refunds',
|
||||||
|
backgroundColor: 'rgb(255, 221, 51)',
|
||||||
|
borderColor: 'rgb(255, 221, 51)',
|
||||||
|
data: subscriptionRefundingOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subscription Cancels',
|
||||||
|
backgroundColor: 'rgb(255, 99, 132)',
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
data: subscriptionCancelledOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subscription Reactivations',
|
||||||
|
backgroundColor: 'rgb(221, 51, 255)',
|
||||||
|
borderColor: 'rgb(221, 51, 255)',
|
||||||
|
data: subscriptionReactivatedOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRegistrationOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const userDeletionOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
|
||||||
|
const usersLinerOverTimeConfig = {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: userRegistrationOverTime?.counts.map((count: { periodKey: any }) => count.periodKey),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'User Registrations',
|
||||||
|
backgroundColor: 'rgb(25, 255, 140)',
|
||||||
|
borderColor: 'rgb(25, 255, 140)',
|
||||||
|
data: userRegistrationOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Account Deletions',
|
||||||
|
backgroundColor: 'rgb(255, 99, 132)',
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
data: userDeletionOverTime?.counts.map((count: { totalCount: any }) => count.totalCount),
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const quarters = [Period.Q1ThisYear, Period.Q2ThisYear, Period.Q3ThisYear, Period.Q4ThisYear]
|
||||||
|
const quarterlyUserRegistrations = []
|
||||||
|
const quarterlySubscriptionPurchases = []
|
||||||
|
const quarterlySubscriptionRenewals = []
|
||||||
|
for (const quarter of quarters) {
|
||||||
|
const registrations =
|
||||||
|
data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.Register && a.period === quarter,
|
||||||
|
)?.totalCount ?? 0
|
||||||
|
const purchases =
|
||||||
|
data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === quarter,
|
||||||
|
)?.totalCount ?? 0
|
||||||
|
const renewals =
|
||||||
|
data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === quarter,
|
||||||
|
)?.totalCount ?? 0
|
||||||
|
quarterlyUserRegistrations.push(registrations)
|
||||||
|
quarterlySubscriptionPurchases.push(purchases)
|
||||||
|
quarterlySubscriptionRenewals.push(renewals)
|
||||||
|
}
|
||||||
|
|
||||||
|
const quarterlyConfig = {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'User Registrations',
|
||||||
|
backgroundColor: 'rgba(255, 99, 132, 0.5)',
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
borderWidth: 1,
|
||||||
|
data: quarterlyUserRegistrations,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subscription Purchases',
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
||||||
|
borderColor: 'rgb(54, 162, 235)',
|
||||||
|
borderWidth: 1,
|
||||||
|
data: quarterlySubscriptionPurchases,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Subscription Renewals',
|
||||||
|
backgroundColor: 'rgb(25, 255, 140, 0.5)',
|
||||||
|
borderColor: 'rgb(25, 255, 140)',
|
||||||
|
borderWidth: 1,
|
||||||
|
data: quarterlySubscriptionRenewals,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Quarterly Performance',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
datalabels: {
|
||||||
|
anchor: 'center',
|
||||||
|
align: 'center',
|
||||||
|
color: '#666',
|
||||||
|
font: {
|
||||||
|
weight: 'normal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthlyChurnRates = data.churn.values.map((value: { rate: number }) => +value.rate.toFixed(2))
|
||||||
|
|
||||||
|
const churnConfig = {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Churn Percent',
|
||||||
|
backgroundColor: 'rgba(255, 99, 132, 0.5)',
|
||||||
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
|
borderWidth: 1,
|
||||||
|
data: monthlyChurnRates,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Monthly Churn Rate',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
datalabels: {
|
||||||
|
anchor: 'center',
|
||||||
|
align: 'center',
|
||||||
|
color: '#666',
|
||||||
|
font: {
|
||||||
|
weight: 'normal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mrrMonthlyOverTime = data.statisticsOverTime
|
||||||
|
.find((a: { name: string; period: Period }) => a.name === 'mrr' && a.period === Period.ThisYear)
|
||||||
|
?.counts.map((count: { totalCount: number }) => +count.totalCount.toFixed(2))
|
||||||
|
|
||||||
|
const mrrMonthlyConfig = {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'MRR',
|
||||||
|
backgroundColor: 'rgba(25, 255, 140, 0.5)',
|
||||||
|
borderColor: 'rgb(25, 255, 140)',
|
||||||
|
borderWidth: 1,
|
||||||
|
data: mrrMonthlyOverTime,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Monthly MRR',
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
datalabels: {
|
||||||
|
anchor: 'center',
|
||||||
|
align: 'center',
|
||||||
|
color: '#666',
|
||||||
|
font: {
|
||||||
|
weight: 'normal',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscriptions: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
|
||||||
|
JSON.stringify(subscriptionsLinerOverTimeConfig),
|
||||||
|
)}`,
|
||||||
|
users: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(usersLinerOverTimeConfig))}`,
|
||||||
|
quarterlyPerformance: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(
|
||||||
|
JSON.stringify(quarterlyConfig),
|
||||||
|
)}`,
|
||||||
|
churn: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(churnConfig))}`,
|
||||||
|
mrrMonthly: `https://quickchart.io/chart?width=800&c=${encodeURIComponent(JSON.stringify(mrrMonthlyConfig))}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const html = (data: any, timer: TimerInterface) => {
|
||||||
|
const chartUrls = getChartUrls(data)
|
||||||
|
|
||||||
|
const successfullPaymentsActivity = data.activityStatistics.find(
|
||||||
|
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentSuccess && Period.Yesterday,
|
||||||
|
)
|
||||||
|
const failedPaymentsActivity = data.activityStatistics.find(
|
||||||
|
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.PaymentFailed && Period.Yesterday,
|
||||||
|
)
|
||||||
|
const limitedDiscountPurchasedActivity = data.activityStatistics.find(
|
||||||
|
(a: { name: AnalyticsActivity }) => a.name === AnalyticsActivity.LimitedDiscountOfferPurchased && Period.Yesterday,
|
||||||
|
)
|
||||||
|
const subscriptionPurchasingOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionPurchased && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const subscriptionRenewingOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionRenewed && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const subscriptionRefundingOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionRefunded && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const subscriptionCancelledOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionCancelled && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const subscriptionReactivatedOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.SubscriptionReactivated && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const userRegistrationOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.Register && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const userDeletionOverTime = data.activityStatisticsOverTime.find(
|
||||||
|
(a: { name: AnalyticsActivity; period: Period }) =>
|
||||||
|
a.name === AnalyticsActivity.DeleteAccount && a.period === Period.Last30Days,
|
||||||
|
)
|
||||||
|
const incomeMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const refundMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const incomeYesterday = incomeMeasureYesterday?.totalValue ?? 0
|
||||||
|
const refundsYesterday = refundMeasureYesterday?.totalValue ?? 0
|
||||||
|
const revenueYesterday = incomeYesterday - refundsYesterday
|
||||||
|
|
||||||
|
const subscriptionLengthMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const subscriptionLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(subscriptionLengthMeasureYesterday?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscriptionRemainingTimePercentageMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const subscriptionRemainingTimePercentageYesterday = Math.floor(
|
||||||
|
subscriptionRemainingTimePercentageMeasureYesterday?.average ?? 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const registrationLengthMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const registrationLengthDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(registrationLengthMeasureYesterday?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const registrationToSubscriptionMeasureYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const registrationToSubscriptionDurationYesterday = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(registrationToSubscriptionMeasureYesterday?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const incomeMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.Income && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const refundMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.Refunds && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const incomeThisMonth = incomeMeasureThisMonth?.totalValue ?? 0
|
||||||
|
const refundsThisMonth = refundMeasureThisMonth?.totalValue ?? 0
|
||||||
|
const revenueThisMonth = incomeThisMonth - refundsThisMonth
|
||||||
|
|
||||||
|
const subscriptionLengthMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.SubscriptionLength && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const subscriptionLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(subscriptionLengthMeasureThisMonth?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscriptionRemainingTimePercentageMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const subscriptionRemainingTimePercentageThisMonth = Math.floor(
|
||||||
|
subscriptionRemainingTimePercentageMeasureThisMonth?.average ?? 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
const registrationLengthMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RegistrationLength && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const registrationLengthDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(registrationLengthMeasureThisMonth?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const registrationToSubscriptionMeasureThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.RegistrationToSubscriptionTime && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const registrationToSubscriptionDurationThisMonth = timer.convertMicrosecondsToTimeStructure(
|
||||||
|
Math.floor(registrationToSubscriptionMeasureThisMonth?.average ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const plusSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const proSubscriptionsInitialAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const proSubscriptionsInitialMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const proSubscriptionsRenewingAnnualPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const proSubscriptionsRenewingMonthlyPaymentsYesterday = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.Yesterday,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const plusSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const proSubscriptionsInitialAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome && a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const proSubscriptionsInitialMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const proSubscriptionsRenewingAnnualPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
const proSubscriptionsRenewingMonthlyPaymentsThisMonth = data.statisticMeasures.find(
|
||||||
|
(a: { name: string; period: Period }) =>
|
||||||
|
a.name === StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome &&
|
||||||
|
a.period === Period.ThisMonth,
|
||||||
|
)
|
||||||
|
|
||||||
|
const mrrOverTime = data.statisticsOverTime.find(
|
||||||
|
(a: { name: string; period: number }) => a.name === 'mrr' && a.period === 27,
|
||||||
|
)
|
||||||
|
const monthlyPlansMrrOverTime = data.statisticsOverTime.find(
|
||||||
|
(a: { name: string; period: number }) => a.name === 'monthly-plans-mrr' && a.period === 27,
|
||||||
|
)
|
||||||
|
const annualPlansMrrOverTime = data.statisticsOverTime.find(
|
||||||
|
(a: { name: string; period: number }) => a.name === 'annual-plans-mrr' && a.period === 27,
|
||||||
|
)
|
||||||
|
const fiveYearPlansMrrOverTime = data.statisticsOverTime.find(
|
||||||
|
(a: { name: string; period: number }) => a.name === 'five-year-plans-mrr' && a.period === 27,
|
||||||
|
)
|
||||||
|
const proPlansMrrOverTime = data.statisticsOverTime.find(
|
||||||
|
(a: { name: string; period: number }) => a.name === 'pro-plans-mrr' && a.period === 27,
|
||||||
|
)
|
||||||
|
const plusPlansMrrOverTime = data.statisticsOverTime.find(
|
||||||
|
(a: { name: string; period: number }) => a.name === 'plus-plans-mrr' && a.period === 27,
|
||||||
|
)
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
const thisMonthPeriodKey = `${today.getFullYear().toString()}-${(today.getMonth() + 1).toString()}`
|
||||||
|
const thisMonthChurn = data.churn.values.find(
|
||||||
|
(value: { periodKey: string }) => value.periodKey === thisMonthPeriodKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalActiveUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveUsers, data)
|
||||||
|
const totalActiveFreeUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveFreeUsers, data)
|
||||||
|
const totalActivePlusUsers = countActiveUsers(StatisticMeasureName.NAMES.ActivePlusUsers, data)
|
||||||
|
const totalActiveProUsers = countActiveUsers(StatisticMeasureName.NAMES.ActiveProUsers, data)
|
||||||
|
|
||||||
|
return ` <div>
|
||||||
|
<p>Hello,</p>
|
||||||
|
<p>
|
||||||
|
<strong>Here are some statistics from yesterday:</strong>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Active Users</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Total:</b> ${totalActiveUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>By Subscription Type:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>FREE:</b> ${totalActiveFreeUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PLUS:</b> ${totalActivePlusUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PRO:</b> ${totalActiveProUsers.yesterday.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Payments</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Revenue: <b>$${revenueYesterday.toLocaleString('en-US')}</b> (Income: $
|
||||||
|
${incomeYesterday.toLocaleString('en-US')}, Refunds: $${refundsYesterday.toLocaleString('en-US')})
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Successfull payments: <b>${successfullPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Failed payments: <b>${failedPaymentsActivity?.totalCount.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>MRR Breakdown</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Total:</b> $${mrrOverTime?.counts[mrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>By Subscription Type:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>PLUS:</b> $
|
||||||
|
${plusPlansMrrOverTime?.counts[plusPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PRO:</b> $
|
||||||
|
${proPlansMrrOverTime?.counts[proPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>By Billing Frequency:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Monthly:</b> $
|
||||||
|
${monthlyPlansMrrOverTime?.counts[monthlyPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
|
||||||
|
'en-US',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Annual:</b> $
|
||||||
|
${annualPlansMrrOverTime?.counts[annualPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
|
||||||
|
'en-US',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>5-year:</b> $
|
||||||
|
${fiveYearPlansMrrOverTime?.counts[fiveYearPlansMrrOverTime?.counts.length - 1].totalCount.toLocaleString(
|
||||||
|
'en-US',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Income Breakdown</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Plus Subscription:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>${plusSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
|
||||||
|
<b>$${plusSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${plusSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
|
||||||
|
<b>$${plusSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
|
||||||
|
<b>$${plusSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${plusSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
|
||||||
|
<b>$${plusSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Pro Subscription:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>${proSubscriptionsInitialMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
|
||||||
|
<b>$${proSubscriptionsInitialMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${proSubscriptionsInitialAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
|
||||||
|
<b>$${proSubscriptionsInitialAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${proSubscriptionsRenewingMonthlyPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
|
||||||
|
<b>$${proSubscriptionsRenewingMonthlyPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${proSubscriptionsRenewingAnnualPaymentsYesterday?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
|
||||||
|
<b>$${proSubscriptionsRenewingAnnualPaymentsYesterday?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Users</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Number of users registered:${' '}
|
||||||
|
<b>
|
||||||
|
${userRegistrationOverTime?.counts[userRegistrationOverTime?.counts.length - 1]?.totalCount.toLocaleString(
|
||||||
|
'en-US',
|
||||||
|
)}
|
||||||
|
</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of users unregistered:${' '}
|
||||||
|
<b>
|
||||||
|
${userDeletionOverTime?.counts[userDeletionOverTime?.counts.length - 1]?.totalCount.toLocaleString('en-US')}
|
||||||
|
</b>${' '}
|
||||||
|
(average account duration: ${registrationLengthDurationYesterday.days} days${' '}
|
||||||
|
${registrationLengthDurationYesterday.hours} hours ${registrationLengthDurationYesterday.minutes} minutes)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Subscriptions</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions purchased:${' '}
|
||||||
|
<b>
|
||||||
|
${subscriptionPurchasingOverTime?.counts[
|
||||||
|
subscriptionPurchasingOverTime?.counts.length - 1
|
||||||
|
]?.totalCount.toLocaleString('en-US')}
|
||||||
|
</b>${' '}
|
||||||
|
(includes <b>${limitedDiscountPurchasedActivity?.totalCount.toLocaleString('en-US')}</b> limited time
|
||||||
|
offer purchases)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions renewed:${' '}
|
||||||
|
<b>
|
||||||
|
${subscriptionRenewingOverTime?.counts[
|
||||||
|
subscriptionRenewingOverTime?.counts.length - 1
|
||||||
|
]?.totalCount.toLocaleString('en-US')}
|
||||||
|
</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions refunded:${' '}
|
||||||
|
<b>
|
||||||
|
${subscriptionRefundingOverTime?.counts[
|
||||||
|
subscriptionRefundingOverTime?.counts.length - 1
|
||||||
|
]?.totalCount.toLocaleString('en-US')}
|
||||||
|
</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions cancelled:${' '}
|
||||||
|
<b>
|
||||||
|
${subscriptionCancelledOverTime?.counts[
|
||||||
|
subscriptionCancelledOverTime?.counts.length - 1
|
||||||
|
]?.totalCount.toLocaleString('en-US')}
|
||||||
|
</b>${' '}
|
||||||
|
(average subscription duration: ${subscriptionLengthDurationYesterday.days} days${' '}
|
||||||
|
${subscriptionLengthDurationYesterday.hours} hours ${subscriptionLengthDurationYesterday.minutes} minutes,
|
||||||
|
average remaining subscription percentage: ${subscriptionRemainingTimePercentageYesterday}%)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions reactivated:${' '}
|
||||||
|
<b>
|
||||||
|
${subscriptionReactivatedOverTime?.counts[
|
||||||
|
subscriptionReactivatedOverTime?.counts.length - 1
|
||||||
|
]?.totalCount.toLocaleString('en-US')}
|
||||||
|
</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Average time from registration to subscription purchase:${' '}
|
||||||
|
<b>
|
||||||
|
${registrationToSubscriptionDurationYesterday.days} days${' '}
|
||||||
|
${registrationToSubscriptionDurationYesterday.hours} hours${' '}
|
||||||
|
${registrationToSubscriptionDurationYesterday.minutes} minutes
|
||||||
|
</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
<strong>Here are some statistics from last 30 days:</strong>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Active Users (Average)</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Total:</b> ${totalActiveUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>By Subscription Type:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>FREE:</b> ${totalActiveFreeUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PLUS:</b> ${totalActivePlusUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>PRO:</b> ${totalActiveProUsers.last30Days.toLocaleString('en-US')}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Payments (This Month)</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Revenue: <b>$${revenueThisMonth.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Income: <b>$${incomeThisMonth.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Refunds: <b>$${refundsThisMonth.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Income Breakdown (This Month)</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Plus Subscription:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
|
||||||
|
<b>$${plusSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${plusSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
|
||||||
|
<b>$${plusSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
|
||||||
|
<b>$${plusSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
|
||||||
|
<b>$${plusSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Pro Subscription:</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>${proSubscriptionsInitialMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>initial</i> payments on <u>monhtly</u> plan, totaling${' '}
|
||||||
|
<b>$${proSubscriptionsInitialMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${proSubscriptionsInitialAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>initial</i> payments on <u>annual</u> plan, totaling${' '}
|
||||||
|
<b>$${proSubscriptionsInitialAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>renewing</i> payments on <u>monthly</u> plan, totaling${' '}
|
||||||
|
<b>$${proSubscriptionsRenewingMonthlyPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>${proSubscriptionsRenewingAnnualPaymentsThisMonth?.increments.toLocaleString('en-US')}</b>${' '}
|
||||||
|
<i>renewing</i> payments on <u>annual</u> plan, totaling${' '}
|
||||||
|
<b>$${proSubscriptionsRenewingAnnualPaymentsThisMonth?.totalValue.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Users</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Number of users registered: <b>${userRegistrationOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of users unregistered: <b>${userDeletionOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Average account duration this month:${' '}
|
||||||
|
<b>
|
||||||
|
${registrationLengthDurationThisMonth.days} days ${registrationLengthDurationThisMonth.hours} hours${' '}
|
||||||
|
${registrationLengthDurationThisMonth.minutes} minutes
|
||||||
|
</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Subscriptions</b>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions purchased:${' '}
|
||||||
|
<b>${subscriptionPurchasingOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions renewed:${' '}
|
||||||
|
<b>${subscriptionRenewingOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions refunded:${' '}
|
||||||
|
<b>${subscriptionRefundingOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions cancelled:${' '}
|
||||||
|
<b>${subscriptionCancelledOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Number of subscriptions reactivated:${' '}
|
||||||
|
<b>${subscriptionReactivatedOverTime?.totalCount.toLocaleString('en-US')}</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Average subscription duration this month:${' '}
|
||||||
|
<b>
|
||||||
|
${subscriptionLengthDurationThisMonth.days} days ${subscriptionLengthDurationThisMonth.hours} hours${' '}
|
||||||
|
${subscriptionLengthDurationThisMonth.minutes} minutes
|
||||||
|
</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Average subscription remaining percentage this month:${' '}
|
||||||
|
<b>${subscriptionRemainingTimePercentageThisMonth}%</b>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Average time from registration to subscription purchase this month:${' '}
|
||||||
|
<b>
|
||||||
|
${registrationToSubscriptionDurationThisMonth.days} days${' '}
|
||||||
|
${registrationToSubscriptionDurationThisMonth.hours} hours${' '}
|
||||||
|
${registrationToSubscriptionDurationThisMonth.minutes} minutes
|
||||||
|
</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
<strong>Here is the MRR Monthly chart this year:</strong>
|
||||||
|
</p>
|
||||||
|
<img src=${chartUrls.mrrMonthly}></img>
|
||||||
|
<p>
|
||||||
|
<strong>Here is the subscription chart over 30 days:</strong>
|
||||||
|
</p>
|
||||||
|
<img src=${chartUrls.subscriptions}></img>
|
||||||
|
<p>
|
||||||
|
<strong>Here is the users chart over 30 days:</strong>
|
||||||
|
</p>
|
||||||
|
<img src=${chartUrls.users}></img>
|
||||||
|
<p>
|
||||||
|
<strong>Here is the monthly churn rate percentage:</strong>
|
||||||
|
</p>
|
||||||
|
<p>✅ GREAT! Up to 7% 🔶 OKAY: 8-10% 🩸 BAD: 11 -15 % 🚨 TERRIBLE! 16-20%</p>
|
||||||
|
<p>Churn is calculated by the following formula:</p>
|
||||||
|
<p>
|
||||||
|
( Existing Customers Churn [${thisMonthChurn?.existingCustomersChurn}] + New Customers Churn [
|
||||||
|
${thisMonthChurn?.newCustomersChurn}] ) * 100 / Average Customers Count This Month [
|
||||||
|
${thisMonthChurn?.averageCustomersCount}]
|
||||||
|
</p>
|
||||||
|
<img src=${chartUrls.churn}></img>
|
||||||
|
<p>
|
||||||
|
<strong>Here is quarterly performance chart:</strong>
|
||||||
|
</p>
|
||||||
|
<img src=${chartUrls.quarterlyPerformance}></img>
|
||||||
|
<p>Thanks,SN</p>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
@@ -18,5 +18,5 @@ export class AnalyticsEntity {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
@Index('email')
|
@Index('email')
|
||||||
declare userEmail: string
|
declare username: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
|
|
||||||
import { DomainEventService, DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
|
import { DomainEventService, EmailRequestedEvent } from '@standardnotes/domain-events'
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable } from 'inversify'
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
@@ -9,55 +9,20 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
|
|||||||
@injectable()
|
@injectable()
|
||||||
export class DomainEventFactory implements DomainEventFactoryInterface {
|
export class DomainEventFactory implements DomainEventFactoryInterface {
|
||||||
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
|
||||||
|
createEmailRequestedEvent(dto: {
|
||||||
createDailyAnalyticsReportGeneratedEvent(dto: {
|
userEmail: string
|
||||||
activityStatistics: Array<{
|
messageIdentifier: string
|
||||||
name: string
|
level: string
|
||||||
retention: number
|
body: string
|
||||||
totalCount: number
|
subject: string
|
||||||
}>
|
}): EmailRequestedEvent {
|
||||||
statisticMeasures: Array<{
|
|
||||||
name: string
|
|
||||||
totalValue: number
|
|
||||||
average: number
|
|
||||||
increments: number
|
|
||||||
period: number
|
|
||||||
}>
|
|
||||||
activityStatisticsOverTime: Array<{
|
|
||||||
name: string
|
|
||||||
period: number
|
|
||||||
counts: Array<{
|
|
||||||
periodKey: string
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
statisticsOverTime: Array<{
|
|
||||||
name: string
|
|
||||||
period: number
|
|
||||||
counts: Array<{
|
|
||||||
periodKey: string
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
}>
|
|
||||||
churn: {
|
|
||||||
periodKeys: Array<string>
|
|
||||||
values: Array<{
|
|
||||||
rate: number
|
|
||||||
periodKey: string
|
|
||||||
averageCustomersCount: number
|
|
||||||
existingCustomersChurn: number
|
|
||||||
newCustomersChurn: number
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
}): DailyAnalyticsReportGeneratedEvent {
|
|
||||||
return {
|
return {
|
||||||
type: 'DAILY_ANALYTICS_REPORT_GENERATED',
|
type: 'EMAIL_REQUESTED',
|
||||||
createdAt: this.timer.getUTCDate(),
|
createdAt: this.timer.getUTCDate(),
|
||||||
meta: {
|
meta: {
|
||||||
correlation: {
|
correlation: {
|
||||||
userIdentifier: '',
|
userIdentifier: dto.userEmail,
|
||||||
userIdentifierType: 'uuid',
|
userIdentifierType: 'email',
|
||||||
},
|
},
|
||||||
origin: DomainEventService.Analytics,
|
origin: DomainEventService.Analytics,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,45 +1,11 @@
|
|||||||
import { DailyAnalyticsReportGeneratedEvent } from '@standardnotes/domain-events'
|
import { EmailRequestedEvent } from '@standardnotes/domain-events'
|
||||||
|
|
||||||
export interface DomainEventFactoryInterface {
|
export interface DomainEventFactoryInterface {
|
||||||
createDailyAnalyticsReportGeneratedEvent(dto: {
|
createEmailRequestedEvent(dto: {
|
||||||
activityStatistics: Array<{
|
userEmail: string
|
||||||
name: string
|
messageIdentifier: string
|
||||||
retention: number
|
level: string
|
||||||
totalCount: number
|
body: string
|
||||||
}>
|
subject: string
|
||||||
statisticMeasures: Array<{
|
}): EmailRequestedEvent
|
||||||
name: string
|
|
||||||
totalValue: number
|
|
||||||
average: number
|
|
||||||
increments: number
|
|
||||||
period: number
|
|
||||||
}>
|
|
||||||
activityStatisticsOverTime: Array<{
|
|
||||||
name: string
|
|
||||||
period: number
|
|
||||||
counts: Array<{
|
|
||||||
periodKey: string
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
statisticsOverTime: Array<{
|
|
||||||
name: string
|
|
||||||
period: number
|
|
||||||
counts: Array<{
|
|
||||||
periodKey: string
|
|
||||||
totalCount: number
|
|
||||||
}>
|
|
||||||
}>
|
|
||||||
churn: {
|
|
||||||
periodKeys: Array<string>
|
|
||||||
values: Array<{
|
|
||||||
rate: number
|
|
||||||
periodKey: string
|
|
||||||
averageCustomersCount: number
|
|
||||||
existingCustomersChurn: number
|
|
||||||
newCustomersChurn: number
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
}): DailyAnalyticsReportGeneratedEvent
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events'
|
|
||||||
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
|
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
|
||||||
|
|
||||||
describe('AccountDeletionRequestedEventHandler', () => {
|
|
||||||
let event: AccountDeletionRequestedEvent
|
|
||||||
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let timer: TimerInterface
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new AccountDeletionRequestedEventHandler(analyticsEntityRepository, analyticsStore, statisticsStore, timer)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.payload = {
|
|
||||||
userUuid: '1-2-3',
|
|
||||||
userCreatedAtTimestamp: 1,
|
|
||||||
regularSubscriptionUuid: '2-3-4',
|
|
||||||
}
|
|
||||||
|
|
||||||
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
|
|
||||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue({ id: 3 })
|
|
||||||
analyticsEntityRepository.remove = jest.fn()
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
timer = {} as jest.Mocked<TimerInterface>
|
|
||||||
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark account deletion and registration length', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['DeleteAccount'], 3, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith('registration-length', 122, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
expect(analyticsEntityRepository.remove).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not mark anything if entity is not found', async () => {
|
|
||||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).not.toHaveBeenCalled()
|
|
||||||
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
|
|
||||||
expect(analyticsEntityRepository.remove).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
|
||||||
import { TimerInterface } from '@standardnotes/time'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
|||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.Timer) private timer: TimerInterface,
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
|
||||||
@@ -33,12 +35,19 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
|
|||||||
])
|
])
|
||||||
|
|
||||||
const registrationLength = this.timer.getTimestampInMicroseconds() - event.payload.userCreatedAtTimestamp
|
const registrationLength = this.timer.getTimestampInMicroseconds() - event.payload.userCreatedAtTimestamp
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.RegistrationLength, registrationLength, [
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.RegistrationLength, registrationLength, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
await this.analyticsEntityRepository.remove(analyticsEntity)
|
await this.analyticsEntityRepository.remove(analyticsEntity)
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsEntity.id.toString(),
|
||||||
|
user_created_at: this.timer.convertMicrosecondsToDate(event.payload.userCreatedAtTimestamp),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { PaymentFailedEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { PaymentFailedEventHandler } from './PaymentFailedEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
|
|
||||||
describe('PaymentFailedEventHandler', () => {
|
|
||||||
let event: PaymentFailedEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
|
|
||||||
const createHandler = () => new PaymentFailedEventHandler(getUserAnalyticsId, analyticsStore)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<PaymentFailedEvent>
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark payment failed for analytics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
@@ -12,6 +13,7 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
|||||||
constructor(
|
constructor(
|
||||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: PaymentFailedEvent): Promise<void> {
|
async handle(event: PaymentFailedEvent): Promise<void> {
|
||||||
@@ -21,5 +23,11 @@ export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
|||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { PaymentSuccessEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { PaymentSuccessEventHandler } from './PaymentSuccessEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
|
|
||||||
describe('PaymentSuccessEventHandler', () => {
|
|
||||||
let event: PaymentSuccessEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new PaymentSuccessEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, logger)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<PaymentSuccessEvent>
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
amount: 12.45,
|
|
||||||
billingFrequency: 12,
|
|
||||||
paymentType: 'initial',
|
|
||||||
subscriptionName: 'PRO_PLAN',
|
|
||||||
}
|
|
||||||
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.warn = jest.fn()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark payment success for analytics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
'pro-subscription-initial-annual-payments-income',
|
|
||||||
12.45,
|
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark non-detailed payment success statistics for analytics', async () => {
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
amount: 12.45,
|
|
||||||
billingFrequency: 13,
|
|
||||||
paymentType: 'initial',
|
|
||||||
subscriptionName: 'PRO_PLAN',
|
|
||||||
}
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).toBeCalledTimes(1)
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(1, 'income', 12.45, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
|
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
|
||||||
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
@@ -20,15 +21,27 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
[
|
[
|
||||||
PaymentType.Initial,
|
PaymentType.Initial,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionInitialAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
PaymentType.Renewal,
|
PaymentType.Renewal,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.PlusSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
@@ -39,15 +52,27 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
[
|
[
|
||||||
PaymentType.Initial,
|
PaymentType.Initial,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionInitialMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionInitialAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
PaymentType.Renewal,
|
PaymentType.Renewal,
|
||||||
new Map([
|
new Map([
|
||||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome],
|
[
|
||||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome],
|
SubscriptionBillingFrequency.Monthly,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingMonthlyPaymentsIncome,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
SubscriptionBillingFrequency.Annual,
|
||||||
|
StatisticMeasureName.NAMES.ProSubscriptionRenewingAnnualPaymentsIncome,
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
@@ -59,6 +84,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: PaymentSuccessEvent): Promise<void> {
|
async handle(event: PaymentSuccessEvent): Promise<void> {
|
||||||
@@ -69,7 +95,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
const statisticMeasures = [StatisticsMeasure.Income]
|
const statisticMeasures = [StatisticMeasureName.NAMES.Income]
|
||||||
|
|
||||||
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
|
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
|
||||||
?.get(event.payload.paymentType as PaymentType)
|
?.get(event.payload.paymentType as PaymentType)
|
||||||
@@ -89,5 +115,19 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
|||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
amount: event.payload.amount,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
payment_type: event.payload.paymentType,
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.track_charge(analyticsId.toString(), event.payload.amount)
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { RefundProcessedEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { RefundProcessedEventHandler } from './RefundProcessedEventHandler'
|
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
|
|
||||||
describe('RefundProcessedEventHandler', () => {
|
|
||||||
let event: RefundProcessedEvent
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
|
|
||||||
const createHandler = () => new RefundProcessedEventHandler(statisticsStore)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<RefundProcessedEvent>
|
|
||||||
event.payload = {
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
amount: 12.45,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark refunds for statistics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.Refunds, 12.45, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,20 +1,36 @@
|
|||||||
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, RefundProcessedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
|
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
|
||||||
constructor(@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface) {}
|
constructor(
|
||||||
|
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||||
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
) {}
|
||||||
|
|
||||||
async handle(event: RefundProcessedEvent): Promise<void> {
|
async handle(event: RefundProcessedEvent): Promise<void> {
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.Refunds, event.payload.amount, [
|
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||||
|
|
||||||
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
amount: event.payload.amount,
|
||||||
|
})
|
||||||
|
this.mixpanelClient.people.track_charge(analyticsId.toString(), -event.payload.amount)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { DomainEventHandlerInterface, StatisticPersistenceRequestedEvent } from '@standardnotes/domain-events'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
import { PersistStatistic } from '../UseCase/PersistStatistic/PersistStatistic'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
|
export class StatisticPersistenceRequestedEventHandler implements DomainEventHandlerInterface {
|
||||||
|
constructor(
|
||||||
|
private persistStatistic: PersistStatistic,
|
||||||
|
private timer: TimerInterface,
|
||||||
|
private logger: Logger,
|
||||||
|
private mixpanelClient: Mixpanel | null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async handle(event: StatisticPersistenceRequestedEvent): Promise<void> {
|
||||||
|
const result = await this.persistStatistic.execute({
|
||||||
|
date: this.timer.convertMicrosecondsToDate(event.payload.date),
|
||||||
|
statisticMeasureName: event.payload.statisticMeasureName,
|
||||||
|
value: event.payload.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.isFailed()) {
|
||||||
|
this.logger.error(result.getError())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: 'global-stats',
|
||||||
|
statistic: event.payload.statisticMeasureName,
|
||||||
|
value: event.payload.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
import { SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { SubscriptionCancelledEventHandler } from './SubscriptionCancelledEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionCancelledEventHandler', () => {
|
|
||||||
let event: SubscriptionCancelledEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionCancelledEventHandler(
|
|
||||||
getUserAnalyticsId,
|
|
||||||
analyticsStore,
|
|
||||||
statisticsStore,
|
|
||||||
saveRevenueModification,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionCancelledEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_CANCELLED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.ProPlan,
|
|
||||||
subscriptionCreatedAt: 1642395451515000,
|
|
||||||
subscriptionUpdatedAt: 1642395451515001,
|
|
||||||
lastPayedAt: 1642395451515001,
|
|
||||||
subscriptionEndsAt: 1642395451515000 + 10,
|
|
||||||
timestamp: 1,
|
|
||||||
offline: false,
|
|
||||||
replaced: false,
|
|
||||||
userExistingSubscriptionsCount: 1,
|
|
||||||
billingFrequency: 1,
|
|
||||||
payAmount: 12.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should track subscription cancelled statistics', async () => {
|
|
||||||
event.payload.timestamp = 1642395451516000
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalledWith(StatisticsMeasure.SubscriptionLength, 1000, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not track statistics for subscriptions that are in a legacy 5 year plan', async () => {
|
|
||||||
event.payload.timestamp = 1642395451516000
|
|
||||||
event.payload.subscriptionEndsAt = 1642395451515000 + 126_230_400_000_001
|
|
||||||
event.payload.subscriptionCreatedAt = 1642395451515000
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
event.payload.timestamp = 1642395451516000
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
import { Email } from '@standardnotes/domain-core'
|
import { Username } from '@standardnotes/domain-core'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
||||||
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||||
@@ -22,6 +24,8 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||||
@@ -41,7 +45,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
payedAmount: event.payload.payAmount,
|
payedAmount: event.payload.payAmount,
|
||||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||||
subscriptionId: event.payload.subscriptionId,
|
subscriptionId: event.payload.subscriptionId,
|
||||||
userEmail: Email.create(event.payload.userEmail).getValue(),
|
username: Username.create(event.payload.userEmail).getValue(),
|
||||||
userUuid,
|
userUuid,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -50,6 +54,22 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
subscription_created_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionCreatedAt),
|
||||||
|
subscription_updated_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionUpdatedAt),
|
||||||
|
last_payed_at: this.timer.convertMicrosecondsToDate(event.payload.lastPayedAt),
|
||||||
|
subscription_ends_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionEndsAt),
|
||||||
|
offline: event.payload.offline,
|
||||||
|
replaced: event.payload.replaced,
|
||||||
|
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
|
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
|
||||||
@@ -58,7 +78,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt
|
const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.SubscriptionLength, subscriptionLength, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
@@ -70,7 +90,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
|
|||||||
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
||||||
|
|
||||||
await this.statisticsStore.incrementMeasure(
|
await this.statisticsStore.incrementMeasure(
|
||||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
StatisticMeasureName.NAMES.RemainingSubscriptionTimePercentage,
|
||||||
remainingSubscriptionPercentage,
|
remainingSubscriptionPercentage,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionExpiredEvent } from '@standardnotes/domain-events'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
|
|
||||||
import { SubscriptionExpiredEventHandler } from './SubscriptionExpiredEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionExpiredEventHandler', () => {
|
|
||||||
let event: SubscriptionExpiredEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionExpiredEventHandler(
|
|
||||||
getUserAnalyticsId,
|
|
||||||
analyticsStore,
|
|
||||||
statisticsStore,
|
|
||||||
saveRevenueModification,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionExpiredEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_EXPIRED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.PlusPlan,
|
|
||||||
timestamp: 1,
|
|
||||||
offline: false,
|
|
||||||
totalActiveSubscriptionsCount: 123,
|
|
||||||
userExistingSubscriptionsCount: 2,
|
|
||||||
billingFrequency: 1,
|
|
||||||
payAmount: 12.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.setMeasure = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update analytics and statistics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
expect(statisticsStore.setMeasure).toHaveBeenCalled()
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { Username } from '@standardnotes/domain-core'
|
||||||
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
import { Email } from '@standardnotes/domain-core'
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
@@ -22,6 +23,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
|||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
async handle(event: SubscriptionExpiredEvent): Promise<void> {
|
||||||
@@ -33,7 +35,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
|||||||
)
|
)
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(
|
await this.statisticsStore.setMeasure(
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
event.payload.totalActiveSubscriptionsCount,
|
event.payload.totalActiveSubscriptionsCount,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||||
)
|
)
|
||||||
@@ -45,7 +47,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
|||||||
payedAmount: event.payload.payAmount,
|
payedAmount: event.payload.payAmount,
|
||||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||||
subscriptionId: event.payload.subscriptionId,
|
subscriptionId: event.payload.subscriptionId,
|
||||||
userEmail: Email.create(event.payload.userEmail).getValue(),
|
username: Username.create(event.payload.userEmail).getValue(),
|
||||||
userUuid,
|
userUuid,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -54,5 +56,18 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
offline: event.payload.offline,
|
||||||
|
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
|
|
||||||
import { SubscriptionPurchasedEventHandler } from './SubscriptionPurchasedEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionPurchasedEventHandler', () => {
|
|
||||||
let event: SubscriptionPurchasedEvent
|
|
||||||
let subscriptionExpiresAt: number
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionPurchasedEventHandler(
|
|
||||||
getUserAnalyticsId,
|
|
||||||
analyticsStore,
|
|
||||||
statisticsStore,
|
|
||||||
saveRevenueModification,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.incrementMeasure = jest.fn()
|
|
||||||
statisticsStore.setMeasure = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionPurchasedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_PURCHASED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.ProPlan,
|
|
||||||
subscriptionExpiresAt,
|
|
||||||
timestamp: 60,
|
|
||||||
offline: false,
|
|
||||||
discountCode: null,
|
|
||||||
limitedDiscountPurchased: false,
|
|
||||||
newSubscriber: true,
|
|
||||||
totalActiveSubscriptionsCount: 123,
|
|
||||||
userRegisteredAt: 23,
|
|
||||||
billingFrequency: 12,
|
|
||||||
payAmount: 29.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
analyticsStore.unmarkActivity = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark subscription creation statistics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).toHaveBeenCalled()
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should not measure registration to subscription time if this is not user's first subscription", async () => {
|
|
||||||
event.payload.newSubscriber = false
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(statisticsStore.incrementMeasure).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should update analytics on limited discount offer purchasing', async () => {
|
|
||||||
event.payload.limitedDiscountPurchased = true
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['limited-discount-offer-purchased'], 3, [Period.Today])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
|
import { Username } from '@standardnotes/domain-core'
|
||||||
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
import { Email } from '@standardnotes/domain-core'
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
@@ -22,6 +24,8 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
|||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
|
||||||
@@ -45,18 +49,18 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
|||||||
|
|
||||||
if (event.payload.newSubscriber) {
|
if (event.payload.newSubscriber) {
|
||||||
await this.statisticsStore.incrementMeasure(
|
await this.statisticsStore.incrementMeasure(
|
||||||
StatisticsMeasure.RegistrationToSubscriptionTime,
|
StatisticMeasureName.NAMES.RegistrationToSubscriptionTime,
|
||||||
event.payload.timestamp - event.payload.userRegisteredAt,
|
event.payload.timestamp - event.payload.userRegisteredAt,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||||
)
|
)
|
||||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.NewCustomers, 1, [
|
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.NewCustomers, 1, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
])
|
])
|
||||||
await this.statisticsStore.setMeasure(
|
await this.statisticsStore.setMeasure(
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
event.payload.totalActiveSubscriptionsCount,
|
event.payload.totalActiveSubscriptionsCount,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||||
)
|
)
|
||||||
@@ -69,7 +73,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
|||||||
payedAmount: event.payload.payAmount,
|
payedAmount: event.payload.payAmount,
|
||||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||||
subscriptionId: event.payload.subscriptionId,
|
subscriptionId: event.payload.subscriptionId,
|
||||||
userEmail: Email.create(event.payload.userEmail).getValue(),
|
username: Username.create(event.payload.userEmail).getValue(),
|
||||||
userUuid,
|
userUuid,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -78,5 +82,22 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
|
||||||
|
offline: event.payload.offline,
|
||||||
|
discount_code: event.payload.discountCode,
|
||||||
|
limited_discount_purchased: event.payload.limitedDiscountPurchased,
|
||||||
|
new_subscriber: event.payload.newSubscriber,
|
||||||
|
user_registered_at: this.timer.convertMicrosecondsToDate(event.payload.userRegisteredAt),
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
|
||||||
|
|
||||||
import { SubscriptionReactivatedEventHandler } from './SubscriptionReactivatedEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
|
|
||||||
describe('SubscriptionReactivatedEventHandler', () => {
|
|
||||||
let event: SubscriptionReactivatedEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
|
|
||||||
const createHandler = () => new SubscriptionReactivatedEventHandler(analyticsStore, getUserAnalyticsId)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
event = {} as jest.Mocked<SubscriptionReactivatedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.payload = {
|
|
||||||
previousSubscriptionId: 1,
|
|
||||||
currentSubscriptionId: 2,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.PlusPlan,
|
|
||||||
subscriptionExpiresAt: 5,
|
|
||||||
discountCode: 'exit-20',
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark subscription reactivated activity for analytics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['subscription-reactivated'], 3, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionReactivatedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
@@ -12,6 +14,8 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
|
|||||||
constructor(
|
constructor(
|
||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
|
async handle(event: SubscriptionReactivatedEvent): Promise<void> {
|
||||||
@@ -21,5 +25,16 @@ export class SubscriptionReactivatedEventHandler implements DomainEventHandlerIn
|
|||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
|
||||||
|
discount_code: event.payload.discountCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
|
|
||||||
import { SubscriptionRefundedEventHandler } from './SubscriptionRefundedEventHandler'
|
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionRefundedEventHandler', () => {
|
|
||||||
let event: SubscriptionRefundedEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let statisticsStore: StatisticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionRefundedEventHandler(
|
|
||||||
getUserAnalyticsId,
|
|
||||||
analyticsStore,
|
|
||||||
statisticsStore,
|
|
||||||
saveRevenueModification,
|
|
||||||
logger,
|
|
||||||
)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionRefundedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_REFUNDED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.PlusPlan,
|
|
||||||
timestamp: 1,
|
|
||||||
offline: false,
|
|
||||||
userExistingSubscriptionsCount: 3,
|
|
||||||
totalActiveSubscriptionsCount: 1,
|
|
||||||
billingFrequency: 1,
|
|
||||||
payAmount: 12.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(true)
|
|
||||||
|
|
||||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
|
||||||
statisticsStore.setMeasure = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark churn for new customer', async () => {
|
|
||||||
event.payload.userExistingSubscriptionsCount = 1
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.SubscriptionRefunded], 3, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.NewCustomersChurn], 3, [
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should mark churn for existing customer', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not mark churn if customer did not purchase subscription in defined analytic periods', async () => {
|
|
||||||
analyticsStore.wasActivityDone = jest.fn().mockReturnValue(false)
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).not.toHaveBeenCalledWith([AnalyticsActivity.ExistingCustomersChurn], 3, [
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { Username } from '@standardnotes/domain-core'
|
||||||
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
import { Email } from '@standardnotes/domain-core'
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
@@ -22,6 +23,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
|||||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
async handle(event: SubscriptionRefundedEvent): Promise<void> {
|
||||||
@@ -41,7 +43,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
|||||||
payedAmount: event.payload.payAmount,
|
payedAmount: event.payload.payAmount,
|
||||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||||
subscriptionId: event.payload.subscriptionId,
|
subscriptionId: event.payload.subscriptionId,
|
||||||
userEmail: Email.create(event.payload.userEmail).getValue(),
|
username: Username.create(event.payload.userEmail).getValue(),
|
||||||
userUuid,
|
userUuid,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -50,6 +52,18 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
|
||||||
|
offline: event.payload.offline,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', 'free')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async markChurnActivity(analyticsId: number, event: SubscriptionRefundedEvent): Promise<void> {
|
private async markChurnActivity(analyticsId: number, event: SubscriptionRefundedEvent): Promise<void> {
|
||||||
@@ -70,7 +84,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.statisticsStore.setMeasure(
|
await this.statisticsStore.setMeasure(
|
||||||
StatisticsMeasure.TotalCustomers,
|
StatisticMeasureName.NAMES.TotalCustomers,
|
||||||
event.payload.totalActiveSubscriptionsCount,
|
event.payload.totalActiveSubscriptionsCount,
|
||||||
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
[Period.Today, Period.ThisWeek, Period.ThisMonth, Period.ThisYear],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { SubscriptionName } from '@standardnotes/common'
|
|
||||||
import { SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
|
||||||
import { Result } from '@standardnotes/domain-core'
|
|
||||||
|
|
||||||
import { SubscriptionRenewedEventHandler } from './SubscriptionRenewedEventHandler'
|
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
|
|
||||||
import { RevenueModification } from '../Revenue/RevenueModification'
|
|
||||||
import { Logger } from 'winston'
|
|
||||||
|
|
||||||
describe('SubscriptionRenewedEventHandler', () => {
|
|
||||||
let event: SubscriptionRenewedEvent
|
|
||||||
let getUserAnalyticsId: GetUserAnalyticsId
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
let saveRevenueModification: SaveRevenueModification
|
|
||||||
let logger: Logger
|
|
||||||
|
|
||||||
const createHandler = () =>
|
|
||||||
new SubscriptionRenewedEventHandler(getUserAnalyticsId, analyticsStore, saveRevenueModification, logger)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = {} as jest.Mocked<Logger>
|
|
||||||
logger.error = jest.fn()
|
|
||||||
|
|
||||||
event = {} as jest.Mocked<SubscriptionRenewedEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.type = 'SUBSCRIPTION_RENEWED'
|
|
||||||
event.payload = {
|
|
||||||
subscriptionId: 1,
|
|
||||||
userEmail: 'test@test.com',
|
|
||||||
subscriptionName: SubscriptionName.ProPlan,
|
|
||||||
subscriptionExpiresAt: 2,
|
|
||||||
timestamp: 1,
|
|
||||||
offline: false,
|
|
||||||
billingFrequency: 1,
|
|
||||||
payAmount: 12.99,
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
|
||||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
analyticsStore.unmarkActivity = jest.fn()
|
|
||||||
|
|
||||||
saveRevenueModification = {} as jest.Mocked<SaveRevenueModification>
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.ok<RevenueModification>())
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should track subscription renewed statistics', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
|
||||||
expect(analyticsStore.unmarkActivity).toHaveBeenCalled()
|
|
||||||
expect(saveRevenueModification.execute).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should log failure to save revenue modification', async () => {
|
|
||||||
saveRevenueModification.execute = jest.fn().mockReturnValue(Result.fail('Oops'))
|
|
||||||
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(logger.error).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
import { Email } from '@standardnotes/domain-core'
|
import { Username } from '@standardnotes/domain-core'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||||
@@ -11,6 +12,7 @@ import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/Save
|
|||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
import { SubscriptionPlanName } from '../Subscription/SubscriptionPlanName'
|
||||||
import { Logger } from 'winston'
|
import { Logger } from 'winston'
|
||||||
|
import { TimerInterface } from '@standardnotes/time'
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
|
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
|
||||||
@@ -19,6 +21,8 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
|||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
|
||||||
@inject(TYPES.Logger) private logger: Logger,
|
@inject(TYPES.Logger) private logger: Logger,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
|
@inject(TYPES.Timer) private timer: TimerInterface,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
||||||
@@ -41,7 +45,7 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
|||||||
payedAmount: event.payload.payAmount,
|
payedAmount: event.payload.payAmount,
|
||||||
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
|
||||||
subscriptionId: event.payload.subscriptionId,
|
subscriptionId: event.payload.subscriptionId,
|
||||||
userEmail: Email.create(event.payload.userEmail).getValue(),
|
username: Username.create(event.payload.userEmail).getValue(),
|
||||||
userUuid,
|
userUuid,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -50,5 +54,17 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
|
|||||||
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsId.toString(),
|
||||||
|
subscription_name: event.payload.subscriptionName,
|
||||||
|
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
|
||||||
|
offline: event.payload.offline,
|
||||||
|
billing_frequency: event.payload.billingFrequency,
|
||||||
|
pay_amount: event.payload.payAmount,
|
||||||
|
})
|
||||||
|
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import 'reflect-metadata'
|
|
||||||
|
|
||||||
import { UserRegisteredEvent } from '@standardnotes/domain-events'
|
|
||||||
import { ProtocolVersion } from '@standardnotes/common'
|
|
||||||
|
|
||||||
import { UserRegisteredEventHandler } from './UserRegisteredEventHandler'
|
|
||||||
import { AnalyticsEntityRepositoryInterface } from '../Entity/AnalyticsEntityRepositoryInterface'
|
|
||||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
|
||||||
import { Period } from '../Time/Period'
|
|
||||||
|
|
||||||
describe('UserRegisteredEventHandler', () => {
|
|
||||||
let analyticsEntityRepository: AnalyticsEntityRepositoryInterface
|
|
||||||
let event: UserRegisteredEvent
|
|
||||||
let analyticsStore: AnalyticsStoreInterface
|
|
||||||
|
|
||||||
const createHandler = () => new UserRegisteredEventHandler(analyticsEntityRepository, analyticsStore)
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
event = {} as jest.Mocked<UserRegisteredEvent>
|
|
||||||
event.createdAt = new Date(1)
|
|
||||||
event.payload = {
|
|
||||||
userUuid: '1-2-3',
|
|
||||||
email: 'test@test.te',
|
|
||||||
protocolVersion: ProtocolVersion.V004,
|
|
||||||
}
|
|
||||||
|
|
||||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
|
||||||
analyticsStore.markActivity = jest.fn()
|
|
||||||
|
|
||||||
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
|
|
||||||
analyticsEntityRepository.save = jest.fn().mockImplementation((entity) => ({
|
|
||||||
...entity,
|
|
||||||
id: 1,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should save analytics entity upon user registration', async () => {
|
|
||||||
await createHandler().handle(event)
|
|
||||||
|
|
||||||
expect(analyticsEntityRepository.save).toHaveBeenCalled()
|
|
||||||
expect(analyticsStore.markActivity).toHaveBeenCalledWith(['register'], 1, [
|
|
||||||
Period.Today,
|
|
||||||
Period.ThisWeek,
|
|
||||||
Period.ThisMonth,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
|
import { DomainEventHandlerInterface, UserRegisteredEvent } from '@standardnotes/domain-events'
|
||||||
import { inject, injectable } from 'inversify'
|
import { inject, injectable, optional } from 'inversify'
|
||||||
|
import { Mixpanel } from 'mixpanel'
|
||||||
|
|
||||||
import TYPES from '../../Bootstrap/Types'
|
import TYPES from '../../Bootstrap/Types'
|
||||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||||
@@ -13,12 +14,13 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
|||||||
constructor(
|
constructor(
|
||||||
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
@inject(TYPES.AnalyticsEntityRepository) private analyticsEntityRepository: AnalyticsEntityRepositoryInterface,
|
||||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||||
|
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handle(event: UserRegisteredEvent): Promise<void> {
|
async handle(event: UserRegisteredEvent): Promise<void> {
|
||||||
let analyticsEntity = new AnalyticsEntity()
|
let analyticsEntity = new AnalyticsEntity()
|
||||||
analyticsEntity.userUuid = event.payload.userUuid
|
analyticsEntity.userUuid = event.payload.userUuid
|
||||||
analyticsEntity.userEmail = event.payload.email
|
analyticsEntity.username = event.payload.email
|
||||||
analyticsEntity = await this.analyticsEntityRepository.save(analyticsEntity)
|
analyticsEntity = await this.analyticsEntityRepository.save(analyticsEntity)
|
||||||
|
|
||||||
await this.analyticsStore.markActivity([AnalyticsActivity.Register], analyticsEntity.id, [
|
await this.analyticsStore.markActivity([AnalyticsActivity.Register], analyticsEntity.id, [
|
||||||
@@ -26,5 +28,17 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
|||||||
Period.ThisWeek,
|
Period.ThisWeek,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (this.mixpanelClient !== null) {
|
||||||
|
this.mixpanelClient.track(event.type, {
|
||||||
|
distinct_id: analyticsEntity.id.toString(),
|
||||||
|
protocol_version: event.payload.protocolVersion,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mixpanelClient.people.set(analyticsEntity.id.toString(), {
|
||||||
|
subscription: 'free',
|
||||||
|
protocol_version: event.payload.protocolVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { injectable } from 'inversify'
|
import { injectable } from 'inversify'
|
||||||
import { Email, MapperInterface, UniqueEntityId } from '@standardnotes/domain-core'
|
import { MapperInterface, UniqueEntityId, Username } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import { TypeORMRevenueModification } from '../../Infra/TypeORM/TypeORMRevenueModification'
|
import { TypeORMRevenueModification } from '../../Infra/TypeORM/TypeORMRevenueModification'
|
||||||
import { MonthlyRevenue } from '../Revenue/MonthlyRevenue'
|
import { MonthlyRevenue } from '../Revenue/MonthlyRevenue'
|
||||||
@@ -14,7 +14,7 @@ export class RevenueModificationMap implements MapperInterface<RevenueModificati
|
|||||||
toDomain(persistence: TypeORMRevenueModification): RevenueModification {
|
toDomain(persistence: TypeORMRevenueModification): RevenueModification {
|
||||||
const userOrError = User.create(
|
const userOrError = User.create(
|
||||||
{
|
{
|
||||||
email: Email.create(persistence.userEmail).getValue(),
|
username: Username.create(persistence.username).getValue(),
|
||||||
},
|
},
|
||||||
new UniqueEntityId(persistence.userUuid),
|
new UniqueEntityId(persistence.userUuid),
|
||||||
)
|
)
|
||||||
@@ -70,7 +70,7 @@ export class RevenueModificationMap implements MapperInterface<RevenueModificati
|
|||||||
persistence.previousMonthlyRevenue = domain.props.previousMonthlyRevenue.value
|
persistence.previousMonthlyRevenue = domain.props.previousMonthlyRevenue.value
|
||||||
persistence.subscriptionId = subscription.id.toValue() as number
|
persistence.subscriptionId = subscription.id.toValue() as number
|
||||||
persistence.subscriptionPlan = subscription.props.planName.value
|
persistence.subscriptionPlan = subscription.props.planName.value
|
||||||
persistence.userEmail = user.props.email.value
|
persistence.username = user.props.username.value
|
||||||
persistence.userUuid = user.id.toString()
|
persistence.userUuid = user.id.toString()
|
||||||
persistence.createdAt = domain.props.createdAt
|
persistence.createdAt = domain.props.createdAt
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Email } from '@standardnotes/domain-core'
|
import { Username } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import { Subscription } from '../Subscription/Subscription'
|
import { Subscription } from '../Subscription/Subscription'
|
||||||
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
|
||||||
@@ -19,7 +19,7 @@ describe('RevenueModification', () => {
|
|||||||
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
|
||||||
}).getValue()
|
}).getValue()
|
||||||
user = User.create({
|
user = User.create({
|
||||||
email: Email.create('test@test.te').getValue(),
|
username: Username.create('test@test.te').getValue(),
|
||||||
}).getValue()
|
}).getValue()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Result, Entity, UniqueEntityId } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { StatisticMeasureProps } from './StatisticMeasureProps'
|
||||||
|
|
||||||
|
export class StatisticMeasure extends Entity<StatisticMeasureProps> {
|
||||||
|
get id(): UniqueEntityId {
|
||||||
|
return this._id
|
||||||
|
}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
return this.props.name.value
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): number {
|
||||||
|
return this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(props: StatisticMeasureProps, id?: UniqueEntityId) {
|
||||||
|
super(props, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(props: StatisticMeasureProps, id?: UniqueEntityId): Result<StatisticMeasure> {
|
||||||
|
return Result.ok<StatisticMeasure>(new StatisticMeasure(props, id))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { StatisticMeasureName } from './StatisticMeasureName'
|
||||||
|
|
||||||
|
describe('StatisticMeasureName', () => {
|
||||||
|
it('should create a value object', () => {
|
||||||
|
const valueOrError = StatisticMeasureName.create('pro-subscription-initial-monthly-payments-income')
|
||||||
|
|
||||||
|
expect(valueOrError.isFailed()).toBeFalsy()
|
||||||
|
expect(valueOrError.getValue().value).toEqual('pro-subscription-initial-monthly-payments-income')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create an invalid value object', () => {
|
||||||
|
for (const value of ['', undefined, null, 0, 'foobar']) {
|
||||||
|
const valueOrError = StatisticMeasureName.create(value as string)
|
||||||
|
|
||||||
|
expect(valueOrError.isFailed()).toBeTruthy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ValueObject, Result } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
import { StatisticMeasureNameProps } from './StatisticMeasureNameProps'
|
||||||
|
|
||||||
|
export class StatisticMeasureName extends ValueObject<StatisticMeasureNameProps> {
|
||||||
|
static readonly NAMES = {
|
||||||
|
Income: 'income',
|
||||||
|
PlusSubscriptionInitialMonthlyPaymentsIncome: 'plus-subscription-initial-monthly-payments-income',
|
||||||
|
ProSubscriptionInitialMonthlyPaymentsIncome: 'pro-subscription-initial-monthly-payments-income',
|
||||||
|
PlusSubscriptionInitialAnnualPaymentsIncome: 'plus-subscription-initial-annual-payments-income',
|
||||||
|
ProSubscriptionInitialAnnualPaymentsIncome: 'pro-subscription-initial-annual-payments-income',
|
||||||
|
PlusSubscriptionRenewingMonthlyPaymentsIncome: 'plus-subscription-renewing-monthly-payments-income',
|
||||||
|
ProSubscriptionRenewingMonthlyPaymentsIncome: 'pro-subscription-renewing-monthly-payments-income',
|
||||||
|
PlusSubscriptionRenewingAnnualPaymentsIncome: 'plus-subscription-renewing-annual-payments-income',
|
||||||
|
ProSubscriptionRenewingAnnualPaymentsIncome: 'pro-subscription-renewing-annual-payments-income',
|
||||||
|
SubscriptionLength: 'subscription-length',
|
||||||
|
RegistrationLength: 'registration-length',
|
||||||
|
RegistrationToSubscriptionTime: 'registration-to-subscription-time',
|
||||||
|
RemainingSubscriptionTimePercentage: 'remaining-subscription-time-percentage',
|
||||||
|
Refunds: 'refunds',
|
||||||
|
NewCustomers: 'new-customers',
|
||||||
|
TotalCustomers: 'total-customers',
|
||||||
|
MRR: 'mrr',
|
||||||
|
MonthlyPlansMRR: 'monthly-plans-mrr',
|
||||||
|
AnnualPlansMRR: 'annual-plans-mrr',
|
||||||
|
FiveYearPlansMRR: 'five-year-plans-mrr',
|
||||||
|
ProPlansMRR: 'pro-plans-mrr',
|
||||||
|
PlusPlansMRR: 'plus-plans-mrr',
|
||||||
|
ActiveUsers: 'active-users',
|
||||||
|
ActiveProUsers: 'active-pro-users',
|
||||||
|
ActivePlusUsers: 'active-plus-users',
|
||||||
|
ActiveFreeUsers: 'active-free-users',
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): string {
|
||||||
|
return this.props.value
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(props: StatisticMeasureNameProps) {
|
||||||
|
super(props)
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(name: string): Result<StatisticMeasureName> {
|
||||||
|
const isValidName = Object.values(this.NAMES).includes(name)
|
||||||
|
if (!isValidName) {
|
||||||
|
return Result.fail<StatisticMeasureName>(`Invalid statistics measure name: ${name}`)
|
||||||
|
} else {
|
||||||
|
return Result.ok<StatisticMeasureName>(new StatisticMeasureName({ value: name }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface StatisticMeasureNameProps {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { StatisticMeasureName } from './StatisticMeasureName'
|
||||||
|
|
||||||
|
export interface StatisticMeasureProps {
|
||||||
|
name: StatisticMeasureName
|
||||||
|
value: number
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { StatisticMeasure } from './StatisticMeasure'
|
||||||
|
|
||||||
|
export interface StatisticMeasureRepositoryInterface {
|
||||||
|
save(statisticMeasure: StatisticMeasure): Promise<void>
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
export enum StatisticsMeasure {
|
|
||||||
Income = 'income',
|
|
||||||
PlusSubscriptionInitialMonthlyPaymentsIncome = 'plus-subscription-initial-monthly-payments-income',
|
|
||||||
ProSubscriptionInitialMonthlyPaymentsIncome = 'pro-subscription-initial-monthly-payments-income',
|
|
||||||
PlusSubscriptionInitialAnnualPaymentsIncome = 'plus-subscription-initial-annual-payments-income',
|
|
||||||
ProSubscriptionInitialAnnualPaymentsIncome = 'pro-subscription-initial-annual-payments-income',
|
|
||||||
PlusSubscriptionRenewingMonthlyPaymentsIncome = 'plus-subscription-renewing-monthly-payments-income',
|
|
||||||
ProSubscriptionRenewingMonthlyPaymentsIncome = 'pro-subscription-renewing-monthly-payments-income',
|
|
||||||
PlusSubscriptionRenewingAnnualPaymentsIncome = 'plus-subscription-renewing-annual-payments-income',
|
|
||||||
ProSubscriptionRenewingAnnualPaymentsIncome = 'pro-subscription-renewing-annual-payments-income',
|
|
||||||
SubscriptionLength = 'subscription-length',
|
|
||||||
RegistrationLength = 'registration-length',
|
|
||||||
RegistrationToSubscriptionTime = 'registration-to-subscription-time',
|
|
||||||
RemainingSubscriptionTimePercentage = 'remaining-subscription-time-percentage',
|
|
||||||
Refunds = 'refunds',
|
|
||||||
NewCustomers = 'new-customers',
|
|
||||||
TotalCustomers = 'total-customers',
|
|
||||||
MRR = 'mrr',
|
|
||||||
MonthlyPlansMRR = 'monthly-plans-mrr',
|
|
||||||
AnnualPlansMRR = 'annual-plans-mrr',
|
|
||||||
FiveYearPlansMRR = 'five-year-plans-mrr',
|
|
||||||
ProPlansMRR = 'pro-plans-mrr',
|
|
||||||
PlusPlansMRR = 'plus-plans-mrr',
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Period } from '../Time/Period'
|
import { Period } from '../Time/Period'
|
||||||
import { StatisticsMeasure } from './StatisticsMeasure'
|
|
||||||
|
|
||||||
export interface StatisticsStoreInterface {
|
export interface StatisticsStoreInterface {
|
||||||
incrementSNJSVersionUsage(snjsVersion: string): Promise<void>
|
incrementSNJSVersionUsage(snjsVersion: string): Promise<void>
|
||||||
@@ -8,13 +7,13 @@ export interface StatisticsStoreInterface {
|
|||||||
getYesterdaySNJSUsage(): Promise<Array<{ version: string; count: number }>>
|
getYesterdaySNJSUsage(): Promise<Array<{ version: string; count: number }>>
|
||||||
getYesterdayApplicationUsage(): Promise<Array<{ version: string; count: number }>>
|
getYesterdayApplicationUsage(): Promise<Array<{ version: string; count: number }>>
|
||||||
getYesterdayOutOfSyncIncidents(): Promise<number>
|
getYesterdayOutOfSyncIncidents(): Promise<number>
|
||||||
incrementMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
|
incrementMeasure(measure: string, value: number, periods: Period[]): Promise<void>
|
||||||
setMeasure(measure: StatisticsMeasure, value: number, periods: Period[]): Promise<void>
|
setMeasure(measure: string, value: number, periods: Period[]): Promise<void>
|
||||||
getMeasureAverage(measure: StatisticsMeasure, period: Period): Promise<number>
|
getMeasureAverage(measure: string, period: Period): Promise<number>
|
||||||
getMeasureTotal(measure: StatisticsMeasure, periodOrPeriodKey: Period | string): Promise<number>
|
getMeasureTotal(measure: string, periodOrPeriodKey: Period | string): Promise<number>
|
||||||
getMeasureIncrementCounts(measure: StatisticsMeasure, period: Period): Promise<number>
|
getMeasureIncrementCounts(measure: string, period: Period): Promise<number>
|
||||||
calculateTotalCountOverPeriod(
|
calculateTotalCountOverPeriod(
|
||||||
measure: StatisticsMeasure,
|
measure: string,
|
||||||
period: Period,
|
period: Period,
|
||||||
): Promise<Array<{ periodKey: string; totalCount: number }>>
|
): Promise<Array<{ periodKey: string; totalCount: number }>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export class PeriodKeyGenerator implements PeriodKeyGeneratorInterface {
|
|||||||
return `${this.getYear(date)}-${this.getMonth(date)}`
|
return `${this.getYear(date)}-${this.getMonth(date)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDailyKey(date?: Date): string {
|
getDailyKey(date?: Date): string {
|
||||||
date = date ?? new Date()
|
date = date ?? new Date()
|
||||||
|
|
||||||
return `${this.getYear(date)}-${this.getMonth(date)}-${this.getDayOfTheMonth(date)}`
|
return `${this.getYear(date)}-${this.getMonth(date)}-${this.getDayOfTheMonth(date)}`
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Period } from './Period'
|
|||||||
|
|
||||||
export interface PeriodKeyGeneratorInterface {
|
export interface PeriodKeyGeneratorInterface {
|
||||||
getPeriodKey(period: Period): string
|
getPeriodKey(period: Period): string
|
||||||
|
getDailyKey(date?: Date): string
|
||||||
convertPeriodKeyToPeriod(periodKey: string): Period
|
convertPeriodKeyToPeriod(periodKey: string): Period
|
||||||
getDiscretePeriodKeys(period: Period): string[]
|
getDiscretePeriodKeys(period: Period): string[]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import 'reflect-metadata'
|
import 'reflect-metadata'
|
||||||
|
|
||||||
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
import { RevenueModificationRepositoryInterface } from '../../Revenue/RevenueModificationRepositoryInterface'
|
||||||
import { StatisticsMeasure } from '../../Statistics/StatisticsMeasure'
|
import { StatisticMeasureName } from '../../Statistics/StatisticMeasureName'
|
||||||
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
import { StatisticsStoreInterface } from '../../Statistics/StatisticsStoreInterface'
|
||||||
import { Period } from '../../Time/Period'
|
import { Period } from '../../Time/Period'
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ describe('CalculateMonthlyRecurringRevenue', () => {
|
|||||||
it('should calculate the MRR diff and persist it as a statistic', async () => {
|
it('should calculate the MRR diff and persist it as a statistic', async () => {
|
||||||
await createUseCase().execute({})
|
await createUseCase().execute({})
|
||||||
|
|
||||||
expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticsMeasure.MRR, 123.45, [
|
expect(statisticsStore.setMeasure).toHaveBeenCalledWith(StatisticMeasureName.NAMES.MRR, 123.45, [
|
||||||
Period.Today,
|
Period.Today,
|
||||||
Period.ThisMonth,
|
Period.ThisMonth,
|
||||||
Period.ThisYear,
|
Period.ThisYear,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user