diff --git a/satellite/analytics/service.go b/satellite/analytics/service.go index eda445f8c..728f9ba0a 100644 --- a/satellite/analytics/service.go +++ b/satellite/analytics/service.go @@ -15,6 +15,8 @@ import ( ) const ( + eventInviteLinkClicked = "Invite Link Clicked" + eventInviteLinkSignup = "Invite Link Signup" eventAccountCreated = "Account Created" eventSignedIn = "Signed In" eventProjectCreated = "Project Created" @@ -734,3 +736,35 @@ func (service *Service) TrackExpiredCreditRemoved(userID uuid.UUID, customerID, 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, + }) +} diff --git a/satellite/console/consoleweb/consoleapi/auth.go b/satellite/console/consoleweb/consoleapi/auth.go index eb7f336b3..eeaed304f 100644 --- a/satellite/console/consoleweb/consoleapi/auth.go +++ b/satellite/console/consoleweb/consoleapi/auth.go @@ -319,6 +319,26 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) { return } + invites, err := a.service.GetInvitesByEmail(ctx, registerData.Email) + if err != nil { + a.log.Error("Could not get invitations", zap.String("email", registerData.Email), zap.Error(err)) + } else if len(invites) > 0 { + var firstInvite console.ProjectInvitation + for _, inv := range invites { + if inv.InviterID != nil && (firstInvite.CreatedAt.IsZero() || inv.CreatedAt.Before(firstInvite.CreatedAt)) { + firstInvite = inv + } + } + if firstInvite.InviterID != nil { + inviter, err := a.service.GetUser(ctx, *firstInvite.InviterID) + if err != nil { + a.log.Error("Error getting inviter info", zap.String("ID", firstInvite.InviterID.String()), zap.Error(err)) + } else { + a.analytics.TrackInviteLinkSignup(inviter.Email, registerData.Email) + } + } + } + // see if referrer was provided in URL query, otherwise use the Referer header in the request. referrer := r.URL.Query().Get("referrer") if referrer == "" { diff --git a/satellite/console/consoleweb/consoleapi/auth_test.go b/satellite/console/consoleweb/consoleapi/auth_test.go index 87f9c2114..617ae56f7 100644 --- a/satellite/console/consoleweb/consoleapi/auth_test.go +++ b/satellite/console/consoleweb/consoleapi/auth_test.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "math/rand" "net/http" @@ -26,6 +27,7 @@ import ( "storj.io/common/testcontext" "storj.io/common/testrand" + "storj.io/common/uuid" "storj.io/storj/private/post" "storj.io/storj/private/testplanet" "storj.io/storj/satellite" @@ -151,6 +153,82 @@ func TestAuth_Register(t *testing.T) { }) } +func TestAuth_RegisterWithInvitation(t *testing.T) { + testplanet.Run(t, testplanet.Config{ + SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1, + Reconfigure: testplanet.Reconfigure{ + Satellite: func(log *zap.Logger, index int, config *satellite.Config) { + config.Console.OpenRegistrationEnabled = true + config.Console.RateLimit.Burst = 10 + config.Mail.AuthType = "nomail" + }, + }, + }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { + for i := 0; i < 2; i++ { + email := fmt.Sprintf("user%d@test.test", i) + // test with nil and non-nil inviter ID to make sure nil pointer dereference doesn't occur + // since nil ID is technically possible + var inviter *uuid.UUID + if i == 1 { + id := planet.Uplinks[0].Projects[0].Owner.ID + inviter = &id + } + _, err := planet.Satellites[0].API.DB.Console().ProjectInvitations().Upsert(ctx, &console.ProjectInvitation{ + ProjectID: planet.Uplinks[0].Projects[0].ID, + Email: email, + InviterID: inviter, + }) + require.NoError(t, err) + + registerData := struct { + FullName string `json:"fullName"` + ShortName string `json:"shortName"` + Email string `json:"email"` + Partner string `json:"partner"` + UserAgent string `json:"userAgent"` + Password string `json:"password"` + SecretInput string `json:"secret"` + ReferrerUserID string `json:"referrerUserId"` + IsProfessional bool `json:"isProfessional"` + Position string `json:"Position"` + CompanyName string `json:"CompanyName"` + EmployeeCount string `json:"EmployeeCount"` + SignupPromoCode string `json:"signupPromoCode"` + }{ + FullName: "testuser", + ShortName: "test", + Email: email, + Password: "abc123", + IsProfessional: true, + Position: "testposition", + CompanyName: "companytestname", + EmployeeCount: "0", + SignupPromoCode: "STORJ50", + } + + jsonBody, err := json.Marshal(registerData) + require.NoError(t, err) + + url := planet.Satellites[0].ConsoleURL() + "/api/v0/auth/register" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(jsonBody)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + result, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer func() { + err = result.Body.Close() + require.NoError(t, err) + }() + require.Equal(t, http.StatusOK, result.StatusCode) + require.Len(t, planet.Satellites, 1) + // this works only because we configured 'nomail' above. Mail send simulator won't click to activation link. + _, users, err := planet.Satellites[0].API.Console.Service.GetUserByEmailWithUnverified(ctx, registerData.Email) + require.NoError(t, err) + require.Len(t, users, 1) + } + }) +} + func TestDeleteAccount(t *testing.T) { ctx := testcontext.New(t) log := testplanet.NewLogger(t) diff --git a/satellite/console/consoleweb/server.go b/satellite/console/consoleweb/server.go index 155c28977..4e577e172 100644 --- a/satellite/console/consoleweb/server.go +++ b/satellite/console/consoleweb/server.go @@ -927,6 +927,8 @@ func (server *Server) handleInvited(w http.ResponseWriter, r *http.Request) { } params.Add("inviter", name) params.Add("inviter_email", inviter.Email) + + server.analytics.TrackInviteLinkClicked(inviter.Email, invite.Email) } proj, err := server.service.GetProjectNoAuth(ctx, invite.ProjectID) diff --git a/satellite/console/service.go b/satellite/console/service.go index 0893b9b9f..9af84111c 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -3782,6 +3782,13 @@ func (s *Service) IsProjectInvitationExpired(invite *ProjectInvitation) bool { return time.Now().After(invite.CreatedAt.Add(s.config.ProjectInvitationExpiration)) } +// GetInvitesByEmail returns project invites by email. +func (s *Service) GetInvitesByEmail(ctx context.Context, email string) (invites []ProjectInvitation, err error) { + defer mon.Task()(&ctx)(&err) + + return s.store.ProjectInvitations().GetByEmail(ctx, email) +} + // GetInviteByToken returns a project invite given an invite token. func (s *Service) GetInviteByToken(ctx context.Context, token string) (invite *ProjectInvitation, err error) { defer mon.Task()(&ctx)(&err)