diff --git a/satellite/analytics/service.go b/satellite/analytics/service.go index 860b9ebb0..ba5a59ca3 100644 --- a/satellite/analytics/service.go +++ b/satellite/analytics/service.go @@ -12,6 +12,9 @@ import ( const ( eventAccountCreated = "Account Created" + eventSignedIn = "Signed In" + eventProjectCreated = "Project Created" + eventAccessGrantCreated = "Access Grant Created" gatewayCredentialsCreated = "Credentials Created" ) @@ -121,6 +124,49 @@ func (service *Service) TrackCreateUser(fields TrackCreateUserFields) { }) } +// TrackSignedIn sends an "Signed In" event to Segment. +func (service *Service) TrackSignedIn(userID uuid.UUID, email string) { + 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: eventSignedIn, + Properties: props, + }) +} + +// TrackProjectCreated sends an "Project Created" event to Segment. +func (service *Service) TrackProjectCreated(userID, projectID uuid.UUID, currentProjectCount int) { + + props := segment.NewProperties() + props.Set("project_count", currentProjectCount) + props.Set("project_id", projectID.String()) + + service.enqueueMessage(segment.Track{ + UserId: userID.String(), + Event: eventProjectCreated, + Properties: props, + }) +} + +// TrackAccessGrantCreated sends an "Access Grant Created" event to Segment. +func (service *Service) TrackAccessGrantCreated(userID uuid.UUID) { + + service.enqueueMessage(segment.Track{ + UserId: userID.String(), + Event: eventAccessGrantCreated, + }) +} + // 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) { diff --git a/satellite/api.go b/satellite/api.go index 5826df948..7b314c6e6 100644 --- a/satellite/api.go +++ b/satellite/api.go @@ -362,6 +362,15 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, ) } + { // setup analytics service + peer.Analytics.Service = analytics.NewService(peer.Log.Named("analytics:service"), config.Analytics, config.Console.SatelliteName) + + peer.Services.Add(lifecycle.Item{ + Name: "analytics:service", + Close: peer.Analytics.Service.Close, + }) + } + { // setup metainfo peer.Metainfo.Database = pointerDB peer.Metainfo.Metabase = metabaseDB @@ -555,15 +564,6 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, }) } - { // setup analytics service - peer.Analytics.Service = analytics.NewService(peer.Log.Named("analytics:service"), config.Analytics, config.Console.SatelliteName) - - peer.Services.Add(lifecycle.Item{ - Name: "analytics:service", - Close: peer.Analytics.Service.Close, - }) - } - { // setup console consoleConfig := config.Console peer.Console.Listener, err = net.Listen("tcp", consoleConfig.Address) @@ -583,6 +583,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, peer.DB.Buckets(), peer.Marketing.PartnersService, peer.Payments.Accounts, + peer.Analytics.Service, consoleConfig.Config, config.Payments.MinCoinPayment, ) diff --git a/satellite/console/consoleweb/consoleql/mutation_test.go b/satellite/console/consoleweb/consoleql/mutation_test.go index eee7b2e83..6c9697c58 100644 --- a/satellite/console/consoleweb/consoleql/mutation_test.go +++ b/satellite/console/consoleweb/consoleql/mutation_test.go @@ -22,6 +22,7 @@ import ( "storj.io/storj/satellite" "storj.io/storj/satellite/accounting" "storj.io/storj/satellite/accounting/live" + "storj.io/storj/satellite/analytics" "storj.io/storj/satellite/console" "storj.io/storj/satellite/console/consoleauth" "storj.io/storj/satellite/console/consoleweb/consoleql" @@ -59,6 +60,8 @@ func TestGraphqlMutation(t *testing.T) { }, ) + analyticsService := analytics.NewService(log, analytics.Config{}, "test-satellite") + redis, err := testredis.Mini(ctx) require.NoError(t, err) defer ctx.Check(redis.Close) @@ -108,6 +111,7 @@ func TestGraphqlMutation(t *testing.T) { db.Buckets(), partnersService, paymentsService.Accounts(), + analyticsService, console.Config{PasswordCost: console.TestPasswordCost, DefaultProjectLimit: 5}, 5000, ) diff --git a/satellite/console/consoleweb/consoleql/query_test.go b/satellite/console/consoleweb/consoleql/query_test.go index b2c13868f..63678988d 100644 --- a/satellite/console/consoleweb/consoleql/query_test.go +++ b/satellite/console/consoleweb/consoleql/query_test.go @@ -19,6 +19,7 @@ import ( "storj.io/storj/satellite" "storj.io/storj/satellite/accounting" "storj.io/storj/satellite/accounting/live" + "storj.io/storj/satellite/analytics" "storj.io/storj/satellite/console" "storj.io/storj/satellite/console/consoleauth" "storj.io/storj/satellite/console/consoleweb/consoleql" @@ -43,6 +44,8 @@ func TestGraphqlQuery(t *testing.T) { }, ) + analyticsService := analytics.NewService(log, analytics.Config{}, "test-satellite") + redis, err := testredis.Mini(ctx) require.NoError(t, err) defer ctx.Check(redis.Close) @@ -92,6 +95,7 @@ func TestGraphqlQuery(t *testing.T) { db.Buckets(), partnersService, paymentsService.Accounts(), + analyticsService, console.Config{PasswordCost: console.TestPasswordCost, DefaultProjectLimit: 5}, 5000, ) diff --git a/satellite/console/service.go b/satellite/console/service.go index ad6e3a235..f4909ef03 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -24,6 +24,7 @@ import ( "storj.io/common/storj" "storj.io/common/uuid" "storj.io/storj/satellite/accounting" + "storj.io/storj/satellite/analytics" "storj.io/storj/satellite/console/consoleauth" "storj.io/storj/satellite/payments" "storj.io/storj/satellite/rewards" @@ -93,6 +94,7 @@ type Service struct { buckets Buckets partners *rewards.PartnersService accounts payments.Accounts + analytics *analytics.Service config Config @@ -113,7 +115,7 @@ type PaymentsService struct { } // NewService returns new instance of Service. -func NewService(log *zap.Logger, signer Signer, store DB, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets Buckets, partners *rewards.PartnersService, accounts payments.Accounts, config Config, minCoinPayment int64) (*Service, error) { +func NewService(log *zap.Logger, signer Signer, store DB, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets Buckets, partners *rewards.PartnersService, accounts payments.Accounts, analytics *analytics.Service, config Config, minCoinPayment int64) (*Service, error) { if signer == nil { return nil, errs.New("signer can't be nil") } @@ -137,6 +139,7 @@ func NewService(log *zap.Logger, signer Signer, store DB, projectAccounting acco buckets: buckets, partners: partners, accounts: accounts, + analytics: analytics, config: config, minCoinPayment: minCoinPayment, }, nil @@ -431,6 +434,7 @@ func (paymentService PaymentsService) TokenDeposit(ctx context.Context, amount i } tx, err := paymentService.service.accounts.StorjTokens().Deposit(ctx, auth.User.ID, amount) + return tx, Error.Wrap(err) } @@ -780,6 +784,8 @@ func (s *Service) Token(ctx context.Context, email, password string) (token stri } s.auditLog(ctx, "login", &user.ID, user.Email) + s.analytics.TrackSignedIn(user.ID, user.Email) + return token, nil } @@ -990,7 +996,7 @@ func (s *Service) CreateProject(ctx context.Context, projectInfo ProjectInfo) (p return nil, Error.Wrap(err) } - err = s.checkProjectLimit(ctx, auth.User.ID) + currentProjectCount, err := s.checkProjectLimit(ctx, auth.User.ID) if err != nil { return nil, ErrProjLimit.Wrap(err) } @@ -1021,6 +1027,7 @@ func (s *Service) CreateProject(ctx context.Context, projectInfo ProjectInfo) (p } } + var projectID uuid.UUID err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error { p, err = tx.Projects().Insert(ctx, &Project{ @@ -1041,12 +1048,17 @@ func (s *Service) CreateProject(ctx context.Context, projectInfo ProjectInfo) (p return Error.Wrap(err) } + projectID = p.ID + return nil }) + if err != nil { return nil, Error.Wrap(err) } + s.analytics.TrackProjectCreated(auth.User.ID, projectID, currentProjectCount+1) + // ToDo: check if this is actually the right place. err = s.accounts.Coupons().AddPromotionalCoupon(ctx, auth.User.ID) if err != nil { @@ -1276,6 +1288,8 @@ func (s *Service) CreateAPIKey(ctx context.Context, projectID uuid.UUID, name st return nil, nil, Error.Wrap(err) } + s.analytics.TrackAccessGrantCreated(auth.User.ID) + return info, key, nil } @@ -1598,12 +1612,12 @@ func (s *Service) checkProjectCanBeDeleted(ctx context.Context, project uuid.UUI } // checkProjectLimit is used to check if user is able to create a new project. -func (s *Service) checkProjectLimit(ctx context.Context, userID uuid.UUID) (err error) { +func (s *Service) checkProjectLimit(ctx context.Context, userID uuid.UUID) (currentProjects int, err error) { defer mon.Task()(&ctx)(&err) limit, err := s.store.Users().GetProjectLimit(ctx, userID) if err != nil { - return Error.Wrap(err) + return 0, Error.Wrap(err) } if limit == 0 { limit = s.config.DefaultProjectLimit @@ -1611,14 +1625,14 @@ func (s *Service) checkProjectLimit(ctx context.Context, userID uuid.UUID) (err projects, err := s.GetUsersProjects(ctx) if err != nil { - return Error.Wrap(err) + return 0, Error.Wrap(err) } if len(projects) >= limit { - return ErrProjLimit.New(projLimitErrMsg) + return 0, ErrProjLimit.New(projLimitErrMsg) } - return nil + return len(projects), nil } // CreateRegToken creates new registration token. Needed for testing.