grok-build-upstream-mirror/crates/codegen/xai-grok-telemetry/src/config.rs
grokkybara[bot] a5727c5960 Synced from monorepo
Changes:
- Non-blocking coding-data sharing upsell banner
- Consolidate remediation in Doctor
- Auto mode defers fail-closed gate asks to the classifier
- Coalesce marketplace list fetches
- Allow removing a marketplace source by name
- Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal)
- Label failed workspace RPCs with error_kind
- Drop redundant explicit tonic/prost deps from xai-grok-shell
- Report real exit codes for completed background shells
- Narrow the date-rollover reminder to date-bearing templates
- Wire toolOverrides through the session and agent
- Security: Bash(git:*) allowlist matches whole command chain by prefix
- Split prompt-trigger telemetry and record classifier provenance
- Raise connectors-manager timeout to 60s
- Auto classifier honors recorded approvals for repeat actions
- Apply doctor fixes in the TUI
- Auto-mode classifier timeouts prompt instead of silently denying
- Scope subagent completion drains to the owning session
- Add the toolOverrides wire types
- Set client_identifier=grok-agent-sdk
- Accept both spellings of the workspace-teleport kill switch
- Persist one-shot occurrence journal
- Stop turns that poll the exact same tool call 16x in a row
- Copy compaction checkpoint files when forking sessions
- Auto-focus permission prompt from scrollback
- Esc cancels the running turn in non-vim and minimal modes
- List Ctrl+Z undo and redo in keyboard shortcuts
- Out-of-process macOS mic capture
- Show active auth mode on session-info
- Install the npm binary under $GROK_HOME
- Remove hover/click dead zones between dashboard items
- Route startup warnings to doctor
- Document [feedback.user] author identity config
- Extend bang command timeout
- Close combine-queued edit-hold race
- Integrate relocation recovery
- Expose privacy notice rollout flag
- Break harness discovery ref cycle so connections can idle-evict
- Shift/Alt+Enter inserts newline when editing a queued prompt
- Gate project Claude permissions on folder trust
- Echo response.create.event_id on response.created
- Toast when session creation fails from disk full
- Add shared test process lifecycle
- Enable dynamic workflows by default
- Add relocation transaction state machine
- Add shared test sandbox
- Surface auth failures on model-switch compact
- Persist durable scheduler expiry
- Confirm before removing extensions-modal items
- Re-run compact and prompt after login when compact hit expired auth
- Recap sends hosted tools under backend search
2026-07-22 19:22:27 +01:00

237 lines
9.3 KiB
Rust

//! Telemetry-engine configuration.
//!
//! Extracted from `xai-grok-shell::agent::config` so the data-collector
//! engine can construct a [`TelemetryClient`](crate::client::TelemetryClient)
//! without a build-time dependency on the shell.
//!
//! Shell still re-exports these types from their original paths so existing
//! call sites (and `Config` derive impls) compile unchanged.
use serde::{Deserialize, Serialize};
/// Telemetry mode: `true`/`false` (legacy bool) or `"session_metrics"` (string).
///
/// - `Disabled` -- nothing sent (enterprise default)
/// - `SessionMetrics` -- metadata-only lifecycle events, no content
/// - `Enabled` -- full product telemetry (events + Mixpanel)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TelemetryMode {
#[default]
Disabled,
SessionMetrics,
Enabled,
}
impl TelemetryMode {
pub fn is_disabled(&self) -> bool {
matches!(self, Self::Disabled)
}
pub fn is_enabled(&self) -> bool {
matches!(self, Self::Enabled)
}
/// True for both `SessionMetrics` and `Enabled`.
pub fn session_metrics_enabled(&self) -> bool {
matches!(self, Self::SessionMetrics | Self::Enabled)
}
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" | "enabled" | "full" => Some(Self::Enabled),
"0" | "false" | "no" | "off" | "disabled" => Some(Self::Disabled),
"session-metrics" | "session_metrics" => Some(Self::SessionMetrics),
_ => None,
}
}
}
impl std::fmt::Display for TelemetryMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Disabled => write!(f, "false"),
Self::SessionMetrics => write!(f, "session_metrics"),
Self::Enabled => write!(f, "true"),
}
}
}
impl From<bool> for TelemetryMode {
fn from(b: bool) -> Self {
if b { Self::Enabled } else { Self::Disabled }
}
}
impl serde::Serialize for TelemetryMode {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::Disabled => serializer.serialize_bool(false),
Self::Enabled => serializer.serialize_bool(true),
Self::SessionMetrics => serializer.serialize_str("session_metrics"),
}
}
}
/// Wire format for `[features] telemetry`: accepts `true`, `false`, or `"session_metrics"`.
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum TelemetryModeValue {
Bool(bool),
Str(String),
}
impl<'de> serde::Deserialize<'de> for TelemetryMode {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
match TelemetryModeValue::deserialize(deserializer)? {
TelemetryModeValue::Bool(b) => Ok(Self::from(b)),
TelemetryModeValue::Str(s) => Ok(Self::parse(&s).unwrap_or_else(|| {
tracing::warn!(
value = %s,
"TELEMETRY_MODE_UNKNOWN: unrecognized telemetry mode; treating as disabled",
);
Self::Disabled
})),
}
}
}
/// Parse an env var as a `TelemetryMode`. Returns `None` if unset or empty.
pub fn env_telemetry_mode(name: &str) -> Option<TelemetryMode> {
let value = std::env::var(name).ok()?;
TelemetryMode::parse(&value)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TelemetryConfig {
/// Declared for `serde_ignored`. Actual toggle is `[features] telemetry`.
#[serde(default)]
pub enabled: Option<bool>,
pub events_url: Option<String>,
pub events_api_key: Option<String>,
pub mixpanel_token: Option<String>,
pub mixpanel_enabled: bool,
/// `None` = inherit from `[features] telemetry`. `Some(false)` = disable GCS uploads only.
pub trace_upload: Option<bool>,
/// External OTEL master switch (`= GROK_EXTERNAL_OTEL`, env wins).
pub otel_enabled: Option<bool>,
/// External OTEL metrics exporter: `otlp` | `console` | `none`.
pub otel_metrics_exporter: Option<String>,
/// External OTEL logs/events exporter: `otlp` | `console` | `none`.
pub otel_logs_exporter: Option<String>,
/// External OTLP base endpoint (`/v1/logs`, `/v1/metrics` appended for HTTP).
pub otel_endpoint: Option<String>,
/// External OTLP transport: `http/protobuf` | `grpc`.
#[serde(alias = "otel_transport")]
pub otel_protocol: Option<String>,
/// External OTEL content gate (admins can pin to `false` via requirements).
pub otel_log_user_prompts: Option<bool>,
/// External OTEL content gate (admins can pin to `false` via requirements).
pub otel_log_tool_details: Option<bool>,
}
fn internal_defaults() -> (Option<String>, Option<String>, Option<String>, bool) {
(None, None, None, false)
}
fn build_env_default(value: Option<&'static str>) -> Option<String> {
value
.map(str::trim)
.filter(|v| !v.is_empty())
.map(str::to_owned)
}
impl Default for TelemetryConfig {
fn default() -> Self {
let (baked_url, baked_key, baked_token, baked_enabled) = internal_defaults();
let build_url = build_env_default(option_env!("GROK_TELEMETRY_BUILD_EVENTS_URL"));
let build_key = build_env_default(option_env!("GROK_TELEMETRY_BUILD_EVENTS_API_KEY"));
let build_token = build_env_default(option_env!("GROK_TELEMETRY_BUILD_MIXPANEL_TOKEN"));
let mixpanel_enabled = baked_enabled || build_token.is_some();
let (events_url, events_api_key, mixpanel_token) = (
build_url.or(baked_url),
build_key.or(baked_key),
build_token.or(baked_token),
);
Self {
enabled: None,
events_url,
events_api_key,
mixpanel_token,
mixpanel_enabled,
trace_upload: None,
otel_enabled: None,
otel_metrics_exporter: None,
otel_logs_exporter: None,
otel_endpoint: None,
otel_protocol: None,
otel_log_user_prompts: None,
otel_log_tool_details: None,
}
}
}
impl TelemetryConfig {
pub fn apply_env_overrides(&mut self) {
self.normalize();
if let Some(value) = Self::env_override("GROK_TELEMETRY_EVENTS_URL") {
self.events_url = value;
}
if let Some(value) = Self::env_override("GROK_TELEMETRY_EVENTS_API_KEY") {
self.events_api_key = value;
}
if let Some(value) = Self::env_override("GROK_TELEMETRY_MIXPANEL_TOKEN") {
self.mixpanel_token = value;
}
if let Some(value) = env_bool("GROK_TELEMETRY_MIXPANEL_ENABLED") {
self.mixpanel_enabled = value;
}
if let Some(value) = env_bool("GROK_TELEMETRY_TRACE_UPLOAD") {
self.trace_upload = Some(value);
}
}
fn normalize(&mut self) {
self.events_url = Self::normalize_optional_string(self.events_url.take());
self.events_api_key = Self::normalize_optional_string(self.events_api_key.take());
self.mixpanel_token = Self::normalize_optional_string(self.mixpanel_token.take());
}
fn env_override(name: &str) -> Option<Option<String>> {
match std::env::var(name) {
Ok(value) => Some(Self::normalize_optional_string(Some(value))),
Err(_) => None,
}
}
fn normalize_optional_string(value: Option<String>) -> Option<String> {
value.and_then(|raw| {
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
})
}
}
/// Parse an env var as a boolean. Returns `None` if unset or unrecognized.
///
/// Local copy of `xai_grok_shell::agent::config::env_bool` so this crate
/// stays free of a shell back-edge. Shell keeps its own copy for callers
/// outside the telemetry config path.
fn env_bool(name: &str) -> Option<bool> {
let value = std::env::var(name).ok()?;
match value.trim().to_ascii_lowercase().as_str() {
"" => None,
"1" | "true" | "yes" | "on" | "enabled" => Some(true),
"0" | "false" | "no" | "off" | "disabled" => Some(false),
_ => None,
}
}
/// Derive a stable deployment ID (UUIDv5) from the deployment key.
pub fn deployment_id_from_key(key: &str) -> String {
uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, key.as_bytes()).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_env_default_normalizes() {
assert_eq!(build_env_default(None), None);
assert_eq!(build_env_default(Some("")), None);
assert_eq!(build_env_default(Some(" \t ")), None);
assert_eq!(build_env_default(Some(" key ")), Some("key".to_owned()));
}
#[test]
fn default_is_build_env_layer_when_feature_off() {
let cfg = TelemetryConfig::default();
let url = build_env_default(option_env!("GROK_TELEMETRY_BUILD_EVENTS_URL"));
let key = build_env_default(option_env!("GROK_TELEMETRY_BUILD_EVENTS_API_KEY"));
let token = build_env_default(option_env!("GROK_TELEMETRY_BUILD_MIXPANEL_TOKEN"));
assert_eq!(cfg.mixpanel_enabled, token.is_some());
assert_eq!(cfg.events_url, url);
assert_eq!(cfg.events_api_key, key);
assert_eq!(cfg.mixpanel_token, token);
}
}