5d727becb4
Implemented UI error tracking. We use satellite analytics service to track the fact that UI error occurred and send minimal info to Segment (not Hubspot). We send only the fact that UI error occurred and the place where this error occurred. Extended notificator plugin error function to include the place where error occurred. I made the place argument nullable to be always explicitly provided (build fails if place is not provided). If place is not null then error event is triggered in the background. Issue: https://github.com/storj/storj-private/issues/107 Change-Id: I7d129fb29629979f5be6ff5dea37ad19b1a2397e
528 lines
16 KiB
Go
528 lines
16 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 (
|
|
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"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// 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} {
|
|
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
|
|
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("lifecyclestage", "other")
|
|
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)
|
|
}
|
|
|
|
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,
|
|
})
|
|
|
|
service.hubspot.EnqueueEvent(email, service.satelliteName+"_"+eventSignedIn, map[string]interface{}{
|
|
"userid": userID.String(),
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
|
|
service.hubspot.EnqueueEvent(email, service.satelliteName+"_"+eventProjectCreated, map[string]interface{}{
|
|
"userid": userID.String(),
|
|
"project_count": currentProjectCount,
|
|
"project_id": projectID.String(),
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
|
|
service.hubspot.EnqueueEvent(email, service.satelliteName+"_"+eventAccessGrantCreated, map[string]interface{}{
|
|
"userid": userID.String(),
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
|
|
service.hubspot.EnqueueEvent(email, service.satelliteName+"_"+eventAccountVerified, map[string]interface{}{
|
|
"userid": userID.String(),
|
|
})
|
|
}
|
|
|
|
// 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) {
|
|
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)
|
|
|
|
service.enqueueMessage(segment.Track{
|
|
UserId: userID.String(),
|
|
Event: service.satelliteName + " " + eventName,
|
|
Properties: props,
|
|
})
|
|
|
|
service.hubspot.EnqueueEvent(email, service.satelliteName+"_"+eventName, map[string]interface{}{
|
|
"userid": userID.String(),
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
|
|
service.hubspot.EnqueueEvent(email, service.satelliteName+"_"+eventName, map[string]interface{}{
|
|
"userid": userID.String(),
|
|
"link": link,
|
|
})
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
|
|
}
|