mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[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](https://github.com/openai/codex/pull/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`
This commit is contained in:
@@ -9,6 +9,7 @@ use crate::facts::AnalyticsJsonRpcError;
|
||||
use crate::facts::AppInvocation;
|
||||
use crate::facts::AppMentionedInput;
|
||||
use crate::facts::AppUsedInput;
|
||||
use crate::facts::CodexGoalEvent;
|
||||
use crate::facts::CustomAnalyticsFact;
|
||||
use crate::facts::HookRunFact;
|
||||
use crate::facts::HookRunInput;
|
||||
@@ -246,6 +247,12 @@ impl AnalyticsEventsClient {
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn track_goal_event(&self, event: CodexGoalEvent) {
|
||||
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::Goal(Box::new(
|
||||
event,
|
||||
))));
|
||||
}
|
||||
|
||||
pub fn track_turn_resolved_config(&self, fact: TurnResolvedConfigFact) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::TurnResolvedConfig(Box::new(fact)),
|
||||
|
||||
@@ -4,12 +4,14 @@ use crate::facts::AcceptedLineFingerprint;
|
||||
use crate::facts::AppInvocation;
|
||||
use crate::facts::CodexCompactionEvent;
|
||||
use crate::facts::CodexErrKind;
|
||||
use crate::facts::CodexGoalEvent;
|
||||
use crate::facts::CompactionImplementation;
|
||||
use crate::facts::CompactionPhase;
|
||||
use crate::facts::CompactionReason;
|
||||
use crate::facts::CompactionStatus;
|
||||
use crate::facts::CompactionStrategy;
|
||||
use crate::facts::CompactionTrigger;
|
||||
use crate::facts::GoalEventKind;
|
||||
use crate::facts::HookRunFact;
|
||||
use crate::facts::InvocationType;
|
||||
use crate::facts::PluginState;
|
||||
@@ -63,6 +65,7 @@ pub(crate) enum TrackEventRequest {
|
||||
AppUsed(CodexAppUsedEventRequest),
|
||||
HookRun(CodexHookRunEventRequest),
|
||||
Compaction(Box<CodexCompactionEventRequest>),
|
||||
Goal(Box<CodexGoalEventRequest>),
|
||||
TurnEvent(Box<CodexTurnEventRequest>),
|
||||
TurnSteer(CodexTurnSteerEventRequest),
|
||||
CommandExecution(CodexCommandExecutionEventRequest),
|
||||
@@ -771,6 +774,30 @@ pub(crate) struct CodexCompactionEventRequest {
|
||||
pub(crate) event_params: CodexCompactionEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexGoalEventParams {
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) turn_id: Option<String>,
|
||||
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
||||
pub(crate) runtime: CodexRuntimeMetadata,
|
||||
pub(crate) thread_source: Option<ThreadSource>,
|
||||
pub(crate) subagent_source: Option<String>,
|
||||
pub(crate) parent_thread_id: Option<String>,
|
||||
pub(crate) goal_id: String,
|
||||
pub(crate) event_kind: GoalEventKind,
|
||||
pub(crate) goal_status: codex_state::ThreadGoalStatus,
|
||||
pub(crate) has_token_budget: bool,
|
||||
pub(crate) cumulative_tokens_accounted: Option<i64>,
|
||||
pub(crate) cumulative_time_accounted_seconds: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexGoalEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexGoalEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexTurnEventParams {
|
||||
pub(crate) thread_id: String,
|
||||
@@ -979,6 +1006,33 @@ pub(crate) fn codex_compaction_event_params(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn codex_goal_event_params(
|
||||
input: CodexGoalEvent,
|
||||
session_id: String,
|
||||
app_server_client: CodexAppServerClientMetadata,
|
||||
runtime: CodexRuntimeMetadata,
|
||||
thread_source: Option<ThreadSource>,
|
||||
subagent_source: Option<String>,
|
||||
parent_thread_id: Option<String>,
|
||||
) -> CodexGoalEventParams {
|
||||
CodexGoalEventParams {
|
||||
thread_id: input.thread_id,
|
||||
session_id,
|
||||
turn_id: input.turn_id,
|
||||
app_server_client,
|
||||
runtime,
|
||||
thread_source,
|
||||
subagent_source,
|
||||
parent_thread_id,
|
||||
goal_id: input.goal_id,
|
||||
event_kind: input.event_kind,
|
||||
goal_status: input.goal_status,
|
||||
has_token_budget: input.has_token_budget,
|
||||
cumulative_tokens_accounted: input.cumulative_tokens_accounted,
|
||||
cumulative_time_accounted_seconds: input.cumulative_time_accounted_seconds,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn codex_plugin_used_metadata(
|
||||
tracking: &TrackEventsContext,
|
||||
plugin: PluginTelemetryMetadata,
|
||||
|
||||
@@ -417,6 +417,27 @@ pub struct CodexCompactionEvent {
|
||||
pub duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GoalEventKind {
|
||||
Created,
|
||||
UsageAccounted,
|
||||
StatusChanged,
|
||||
Cleared,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CodexGoalEvent {
|
||||
pub thread_id: String,
|
||||
pub turn_id: Option<String>,
|
||||
pub goal_id: String,
|
||||
pub event_kind: GoalEventKind,
|
||||
pub goal_status: codex_state::ThreadGoalStatus,
|
||||
pub has_token_budget: bool,
|
||||
pub cumulative_tokens_accounted: Option<i64>,
|
||||
pub cumulative_time_accounted_seconds: Option<i64>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum AnalyticsFact {
|
||||
Initialize {
|
||||
@@ -468,6 +489,7 @@ pub(crate) enum AnalyticsFact {
|
||||
pub(crate) enum CustomAnalyticsFact {
|
||||
SubAgentThreadStarted(SubAgentThreadStartedInput),
|
||||
Compaction(Box<CodexCompactionEvent>),
|
||||
Goal(Box<CodexGoalEvent>),
|
||||
GuardianReview(Box<GuardianReviewEventParams>),
|
||||
TurnResolvedConfig(Box<TurnResolvedConfigFact>),
|
||||
TurnTokenUsage(Box<TurnTokenUsageFact>),
|
||||
|
||||
@@ -24,6 +24,7 @@ pub use facts::AcceptedLineFingerprint;
|
||||
pub use facts::AnalyticsJsonRpcError;
|
||||
pub use facts::AppInvocation;
|
||||
pub use facts::CodexCompactionEvent;
|
||||
pub use facts::CodexGoalEvent;
|
||||
pub use facts::CodexTurnSteerEvent;
|
||||
pub use facts::CompactionImplementation;
|
||||
pub use facts::CompactionPhase;
|
||||
@@ -31,6 +32,7 @@ pub use facts::CompactionReason;
|
||||
pub use facts::CompactionStatus;
|
||||
pub use facts::CompactionStrategy;
|
||||
pub use facts::CompactionTrigger;
|
||||
pub use facts::GoalEventKind;
|
||||
pub use facts::HookRunFact;
|
||||
pub use facts::InputError;
|
||||
pub use facts::InvocationType;
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::events::CodexDynamicToolCallEventParams;
|
||||
use crate::events::CodexDynamicToolCallEventRequest;
|
||||
use crate::events::CodexFileChangeEventParams;
|
||||
use crate::events::CodexFileChangeEventRequest;
|
||||
use crate::events::CodexGoalEventRequest;
|
||||
use crate::events::CodexHookRunEventRequest;
|
||||
use crate::events::CodexImageGenerationEventParams;
|
||||
use crate::events::CodexImageGenerationEventRequest;
|
||||
@@ -51,6 +52,7 @@ use crate::events::TrackEventRequest;
|
||||
use crate::events::WebSearchActionKind;
|
||||
use crate::events::codex_app_metadata;
|
||||
use crate::events::codex_compaction_event_params;
|
||||
use crate::events::codex_goal_event_params;
|
||||
use crate::events::codex_hook_run_metadata;
|
||||
use crate::events::codex_plugin_metadata;
|
||||
use crate::events::codex_plugin_used_metadata;
|
||||
@@ -62,6 +64,7 @@ use crate::facts::AnalyticsJsonRpcError;
|
||||
use crate::facts::AppMentionedInput;
|
||||
use crate::facts::AppUsedInput;
|
||||
use crate::facts::CodexCompactionEvent;
|
||||
use crate::facts::CodexGoalEvent;
|
||||
use crate::facts::CustomAnalyticsFact;
|
||||
use crate::facts::HookRunInput;
|
||||
use crate::facts::PluginState;
|
||||
@@ -192,6 +195,16 @@ impl<'a> AnalyticsDropSite<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn goal(input: &'a CodexGoalEvent) -> Self {
|
||||
Self {
|
||||
event_name: "goal",
|
||||
thread_id: &input.thread_id,
|
||||
turn_id: input.turn_id.as_deref(),
|
||||
review_id: None,
|
||||
item_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_item(
|
||||
notification: &'a codex_app_server_protocol::ItemCompletedNotification,
|
||||
item_id: &'a str,
|
||||
@@ -461,6 +474,9 @@ impl AnalyticsReducer {
|
||||
CustomAnalyticsFact::Compaction(input) => {
|
||||
self.ingest_compaction(*input, out);
|
||||
}
|
||||
CustomAnalyticsFact::Goal(input) => {
|
||||
self.ingest_goal(*input, out);
|
||||
}
|
||||
CustomAnalyticsFact::GuardianReview(input) => {
|
||||
self.ingest_guardian_review(*input, out);
|
||||
}
|
||||
@@ -1271,6 +1287,26 @@ impl AnalyticsReducer {
|
||||
)));
|
||||
}
|
||||
|
||||
fn ingest_goal(&mut self, input: CodexGoalEvent, out: &mut Vec<TrackEventRequest>) {
|
||||
let Some((connection_state, thread_metadata)) =
|
||||
self.thread_context_or_warn(AnalyticsDropSite::goal(&input))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
out.push(TrackEventRequest::Goal(Box::new(CodexGoalEventRequest {
|
||||
event_type: "codex_goal_event",
|
||||
event_params: codex_goal_event_params(
|
||||
input,
|
||||
thread_metadata.session_id.clone(),
|
||||
connection_state.app_server_client.clone(),
|
||||
connection_state.runtime.clone(),
|
||||
thread_metadata.thread_source,
|
||||
thread_metadata.subagent_source.clone(),
|
||||
thread_metadata.parent_thread_id.clone(),
|
||||
),
|
||||
})));
|
||||
}
|
||||
|
||||
fn ingest_guardian_review_completed(
|
||||
&mut self,
|
||||
notification: codex_app_server_protocol::ItemGuardianApprovalReviewCompletedNotification,
|
||||
|
||||
Reference in New Issue
Block a user