mirror of
https://github.com/openai/codex.git
synced 2026-05-03 10:56:37 +00:00
- add event for thread initialization - thread/start, thread/fork, thread/resume - feature flagged behind `FeatureFlag::GeneralAnalytics` - does not yet support threads started by subagents PR stack: - --> [[telemetry] thread events #15690](https://github.com/openai/codex/pull/15690) - [[telemetry] subagent events #15915](https://github.com/openai/codex/pull/15915) - [[telemetry] turn events #15591](https://github.com/openai/codex/pull/15591) - [[telemetry] steer events #15697](https://github.com/openai/codex/pull/15697) - [[telemetry] queued prompt data #15804](https://github.com/openai/codex/pull/15804) Sample extracted logs in Codex-backend ``` INFO | 2026-03-29 16:39:37 | codex_backend.routers.analytics_events | analytics_events.track_analytics_events:398 | Tracked analytics event codex_thread_initialized thread_id=019d3bf7-9f5f-7f82-9877-6d48d1052531 product_surface=codex product_client_id=CODEX_CLI client_name=codex-tui client_version=0.0.0 rpc_transport=in_process experimental_api_enabled=True codex_rs_version=0.0.0 runtime_os=macos runtime_os_version=26.4.0 runtime_arch=aarch64 model=gpt-5.3-codex ephemeral=False thread_source=user initialization_mode=new subagent_source=None parent_thread_id=None created_at=1774827577 | INFO | 2026-03-29 16:45:46 | codex_backend.routers.analytics_events | analytics_events.track_analytics_events:398 | Tracked analytics event codex_thread_initialized thread_id=019d3b84-5731-79d0-9b3b-9c6efe5f5066 product_surface=codex product_client_id=CODEX_CLI client_name=codex-tui client_version=0.0.0 rpc_transport=in_process experimental_api_enabled=True codex_rs_version=0.0.0 runtime_os=macos runtime_os_version=26.4.0 runtime_arch=aarch64 model=gpt-5.3-codex ephemeral=False thread_source=user initialization_mode=resumed subagent_source=None parent_thread_id=None created_at=1774820022 | INFO | 2026-03-29 16:45:49 | codex_backend.routers.analytics_events | analytics_events.track_analytics_events:398 | Tracked analytics event codex_thread_initialized thread_id=019d3bfd-4cd6-7c12-a13e-48cef02e8c4d product_surface=codex product_client_id=CODEX_CLI client_name=codex-tui client_version=0.0.0 rpc_transport=in_process experimental_api_enabled=True codex_rs_version=0.0.0 runtime_os=macos runtime_os_version=26.4.0 runtime_arch=aarch64 model=gpt-5.3-codex ephemeral=False thread_source=user initialization_mode=forked subagent_source=None parent_thread_id=None created_at=1774827949 | INFO | 2026-03-29 17:20:29 | codex_backend.routers.analytics_events | analytics_events.track_analytics_events:398 | Tracked analytics event codex_thread_initialized thread_id=019d3c1d-0412-7ed2-ad24-c9c0881a36b0 product_surface=codex product_client_id=CODEX_SERVICE_EXEC client_name=codex_exec client_version=0.0.0 rpc_transport=in_process experimental_api_enabled=True codex_rs_version=0.0.0 runtime_os=macos runtime_os_version=26.4.0 runtime_arch=aarch64 model=gpt-5.3-codex ephemeral=False thread_source=user initialization_mode=new subagent_source=None parent_thread_id=None created_at=1774830027 | ``` Notes - `product_client_id` gets canonicalized in codex-backend - subagent threads are addressed in a following pr
278 lines
9.3 KiB
Rust
278 lines
9.3 KiB
Rust
use crate::Feature;
|
|
use crate::FeatureConfigSource;
|
|
use crate::FeatureOverrides;
|
|
use crate::Features;
|
|
use crate::FeaturesToml;
|
|
use crate::Stage;
|
|
use crate::feature_for_key;
|
|
use crate::unstable_features_warning_event;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_protocol::protocol::WarningEvent;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::BTreeMap;
|
|
use toml::Table;
|
|
use toml::Value as TomlValue;
|
|
|
|
#[test]
|
|
fn under_development_features_are_disabled_by_default() {
|
|
for spec in crate::FEATURES {
|
|
if matches!(spec.stage, Stage::UnderDevelopment) {
|
|
assert_eq!(
|
|
spec.default_enabled, false,
|
|
"feature `{}` is under development and must be disabled by default",
|
|
spec.key
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn default_enabled_features_are_stable() {
|
|
for spec in crate::FEATURES {
|
|
if spec.default_enabled {
|
|
assert!(
|
|
matches!(spec.stage, Stage::Stable | Stage::Removed),
|
|
"feature `{}` is enabled by default but is not stable/removed ({:?})",
|
|
spec.key,
|
|
spec.stage
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn use_legacy_landlock_is_stable_and_disabled_by_default() {
|
|
assert_eq!(Feature::UseLegacyLandlock.stage(), Stage::Stable);
|
|
assert_eq!(Feature::UseLegacyLandlock.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn use_linux_sandbox_bwrap_is_removed_and_disabled_by_default() {
|
|
assert_eq!(Feature::UseLinuxSandboxBwrap.stage(), Stage::Removed);
|
|
assert_eq!(Feature::UseLinuxSandboxBwrap.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn js_repl_is_experimental_and_user_toggleable() {
|
|
let spec = Feature::JsRepl.info();
|
|
let stage = spec.stage;
|
|
let expected_node_version = include_str!("../../node-version.txt").trim_end();
|
|
|
|
assert!(matches!(stage, Stage::Experimental { .. }));
|
|
assert_eq!(stage.experimental_menu_name(), Some("JavaScript REPL"));
|
|
assert_eq!(
|
|
stage.experimental_menu_description().map(str::to_owned),
|
|
Some(format!(
|
|
"Enable a persistent Node-backed JavaScript REPL for interactive website debugging and other inline JavaScript execution capabilities. Requires Node >= v{expected_node_version} installed."
|
|
))
|
|
);
|
|
assert_eq!(Feature::JsRepl.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn code_mode_only_requires_code_mode() {
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::CodeModeOnly);
|
|
features.normalize_dependencies();
|
|
|
|
assert_eq!(features.enabled(Feature::CodeModeOnly), true);
|
|
assert_eq!(features.enabled(Feature::CodeMode), true);
|
|
}
|
|
|
|
#[test]
|
|
fn guardian_approval_is_experimental_and_user_toggleable() {
|
|
let spec = Feature::GuardianApproval.info();
|
|
let stage = spec.stage;
|
|
|
|
assert!(matches!(stage, Stage::Experimental { .. }));
|
|
assert_eq!(stage.experimental_menu_name(), Some("Guardian Approvals"));
|
|
assert_eq!(
|
|
stage.experimental_menu_description().map(str::to_owned),
|
|
Some(
|
|
"When Codex needs approval for higher-risk actions (e.g. sandbox escapes or blocked network access), route eligible approval requests to a carefully-prompted security reviewer subagent rather than blocking the agent on your input. This can consume significantly more tokens because it runs a subagent on every approval request.".to_string()
|
|
)
|
|
);
|
|
assert_eq!(stage.experimental_announcement(), None);
|
|
assert_eq!(Feature::GuardianApproval.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_is_under_development() {
|
|
assert_eq!(
|
|
Feature::ExecPermissionApprovals.stage(),
|
|
Stage::UnderDevelopment
|
|
);
|
|
assert_eq!(Feature::ExecPermissionApprovals.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn request_permissions_tool_is_under_development() {
|
|
assert_eq!(
|
|
Feature::RequestPermissionsTool.stage(),
|
|
Stage::UnderDevelopment
|
|
);
|
|
assert_eq!(Feature::RequestPermissionsTool.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_suggest_is_stable_and_enabled_by_default() {
|
|
assert_eq!(Feature::ToolSuggest.stage(), Stage::Stable);
|
|
assert_eq!(Feature::ToolSuggest.default_enabled(), true);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_search_is_under_development_and_disabled_by_default() {
|
|
assert_eq!(Feature::ToolSearch.stage(), Stage::UnderDevelopment);
|
|
assert_eq!(Feature::ToolSearch.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn general_analytics_is_under_development_and_disabled_by_default() {
|
|
assert_eq!(Feature::GeneralAnalytics.stage(), Stage::UnderDevelopment);
|
|
assert_eq!(Feature::GeneralAnalytics.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn use_linux_sandbox_bwrap_is_a_removed_feature_key() {
|
|
assert_eq!(
|
|
feature_for_key("use_legacy_landlock"),
|
|
Some(Feature::UseLegacyLandlock)
|
|
);
|
|
assert_eq!(
|
|
feature_for_key("use_linux_sandbox_bwrap"),
|
|
Some(Feature::UseLinuxSandboxBwrap)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn image_generation_is_under_development() {
|
|
assert_eq!(Feature::ImageGeneration.stage(), Stage::UnderDevelopment);
|
|
assert_eq!(Feature::ImageGeneration.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_call_mcp_elicitation_is_stable_and_enabled_by_default() {
|
|
assert_eq!(Feature::ToolCallMcpElicitation.stage(), Stage::Stable);
|
|
assert_eq!(Feature::ToolCallMcpElicitation.default_enabled(), true);
|
|
}
|
|
|
|
#[test]
|
|
fn image_detail_original_feature_is_under_development() {
|
|
assert_eq!(
|
|
Feature::ImageDetailOriginal.stage(),
|
|
Stage::UnderDevelopment
|
|
);
|
|
assert_eq!(Feature::ImageDetailOriginal.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn collab_is_legacy_alias_for_multi_agent() {
|
|
assert_eq!(feature_for_key("multi_agent"), Some(Feature::Collab));
|
|
assert_eq!(feature_for_key("collab"), Some(Feature::Collab));
|
|
}
|
|
|
|
#[test]
|
|
fn multi_agent_is_stable_and_enabled_by_default() {
|
|
assert_eq!(Feature::Collab.stage(), Stage::Stable);
|
|
assert_eq!(Feature::Collab.default_enabled(), true);
|
|
}
|
|
|
|
#[test]
|
|
fn enable_fanout_is_under_development() {
|
|
assert_eq!(Feature::SpawnCsv.stage(), Stage::UnderDevelopment);
|
|
assert_eq!(Feature::SpawnCsv.default_enabled(), false);
|
|
}
|
|
|
|
#[test]
|
|
fn enable_fanout_normalization_enables_multi_agent_one_way() {
|
|
let mut enable_fanout_features = Features::with_defaults();
|
|
enable_fanout_features.enable(Feature::SpawnCsv);
|
|
enable_fanout_features.normalize_dependencies();
|
|
assert_eq!(enable_fanout_features.enabled(Feature::SpawnCsv), true);
|
|
assert_eq!(enable_fanout_features.enabled(Feature::Collab), true);
|
|
|
|
let mut collab_features = Features::with_defaults();
|
|
collab_features.enable(Feature::Collab);
|
|
collab_features.normalize_dependencies();
|
|
assert_eq!(collab_features.enabled(Feature::Collab), true);
|
|
assert_eq!(collab_features.enabled(Feature::SpawnCsv), false);
|
|
}
|
|
|
|
#[test]
|
|
fn apps_require_feature_flag_and_chatgpt_auth() {
|
|
let mut features = Features::with_defaults();
|
|
assert!(!features.apps_enabled_for_auth(/*auth*/ None));
|
|
|
|
features.enable(Feature::Apps);
|
|
assert!(!features.apps_enabled_for_auth(/*auth*/ None));
|
|
|
|
let api_key_auth = codex_login::CodexAuth::from_api_key("test-api-key");
|
|
assert!(!features.apps_enabled_for_auth(Some(&api_key_auth)));
|
|
|
|
let chatgpt_auth = codex_login::CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
|
assert!(features.apps_enabled_for_auth(Some(&chatgpt_auth)));
|
|
}
|
|
|
|
#[test]
|
|
fn from_sources_applies_base_profile_and_overrides() {
|
|
let mut base_entries = BTreeMap::new();
|
|
base_entries.insert("plugins".to_string(), true);
|
|
let base_features = FeaturesToml {
|
|
entries: base_entries,
|
|
};
|
|
|
|
let mut profile_entries = BTreeMap::new();
|
|
profile_entries.insert("code_mode_only".to_string(), true);
|
|
let profile_features = FeaturesToml {
|
|
entries: profile_entries,
|
|
};
|
|
|
|
let features = Features::from_sources(
|
|
FeatureConfigSource {
|
|
features: Some(&base_features),
|
|
..Default::default()
|
|
},
|
|
FeatureConfigSource {
|
|
features: Some(&profile_features),
|
|
include_apply_patch_tool: Some(true),
|
|
..Default::default()
|
|
},
|
|
FeatureOverrides {
|
|
web_search_request: Some(false),
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
assert_eq!(features.enabled(Feature::Plugins), true);
|
|
assert_eq!(features.enabled(Feature::CodeModeOnly), true);
|
|
assert_eq!(features.enabled(Feature::CodeMode), true);
|
|
assert_eq!(features.enabled(Feature::ApplyPatchFreeform), true);
|
|
assert_eq!(features.enabled(Feature::WebSearchRequest), false);
|
|
}
|
|
|
|
#[test]
|
|
fn unstable_warning_event_only_mentions_enabled_under_development_features() {
|
|
let mut configured_features = Table::new();
|
|
configured_features.insert("child_agents_md".to_string(), TomlValue::Boolean(true));
|
|
configured_features.insert("personality".to_string(), TomlValue::Boolean(true));
|
|
configured_features.insert("unknown".to_string(), TomlValue::Boolean(true));
|
|
|
|
let mut features = Features::with_defaults();
|
|
features.enable(Feature::ChildAgentsMd);
|
|
|
|
let warning = unstable_features_warning_event(
|
|
Some(&configured_features),
|
|
/*suppress_unstable_features_warning*/ false,
|
|
&features,
|
|
"/tmp/config.toml",
|
|
)
|
|
.expect("warning event");
|
|
|
|
let EventMsg::Warning(WarningEvent { message }) = warning.msg else {
|
|
panic!("expected warning event");
|
|
};
|
|
assert!(message.contains("child_agents_md"));
|
|
assert!(!message.contains("personality"));
|
|
assert!(message.contains("/tmp/config.toml"));
|
|
}
|