Compare commits

...

2 Commits

Author SHA1 Message Date
standardci
02705ea3ad chore(release): publish new version
- @standardnotes/analytics@2.19.0
2022-12-30 11:44:19 +00:00
Karol Sójko
df6e3f06a6 feat(analytics): add mixpanel events tracking 2022-12-30 12:41:42 +01:00
15 changed files with 193 additions and 14 deletions

View File

@@ -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.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

View File

@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.18.0",
"version": "2.19.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},

View File

@@ -226,6 +226,7 @@ export class ContainerConfigLoader {
container.get(TYPES.PersistStatistic),
container.get(TYPES.Timer),
container.get(TYPES.Logger),
env.get('MIXPANEL_TOKEN', true) ? container.get(TYPES.MixpanelClient) : null,
),
)

View File

@@ -1,6 +1,7 @@
import { AccountDeletionRequestedEvent, DomainEventHandlerInterface } from '@standardnotes/domain-events'
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 { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -17,6 +18,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Timer) private timer: TimerInterface,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
@@ -40,5 +42,12 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
])
await this.analyticsEntityRepository.remove(analyticsEntity)
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsEntity.id.toString(),
user_created_at: this.timer.convertMicrosecondsToDate(event.payload.userCreatedAtTimestamp),
})
}
}
}

View File

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

View File

@@ -1,6 +1,7 @@
import { PaymentType, SubscriptionBillingFrequency, SubscriptionName } from '@standardnotes/common'
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 TYPES from '../../Bootstrap/Types'
@@ -83,6 +84,7 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
@inject(TYPES.AnalyticsStore) private analyticsStore: AnalyticsStoreInterface,
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: PaymentSuccessEvent): Promise<void> {
@@ -113,5 +115,19 @@ export class PaymentSuccessEventHandler implements DomainEventHandlerInterface {
Period.ThisMonth,
])
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
amount: event.payload.amount,
billing_frequency: event.payload.billingFrequency,
payment_type: event.payload.paymentType,
subscription_name: event.payload.subscriptionName,
})
this.mixpanelClient.people.track_charge(analyticsId.toString(), event.payload.amount)
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}

View File

@@ -1,20 +1,36 @@
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 { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { StatisticsStoreInterface } from '../Statistics/StatisticsStoreInterface'
import { Period } from '../Time/Period'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@injectable()
export class RefundProcessedEventHandler implements DomainEventHandlerInterface {
constructor(@inject(TYPES.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> {
const { analyticsId } = await this.getUserAnalyticsId.execute({ userEmail: event.payload.userEmail })
await this.statisticsStore.incrementMeasure(StatisticMeasureName.NAMES.Refunds, event.payload.amount, [
Period.Today,
Period.ThisWeek,
Period.ThisMonth,
])
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
amount: event.payload.amount,
})
this.mixpanelClient.people.track_charge(analyticsId.toString(), -event.payload.amount)
}
}
}

View File

@@ -2,9 +2,15 @@ import { DomainEventHandlerInterface, StatisticPersistenceRequestedEvent } from
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) {}
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({
@@ -16,5 +22,13 @@ export class StatisticPersistenceRequestedEventHandler implements DomainEventHan
if (result.isFailed()) {
this.logger.error(result.getError())
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: 'global-stats',
statistic: event.payload.statisticMeasureName,
value: event.payload.value,
})
}
}
}

View File

@@ -1,7 +1,8 @@
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Logger } from 'winston'
import { Username } from '@standardnotes/domain-core'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -13,6 +14,7 @@ import { Period } from '../Time/Period'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
import { SaveRevenueModification } from '../UseCase/SaveRevenueModification/SaveRevenueModification'
import { StatisticMeasureName } from '../Statistics/StatisticMeasureName'
import { TimerInterface } from '@standardnotes/time'
@injectable()
export class SubscriptionCancelledEventHandler implements DomainEventHandlerInterface {
@@ -22,6 +24,8 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionCancelledEvent): Promise<void> {
@@ -50,6 +54,22 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_created_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionCreatedAt),
subscription_updated_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionUpdatedAt),
last_payed_at: this.timer.convertMicrosecondsToDate(event.payload.lastPayedAt),
subscription_ends_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionEndsAt),
offline: event.payload.offline,
replaced: event.payload.replaced,
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
}
}
private async trackSubscriptionStatistics(event: SubscriptionCancelledEvent) {

View File

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

View File

@@ -1,7 +1,9 @@
import { Username } from '@standardnotes/domain-core'
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 { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -22,6 +24,8 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
@inject(TYPES.Timer) private timer: TimerInterface,
) {}
async handle(event: SubscriptionPurchasedEvent): Promise<void> {
@@ -78,5 +82,23 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
subscription_expires_at: this.timer.convertMicrosecondsToDate(event.payload.subscriptionExpiresAt),
offline: event.payload.offline,
discount_code: event.payload.discountCode,
limited_discount_purchased: event.payload.limitedDiscountPurchased,
new_subscriber: event.payload.newSubscriber,
total_active_subscriptions_count: event.payload.totalActiveSubscriptionsCount,
user_registered_at: this.timer.convertMicrosecondsToDate(event.payload.userRegisteredAt),
billing_frequency: event.payload.billingFrequency,
pay_amount: event.payload.payAmount,
})
this.mixpanelClient.people.set(analyticsId.toString(), 'subscription', event.payload.subscriptionName)
}
}
}

View File

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

View File

@@ -1,7 +1,8 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { inject, injectable, optional } from 'inversify'
import { Logger } from 'winston'
import { Mixpanel } from 'mixpanel'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -22,6 +23,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
@inject(TYPES.StatisticsStore) private statisticsStore: StatisticsStoreInterface,
@inject(TYPES.SaveRevenueModification) private saveRevenueModification: SaveRevenueModification,
@inject(TYPES.Logger) private logger: Logger,
@inject(TYPES.MixpanelClient) @optional() private mixpanelClient: Mixpanel | null,
) {}
async handle(event: SubscriptionRefundedEvent): Promise<void> {
@@ -50,6 +52,19 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
`[${event.type}][${event.payload.subscriptionId}] Could not save revenue modification: ${result.getError()}`,
)
}
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsId.toString(),
subscription_name: event.payload.subscriptionName,
user_existing_subscriptions_count: event.payload.userExistingSubscriptionsCount,
total_active_subscriptions_count: event.payload.totalActiveSubscriptionsCount,
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> {

View File

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

View File

@@ -31,7 +31,12 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
if (this.mixpanelClient !== null) {
this.mixpanelClient.track(event.type, {
distinct_id: analyticsEntity.id,
distinct_id: analyticsEntity.id.toString(),
protocol_version: event.payload.protocolVersion,
})
this.mixpanelClient.people.set(analyticsEntity.id.toString(), {
subscription: 'free',
protocol_version: event.payload.protocolVersion,
})
}