{satellite/console, web/satellite}: send analytics if invitee signs up

send analytics event if project invite link is clicked and if user
signs up.

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

Change-Id: I41eee5e679a84b9ec325815655684a98624d5656
This commit is contained in:
Cameron 2023-09-13 15:24:05 -04:00 committed by Storj Robot
parent 81d49ada06
commit a2d37bc69a
5 changed files with 141 additions and 0 deletions

View File

@ -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,
})
}

View File

@ -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 == "" {

View File

@ -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)

View File

@ -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)

View File

@ -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)