From 0de21605ed619a4f3dee5701c7f0d3abc326e8ef Mon Sep 17 00:00:00 2001 From: Devin AI Date: Mon, 27 Apr 2026 21:14:08 +0000 Subject: [PATCH 1/2] feat(telemetry): make IdentifyUser idempotent and lazy via persisted LastIdentifiedEmail PostHog person records for users who logged in on a CLI version older than v0.43.59 (which introduced IdentifyUser) were never enriched with the `email` property, because IdentifyUser only fired inside the user auth branch of `infisical login`. Once those users upgraded the CLI, their persisted `LoggedInUserEmail` was used as the PostHog distinctId on every event, but no Identify/Alias was ever sent for them. The same gap existed for `infisical user switch`, which mutates the active email in the config without going through the login flow. Replace IdentifyUser(email) with IdentifyUserIfNeeded() (no args). It reads LoggedInUserEmail from the config, compares against a new LastIdentifiedEmail field, and only enqueues Identify+Alias when the two differ. After enqueueing, it persists LastIdentifiedEmail so the call is a no-op on subsequent invocations. Call IdentifyUserIfNeeded() from CaptureEvent before each Capture so that the very first telemetry event after a CLI upgrade (or a profile switch) lazily enriches the PostHog person record. Also call it from the user-auth login path for clarity, which is now redundant-but-safe because of the persistence guard. Preserve LastIdentifiedEmail in WriteInitalConfig so a fresh login on the same email does not re-fire Identify, while a fresh login on a different email does. --- packages/cmd/login.go | 7 +++-- packages/models/cli.go | 7 +++++ packages/telemetry/telemetry.go | 56 +++++++++++++++++++++++++++------ packages/util/config.go | 1 + 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/packages/cmd/login.go b/packages/cmd/login.go index 43e3d36a..979f50d3 100644 --- a/packages/cmd/login.go +++ b/packages/cmd/login.go @@ -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() diff --git a/packages/models/cli.go b/packages/models/cli.go index 0b8c117f..ebd1da09 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -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 { diff --git a/packages/telemetry/telemetry.go b/packages/telemetry/telemetry.go index d40f5437..5a8a3e8d 100644 --- a/packages/telemetry/telemetry.go +++ b/packages/telemetry/telemetry.go @@ -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, @@ -58,11 +66,33 @@ 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 } @@ -73,8 +103,12 @@ func (t *Telemetry) IdentifyUser(email string) { Set("email", email), }) - // 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. machineId, err := machineid.ID() if err == nil && machineId != "" { anonymousId := "anonymous_cli_" + machineId @@ -84,9 +118,13 @@ func (t *Telemetry) IdentifyUser(email string) { }) } - // 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 { + log.Debug().Err(err).Msg("IdentifyUserIfNeeded: failed to persist LastIdentifiedEmail") + } } func (t *Telemetry) GetDistinctId() (string, error) { diff --git a/packages/util/config.go b/packages/util/config.go index 8d44c84d..99bfb47d 100644 --- a/packages/util/config.go +++ b/packages/util/config.go @@ -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) From be9b59c57c7193428e8934eb6ebeff0a707911c3 Mon Sep 17 00:00:00 2001 From: Devin AI Date: Mon, 27 Apr 2026 21:44:21 +0000 Subject: [PATCH 2/2] fix(telemetry): only persist LastIdentifiedEmail after successful enqueue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Codex review on #196. If posthogClient.Enqueue returns an error (closed client, malformed message, etc.) and we still write LastIdentifiedEmail = email, the guard at the top of IdentifyUserIfNeeded would skip Identify/Alias forever for that email — locking the user out of person enrichment. Capture the error from each Enqueue call. On Identify failure, bail out early without persisting. On Alias failure, also bail out (the anonymous-to-email link is part of the Identify story; we want the next invocation to retry). Only persist LastIdentifiedEmail when both enqueues succeed, so transient failures self-heal on the next CLI invocation. --- packages/telemetry/telemetry.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/telemetry/telemetry.go b/packages/telemetry/telemetry.go index 5a8a3e8d..e2527c19 100644 --- a/packages/telemetry/telemetry.go +++ b/packages/telemetry/telemetry.go @@ -97,11 +97,19 @@ func (t *Telemetry) IdentifyUserIfNeeded() { } // 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 (or before IdentifyUser was added) are linked to @@ -109,13 +117,20 @@ func (t *Telemetry) IdentifyUserIfNeeded() { // 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 + } } // Persist that we've identified this email so we don't re-fire on the @@ -123,7 +138,7 @@ func (t *Telemetry) IdentifyUserIfNeeded() { // one extra Identify enqueue on the next run. configFile.LastIdentifiedEmail = email if err := util.WriteConfigFile(&configFile); err != nil { - log.Debug().Err(err).Msg("IdentifyUserIfNeeded: failed to persist LastIdentifiedEmail") + log.Debug().Err(err).Msgf("IdentifyUserIfNeeded: failed to persist LastIdentifiedEmail [email=%s]", email) } }