Skip to content

Commit

Permalink
satellite/analytics: add trial upgrade analytics
Browse files Browse the repository at this point in the history
add analytics events for user upgraded and upgrade dialog clicked in UI

updates: storj/storj-private#609

Change-Id: I830293c3c49e1ea9fc76c1447fd4a67a91a8d71c
  • Loading branch information
cam-a authored and Storj Robot committed Mar 19, 2024
1 parent d3bf12f commit 048966f
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 10 deletions.
39 changes: 38 additions & 1 deletion satellite/analytics/service.go
Expand Up @@ -5,7 +5,9 @@ package analytics

import (
"context"
"math"
"strings"
"time"

"github.com/zeebo/errs"
"go.uber.org/zap"
Expand Down Expand Up @@ -107,6 +109,8 @@ const (
eventOnboardingAbandoned = "Onboarding Abandoned"
eventPersonalSelected = "Personal Selected"
eventBusinessSelected = "Business Selected"
eventUserUpgraded = "User Upgraded"
eventUpgradeClicked = "Upgrade Clicked"
)

var (
Expand Down Expand Up @@ -200,7 +204,7 @@ func NewService(log *zap.Logger, config Config, satelliteName string) *Service {
eventProjectStorageLimitUpdated, eventProjectBandwidthLimitUpdated, eventProjectInvitationAccepted, eventProjectInvitationDeclined,
eventGalleryViewClicked, eventResendInviteClicked, eventRemoveProjectMemberCLicked, eventCopyInviteLinkClicked, eventUserSignUp,
eventPersonalInfoSubmitted, eventBusinessInfoSubmitted, eventUseCaseSelected, eventOnboardingCompleted, eventOnboardingAbandoned,
eventPersonalSelected, eventBusinessSelected} {
eventPersonalSelected, eventBusinessSelected, eventUserUpgraded, eventUpgradeClicked} {
service.clientEvents[name] = true
}

Expand Down Expand Up @@ -874,3 +878,36 @@ func (service *Service) TrackInviteLinkClicked(inviter, invitee string) {
Properties: props,
})
}

// TrackUserUpgraded sends a "User Upgraded" event to Segment.
func (service *Service) TrackUserUpgraded(userID uuid.UUID, email string, expiration *time.Time) {
if !service.config.Enabled {
return
}

props := segment.NewProperties()

now := time.Now()

props.Set("email", email)

// NOTE: if this runs before legacy free tier migration, old free tier will
// be considered unlimited.
if expiration == nil {
props.Set("trial status", "unlimited")
} else {
if now.After(*expiration) {
props.Set("trial status", "expired")
props.Set("days since expiration", math.Floor(now.Sub(*expiration).Hours()/24))
} else {
props.Set("trial status", "active")
props.Set("days until expiration", math.Floor(expiration.Sub(now).Hours()/24))
}
}

service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventUserUpgraded,
Properties: props,
})
}
7 changes: 6 additions & 1 deletion satellite/console/observerupgradeuser.go
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"storj.io/common/memory"
"storj.io/storj/satellite/analytics"
"storj.io/storj/satellite/payments/billing"
)

Expand All @@ -20,17 +21,19 @@ type UpgradeUserObserver struct {
usageLimitsConfig UsageLimitsConfig
userBalanceForUpgrade int64
freezeService *AccountFreezeService
analyticsService *analytics.Service
nowFn func() time.Time
}

// NewUpgradeUserObserver creates new observer instance.
func NewUpgradeUserObserver(consoleDB DB, transactionsDB billing.TransactionsDB, usageLimitsConfig UsageLimitsConfig, userBalanceForUpgrade int64, freezeService *AccountFreezeService) *UpgradeUserObserver {
func NewUpgradeUserObserver(consoleDB DB, transactionsDB billing.TransactionsDB, usageLimitsConfig UsageLimitsConfig, userBalanceForUpgrade int64, freezeService *AccountFreezeService, analyticsService *analytics.Service) *UpgradeUserObserver {
return &UpgradeUserObserver{
consoleDB: consoleDB,
transactionsDB: transactionsDB,
usageLimitsConfig: usageLimitsConfig,
userBalanceForUpgrade: userBalanceForUpgrade,
freezeService: freezeService,
analyticsService: analyticsService,
nowFn: time.Now,
}
}
Expand Down Expand Up @@ -80,6 +83,8 @@ func (o *UpgradeUserObserver) Process(ctx context.Context, transaction billing.T
return err
}

o.analyticsService.TrackUserUpgraded(user.ID, user.Email, user.TrialExpiration)

projects, err := o.consoleDB.Projects().GetOwn(ctx, user.ID)
if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions satellite/console/service.go
Expand Up @@ -455,6 +455,7 @@ func (payment Payments) upgradeToPaidTier(ctx context.Context, user *User) (err
if err != nil {
return Error.Wrap(err)
}
payment.service.analytics.TrackUserUpgraded(user.ID, user.Email, user.TrialExpiration)

projects, err := payment.service.store.Projects().GetOwn(ctx, user.ID)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion satellite/core.go
Expand Up @@ -533,7 +533,7 @@ func New(log *zap.Logger, full *identity.FullIdentity, db DB,

freezeService := console.NewAccountFreezeService(peer.DB.Console(), peer.Analytics.Service, config.Console.AccountFreeze)
choreObservers := billing.ChoreObservers{
UpgradeUser: console.NewUpgradeUserObserver(peer.DB.Console(), peer.DB.Billing(), config.Console.UsageLimits, config.Console.UserBalanceForUpgrade, freezeService),
UpgradeUser: console.NewUpgradeUserObserver(peer.DB.Console(), peer.DB.Billing(), config.Console.UsageLimits, config.Console.UserBalanceForUpgrade, freezeService, peer.Analytics.Service),
PayInvoices: console.NewInvoiceTokenPaymentObserver(
peer.DB.Console(), peer.Payments.Accounts.Invoices(),
freezeService,
Expand Down
11 changes: 7 additions & 4 deletions satellite/payments/billing/chore_test.go
Expand Up @@ -22,6 +22,7 @@ import (
"storj.io/common/uuid"
"storj.io/storj/private/blockchain"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite/analytics"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/payments/billing"
)
Expand Down Expand Up @@ -76,7 +77,7 @@ func TestChore(t *testing.T) {
assert.Equal(t, expected, actual, "unexpected balance for user %s (%q)", userID, names[userID])
}

runTest := func(ctx *testcontext.Context, t *testing.T, consoleDB console.DB, db billing.TransactionsDB, bonusRate int64, mikeTXs, joeTXs, robertTXs []billing.Transaction, mikeBalance, joeBalance, robertBalance currency.Amount, usageLimitsConfig console.UsageLimitsConfig, userBalanceForUpgrade int64, freezeService *console.AccountFreezeService) {
runTest := func(ctx *testcontext.Context, t *testing.T, consoleDB console.DB, db billing.TransactionsDB, bonusRate int64, mikeTXs, joeTXs, robertTXs []billing.Transaction, mikeBalance, joeBalance, robertBalance currency.Amount, usageLimitsConfig console.UsageLimitsConfig, userBalanceForUpgrade int64, freezeService *console.AccountFreezeService, analyticsService *analytics.Service) {
paymentTypes := []billing.PaymentType{
newFakePaymentType(billing.StorjScanEthereumSource,
[]billing.Transaction{mike1, joe1, joe2},
Expand All @@ -88,7 +89,7 @@ func TestChore(t *testing.T) {
}

choreObservers := billing.ChoreObservers{
UpgradeUser: console.NewUpgradeUserObserver(consoleDB, db, usageLimitsConfig, userBalanceForUpgrade, freezeService),
UpgradeUser: console.NewUpgradeUserObserver(consoleDB, db, usageLimitsConfig, userBalanceForUpgrade, freezeService, analyticsService),
}

chore := billing.NewChore(zaptest.NewLogger(t), paymentTypes, db, time.Hour, false, bonusRate, choreObservers)
Expand Down Expand Up @@ -130,6 +131,7 @@ func TestChore(t *testing.T) {
sat.Config.Console.UsageLimits,
sat.Config.Console.UserBalanceForUpgrade,
freezeService,
sat.API.Analytics.Service,
)
})
})
Expand All @@ -153,6 +155,7 @@ func TestChore(t *testing.T) {
sat.Config.Console.UsageLimits,
sat.Config.Console.UserBalanceForUpgrade,
freezeService,
sat.API.Analytics.Service,
)
})
})
Expand Down Expand Up @@ -191,7 +194,7 @@ func TestChore_UpgradeUserObserver(t *testing.T) {
require.NoError(t, err)

choreObservers := billing.ChoreObservers{
UpgradeUser: console.NewUpgradeUserObserver(db.Console(), db.Billing(), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade, freezeService),
UpgradeUser: console.NewUpgradeUserObserver(db.Console(), db.Billing(), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade, freezeService, sat.API.Analytics.Service),
}

amount1 := int64(200) // $2
Expand Down Expand Up @@ -339,7 +342,7 @@ func TestChore_PayInvoiceObserver(t *testing.T) {
freezeService := console.NewAccountFreezeService(consoleDB, sat.Core.Analytics.Service, sat.Config.Console.AccountFreeze)

choreObservers := billing.ChoreObservers{
UpgradeUser: console.NewUpgradeUserObserver(consoleDB, db.Billing(), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade, freezeService),
UpgradeUser: console.NewUpgradeUserObserver(consoleDB, db.Billing(), sat.Config.Console.UsageLimits, sat.Config.Console.UserBalanceForUpgrade, freezeService, sat.API.Analytics.Service),
PayInvoices: console.NewInvoiceTokenPaymentObserver(consoleDB, sat.Core.Payments.Accounts.Invoices(), freezeService),
}

Expand Down
4 changes: 2 additions & 2 deletions web/satellite/src/api/analytics.ts
Expand Up @@ -19,14 +19,14 @@ export class AnalyticsHttpApi {
* @param eventName - name of the event
* @param props - additional properties to send with the event
*/
public async eventTriggered(eventName: string, props?: {[p: string]: string}): Promise<void> {
public async eventTriggered(eventName: string, props?: Map<string, string>): Promise<void> {
try {
const path = `${this.ROOT_PATH}/event`;
const body = {
eventName: eventName,
};
if (props) {
body['props'] = props;
body['props'] = Object.fromEntries(props);
}
const response = await this.http.post(path, JSON.stringify(body));
if (response.ok) {
Expand Down
11 changes: 11 additions & 0 deletions web/satellite/src/store/modules/appStore.ts
Expand Up @@ -5,6 +5,9 @@ import { reactive } from 'vue';
import { defineStore } from 'pinia';

import { LocalData } from '@/utils/localData';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';

class AppState {
public hasJustLoggedIn = false;
Expand Down Expand Up @@ -32,6 +35,9 @@ class ErrorPageState {
export const useAppStore = defineStore('app', () => {
const state = reactive<AppState>(new AppState());

const analyticsStore = useAnalyticsStore();
const userStore = useUsersStore();

function toggleHasJustLoggedIn(hasJustLoggedIn: boolean | null = null): void {
if (hasJustLoggedIn === null) {
state.hasJustLoggedIn = !state.hasJustLoggedIn;
Expand Down Expand Up @@ -72,6 +78,11 @@ export const useAppStore = defineStore('app', () => {

function toggleUpgradeFlow(isShown?: boolean): void {
state.isUpgradeFlowDialogShown = isShown ?? !state.isUpgradeFlowDialogShown;
if (state.isUpgradeFlowDialogShown) {
const props = new Map();
props.set('expired', String(userStore.state.user.freezeStatus.trialExpiredFrozen));
analyticsStore.eventTriggered(AnalyticsEvent.UPGRADE_CLICKED, props);
}
}

function toggleExpirationDialog(isShown?: boolean): void {
Expand Down
3 changes: 2 additions & 1 deletion web/satellite/src/utils/constants/analyticsEventNames.ts
Expand Up @@ -20,7 +20,7 @@ export enum AnalyticsEvent {
BUCKET_CREATED = 'Bucket Created',
BUCKET_DELETED = 'Bucket Deleted',
ACCESS_GRANT_CREATED = 'Access Grant Created',
API_ACCESS_CREATED = 'API Access Created',
API_ACCESS_CREATED = 'API Access Created',
UPLOAD_FILE_CLICKED = 'Upload File Clicked',
UPLOAD_FOLDER_CLICKED = 'Upload Folder Clicked',
DOWNLOAD_TXT_CLICKED = 'Download txt clicked',
Expand All @@ -47,6 +47,7 @@ export enum AnalyticsEvent {
ONBOARDING_ABANDONED = 'Onboarding Abandoned',
PERSONAL_SELECTED = 'Personal Selected',
BUSINESS_SELECTED = 'Business Selected',
UPGRADE_CLICKED = 'Upgrade Clicked',
}

export enum AnalyticsErrorEventSource {
Expand Down

0 comments on commit 048966f

Please sign in to comment.