Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,11 @@ var loginCmd = &cobra.Command{
}

// Identify the user in PostHog and alias the anonymous machine ID
// so that pre-login CLI events are merged into the same person record
Telemetry.IdentifyUser(userCredentialsToBeStored.Email)
// so that pre-login CLI events are merged into the same person record.
// This call is idempotent (gated on LastIdentifiedEmail in the config),
// and CaptureEvent below will also invoke it as a safety net for users
// who are already logged in on older CLIs that predate IdentifyUser.
Telemetry.IdentifyUserIfNeeded()

// clear backed up secrets from prev account
util.DeleteBackupSecrets()
Expand Down
7 changes: 7 additions & 0 deletions packages/models/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ type ConfigFile struct {
VaultBackendType string `json:"vaultBackendType,omitempty"`
VaultBackendPassphrase string `json:"vaultBackendPassphrase,omitempty"`
Domains []string `json:"domains,omitempty"`
// LastIdentifiedEmail tracks the most recent email for which a PostHog
// Identify/Alias call has been issued. It is used to ensure that telemetry
// person records are enriched with `email` (and aliased from any anonymous
// machine ID) exactly once per email per machine, even when the login
// happened on an older CLI version that predates the IdentifyUser flow,
// or when the email is changed via `infisical user switch`.
LastIdentifiedEmail string `json:"lastIdentifiedEmail,omitempty"`
}

type LoggedInUser struct {
Expand Down
79 changes: 66 additions & 13 deletions packages/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ func (t *Telemetry) CaptureEvent(eventName string, properties posthog.Properties
}

if t.isEnabled {
// Lazily issue the PostHog Identify/Alias for the current logged-in
// user before capturing the event. This catches the case where the
// user logged in on an older CLI version that predates IdentifyUser
// (so their PostHog person record was never enriched with `email`),
// as well as profile switches via `infisical user switch`. The call
// is idempotent and persists its state in the local config file.
t.IdentifyUserIfNeeded()

t.posthogClient.Enqueue(posthog.Capture{
DistinctId: userIdentity,
Event: eventName,
Expand All @@ -58,35 +66,80 @@ func (t *Telemetry) CaptureEvent(eventName string, properties posthog.Properties
}
}

// IdentifyUser sends a PostHog identify call to enrich the person record
// with user properties, and aliases the anonymous machine ID to the user's
// IdentifyUserIfNeeded sends a PostHog Identify call to enrich the person
// record with the user's email, and aliases the anonymous machine ID to the
// email so that pre-login CLI events are merged into the same person.
func (t *Telemetry) IdentifyUser(email string) {
if !t.isEnabled || email == "" {
//
// The call is idempotent: it tracks the last identified email in the local
// config file (`LastIdentifiedEmail`) and skips Identify/Alias when it has
// already been issued for the current `LoggedInUserEmail`. This ensures a
// single Identify is sent per email per machine, even when:
// - the original login happened on a CLI version that predates IdentifyUser,
// - the user changes profiles via `infisical user switch`,
// - subsequent CLI commands run after the original login.
//
// No Close() is performed here β€” the caller (typically CaptureEvent) is
// responsible for flushing the PostHog client after enqueueing.
func (t *Telemetry) IdentifyUserIfNeeded() {
if !t.isEnabled {
return
}

configFile, err := util.GetConfigFile()
if err != nil {
log.Debug().Err(err).Msg("IdentifyUserIfNeeded: failed to read config file")
return
}

email := configFile.LoggedInUserEmail
if email == "" || email == configFile.LastIdentifiedEmail {
return
}

// Identify the user with their email as the distinctId
t.posthogClient.Enqueue(posthog.Identify{
if err := t.posthogClient.Enqueue(posthog.Identify{
DistinctId: email,
Properties: posthog.NewProperties().
Set("email", email),
})
}); err != nil {
// If we couldn't even enqueue the Identify (closed client, malformed
// message, etc.), don't persist LastIdentifiedEmail β€” otherwise the
// guard at the top of this function would lock the user out of ever
// being identified again. Bail out early so the next CLI invocation
// retries naturally.
log.Debug().Err(err).Msgf("IdentifyUserIfNeeded: failed to enqueue Identify [email=%s]", email)
return
}

// Alias the anonymous machine ID to the user's email so that
// any events captured before login are linked to this person
// Alias the anonymous machine ID to the user's email so that any events
// captured before login (or before IdentifyUser was added) are linked to
// the same person record. PostHog only honors the first Alias for a given
// anonymous ID, so subsequent invocations on the same machine are no-ops
// on the server side β€” which is fine, the persisted LastIdentifiedEmail
// guard prevents us from re-enqueueing them anyway.
//
// We only persist LastIdentifiedEmail after both enqueues succeed; a
// failure on Alias means the anonymous-to-email link wasn't recorded,
// so we want the next invocation to retry it.
machineId, err := machineid.ID()
if err == nil && machineId != "" {
anonymousId := "anonymous_cli_" + machineId
t.posthogClient.Enqueue(posthog.Alias{
if err := t.posthogClient.Enqueue(posthog.Alias{
DistinctId: email,
Alias: anonymousId,
})
}); err != nil {
log.Debug().Err(err).Msgf("IdentifyUserIfNeeded: failed to enqueue Alias [email=%s] [anonymousId=%s]", email, anonymousId)
return
}
}

// Note: no Close() here β€” the caller is responsible for ensuring
// CaptureEvent (which calls Close) runs after IdentifyUser to flush
// all enqueued events (Identify, Alias, and Capture).
// Persist that we've identified this email so we don't re-fire on the
// next CLI invocation. A failure here is non-fatal β€” the worst case is
// one extra Identify enqueue on the next run.
configFile.LastIdentifiedEmail = email
if err := util.WriteConfigFile(&configFile); err != nil {
Comment thread
carlosmonastyrski marked this conversation as resolved.
log.Debug().Err(err).Msgf("IdentifyUserIfNeeded: failed to persist LastIdentifiedEmail [email=%s]", email)
}
}

func (t *Telemetry) GetDistinctId() (string, error) {
Expand Down
1 change: 1 addition & 0 deletions packages/util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
VaultBackendType: existingConfigFile.VaultBackendType,
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
Domains: existingConfigFile.Domains,
LastIdentifiedEmail: existingConfigFile.LastIdentifiedEmail,
}

configFileMarshalled, err := json.Marshal(configFile)
Expand Down
Loading