Skip to content
Draft
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
15 changes: 10 additions & 5 deletions packages/cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ import (

const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com"

// AGENT_USER_AGENT identifies agent traffic to the backend so it can be
// attributed separately from interactive CLI usage (which sends api.USER_AGENT).
// The backend matches on the "infisical-agent" prefix.
var AGENT_USER_AGENT = "infisical-agent/" + util.CLI_VERSION

const CACHE_TYPE_KUBERNETES = "kubernetes"

const DYNAMIC_SECRET_LEASE_TEMPLATE = "dynamic-secret-lease-%s-%s-%s-%s-%s-%s"
Expand Down Expand Up @@ -988,7 +993,7 @@ func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *Dyn

temporaryInfisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
UserAgent: AGENT_USER_AGENT,
AutoTokenRefresh: false,
RetryRequestsConfig: agentManager.SdkRetryConfig(),
})
Expand Down Expand Up @@ -1190,7 +1195,7 @@ func NewAgentManager(options NewAgentMangerOptions) *AgentManager {

agentManager.infisicalClient = infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT, // ? Should we perhaps use a different user agent for the Agent for better analytics?
UserAgent: AGENT_USER_AGENT,
AutoTokenRefresh: true,
CustomHeaders: customHeaders,
RetryRequestsConfig: retryConfig,
Expand Down Expand Up @@ -1510,7 +1515,7 @@ func revokeDynamicSecretLease(accessToken, projectSlug, environment, secretPath,

temporaryInfisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
UserAgent: AGENT_USER_AGENT,
AutoTokenRefresh: false,
CustomHeaders: customHeaders,
RetryRequestsConfig: retryConfig,
Expand Down Expand Up @@ -1633,7 +1638,7 @@ func (tm *AgentManager) RevokeCredentials() error {

temporaryInfisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
UserAgent: AGENT_USER_AGENT,
AutoTokenRefresh: false,
CustomHeaders: customHeaders,
})
Expand Down Expand Up @@ -1663,7 +1668,7 @@ func (tm *AgentManager) RevokeCredentials() error {
if !slices.Contains(deletedTokens, token) {
temporaryInfisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
UserAgent: AGENT_USER_AGENT,
AutoTokenRefresh: false,
CustomHeaders: customHeaders,
})
Expand Down
38 changes: 35 additions & 3 deletions packages/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -130,10 +131,14 @@ func init() {
RootCmd.PersistentFlags().StringP("log-level", "l", "", "log level (trace, debug, info, warn, error, fatal)")
RootCmd.PersistentFlags().StringVar(&logFormat, "log-format", "", "log output format: console (default, colored), plain (no color), json (structured). Set NO_COLOR=1 to disable colors in console mode. Can also set via LOG_FORMAT env var.")
RootCmd.PersistentFlags().StringVar(&logDestination, "log-destination", "", "log output destination: stderr (default), stdout. Can also set via LOG_DESTINATION env var.")
RootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary")
RootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary. Can also opt out by setting the INFISICAL_TELEMETRY_ENABLED environment variable to false.")
RootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL), "Point the CLI to your Infisical instance (e.g., https://eu.infisical.com for EU Cloud, or https://your-instance.com for self-hosted). Can also set via INFISICAL_DOMAIN environment variable or the 'domain' field in .infisical.json. Required for non-US Cloud users.")
RootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
RootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
// The Telemetry instance is constructed in init(), before cobra has
// parsed argv, so the --telemetry flag can only be applied here.
Telemetry.SetEnabled(resolveTelemetryEnabled(cmd))

silent, err := cmd.Flags().GetBool("silent")
if err != nil {
util.HandleError(err)
Expand All @@ -158,8 +163,35 @@ func init() {

}

isTelemetryOn, _ := RootCmd.PersistentFlags().GetBool("telemetry")
Telemetry = telemetry.NewTelemetry(isTelemetryOn)
// The --telemetry flag cannot be read here: argv has not been parsed yet, so
// GetBool would always return the registered default. Construct the instance
// from the environment opt-out alone and apply the flag in PersistentPreRun.
Telemetry = telemetry.NewTelemetry(telemetryEnabledFromEnv())
}

// telemetryEnabledFromEnv reads the INFISICAL_TELEMETRY_ENABLED environment
// variable. Anything that does not parse as a bool (including unset) leaves
// telemetry enabled.
func telemetryEnabledFromEnv() bool {
if value := os.Getenv(util.INFISICAL_TELEMETRY_ENABLED_NAME); value != "" {
if enabled, err := strconv.ParseBool(value); err == nil {
return enabled
}
}
return true
}

// resolveTelemetryEnabled resolves the telemetry opt-out by precedence: an
// explicitly set --telemetry flag wins, then the INFISICAL_TELEMETRY_ENABLED
// environment variable, then the default (enabled). Must run after flag
// parsing (PersistentPreRun, not init) so cmd.Flags() is reliable.
func resolveTelemetryEnabled(cmd *cobra.Command) bool {
if cmd.Flags().Changed("telemetry") {
if enabled, err := cmd.Flags().GetBool("telemetry"); err == nil {
return enabled
}
}
return telemetryEnabledFromEnv()
}

func initLog() {
Expand Down
46 changes: 46 additions & 0 deletions packages/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ func (t *Telemetry) CaptureEvent(eventName string, properties posthog.Properties
}

if orgId := t.resolveOrganizationId(); orgId != "" {
// Set the organization both as a flat property (so property-based
// filters and joins against backend events work) and as a PostHog
// group (for group analytics).
if capture.Properties == nil {
capture.Properties = posthog.NewProperties()
}
capture.Properties.Set("organizationId", orgId)
capture.Groups = posthog.NewGroups().Set("organization", orgId)
}

Expand All @@ -80,6 +87,15 @@ func (t *Telemetry) CaptureEvent(eventName string, properties posthog.Properties
}
}

// SetEnabled turns event capture on or off. It exists so the opt-out surfaces
// (--telemetry flag, INFISICAL_TELEMETRY_ENABLED env var) can be applied after
// cobra has actually parsed argv: the Telemetry instance is constructed in
// init(), before flag values are available. Capture can never be enabled
// without a PostHog client (i.e. when no API key was compiled in).
func (t *Telemetry) SetEnabled(enabled bool) {
t.isEnabled = enabled && t.posthogClient != nil
}

// SetActor records who the command is acting as, for commands that resolve their
// own credential and so have nothing in the environment to read.
func (t *Telemetry) SetActor(identityId, orgId string) {
Expand All @@ -89,14 +105,44 @@ func (t *Telemetry) SetActor(identityId, orgId string) {

// Once an actor is set, its organization is authoritative even when empty: falling
// back to the environment would group the event under a different actor's org.
//
// The resolution priority mirrors GetDistinctId: SetActor, then the logged-in
// user's session JWT, then a machine-identity access token from the environment.
// A logged-in user is authoritative even when their token carries no
// organization, for the same reason as SetActor: their events are attributed to
// their email, so grouping them under an env-token's org would be wrong.
func (t *Telemetry) resolveOrganizationId() string {
if t.attachedIdentityId != "" || t.attachedOrgId != "" {
return t.attachedOrgId
}
if orgId, ok := loggedInUserOrganizationId(); ok {
return orgId
}
_, orgId := machineIdentityClaimsFromEnv()
return orgId
}

// loggedInUserOrganizationId reads the `organizationId` claim from the
// logged-in user's session JWT in the system keyring. The second return value
// reports whether a logged-in user exists at all (their org claim may still be
// empty, e.g. before an organization is selected). Best-effort and silent on
// failure, like the rest of the telemetry claim parsing: nothing from the
// token itself is kept and no server call is made.
func loggedInUserOrganizationId() (orgId string, loggedIn bool) {
configFile, err := util.GetConfigFile()
if err != nil || configFile.LoggedInUserEmail == "" {
return "", false
}

userCreds, err := util.GetUserCredsFromKeyRing(configFile.LoggedInUserEmail)
if err != nil {
return "", true
}

_, orgId = IdentityClaimsFromToken(userCreds.JTWToken)
return orgId, true
}

// 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.
Expand Down
1 change: 1 addition & 0 deletions packages/util/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
INFISICAL_ENVIRONMENT_NAME = "INFISICAL_ENVIRONMENT"
INFISICAL_SECRET_PATH_NAME = "INFISICAL_SECRET_PATH"
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
INFISICAL_TELEMETRY_ENABLED_NAME = "INFISICAL_TELEMETRY_ENABLED"

// Agent proxy (connect)
INFISICAL_AGENT_PROXY_ADDRESS_NAME = "INFISICAL_AGENT_PROXY_ADDRESS"
Expand Down
Loading