Skip to content

Commit 2ab5a44

Browse files
fix(user): call identify after signup/login
1 parent 0054cca commit 2ab5a44

4 files changed

Lines changed: 115 additions & 5 deletions

File tree

pkg/cmd/auth/login/login.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package login
22

33
import (
4+
"context"
45
"fmt"
56

67
"github.com/AlecAivazis/survey/v2"
@@ -14,6 +15,7 @@ import (
1415
"github.com/algolia/cli/pkg/config"
1516
"github.com/algolia/cli/pkg/iostreams"
1617
"github.com/algolia/cli/pkg/prompt"
18+
"github.com/algolia/cli/pkg/telemetry"
1719
"github.com/algolia/cli/pkg/validators"
1820
)
1921

@@ -71,7 +73,7 @@ func NewLoginCmd(f *cmdutil.Factory) *cobra.Command {
7173
`),
7274
Args: validators.NoArgs(),
7375
RunE: func(cmd *cobra.Command, args []string) error {
74-
return runLoginCmd(opts)
76+
return runLoginCmd(cmd.Context(), opts)
7577
},
7678
}
7779

@@ -83,13 +85,13 @@ func NewLoginCmd(f *cmdutil.Factory) *cobra.Command {
8385
return cmd
8486
}
8587

86-
func runLoginCmd(opts *LoginOptions) error {
87-
return RunOAuthFlow(opts, false)
88+
func runLoginCmd(ctx context.Context, opts *LoginOptions) error {
89+
return RunOAuthFlow(ctx, opts, false)
8890
}
8991

9092
// RunOAuthFlow runs the full browser-based OAuth + profile setup flow.
9193
// If signup is true, the browser opens to the sign-up page instead of sign-in.
92-
func RunOAuthFlow(opts *LoginOptions, signup bool) error {
94+
func RunOAuthFlow(ctx context.Context, opts *LoginOptions, signup bool) error {
9395
cs := opts.IO.ColorScheme()
9496
client := opts.NewDashboardClient(auth.OAuthClientID())
9597

@@ -99,6 +101,8 @@ func RunOAuthFlow(opts *LoginOptions, signup bool) error {
99101
return err
100102
}
101103

104+
identifyAuthenticatedUser(ctx)
105+
102106
opts.IO.StartProgressIndicatorWithLabel("Fetching applications")
103107
apps, err := client.ListApplications(accessToken)
104108
opts.IO.StopProgressIndicator()
@@ -138,6 +142,32 @@ func RunOAuthFlow(opts *LoginOptions, signup bool) error {
138142
return apputil.ConfigureProfile(opts.IO, opts.Config, appDetails, profileName, opts.Default)
139143
}
140144

145+
// identifyAuthenticatedUser emits a telemetry Identify for the user that just
146+
// authenticated. It is a no-op when no identified token is present.
147+
func identifyAuthenticatedUser(ctx context.Context) {
148+
if applyStoredIdentity(ctx) {
149+
telemetry.IdentifyOnce(ctx)
150+
}
151+
}
152+
153+
// applyStoredIdentity copies the persisted user identity from the stored token
154+
// onto the request's telemetry metadata. It reports whether an identity was
155+
// applied.
156+
func applyStoredIdentity(ctx context.Context) bool {
157+
token := auth.LoadToken()
158+
if token == nil || token.UserID == "" {
159+
return false
160+
}
161+
162+
metadata := telemetry.GetEventMetadata(ctx)
163+
if metadata == nil {
164+
return false
165+
}
166+
167+
metadata.SetUser(token.UserID, token.Email, token.Name)
168+
return true
169+
}
170+
141171
// reuseExistingAPIKey checks if a local profile already has an API key for
142172
// the given application. If so, it sets app.APIKey and returns true.
143173
func reuseExistingAPIKey(cfg config.IConfig, app *dashboard.Application) bool {

pkg/cmd/auth/login/login_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package login
22

33
import (
4+
"context"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
78
"github.com/stretchr/testify/require"
9+
"github.com/zalando/go-keyring"
810

911
"github.com/algolia/cli/api/dashboard"
12+
"github.com/algolia/cli/pkg/auth"
1013
"github.com/algolia/cli/pkg/cmdutil"
1114
"github.com/algolia/cli/pkg/iostreams"
15+
"github.com/algolia/cli/pkg/telemetry"
1216
"github.com/algolia/cli/test"
1317
)
1418

@@ -71,6 +75,61 @@ func TestSelectApplication_ByName_NotFound(t *testing.T) {
7175
assert.Contains(t, err.Error(), "not found")
7276
}
7377

78+
func TestApplyStoredIdentity_SetsMetadataFromToken(t *testing.T) {
79+
keyring.MockInit()
80+
t.Cleanup(auth.ClearToken)
81+
82+
err := auth.SaveToken(&dashboard.OAuthTokenResponse{
83+
AccessToken: "access",
84+
RefreshToken: "refresh",
85+
ExpiresIn: 3600,
86+
User: &dashboard.User{
87+
ID: 42,
88+
Email: "user@example.com",
89+
Name: "Ada Lovelace",
90+
},
91+
})
92+
require.NoError(t, err)
93+
94+
metadata := telemetry.NewEventMetadata()
95+
ctx := telemetry.WithEventMetadata(context.Background(), metadata)
96+
97+
assert.True(t, applyStoredIdentity(ctx))
98+
assert.Equal(t, "42", metadata.UserID)
99+
assert.Equal(t, "user@example.com", metadata.Email)
100+
assert.Equal(t, "Ada Lovelace", metadata.Name)
101+
}
102+
103+
func TestApplyStoredIdentity_NoTokenReturnsFalse(t *testing.T) {
104+
keyring.MockInit()
105+
auth.ClearToken()
106+
107+
metadata := telemetry.NewEventMetadata()
108+
ctx := telemetry.WithEventMetadata(context.Background(), metadata)
109+
110+
assert.False(t, applyStoredIdentity(ctx))
111+
assert.Empty(t, metadata.UserID)
112+
}
113+
114+
func TestApplyStoredIdentity_TokenWithoutIdentityReturnsFalse(t *testing.T) {
115+
keyring.MockInit()
116+
t.Cleanup(auth.ClearToken)
117+
118+
// Token persisted before identity was tracked (no user object).
119+
err := auth.SaveToken(&dashboard.OAuthTokenResponse{
120+
AccessToken: "access",
121+
RefreshToken: "refresh",
122+
ExpiresIn: 3600,
123+
})
124+
require.NoError(t, err)
125+
126+
metadata := telemetry.NewEventMetadata()
127+
ctx := telemetry.WithEventMetadata(context.Background(), metadata)
128+
129+
assert.False(t, applyStoredIdentity(ctx))
130+
assert.Empty(t, metadata.UserID)
131+
}
132+
74133
func TestSelectApplication_MultipleApps_NonInteractive_NoAppName(t *testing.T) {
75134
io, _, _, _ := iostreams.Test()
76135
opts := &LoginOptions{IO: io}

pkg/cmd/auth/signup/signup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func NewSignupCmd(f *cmdutil.Factory) *cobra.Command {
3434
`),
3535
Args: validators.NoArgs(),
3636
RunE: func(cmd *cobra.Command, args []string) error {
37-
return login.RunOAuthFlow(opts, true)
37+
return login.RunOAuthFlow(cmd.Context(), opts, true)
3838
},
3939
}
4040

pkg/telemetry/telemetry.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"log"
88
"net"
9+
"os"
910
"runtime"
1011

1112
"github.com/segmentio/analytics-go/v3"
@@ -69,6 +70,26 @@ func NewAnalyticsTelemetryClient(debug bool) (TelemetryClient, error) {
6970
return &AnalyticsTelemetryClient{client: client}, nil
7071
}
7172

73+
// IdentifyOnce sends a single Identify event through a short-lived client and
74+
// flushes it before returning. It is meant for one-shot identification (for
75+
// example, right after authentication fills the token) where the command's
76+
// request-scoped client may already have been closed. It honors the same
77+
// ALGOLIA_CLI_TELEMETRY and DEBUG environment variables as the root command and
78+
// fails silently so telemetry never blocks the user.
79+
func IdentifyOnce(ctx context.Context) {
80+
if os.Getenv("ALGOLIA_CLI_TELEMETRY") == "0" {
81+
return
82+
}
83+
84+
client, err := NewAnalyticsTelemetryClient(os.Getenv("DEBUG") != "")
85+
if err != nil {
86+
return
87+
}
88+
defer client.Close()
89+
90+
_ = client.Identify(ctx)
91+
}
92+
7293
// anonymousID is a unique identifier for an anonymous user of the CLI (basically the hash of the mac address)
7394
func anonymousID() string {
7495
addrs, err := net.Interfaces()

0 commit comments

Comments
 (0)