Skip to content

Commit 608b8b1

Browse files
[codex-analytics] emit goal lifecycle analytics (#27078)
## Why - Currently, there is no analytics event for `/goal` behavior - Existing events cannot identify goal execution or its resulting outcome - The original update in [#26182](#26182) was implemented before `/goal` moved into `codex-goal-extension`. ## What Changed - Adds `codex_goal_event` serialization and enrichment to `codex-analytics` - Emits goal events from the canonical `codex-goal-extension` mutation and accounting paths: - `created` when a new logical goal is persisted - `usage_accounted` when cumulative goal usage is persisted - `status_changed` when the stored goal status changes - `cleared` when the goal is deleted - Preserves causal `turn_id` for turn driven events and uses null attribution for external or idle lifecycle events - Changes goal deletion to return the deleted row so `cleared` retains the stable goal ID ## Event Details Includes standard analytics metadata along with goal specific fields: - `goal_id`: Stable ID stored in the local SQLite goal row and shared across the goal's events - `event_kind`: Observed operation (see the 4 lifecycle events cited in the above bullet) - `goal_status`: Resulting or last stored status: `active`, `paused`, `blocked`, `usage_limited`, etc. - `has_token_budget`: Indicates whether a token budget is configured - `turn_id`: Causal turn ID, or null when no causal turn exists - `cumulative_tokens_accounted`: Cumulative tokens on `usage_accounted` events; null otherwise - `cumulative_time_accounted_seconds`: Cumulative active time on `usage_accounted` events; null otherwise ## Validation - `just test -p codex-analytics -p codex-state -p codex-goal-extension` - `just test -p codex-core -E 'test(/goal/)'` - `just test -p codex-app-server` - `cargo build -p codex-analytics -p codex-core -p codex-state -p codex-app-server`
1 parent 4a3eac2 commit 608b8b1

23 files changed

Lines changed: 412 additions & 24 deletions

codex-rs/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/analytics/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ codex-login = { workspace = true }
1919
codex-model-provider = { workspace = true }
2020
codex-plugin = { workspace = true }
2121
codex-protocol = { workspace = true }
22+
codex-state = { workspace = true }
2223
os_info = { workspace = true }
2324
serde = { workspace = true, features = ["derive"] }
2425
serde_json = { workspace = true }

codex-rs/analytics/src/client.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::facts::AnalyticsJsonRpcError;
99
use crate::facts::AppInvocation;
1010
use crate::facts::AppMentionedInput;
1111
use crate::facts::AppUsedInput;
12+
use crate::facts::CodexGoalEvent;
1213
use crate::facts::CustomAnalyticsFact;
1314
use crate::facts::HookRunFact;
1415
use crate::facts::HookRunInput;
@@ -246,6 +247,12 @@ impl AnalyticsEventsClient {
246247
)));
247248
}
248249

250+
pub fn track_goal_event(&self, event: CodexGoalEvent) {
251+
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::Goal(Box::new(
252+
event,
253+
))));
254+
}
255+
249256
pub fn track_turn_resolved_config(&self, fact: TurnResolvedConfigFact) {
250257
self.record_fact(AnalyticsFact::Custom(
251258
CustomAnalyticsFact::TurnResolvedConfig(Box::new(fact)),

codex-rs/analytics/src/events.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ use crate::facts::AcceptedLineFingerprint;
44
use crate::facts::AppInvocation;
55
use crate::facts::CodexCompactionEvent;
66
use crate::facts::CodexErrKind;
7+
use crate::facts::CodexGoalEvent;
78
use crate::facts::CompactionImplementation;
89
use crate::facts::CompactionPhase;
910
use crate::facts::CompactionReason;
1011
use crate::facts::CompactionStatus;
1112
use crate::facts::CompactionStrategy;
1213
use crate::facts::CompactionTrigger;
14+
use crate::facts::GoalEventKind;
1315
use crate::facts::HookRunFact;
1416
use crate::facts::InvocationType;
1517
use crate::facts::PluginState;
@@ -63,6 +65,7 @@ pub(crate) enum TrackEventRequest {
6365
AppUsed(CodexAppUsedEventRequest),
6466
HookRun(CodexHookRunEventRequest),
6567
Compaction(Box<CodexCompactionEventRequest>),
68+
Goal(Box<CodexGoalEventRequest>),
6669
TurnEvent(Box<CodexTurnEventRequest>),
6770
TurnSteer(CodexTurnSteerEventRequest),
6871
CommandExecution(CodexCommandExecutionEventRequest),
@@ -771,6 +774,30 @@ pub(crate) struct CodexCompactionEventRequest {
771774
pub(crate) event_params: CodexCompactionEventParams,
772775
}
773776

777+
#[derive(Serialize)]
778+
pub(crate) struct CodexGoalEventParams {
779+
pub(crate) thread_id: String,
780+
pub(crate) session_id: String,
781+
pub(crate) turn_id: Option<String>,
782+
pub(crate) app_server_client: CodexAppServerClientMetadata,
783+
pub(crate) runtime: CodexRuntimeMetadata,
784+
pub(crate) thread_source: Option<ThreadSource>,
785+
pub(crate) subagent_source: Option<String>,
786+
pub(crate) parent_thread_id: Option<String>,
787+
pub(crate) goal_id: String,
788+
pub(crate) event_kind: GoalEventKind,
789+
pub(crate) goal_status: codex_state::ThreadGoalStatus,
790+
pub(crate) has_token_budget: bool,
791+
pub(crate) cumulative_tokens_accounted: Option<i64>,
792+
pub(crate) cumulative_time_accounted_seconds: Option<i64>,
793+
}
794+
795+
#[derive(Serialize)]
796+
pub(crate) struct CodexGoalEventRequest {
797+
pub(crate) event_type: &'static str,
798+
pub(crate) event_params: CodexGoalEventParams,
799+
}
800+
774801
#[derive(Serialize)]
775802
pub(crate) struct CodexTurnEventParams {
776803
pub(crate) thread_id: String,
@@ -979,6 +1006,33 @@ pub(crate) fn codex_compaction_event_params(
9791006
}
9801007
}
9811008

1009+
pub(crate) fn codex_goal_event_params(
1010+
input: CodexGoalEvent,
1011+
session_id: String,
1012+
app_server_client: CodexAppServerClientMetadata,
1013+
runtime: CodexRuntimeMetadata,
1014+
thread_source: Option<ThreadSource>,
1015+
subagent_source: Option<String>,
1016+
parent_thread_id: Option<String>,
1017+
) -> CodexGoalEventParams {
1018+
CodexGoalEventParams {
1019+
thread_id: input.thread_id,
1020+
session_id,
1021+
turn_id: input.turn_id,
1022+
app_server_client,
1023+
runtime,
1024+
thread_source,
1025+
subagent_source,
1026+
parent_thread_id,
1027+
goal_id: input.goal_id,
1028+
event_kind: input.event_kind,
1029+
goal_status: input.goal_status,
1030+
has_token_budget: input.has_token_budget,
1031+
cumulative_tokens_accounted: input.cumulative_tokens_accounted,
1032+
cumulative_time_accounted_seconds: input.cumulative_time_accounted_seconds,
1033+
}
1034+
}
1035+
9821036
pub(crate) fn codex_plugin_used_metadata(
9831037
tracking: &TrackEventsContext,
9841038
plugin: PluginTelemetryMetadata,

codex-rs/analytics/src/facts.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,27 @@ pub struct CodexCompactionEvent {
417417
pub duration_ms: Option<u64>,
418418
}
419419

420+
#[derive(Clone, Copy, Debug, Serialize)]
421+
#[serde(rename_all = "snake_case")]
422+
pub enum GoalEventKind {
423+
Created,
424+
UsageAccounted,
425+
StatusChanged,
426+
Cleared,
427+
}
428+
429+
#[derive(Clone)]
430+
pub struct CodexGoalEvent {
431+
pub thread_id: String,
432+
pub turn_id: Option<String>,
433+
pub goal_id: String,
434+
pub event_kind: GoalEventKind,
435+
pub goal_status: codex_state::ThreadGoalStatus,
436+
pub has_token_budget: bool,
437+
pub cumulative_tokens_accounted: Option<i64>,
438+
pub cumulative_time_accounted_seconds: Option<i64>,
439+
}
440+
420441
#[allow(dead_code)]
421442
pub(crate) enum AnalyticsFact {
422443
Initialize {
@@ -468,6 +489,7 @@ pub(crate) enum AnalyticsFact {
468489
pub(crate) enum CustomAnalyticsFact {
469490
SubAgentThreadStarted(SubAgentThreadStartedInput),
470491
Compaction(Box<CodexCompactionEvent>),
492+
Goal(Box<CodexGoalEvent>),
471493
GuardianReview(Box<GuardianReviewEventParams>),
472494
TurnResolvedConfig(Box<TurnResolvedConfigFact>),
473495
TurnTokenUsage(Box<TurnTokenUsageFact>),

codex-rs/analytics/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ pub use facts::AcceptedLineFingerprint;
2424
pub use facts::AnalyticsJsonRpcError;
2525
pub use facts::AppInvocation;
2626
pub use facts::CodexCompactionEvent;
27+
pub use facts::CodexGoalEvent;
2728
pub use facts::CodexTurnSteerEvent;
2829
pub use facts::CompactionImplementation;
2930
pub use facts::CompactionPhase;
3031
pub use facts::CompactionReason;
3132
pub use facts::CompactionStatus;
3233
pub use facts::CompactionStrategy;
3334
pub use facts::CompactionTrigger;
35+
pub use facts::GoalEventKind;
3436
pub use facts::HookRunFact;
3537
pub use facts::InputError;
3638
pub use facts::InvocationType;

codex-rs/analytics/src/reducer.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::events::CodexDynamicToolCallEventParams;
1515
use crate::events::CodexDynamicToolCallEventRequest;
1616
use crate::events::CodexFileChangeEventParams;
1717
use crate::events::CodexFileChangeEventRequest;
18+
use crate::events::CodexGoalEventRequest;
1819
use crate::events::CodexHookRunEventRequest;
1920
use crate::events::CodexImageGenerationEventParams;
2021
use crate::events::CodexImageGenerationEventRequest;
@@ -51,6 +52,7 @@ use crate::events::TrackEventRequest;
5152
use crate::events::WebSearchActionKind;
5253
use crate::events::codex_app_metadata;
5354
use crate::events::codex_compaction_event_params;
55+
use crate::events::codex_goal_event_params;
5456
use crate::events::codex_hook_run_metadata;
5557
use crate::events::codex_plugin_metadata;
5658
use crate::events::codex_plugin_used_metadata;
@@ -62,6 +64,7 @@ use crate::facts::AnalyticsJsonRpcError;
6264
use crate::facts::AppMentionedInput;
6365
use crate::facts::AppUsedInput;
6466
use crate::facts::CodexCompactionEvent;
67+
use crate::facts::CodexGoalEvent;
6568
use crate::facts::CustomAnalyticsFact;
6669
use crate::facts::HookRunInput;
6770
use crate::facts::PluginState;
@@ -192,6 +195,16 @@ impl<'a> AnalyticsDropSite<'a> {
192195
}
193196
}
194197

198+
fn goal(input: &'a CodexGoalEvent) -> Self {
199+
Self {
200+
event_name: "goal",
201+
thread_id: &input.thread_id,
202+
turn_id: input.turn_id.as_deref(),
203+
review_id: None,
204+
item_id: None,
205+
}
206+
}
207+
195208
fn tool_item(
196209
notification: &'a codex_app_server_protocol::ItemCompletedNotification,
197210
item_id: &'a str,
@@ -461,6 +474,9 @@ impl AnalyticsReducer {
461474
CustomAnalyticsFact::Compaction(input) => {
462475
self.ingest_compaction(*input, out);
463476
}
477+
CustomAnalyticsFact::Goal(input) => {
478+
self.ingest_goal(*input, out);
479+
}
464480
CustomAnalyticsFact::GuardianReview(input) => {
465481
self.ingest_guardian_review(*input, out);
466482
}
@@ -1271,6 +1287,26 @@ impl AnalyticsReducer {
12711287
)));
12721288
}
12731289

1290+
fn ingest_goal(&mut self, input: CodexGoalEvent, out: &mut Vec<TrackEventRequest>) {
1291+
let Some((connection_state, thread_metadata)) =
1292+
self.thread_context_or_warn(AnalyticsDropSite::goal(&input))
1293+
else {
1294+
return;
1295+
};
1296+
out.push(TrackEventRequest::Goal(Box::new(CodexGoalEventRequest {
1297+
event_type: "codex_goal_event",
1298+
event_params: codex_goal_event_params(
1299+
input,
1300+
thread_metadata.session_id.clone(),
1301+
connection_state.app_server_client.clone(),
1302+
connection_state.runtime.clone(),
1303+
thread_metadata.thread_source,
1304+
thread_metadata.subagent_source.clone(),
1305+
thread_metadata.parent_thread_id.clone(),
1306+
),
1307+
})));
1308+
}
1309+
12741310
fn ingest_guardian_review_completed(
12751311
&mut self,
12761312
notification: codex_app_server_protocol::ItemGuardianApprovalReviewCompletedNotification,

codex-rs/app-server/src/extensions.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::sync::Arc;
22
use std::sync::Weak;
33

4+
use codex_analytics::AnalyticsEventsClient;
45
use codex_app_server_protocol::ServerNotification;
56
use codex_app_server_protocol::ThreadGoal;
67
use codex_app_server_protocol::ThreadGoalUpdatedNotification;
@@ -30,6 +31,7 @@ pub(crate) fn thread_extensions<S>(
3031
event_sink: Arc<dyn ExtensionEventSink>,
3132
auth_manager: Arc<AuthManager>,
3233
state_db: Option<StateDbHandle>,
34+
analytics_events_client: AnalyticsEventsClient,
3335
thread_manager: Weak<ThreadManager>,
3436
goal_service: Arc<GoalService>,
3537
executor_skill_provider: Arc<dyn codex_skills_extension::SkillProvider>,
@@ -42,6 +44,7 @@ where
4244
codex_goal_extension::install_with_backend(
4345
&mut builder,
4446
state_db,
47+
analytics_events_client,
4548
codex_otel::global(),
4649
thread_manager,
4750
goal_service,
@@ -140,7 +143,6 @@ pub(crate) fn guardian_agent_spawner(
140143
mod tests {
141144
use std::time::Duration;
142145

143-
use codex_analytics::AnalyticsEventsClient;
144146
use codex_protocol::protocol::ThreadGoal as CoreThreadGoal;
145147
use codex_protocol::protocol::ThreadGoalStatus;
146148
use codex_protocol::protocol::ThreadGoalUpdatedEvent;

codex-rs/app-server/src/mcp_refresh.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ mod tests {
196196
Arc::new(NoopExtensionEventSink),
197197
auth_manager.clone(),
198198
Some(state_db.clone()),
199+
codex_analytics::AnalyticsEventsClient::disabled(),
199200
thread_manager.clone(),
200201
Arc::new(codex_goal_extension::GoalService::new()),
201202
Arc::clone(&executor_skill_provider),

codex-rs/app-server/src/message_processor.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ impl MessageProcessor {
327327
app_server_extension_event_sink(outgoing.clone(), thread_state_manager.clone()),
328328
auth_manager.clone(),
329329
state_db.clone(),
330+
analytics_events_client.clone(),
330331
thread_manager.clone(),
331332
Arc::clone(&goal_service),
332333
Arc::clone(&executor_skill_provider),

0 commit comments

Comments
 (0)