storj/satellite/analytics/service.go
Cameron e072b37a86 satellite/console: add endpoint to request project limit increase
create endpoint to allow pro users to request project limit increase.

github issue: https://github.com/storj/storj/issues/6298

Change-Id: I96c3dff8bf0906904d199fc2c7ee738f3e6b04a3
2023-10-05 17:21:32 +00:00

739 lines
24 KiB
Go

// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package analytics
import (
"context"
"strings"
"github.com/zeebo/errs"
"go.uber.org/zap"
segment "gopkg.in/segmentio/analytics-go.v3"
"storj.io/common/uuid"
)
const (
eventInviteLinkClicked = "Invite Link Clicked"
eventInviteLinkSignup = "Invite Link Signup"
eventAccountCreated = "Account Created"
eventSignedIn = "Signed In"
eventProjectCreated = "Project Created"
eventAccessGrantCreated = "Access Grant Created"
eventAccountVerified = "Account Verified"
eventGatewayCredentialsCreated = "Credentials Created"
eventPassphraseCreated = "Passphrase Created"
eventExternalLinkClicked = "External Link Clicked"
eventPathSelected = "Path Selected"
eventLinkShared = "Link Shared"
eventObjectUploaded = "Object Uploaded"
eventAPIKeyGenerated = "API Key Generated"
eventCreditCardAdded = "Credit Card Added"
eventUpgradeBannerClicked = "Upgrade Banner Clicked"
eventModalAddCard = "Credit Card Added In Modal"
eventModalAddTokens = "Storj Token Added In Modal"
eventSearchBuckets = "Search Buckets"
eventNavigateProjects = "Navigate Projects"
eventManageProjectsClicked = "Manage Projects Clicked"
eventCreateNewClicked = "Create New Clicked"
eventViewDocsClicked = "View Docs Clicked"
eventViewForumClicked = "View Forum Clicked"
eventViewSupportClicked = "View Support Clicked"
eventCreateAnAccessGrantClicked = "Create an Access Grant Clicked"
eventUploadUsingCliClicked = "Upload Using CLI Clicked"
eventUploadInWebClicked = "Upload In Web Clicked"
eventNewProjectClicked = "New Project Clicked"
eventLogoutClicked = "Logout Clicked"
eventProfileUpdated = "Profile Updated"
eventPasswordChanged = "Password Changed"
eventMfaEnabled = "MFA Enabled"
eventBucketCreated = "Bucket Created"
eventBucketDeleted = "Bucket Deleted"
eventProjectLimitError = "Project Limit Error"
eventAPIAccessCreated = "API Access Created"
eventUploadFileClicked = "Upload File Clicked"
eventUploadFolderClicked = "Upload Folder Clicked"
eventStorjTokenAdded = "Storj Token Added"
eventCreateKeysClicked = "Create Keys Clicked"
eventDownloadTxtClicked = "Download txt clicked"
eventEncryptMyAccessClicked = "Encrypt My Access Clicked"
eventCopyToClipboardClicked = "Copy to Clipboard Clicked"
eventCreateAccessGrantClicked = "Create Access Grant Clicked"
eventCreateS3CredentialsClicked = "Create S3 Credentials Clicked"
eventKeysForCLIClicked = "Create Keys For CLI Clicked"
eventSeePaymentsClicked = "See Payments Clicked"
eventEditPaymentMethodClicked = "Edit Payment Method Clicked"
eventUsageDetailedInfoClicked = "Usage Detailed Info Clicked"
eventAddNewPaymentMethodClicked = "Add New Payment Method Clicked"
eventApplyNewCouponClicked = "Apply New Coupon Clicked"
eventCreditCardRemoved = "Credit Card Removed"
eventCouponCodeApplied = "Coupon Code Applied"
eventInvoiceDownloaded = "Invoice Downloaded"
eventCreditCardAddedFromBilling = "Credit Card Added From Billing"
eventStorjTokenAddedFromBilling = "Storj Token Added From Billing"
eventAddFundsClicked = "Add Funds Clicked"
eventProjectMembersInviteSent = "Project Members Invite Sent"
eventProjectMemberAdded = "Project Member Added"
eventProjectMemberDeleted = "Project Member Deleted"
eventError = "UI error occurred"
eventProjectNameUpdated = "Project Name Updated"
eventProjectDescriptionUpdated = "Project Description Updated"
eventProjectStorageLimitUpdated = "Project Storage Limit Updated"
eventProjectBandwidthLimitUpdated = "Project Bandwidth Limit Updated"
eventAccountFrozen = "Account Frozen"
eventAccountUnfrozen = "Account Unfrozen"
eventAccountUnwarned = "Account Unwarned"
eventAccountFreezeWarning = "Account Freeze Warning"
eventUnpaidLargeInvoice = "Large Invoice Unpaid"
eventUnpaidStorjscanInvoice = "Storjscan Invoice Unpaid"
eventExpiredCreditNeedsRemoval = "Expired Credit Needs Removal"
eventExpiredCreditRemoved = "Expired Credit Removed"
eventProjectInvitationAccepted = "Project Invitation Accepted"
eventProjectInvitationDeclined = "Project Invitation Declined"
eventGalleryViewClicked = "Gallery View Clicked"
eventResendInviteClicked = "Resend Invite Clicked"
eventCopyInviteLinkClicked = "Copy Invite Link Clicked"
eventRemoveProjectMemberCLicked = "Remove Member Clicked"
eventLimitIncreaseRequested = "Limit Increase Requested"
)
var (
// Error is the default error class the analytics package.
Error = errs.Class("analytics service")
)
// Config is a configuration struct for analytics Service.
type Config struct {
SegmentWriteKey string `help:"segment write key" default:""`
Enabled bool `help:"enable analytics reporting" default:"false"`
HubSpot HubSpotConfig
}
// FreezeTracker is an interface for account freeze event tracking methods.
type FreezeTracker interface {
// TrackAccountFrozen sends an account frozen event to Segment.
TrackAccountFrozen(userID uuid.UUID, email string)
// TrackAccountUnfrozen sends an account unfrozen event to Segment.
TrackAccountUnfrozen(userID uuid.UUID, email string)
// TrackAccountUnwarned sends an account unwarned event to Segment.
TrackAccountUnwarned(userID uuid.UUID, email string)
// TrackAccountFreezeWarning sends an account freeze warning event to Segment.
TrackAccountFreezeWarning(userID uuid.UUID, email string)
// TrackLargeUnpaidInvoice sends an event to Segment indicating that a user has not paid a large invoice.
TrackLargeUnpaidInvoice(invID string, userID uuid.UUID, email string)
// TrackStorjscanUnpaidInvoice sends an event to Segment indicating that a user has not paid an invoice, but has storjscan transaction history.
TrackStorjscanUnpaidInvoice(invID string, userID uuid.UUID, email string)
}
// LimitRequestInfo holds data needed to request limit increase.
type LimitRequestInfo struct {
ProjectName string
LimitType string
CurrentLimit string
DesiredLimit string
}
// Service for sending analytics.
//
// architecture: Service
type Service struct {
log *zap.Logger
config Config
satelliteName string
clientEvents map[string]bool
segment segment.Client
hubspot *HubSpotEvents
}
// NewService creates new service for creating sending analytics.
func NewService(log *zap.Logger, config Config, satelliteName string) *Service {
service := &Service{
log: log,
config: config,
satelliteName: satelliteName,
clientEvents: make(map[string]bool),
hubspot: NewHubSpotEvents(log.Named("hubspotclient"), config.HubSpot, satelliteName),
}
if config.Enabled {
service.segment = segment.New(config.SegmentWriteKey)
}
for _, name := range []string{eventGatewayCredentialsCreated, eventPassphraseCreated, eventExternalLinkClicked,
eventPathSelected, eventLinkShared, eventObjectUploaded, eventAPIKeyGenerated, eventUpgradeBannerClicked,
eventModalAddCard, eventModalAddTokens, eventSearchBuckets, eventNavigateProjects, eventManageProjectsClicked,
eventCreateNewClicked, eventViewDocsClicked, eventViewForumClicked, eventViewSupportClicked, eventCreateAnAccessGrantClicked,
eventUploadUsingCliClicked, eventUploadInWebClicked, eventNewProjectClicked, eventLogoutClicked, eventProfileUpdated,
eventPasswordChanged, eventMfaEnabled, eventBucketCreated, eventBucketDeleted, eventAccessGrantCreated, eventAPIAccessCreated,
eventUploadFileClicked, eventUploadFolderClicked, eventCreateKeysClicked, eventDownloadTxtClicked, eventEncryptMyAccessClicked,
eventCopyToClipboardClicked, eventCreateAccessGrantClicked, eventCreateS3CredentialsClicked, eventKeysForCLIClicked,
eventSeePaymentsClicked, eventEditPaymentMethodClicked, eventUsageDetailedInfoClicked, eventAddNewPaymentMethodClicked,
eventApplyNewCouponClicked, eventCreditCardRemoved, eventCouponCodeApplied, eventInvoiceDownloaded, eventCreditCardAddedFromBilling,
eventStorjTokenAddedFromBilling, eventAddFundsClicked, eventProjectMembersInviteSent, eventError, eventProjectNameUpdated, eventProjectDescriptionUpdated,
eventProjectStorageLimitUpdated, eventProjectBandwidthLimitUpdated, eventProjectInvitationAccepted, eventProjectInvitationDeclined,
eventGalleryViewClicked, eventResendInviteClicked, eventRemoveProjectMemberCLicked, eventCopyInviteLinkClicked} {
service.clientEvents[name] = true
}
return service
}
// Run runs the service and use the context in new requests.
func (service *Service) Run(ctx context.Context) error {
if !service.config.Enabled {
return nil
}
return service.hubspot.Run(ctx)
}
// Close closes the Segment client.
func (service *Service) Close() error {
if !service.config.Enabled {
return nil
}
return service.segment.Close()
}
// UserType is a type for distinguishing personal vs. professional users.
type UserType string
const (
// Professional defines a "professional" user type.
Professional UserType = "Professional"
// Personal defines a "personal" user type.
Personal UserType = "Personal"
)
// TrackCreateUserFields contains input data for tracking a create user event.
type TrackCreateUserFields struct {
ID uuid.UUID
AnonymousID string
FullName string
Email string
Type UserType
EmployeeCount string
CompanyName string
StorageNeeds string
JobTitle string
HaveSalesContact bool
OriginHeader string
Referrer string
HubspotUTK string
UserAgent string
}
func (service *Service) enqueueMessage(message segment.Message) {
err := service.segment.Enqueue(message)
if err != nil {
service.log.Error("Error enqueueing message", zap.Error(err))
}
}
// TrackCreateUser sends an "Account Created" event to Segment.
func (service *Service) TrackCreateUser(fields TrackCreateUserFields) {
if !service.config.Enabled {
return
}
fullName := fields.FullName
names := strings.SplitN(fullName, " ", 2)
var firstName string
var lastName string
if len(names) > 1 {
firstName = names[0]
lastName = names[1]
} else {
firstName = fullName
}
traits := segment.NewTraits()
traits.SetFirstName(firstName)
traits.SetLastName(lastName)
traits.SetEmail(fields.Email)
traits.Set("origin_header", fields.OriginHeader)
traits.Set("signup_referrer", fields.Referrer)
traits.Set("account_created", true)
if fields.Type == Professional {
traits.Set("have_sales_contact", fields.HaveSalesContact)
}
if len(fields.UserAgent) > 0 {
traits.Set("signup_partner", fields.UserAgent)
}
service.enqueueMessage(segment.Identify{
UserId: fields.ID.String(),
AnonymousId: fields.AnonymousID,
Traits: traits,
})
props := segment.NewProperties()
props.Set("email", fields.Email)
props.Set("name", fields.FullName)
props.Set("satellite_selected", service.satelliteName)
props.Set("account_type", fields.Type)
props.Set("origin_header", fields.OriginHeader)
props.Set("signup_referrer", fields.Referrer)
props.Set("account_created", true)
if fields.Type == Professional {
props.Set("company_size", fields.EmployeeCount)
props.Set("company_name", fields.CompanyName)
props.Set("job_title", fields.JobTitle)
props.Set("storage_needs", fields.StorageNeeds)
}
service.enqueueMessage(segment.Track{
UserId: fields.ID.String(),
AnonymousId: fields.AnonymousID,
Event: service.satelliteName + " " + eventAccountCreated,
Properties: props,
})
service.hubspot.EnqueueCreateUser(fields)
}
// TrackSignedIn sends an "Signed In" event to Segment.
func (service *Service) TrackSignedIn(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
traits := segment.NewTraits()
traits.SetEmail(email)
service.enqueueMessage(segment.Identify{
UserId: userID.String(),
Traits: traits,
})
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventSignedIn,
Properties: props,
})
}
// TrackProjectCreated sends an "Project Created" event to Segment.
func (service *Service) TrackProjectCreated(userID uuid.UUID, email string, projectID uuid.UUID, currentProjectCount int) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("project_count", currentProjectCount)
props.Set("project_id", projectID.String())
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventProjectCreated,
Properties: props,
})
}
// TrackAccountFrozen sends an account frozen event to Segment.
func (service *Service) TrackAccountFrozen(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventAccountFrozen,
Properties: props,
})
}
// TrackRequestLimitIncrease sends a limit increase request to Segment.
func (service *Service) TrackRequestLimitIncrease(userID uuid.UUID, email string, info LimitRequestInfo) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
props.Set("satellite", service.satelliteName)
if info.ProjectName != "" {
props.Set("project", info.ProjectName)
}
props.Set("type", info.LimitType)
props.Set("currentLimit", info.CurrentLimit)
props.Set("desiredLimit", info.DesiredLimit)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventLimitIncreaseRequested,
Properties: props,
})
}
// TrackAccountUnfrozen sends an account unfrozen event to Segment.
func (service *Service) TrackAccountUnfrozen(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventAccountUnfrozen,
Properties: props,
})
}
// TrackAccountUnwarned sends an account unwarned event to Segment.
func (service *Service) TrackAccountUnwarned(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventAccountUnwarned,
Properties: props,
})
}
// TrackAccountFreezeWarning sends an account freeze warning event to Segment.
func (service *Service) TrackAccountFreezeWarning(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventAccountFreezeWarning,
Properties: props,
})
}
// TrackLargeUnpaidInvoice sends an event to Segment indicating that a user has not paid a large invoice.
func (service *Service) TrackLargeUnpaidInvoice(invID string, userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
props.Set("invoice", invID)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventUnpaidLargeInvoice,
Properties: props,
})
}
// TrackStorjscanUnpaidInvoice sends an event to Segment indicating that a user has not paid an invoice, but has storjscan transaction history.
func (service *Service) TrackStorjscanUnpaidInvoice(invID string, userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
props.Set("invoice", invID)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventUnpaidStorjscanInvoice,
Properties: props,
})
}
// TrackAccessGrantCreated sends an "Access Grant Created" event to Segment.
func (service *Service) TrackAccessGrantCreated(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventAccessGrantCreated,
Properties: props,
})
}
// TrackAccountVerified sends an "Account Verified" event to Segment.
func (service *Service) TrackAccountVerified(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
traits := segment.NewTraits()
traits.SetEmail(email)
service.enqueueMessage(segment.Identify{
UserId: userID.String(),
Traits: traits,
})
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventAccountVerified,
Properties: props,
})
}
// TrackEvent sends an arbitrary event associated with user ID to Segment.
// It is used for tracking occurrences of client-side events.
func (service *Service) TrackEvent(eventName string, userID uuid.UUID, email string, customProps map[string]string) {
if !service.config.Enabled {
return
}
// do not track if the event name is an invalid client-side event
if !service.clientEvents[eventName] {
service.log.Error("Invalid client-triggered event", zap.String("eventName", eventName))
return
}
props := segment.NewProperties()
props.Set("email", email)
for key, value := range customProps {
props.Set(key, value)
}
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventName,
Properties: props,
})
}
// TrackErrorEvent sends an arbitrary error event associated with user ID to Segment.
// It is used for tracking occurrences of client-side errors.
func (service *Service) TrackErrorEvent(userID uuid.UUID, email string, source string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
props.Set("source", source)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventError,
Properties: props,
})
}
// TrackLinkEvent sends an arbitrary event and link associated with user ID to Segment.
// It is used for tracking occurrences of client-side events.
func (service *Service) TrackLinkEvent(eventName string, userID uuid.UUID, email, link string) {
if !service.config.Enabled {
return
}
// do not track if the event name is an invalid client-side event
if !service.clientEvents[eventName] {
service.log.Error("Invalid client-triggered event", zap.String("eventName", eventName))
return
}
props := segment.NewProperties()
props.Set("link", link)
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventName,
Properties: props,
})
}
// TrackCreditCardAdded sends an "Credit Card Added" event to Segment.
func (service *Service) TrackCreditCardAdded(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventCreditCardAdded,
Properties: props,
})
}
// PageVisitEvent sends a page visit event associated with user ID to Segment.
// It is used for tracking occurrences of client-side events.
func (service *Service) PageVisitEvent(pageName string, userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
props.Set("path", pageName)
props.Set("user_id", userID.String())
props.Set("satellite", service.satelliteName)
service.enqueueMessage(segment.Page{
UserId: userID.String(),
Name: "Page Requested",
Properties: props,
})
}
// TrackProjectLimitError sends an "Project Limit Error" event to Segment.
func (service *Service) TrackProjectLimitError(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventProjectLimitError,
Properties: props,
})
}
// TrackStorjTokenAdded sends an "Storj Token Added" event to Segment.
func (service *Service) TrackStorjTokenAdded(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventStorjTokenAdded,
Properties: props,
})
}
// TrackProjectMemberAddition sends an "Project Member Added" event to Segment.
func (service *Service) TrackProjectMemberAddition(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventProjectMemberAdded,
Properties: props,
})
}
// TrackProjectMemberDeletion sends an "Project Member Deleted" event to Segment.
func (service *Service) TrackProjectMemberDeletion(userID uuid.UUID, email string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("email", email)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventProjectMemberDeleted,
Properties: props,
})
}
// TrackExpiredCreditNeedsRemoval sends an "Expired Credit Needs Removal" event to Segment.
func (service *Service) TrackExpiredCreditNeedsRemoval(userID uuid.UUID, customerID, packagePlan string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("customer ID", customerID)
props.Set("package plan", packagePlan)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventExpiredCreditNeedsRemoval,
Properties: props,
})
}
// TrackExpiredCreditRemoved sends an "Expired Credit Removed" event to Segment.
func (service *Service) TrackExpiredCreditRemoved(userID uuid.UUID, customerID, packagePlan string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("customer ID", customerID)
props.Set("package plan", packagePlan)
service.enqueueMessage(segment.Track{
UserId: userID.String(),
Event: service.satelliteName + " " + eventExpiredCreditRemoved,
Properties: props,
})
}
// TrackInviteLinkSignup sends an "Invite Link Signup" event to Segment.
func (service *Service) TrackInviteLinkSignup(inviter, invitee string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("inviter", inviter)
props.Set("invitee", invitee)
service.enqueueMessage(segment.Track{
Event: service.satelliteName + " " + eventInviteLinkSignup,
Properties: props,
})
}
// TrackInviteLinkClicked sends an "Invite Link Clicked" event to Segment.
func (service *Service) TrackInviteLinkClicked(inviter, invitee string) {
if !service.config.Enabled {
return
}
props := segment.NewProperties()
props.Set("inviter", inviter)
props.Set("invitee", invitee)
service.enqueueMessage(segment.Track{
Event: service.satelliteName + " " + eventInviteLinkClicked,
Properties: props,
})
}