satellite/analytics: Add analytics service to satellite

* Set up basic structure of new service.
* Implement a basic analytics track event for user creation.

Change-Id: Ica8c785540b1ef9d848404af307a22f21d33c6aa
This commit is contained in:
Moby von Briesen 2021-03-23 11:52:34 -04:00
parent c4b2d76d1c
commit 3db52491ec
8 changed files with 158 additions and 3 deletions

3
go.mod
View File

@ -27,6 +27,7 @@ require (
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect
github.com/shopspring/decimal v1.2.0
github.com/spacemonkeygo/monkit/v3 v3.0.10
github.com/spf13/cobra v1.0.0
@ -35,6 +36,7 @@ require (
github.com/stretchr/testify v1.7.0
github.com/stripe/stripe-go v70.15.0+incompatible
github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
github.com/zeebo/assert v1.3.0
github.com/zeebo/errs v1.2.2
go.etcd.io/bbolt v1.3.5
@ -45,6 +47,7 @@ require (
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
google.golang.org/api v0.20.0 // indirect
gopkg.in/segmentio/analytics-go.v3 v3.1.0
storj.io/common v0.0.0-20210324105846-0a39fd4f6781
storj.io/drpc v0.0.20
storj.io/monkit-jaeger v0.0.0-20210225162224-66fb37637bf6

7
go.sum
View File

@ -62,6 +62,7 @@ github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCS
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
@ -495,6 +496,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 h1:ZuhckGJ10ulaKkdvJtiAqsLTiPrLaXSdnVgXJKJkTxE=
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -587,6 +590,8 @@ github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhe
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
@ -914,6 +919,8 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/segmentio/analytics-go.v3 v3.1.0 h1:UzxH1uaGZRpMKDhJyBz0pexz6yUoBU3x8bJsRk/HV6U=
gopkg.in/segmentio/analytics-go.v3 v3.1.0/go.mod h1:4QqqlTlSSpVlWA9/9nDcPw+FkM2yv1NQoYjUbL9/JAw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=

View File

@ -0,0 +1,104 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package analytics
import (
"go.uber.org/zap"
segment "gopkg.in/segmentio/analytics-go.v3"
"storj.io/common/uuid"
)
const (
eventAccountCreated = "Account Created"
)
// Config is a configuration struct for analytics Service.
type Config struct {
SegmentWriteKey string `help:"segment write key" default:""`
}
// Service for sending analytics.
//
// architecture: Service
type Service struct {
log *zap.Logger
config Config
satelliteName string
segment segment.Client
}
// NewService creates new service for creating sending analytics.
func NewService(log *zap.Logger, config Config, satelliteName string) *Service {
return &Service{
log: log,
config: config,
satelliteName: satelliteName,
segment: segment.New(config.SegmentWriteKey),
}
}
// Close closes the Segment client.
func (service *Service) Close() error {
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
FullName string
Email string
Type UserType
EmployeeCount string
CompanyName string
JobTitle string
}
// TrackCreateUser sends an "Account Created" event to Segment.
func (service *Service) TrackCreateUser(fields TrackCreateUserFields) {
traits := segment.NewTraits()
traits.SetName(fields.FullName)
traits.SetEmail(fields.Email)
err := service.segment.Enqueue(segment.Identify{
UserId: fields.ID.String(),
Traits: traits,
})
if err != nil {
service.log.Error("Error with identify event", zap.Error(err))
}
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)
if fields.Type == Professional {
props.Set("company_size", fields.EmployeeCount)
props.Set("company_name", fields.CompanyName)
props.Set("job_title", fields.JobTitle)
}
err = service.segment.Enqueue(segment.Track{
UserId: fields.ID.String(),
Event: eventAccountCreated,
Properties: props,
})
if err != nil {
service.log.Error("Error with track event", zap.Error(err))
}
}

View File

@ -31,6 +31,7 @@ import (
"storj.io/storj/private/post/oauth2"
"storj.io/storj/private/version/checker"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/analytics"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/console/consoleauth"
"storj.io/storj/satellite/console/consoleweb"
@ -157,6 +158,10 @@ type API struct {
GracefulExit struct {
Endpoint *gracefulexit.Endpoint
}
Analytics struct {
Service *analytics.Service
}
}
// NewAPI creates a new satellite API process.
@ -556,6 +561,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 console
consoleConfig := config.Console
peer.Console.Listener, err = net.Listen("tcp", consoleConfig.Address)
@ -588,6 +602,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
peer.Console.Service,
peer.Mail.Service,
peer.Marketing.PartnersService,
peer.Analytics.Service,
peer.Console.Listener,
config.Payments.StripeCoinPayments.StripePublicKey,
peer.URL(),

View File

@ -14,6 +14,7 @@ import (
"storj.io/common/uuid"
"storj.io/storj/private/post"
"storj.io/storj/satellite/analytics"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/console/consoleweb/consoleql"
"storj.io/storj/satellite/console/consoleweb/consolewebauth"
@ -38,13 +39,14 @@ type Auth struct {
TermsAndConditionsURL string
ContactInfoURL string
service *console.Service
analytics *analytics.Service
mailService *mailservice.Service
cookieAuth *consolewebauth.CookieAuth
partners *rewards.PartnersService
}
// NewAuth is a constructor for api auth controller.
func NewAuth(log *zap.Logger, service *console.Service, mailService *mailservice.Service, cookieAuth *consolewebauth.CookieAuth, partners *rewards.PartnersService, externalAddress string, letUsKnowURL string, termsAndConditionsURL string, contactInfoURL string) *Auth {
func NewAuth(log *zap.Logger, service *console.Service, mailService *mailservice.Service, cookieAuth *consolewebauth.CookieAuth, partners *rewards.PartnersService, analytics *analytics.Service, externalAddress string, letUsKnowURL string, termsAndConditionsURL string, contactInfoURL string) *Auth {
return &Auth{
log: log,
ExternalAddress: externalAddress,
@ -55,6 +57,7 @@ func NewAuth(log *zap.Logger, service *console.Service, mailService *mailservice
mailService: mailService,
cookieAuth: cookieAuth,
partners: partners,
analytics: analytics,
}
}
@ -163,6 +166,20 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
return
}
trackCreateUserFields := analytics.TrackCreateUserFields{
ID: user.ID,
FullName: user.FullName,
Email: user.Email,
Type: analytics.Personal,
}
if user.IsProfessional {
trackCreateUserFields.Type = analytics.Professional
trackCreateUserFields.EmployeeCount = user.EmployeeCount
trackCreateUserFields.CompanyName = user.CompanyName
trackCreateUserFields.JobTitle = user.Position
}
a.analytics.TrackCreateUser(trackCreateUserFields)
token, err := a.service.GenerateActivationToken(ctx, user.ID, user.Email)
if err != nil {
a.serveJSONError(w, err)

View File

@ -34,6 +34,7 @@ import (
"storj.io/common/uuid"
"storj.io/storj/private/web"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/analytics"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/console/consoleauth"
"storj.io/storj/satellite/console/consoleweb/consoleapi"
@ -102,6 +103,7 @@ type Server struct {
service *console.Service
mailService *mailservice.Service
partners *rewards.PartnersService
analytics *analytics.Service
listener net.Listener
server http.Server
@ -124,7 +126,7 @@ type Server struct {
}
// NewServer creates new instance of console server.
func NewServer(logger *zap.Logger, config Config, service *console.Service, mailService *mailservice.Service, partners *rewards.PartnersService, listener net.Listener, stripePublicKey string, nodeURL storj.NodeURL) *Server {
func NewServer(logger *zap.Logger, config Config, service *console.Service, mailService *mailservice.Service, partners *rewards.PartnersService, analytics *analytics.Service, listener net.Listener, stripePublicKey string, nodeURL storj.NodeURL) *Server {
server := Server{
log: logger,
config: config,
@ -132,6 +134,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
service: service,
mailService: mailService,
partners: partners,
analytics: analytics,
stripePublicKey: stripePublicKey,
rateLimiter: web.NewIPRateLimiter(config.RateLimit),
nodeURL: nodeURL,
@ -170,7 +173,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, mail
server.withAuth(http.HandlerFunc(server.projectUsageLimitsHandler)),
).Methods(http.MethodGet)
authController := consoleapi.NewAuth(logger, service, mailService, server.cookieAuth, partners, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL)
authController := consoleapi.NewAuth(logger, service, mailService, server.cookieAuth, partners, server.analytics, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL)
authRouter := router.PathPrefix("/api/v0/auth").Subrouter()
authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.GetAccount))).Methods(http.MethodGet)
authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.UpdateAccount))).Methods(http.MethodPatch)

View File

@ -20,6 +20,7 @@ import (
"storj.io/storj/satellite/accounting/rolluparchive"
"storj.io/storj/satellite/accounting/tally"
"storj.io/storj/satellite/admin"
"storj.io/storj/satellite/analytics"
"storj.io/storj/satellite/attribution"
"storj.io/storj/satellite/audit"
"storj.io/storj/satellite/compensation"
@ -146,4 +147,6 @@ type Config struct {
Compensation compensation.Config
ProjectLimit accounting.ProjectLimitConfig
Analytics analytics.Config
}

View File

@ -1,6 +1,9 @@
# admin peer http listening address
# admin.address: ""
# segment write key
# analytics.segment-write-key: ""
# how often to run the reservoir chore
# audit.chore-interval: 24h0m0s