mirror of
https://github.com/standardnotes/server
synced 2026-01-20 05:04:28 -05:00
Compare commits
10 Commits
@standardn
...
@standardn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7ef6898be | ||
|
|
2aa57f1f0d | ||
|
|
dcc0e38707 | ||
|
|
037fb2398a | ||
|
|
182512d07c | ||
|
|
a3be4b063d | ||
|
|
a97be4c342 | ||
|
|
5902cbb621 | ||
|
|
afc26d42ca | ||
|
|
51b12d05d4 |
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.48.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.47.0...@standardnotes/analytics@1.48.0) (2022-11-07)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add subscription renewed event handler ([2aa57f1](https://github.com/standardnotes/server/commit/2aa57f1f0ddace741b79e9375b87464eaaba6320))
|
||||
|
||||
# [1.47.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.46.0...@standardnotes/analytics@1.47.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add subscription cancelled event handler ([037fb23](https://github.com/standardnotes/server/commit/037fb2398ae9aaa11682e1a8576bab28c69e0f9b))
|
||||
|
||||
# [1.46.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.45.0...@standardnotes/analytics@1.46.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add payment success event handler ([5902cbb](https://github.com/standardnotes/server/commit/5902cbb6218a5b3982b4c56f8d6644f4f5bc6d32))
|
||||
|
||||
# [1.45.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.44.0...@standardnotes/analytics@1.45.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* add payment failed handler and email to analytics entity ([51b12d0](https://github.com/standardnotes/server/commit/51b12d05d49868db1d61313c4d8b3829994e7eb3))
|
||||
|
||||
# [1.44.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@1.43.0...@standardnotes/analytics@1.44.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
||||
|
||||
export class addEmailToAnalyticsEntity1667568051894 implements MigrationInterface {
|
||||
name = 'addEmailToAnalyticsEntity1667568051894'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('ALTER TABLE `analytics_entities` ADD `user_email` varchar(255) NULL')
|
||||
await queryRunner.query('CREATE INDEX `email` ON `analytics_entities` (`user_email`)')
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP INDEX `email` ON `analytics_entities`')
|
||||
await queryRunner.query('ALTER TABLE `analytics_entities` DROP COLUMN `user_email`')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/analytics",
|
||||
"version": "1.44.0",
|
||||
"version": "1.48.0",
|
||||
"engines": {
|
||||
"node": ">=14.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -35,6 +35,10 @@ import { AnalyticsEntity } from '../Domain/Entity/AnalyticsEntity'
|
||||
import { GetUserAnalyticsId } from '../Domain/UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { UserRegisteredEventHandler } from '../Domain/Handler/UserRegisteredEventHandler'
|
||||
import { AccountDeletionRequestedEventHandler } from '../Domain/Handler/AccountDeletionRequestedEventHandler'
|
||||
import { PaymentFailedEventHandler } from '../Domain/Handler/PaymentFailedEventHandler'
|
||||
import { PaymentSuccessEventHandler } from '../Domain/Handler/PaymentSuccessEventHandler'
|
||||
import { SubscriptionCancelledEventHandler } from '../Domain/Handler/SubscriptionCancelledEventHandler'
|
||||
import { SubscriptionRenewedEventHandler } from '../Domain/Handler/SubscriptionRenewedEventHandler'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const newrelicFormatter = require('@newrelic/winston-enricher')
|
||||
@@ -121,6 +125,14 @@ export class ContainerConfigLoader {
|
||||
container
|
||||
.bind<AccountDeletionRequestedEventHandler>(TYPES.AccountDeletionRequestedEventHandler)
|
||||
.to(AccountDeletionRequestedEventHandler)
|
||||
container.bind<PaymentFailedEventHandler>(TYPES.PaymentFailedEventHandler).to(PaymentFailedEventHandler)
|
||||
container.bind<PaymentSuccessEventHandler>(TYPES.PaymentSuccessEventHandler).to(PaymentSuccessEventHandler)
|
||||
container
|
||||
.bind<SubscriptionCancelledEventHandler>(TYPES.SubscriptionCancelledEventHandler)
|
||||
.to(SubscriptionCancelledEventHandler)
|
||||
container
|
||||
.bind<SubscriptionRenewedEventHandler>(TYPES.SubscriptionRenewedEventHandler)
|
||||
.to(SubscriptionRenewedEventHandler)
|
||||
|
||||
// Services
|
||||
container.bind<DomainEventFactory>(TYPES.DomainEventFactory).to(DomainEventFactory)
|
||||
|
||||
@@ -20,6 +20,10 @@ const TYPES = {
|
||||
// Handlers
|
||||
UserRegisteredEventHandler: Symbol.for('UserRegisteredEventHandler'),
|
||||
AccountDeletionRequestedEventHandler: Symbol.for('AccountDeletionRequestedEventHandler'),
|
||||
PaymentFailedEventHandler: Symbol.for('PaymentFailedEventHandler'),
|
||||
PaymentSuccessEventHandler: Symbol.for('PaymentSuccessEventHandler'),
|
||||
SubscriptionCancelledEventHandler: Symbol.for('SubscriptionCancelledEventHandler'),
|
||||
SubscriptionRenewedEventHandler: Symbol.for('SubscriptionRenewedEventHandler'),
|
||||
// Services
|
||||
DomainEventPublisher: Symbol.for('DomainEventPublisher'),
|
||||
DomainEventSubscriberFactory: Symbol.for('DomainEventSubscriberFactory'),
|
||||
|
||||
@@ -11,4 +11,12 @@ export class AnalyticsEntity {
|
||||
})
|
||||
@Index('user_uuid')
|
||||
declare userUuid: string
|
||||
|
||||
@Column({
|
||||
name: 'user_email',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
})
|
||||
@Index('email')
|
||||
declare userEmail: string
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ export interface AnalyticsEntityRepositoryInterface {
|
||||
save(analyticsEntity: AnalyticsEntity): Promise<AnalyticsEntity>
|
||||
remove(analyticsEntity: AnalyticsEntity): Promise<void>
|
||||
findOneByUserUuid(userUuid: Uuid): Promise<AnalyticsEntity | null>
|
||||
findOneByUserEmail(email: string): Promise<AnalyticsEntity | null>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { PaymentFailedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import { PaymentFailedEventHandler } from './PaymentFailedEventHandler'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
|
||||
describe('PaymentFailedEventHandler', () => {
|
||||
let event: PaymentFailedEvent
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
|
||||
const createHandler = () => new PaymentFailedEventHandler(getUserAnalyticsId, analyticsStore)
|
||||
|
||||
beforeEach(() => {
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<PaymentFailedEvent>
|
||||
event.payload = {
|
||||
userEmail: 'test@test.com',
|
||||
}
|
||||
})
|
||||
|
||||
it('should mark payment failed for analytics', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DomainEventHandlerInterface, PaymentFailedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
@injectable()
|
||||
export class PaymentFailedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentFailedEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentFailed], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { PaymentSuccessEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import { PaymentSuccessEventHandler } from './PaymentSuccessEventHandler'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { Logger } from 'winston'
|
||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
describe('PaymentSuccessEventHandler', () => {
|
||||
let event: PaymentSuccessEvent
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
let logger: Logger
|
||||
|
||||
const createHandler = () =>
|
||||
new PaymentSuccessEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore, logger)
|
||||
|
||||
beforeEach(() => {
|
||||
getUserAnalyticsId = {} as jest.Mocked<GetUserAnalyticsId>
|
||||
getUserAnalyticsId.execute = jest.fn().mockReturnValue({ analyticsId: 3 })
|
||||
|
||||
analyticsStore = {} as jest.Mocked<AnalyticsStoreInterface>
|
||||
analyticsStore.markActivity = jest.fn()
|
||||
|
||||
statisticsStore = {} as jest.Mocked<StatisticsStoreInterface>
|
||||
statisticsStore.incrementMeasure = jest.fn()
|
||||
|
||||
event = {} as jest.Mocked<PaymentSuccessEvent>
|
||||
event.payload = {
|
||||
userEmail: 'test@test.com',
|
||||
amount: 12.45,
|
||||
billingFrequency: 12,
|
||||
paymentType: 'initial',
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
}
|
||||
|
||||
logger = {} as jest.Mocked<Logger>
|
||||
logger.warn = jest.fn()
|
||||
})
|
||||
|
||||
it('should mark payment success for analytics', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
||||
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'pro-subscription-initial-annual-payments-income',
|
||||
12.45,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
})
|
||||
|
||||
it('should mark non-detailed payment success statistics for analytics', async () => {
|
||||
event.payload = {
|
||||
userEmail: 'test@test.com',
|
||||
amount: 12.45,
|
||||
billingFrequency: 13,
|
||||
paymentType: 'initial',
|
||||
subscriptionName: 'PRO_PLAN',
|
||||
}
|
||||
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(statisticsStore.incrementMeasure).toBeCalledTimes(1)
|
||||
expect(statisticsStore.incrementMeasure).toHaveBeenNthCalledWith(1, 'income', 12.45, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
|
||||
import { DomainEventHandlerInterface, PaymentSuccessEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
import { Logger } from 'winston'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
@injectable()
|
||||
export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
|
||||
private readonly DETAILED_MEASURES = new Map([
|
||||
[
|
||||
SubscriptionName.PlusPlan,
|
||||
new Map([
|
||||
[
|
||||
PaymentType.Initial,
|
||||
new Map([
|
||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionInitialMonthlyPaymentsIncome],
|
||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionInitialAnnualPaymentsIncome],
|
||||
]),
|
||||
],
|
||||
[
|
||||
PaymentType.Renewal,
|
||||
new Map([
|
||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.PlusSubscriptionRenewingMonthlyPaymentsIncome],
|
||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.PlusSubscriptionRenewingAnnualPaymentsIncome],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
SubscriptionName.ProPlan,
|
||||
new Map([
|
||||
[
|
||||
PaymentType.Initial,
|
||||
new Map([
|
||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionInitialMonthlyPaymentsIncome],
|
||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionInitialAnnualPaymentsIncome],
|
||||
]),
|
||||
],
|
||||
[
|
||||
PaymentType.Renewal,
|
||||
new Map([
|
||||
[SubscriptionBillingFrequency.Monthly, StatisticsMeasure.ProSubscriptionRenewingMonthlyPaymentsIncome],
|
||||
[SubscriptionBillingFrequency.Annual, StatisticsMeasure.ProSubscriptionRenewingAnnualPaymentsIncome],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
],
|
||||
])
|
||||
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
@inject(TYPES.Logger) private logger: Logger,
|
||||
) {}
|
||||
|
||||
async handle(event: PaymentSuccessEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.PaymentSuccess], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
const statisticMeasures = [StatisticsMeasure.Income]
|
||||
|
||||
const detailedMeasure = this.DETAILED_MEASURES.get(event.payload.subscriptionName as SubscriptionName)
|
||||
?.get(event.payload.paymentType as PaymentType)
|
||||
?.get(event.payload.billingFrequency as SubscriptionBillingFrequency)
|
||||
if (detailedMeasure !== undefined) {
|
||||
statisticMeasures.push(detailedMeasure)
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Could not find detailed measure for: subscription - ${event.payload.subscriptionName}, payment type - ${event.payload.paymentType}, billing frequency - ${event.payload.billingFrequency}`,
|
||||
)
|
||||
}
|
||||
|
||||
for (const measure of statisticMeasures) {
|
||||
await this.statisticsStore.incrementMeasure(measure, event.payload.amount, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
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'
|
||||
|
||||
describe('SubscriptionCancelledEventHandler', () => {
|
||||
let event: SubscriptionCancelledEvent
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
let statisticsStore: StatisticsStoreInterface
|
||||
|
||||
const createHandler = () => new SubscriptionCancelledEventHandler(getUserAnalyticsId, analyticsStore, statisticsStore)
|
||||
|
||||
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<SubscriptionCancelledEvent>
|
||||
event.createdAt = new Date(1)
|
||||
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,
|
||||
}
|
||||
})
|
||||
|
||||
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,
|
||||
])
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
import { StatisticsMeasure } from '../Statistics/StatisticsMeasure'
|
||||
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionCancelledEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionCancelled], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
await this.trackSubscriptionStatistics(event)
|
||||
}
|
||||
|
||||
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {
|
||||
if (this.isLegacy5yearSubscriptionPlan(event.payload.subscriptionEndsAt, event.payload.subscriptionCreatedAt)) {
|
||||
return
|
||||
}
|
||||
|
||||
const subscriptionLength = event.payload.timestamp - event.payload.subscriptionCreatedAt
|
||||
await this.statisticsStore.incrementMeasure(StatisticsMeasure.SubscriptionLength, subscriptionLength, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
|
||||
const remainingSubscriptionTime = event.payload.subscriptionEndsAt - event.payload.timestamp
|
||||
const totalSubscriptionTime = event.payload.subscriptionEndsAt - event.payload.lastPayedAt
|
||||
|
||||
const remainingSubscriptionPercentage = Math.floor((remainingSubscriptionTime / totalSubscriptionTime) * 100)
|
||||
|
||||
await this.statisticsStore.incrementMeasure(
|
||||
StatisticsMeasure.RemainingSubscriptionTimePercentage,
|
||||
remainingSubscriptionPercentage,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
}
|
||||
|
||||
private isLegacy5yearSubscriptionPlan(subscriptionEndsAt: number, subscriptionCreatedAt: number) {
|
||||
const fourYearsInMicroseconds = 126_230_400_000_000
|
||||
|
||||
return subscriptionEndsAt - subscriptionCreatedAt > fourYearsInMicroseconds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'reflect-metadata'
|
||||
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
||||
|
||||
import { SubscriptionRenewedEventHandler } from './SubscriptionRenewedEventHandler'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
|
||||
describe('SubscriptionRenewedEventHandler', () => {
|
||||
let event: SubscriptionRenewedEvent
|
||||
let getUserAnalyticsId: GetUserAnalyticsId
|
||||
let analyticsStore: AnalyticsStoreInterface
|
||||
|
||||
const createHandler = () => new SubscriptionRenewedEventHandler(getUserAnalyticsId, analyticsStore)
|
||||
|
||||
beforeEach(() => {
|
||||
event = {} as jest.Mocked<SubscriptionRenewedEvent>
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
subscriptionId: 1,
|
||||
userEmail: 'test@test.com',
|
||||
subscriptionName: SubscriptionName.ProPlan,
|
||||
subscriptionExpiresAt: 2,
|
||||
timestamp: 1,
|
||||
offline: false,
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
it('should track subscription renewed statistics', async () => {
|
||||
await createHandler().handle(event)
|
||||
|
||||
expect(analyticsStore.markActivity).toHaveBeenCalled()
|
||||
expect(analyticsStore.unmarkActivity).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
|
||||
import { inject, injectable } from 'inversify'
|
||||
|
||||
import TYPES from '../../Bootstrap/Types'
|
||||
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
|
||||
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
|
||||
import { AnalyticsStoreInterface } from '../Analytics/AnalyticsStoreInterface'
|
||||
import { Period } from '../Time/Period'
|
||||
|
||||
@injectable()
|
||||
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
|
||||
constructor(
|
||||
@inject(TYPES.GetUserAnalyticsId) private getUserAnalyticsId: GetUserAnalyticsId,
|
||||
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
|
||||
) {}
|
||||
|
||||
async handle(event: SubscriptionRenewedEvent): Promise<void> {
|
||||
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.SubscriptionRenewed], analyticsId, [
|
||||
Period.Today,
|
||||
Period.ThisWeek,
|
||||
Period.ThisMonth,
|
||||
])
|
||||
await this.analyticsStore.unmarkActivity(
|
||||
[AnalyticsActivity.ExistingCustomersChurn, AnalyticsActivity.NewCustomersChurn],
|
||||
analyticsId,
|
||||
[Period.Today, Period.ThisWeek, Period.ThisMonth],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
|
||||
async handle(event: UserRegisteredEvent): Promise<void> {
|
||||
let analyticsEntity = new AnalyticsEntity()
|
||||
analyticsEntity.userUuid = event.payload.userUuid
|
||||
analyticsEntity.userEmail = event.payload.email
|
||||
analyticsEntity = await this.analyticsEntityRepository.save(analyticsEntity)
|
||||
|
||||
await this.analyticsStore.markActivity([AnalyticsActivity.Register], analyticsEntity.id, [
|
||||
|
||||
@@ -16,12 +16,17 @@ describe('GetUserAnalyticsId', () => {
|
||||
|
||||
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
|
||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(analyticsEntity)
|
||||
analyticsEntityRepository.findOneByUserEmail = jest.fn().mockReturnValue(analyticsEntity)
|
||||
})
|
||||
|
||||
it('should return analytics id for a user', async () => {
|
||||
it('should return analytics id for a user by uuid', async () => {
|
||||
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({ analyticsId: 123 })
|
||||
})
|
||||
|
||||
it('should return analytics id for a user by email', async () => {
|
||||
expect(await createUseCase().execute({ userEmail: 'test@test.te' })).toEqual({ analyticsId: 123 })
|
||||
})
|
||||
|
||||
it('should throw error if user is missing analytics entity', async () => {
|
||||
analyticsEntityRepository.findOneByUserUuid = jest.fn().mockReturnValue(null)
|
||||
let error = null
|
||||
|
||||
@@ -12,7 +12,12 @@ export class GetUserAnalyticsId implements UseCaseInterface {
|
||||
) {}
|
||||
|
||||
async execute(dto: GetUserAnalyticsIdDTO): Promise<GetUserAnalyticsIdResponse> {
|
||||
const analyticsEntity = await this.analyticsEntityRepository.findOneByUserUuid(dto.userUuid)
|
||||
let analyticsEntity = null
|
||||
if (dto.userUuid) {
|
||||
analyticsEntity = await this.analyticsEntityRepository.findOneByUserUuid(dto.userUuid)
|
||||
} else if (dto.userEmail) {
|
||||
analyticsEntity = await this.analyticsEntityRepository.findOneByUserEmail(dto.userEmail)
|
||||
}
|
||||
|
||||
if (analyticsEntity === null) {
|
||||
throw new Error(`Could not find analytics entity for user ${dto.userUuid}`)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { Either, Uuid } from '@standardnotes/common'
|
||||
|
||||
export type GetUserAnalyticsIdDTO = {
|
||||
userUuid: Uuid
|
||||
}
|
||||
export type GetUserAnalyticsIdDTO = Either<
|
||||
{
|
||||
userUuid: Uuid
|
||||
},
|
||||
{
|
||||
userEmail: string
|
||||
}
|
||||
>
|
||||
|
||||
@@ -46,4 +46,15 @@ describe('MySQLAnalyticsEntityRepository', () => {
|
||||
|
||||
expect(result).toEqual(analyticsEntity)
|
||||
})
|
||||
|
||||
it('should find one by user email', async () => {
|
||||
queryBuilder.where = jest.fn().mockReturnThis()
|
||||
queryBuilder.getOne = jest.fn().mockReturnValue(analyticsEntity)
|
||||
|
||||
const result = await createRepository().findOneByUserEmail('test@test.te')
|
||||
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith('analytics_entity.user_email = :email', { email: 'test@test.te' })
|
||||
|
||||
expect(result).toEqual(analyticsEntity)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,13 @@ export class MySQLAnalyticsEntityRepository implements AnalyticsEntityRepository
|
||||
private ormRepository: Repository<AnalyticsEntity>,
|
||||
) {}
|
||||
|
||||
async findOneByUserEmail(email: string): Promise<AnalyticsEntity | null> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('analytics_entity')
|
||||
.where('analytics_entity.user_email = :email', { email })
|
||||
.getOne()
|
||||
}
|
||||
|
||||
async findOneByUserUuid(userUuid: Uuid): Promise<AnalyticsEntity | null> {
|
||||
return this.ormRepository
|
||||
.createQueryBuilder('analytics_entity')
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.36.10](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.36.9...@standardnotes/api-gateway@1.36.10) (2022-11-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.36.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.36.8...@standardnotes/api-gateway@1.36.9) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.36.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.36.7...@standardnotes/api-gateway@1.36.8) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.36.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.36.6...@standardnotes/api-gateway@1.36.7) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
## [1.36.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.36.5...@standardnotes/api-gateway@1.36.6) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api-gateway
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api-gateway",
|
||||
"version": "1.36.6",
|
||||
"version": "1.36.10",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.49.13](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.12...@standardnotes/auth-server@1.49.13) (2022-11-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.49.12](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.11...@standardnotes/auth-server@1.49.12) (2022-11-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **auth:** event specs ([a3be4b0](https://github.com/standardnotes/server/commit/a3be4b063d679c125d152e7025750a6ca1d5d7a1))
|
||||
|
||||
## [1.49.11](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.10...@standardnotes/auth-server@1.49.11) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.49.10](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.9...@standardnotes/auth-server@1.49.10) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
## [1.49.9](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.49.8...@standardnotes/auth-server@1.49.9) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/auth-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/auth-server",
|
||||
"version": "1.49.9",
|
||||
"version": "1.49.13",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -80,6 +80,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '1-2-3',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '2-3-4',
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,10 @@ describe('SubscriptionCancelledEventHandler', () => {
|
||||
timestamp,
|
||||
offline: false,
|
||||
replaced: false,
|
||||
subscriptionCreatedAt: 1,
|
||||
subscriptionEndsAt: 2,
|
||||
subscriptionUpdatedAt: 2,
|
||||
lastPayedAt: 1,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.9.9](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.8...@standardnotes/domain-events-infra@1.9.9) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
## [1.9.8](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.7...@standardnotes/domain-events-infra@1.9.8) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/domain-events-infra
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events-infra",
|
||||
"version": "1.9.8",
|
||||
"version": "1.9.9",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.75.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.74.1...@standardnotes/domain-events@2.75.0) (2022-11-04)
|
||||
|
||||
### Features
|
||||
|
||||
* **analytics:** add subscription cancelled event handler ([037fb23](https://github.com/standardnotes/server/commit/037fb2398ae9aaa11682e1a8576bab28c69e0f9b))
|
||||
|
||||
## [2.74.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.74.0...@standardnotes/domain-events@2.74.1) (2022-11-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/domain-events",
|
||||
"version": "2.74.1",
|
||||
"version": "2.75.0",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,10 @@ export interface SubscriptionCancelledEventPayload {
|
||||
userEmail: string
|
||||
subscriptionId: number
|
||||
subscriptionName: SubscriptionName
|
||||
subscriptionCreatedAt: number
|
||||
subscriptionUpdatedAt: number
|
||||
lastPayedAt: number
|
||||
subscriptionEndsAt: number
|
||||
timestamp: number
|
||||
offline: boolean
|
||||
replaced: boolean
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.6.4](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.3...@standardnotes/event-store@1.6.4) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
## [1.6.3](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.2...@standardnotes/event-store@1.6.3) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/event-store
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/event-store",
|
||||
"version": "1.6.3",
|
||||
"version": "1.6.4",
|
||||
"description": "Event Store Service",
|
||||
"private": true,
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.8.4](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.3...@standardnotes/files-server@1.8.4) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
## [1.8.3](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.2...@standardnotes/files-server@1.8.3) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files-server",
|
||||
"version": "1.8.3",
|
||||
"version": "1.8.4",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.5](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.4...@standardnotes/scheduler-server@1.13.5) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
## [1.13.4](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.3...@standardnotes/scheduler-server@1.13.4) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/scheduler-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/scheduler-server",
|
||||
"version": "1.13.4",
|
||||
"version": "1.13.5",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.21](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.20...@standardnotes/syncing-server@1.10.21) (2022-11-07)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.20](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.19...@standardnotes/syncing-server@1.10.20) (2022-11-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **syncing-server:** event specs ([182512d](https://github.com/standardnotes/syncing-server-js/commit/182512d07c68ae644594404724bf1a0f5e0c71ae))
|
||||
|
||||
## [1.10.19](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.18...@standardnotes/syncing-server@1.10.19) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.18](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.17...@standardnotes/syncing-server@1.10.18) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
## [1.10.17](https://github.com/standardnotes/syncing-server-js/compare/@standardnotes/syncing-server@1.10.16...@standardnotes/syncing-server@1.10.17) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/syncing-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/syncing-server",
|
||||
"version": "1.10.17",
|
||||
"version": "1.10.21",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ describe('AccountDeletionRequestedEventHandler', () => {
|
||||
event.createdAt = new Date(1)
|
||||
event.payload = {
|
||||
userUuid: '2-3-4',
|
||||
userCreatedAtTimestamp: 1,
|
||||
regularSubscriptionUuid: '1-2-3',
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.4](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.3...@standardnotes/websockets-server@1.4.4) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
## [1.4.3](https://github.com/standardnotes/server/compare/@standardnotes/websockets-server@1.4.2...@standardnotes/websockets-server@1.4.3) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/websockets-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/websockets-server",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.4",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.17.4](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.3...@standardnotes/workspace-server@1.17.4) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
## [1.17.3](https://github.com/standardnotes/server/compare/@standardnotes/workspace-server@1.17.2...@standardnotes/workspace-server@1.17.3) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/workspace-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/workspace-server",
|
||||
"version": "1.17.3",
|
||||
"version": "1.17.4",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user