Compare commits

...

7 Commits

Author SHA1 Message Date
James Chu
3e95c582fa Merge remote-tracking branch 'origin/main' into jchu/codex-cli-owner-nudge-analytics-2026.04.22 2026-04-27 18:32:37 -04:00
James Chu
2575cf76a4 Generalize product analytics event tracking 2026-04-27 18:26:35 -04:00
James Chu
f26d65b9c5 Merge remote-tracking branch 'origin/main' into jchu/codex-cli-owner-nudge-analytics-2026.04.22
# Conflicts:
#	codex-rs/app-server/src/message_processor.rs
2026-04-27 16:01:11 -04:00
James Chu
6c70786d88 Format analytics imports 2026-04-27 15:26:56 -04:00
James Chu
094106eda9 Add usage-limit banner analytics plumbing 2026-04-27 11:45:37 -04:00
jchu-oai
42fe140d1a Merge branch 'main' into jchu/codex-cli-owner-nudge-analytics-2026.04.22 2026-04-27 11:41:48 -04:00
James Chu
4cb7129579 Add usage-limit banner analytics plumbing 2026-04-23 17:21:25 -04:00
20 changed files with 596 additions and 3 deletions

View File

@@ -66,6 +66,7 @@ use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::NonSteerableTurnKind;
use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile;
use codex_app_server_protocol::ProductAnalyticsEvent;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy;
use codex_app_server_protocol::ServerNotification;
@@ -74,6 +75,7 @@ use codex_app_server_protocol::Thread;
use codex_app_server_protocol::ThreadResumeResponse;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ThreadStatus as AppServerThreadStatus;
use codex_app_server_protocol::TrackProductAnalyticsEventParams;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnError as AppServerTurnError;
@@ -82,6 +84,8 @@ use codex_app_server_protocol::TurnStartedNotification;
use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_app_server_protocol::UsageLimitBannerAction;
use codex_app_server_protocol::UsageLimitBannerType;
use codex_app_server_protocol::UserInput;
use codex_login::default_client::DEFAULT_ORIGINATOR;
use codex_login::default_client::originator;
@@ -2404,6 +2408,83 @@ async fn turn_completed_without_started_notification_emits_null_started_at() {
assert_eq!(payload["event_params"]["total_tokens"], json!(null));
}
#[tokio::test]
async fn usage_limit_banner_shown_request_emits_credits_event() {
let mut reducer = AnalyticsReducer::default();
let mut out = Vec::new();
reducer
.ingest(
AnalyticsFact::Request {
connection_id: 7,
request_id: RequestId::Integer(1),
request: Box::new(ClientRequest::TrackProductAnalyticsEvent {
request_id: RequestId::Integer(1),
params: TrackProductAnalyticsEventParams {
event: ProductAnalyticsEvent::UsageLimitBanner {
action: UsageLimitBannerAction::Shown,
banner_type: UsageLimitBannerType::WorkspaceMemberCreditsDepleted,
},
},
}),
},
&mut out,
)
.await;
let payload = serde_json::to_value(&out[0]).expect("serialize usage banner event");
assert_eq!(
json!({
"event_type": "codex_usage_limit_banner_shown",
"event_params": {
"platform": "codex_cli",
"banner_type": "workspace_member_credits_depleted",
"limit_reason": "credits",
},
}),
payload,
);
}
#[tokio::test]
async fn usage_limit_banner_click_request_emits_usage_limit_event() {
let mut reducer = AnalyticsReducer::default();
let mut out = Vec::new();
reducer
.ingest(
AnalyticsFact::Request {
connection_id: 7,
request_id: RequestId::Integer(1),
request: Box::new(ClientRequest::TrackProductAnalyticsEvent {
request_id: RequestId::Integer(1),
params: TrackProductAnalyticsEventParams {
event: ProductAnalyticsEvent::UsageLimitBanner {
action: UsageLimitBannerAction::CtaClicked,
banner_type: UsageLimitBannerType::WorkspaceMemberUsageLimitReached,
},
},
}),
},
&mut out,
)
.await;
let payload = serde_json::to_value(&out[0]).expect("serialize usage banner event");
assert_eq!(
json!({
"event_type": "codex_usage_limit_banner_cta_clicked",
"event_params": {
"platform": "codex_cli",
"banner_type": "workspace_member_usage_limit_reached",
"limit_reason": "usage_limit",
"cta_action": "request_increase",
},
}),
payload,
);
}
fn sample_plugin_metadata() -> PluginTelemetryMetadata {
PluginTelemetryMetadata {
plugin_id: PluginId::parse("sample@test").expect("valid plugin id"),

View File

@@ -20,6 +20,8 @@ use crate::facts::TurnSteerResult;
use crate::facts::TurnSubmissionType;
use crate::now_unix_seconds;
use codex_app_server_protocol::CodexErrorInfo;
use codex_app_server_protocol::UsageLimitBannerAction;
use codex_app_server_protocol::UsageLimitBannerType;
use codex_login::default_client::originator;
use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::approvals::NetworkApprovalProtocol;
@@ -61,6 +63,7 @@ pub(crate) enum TrackEventRequest {
Compaction(Box<CodexCompactionEventRequest>),
TurnEvent(Box<CodexTurnEventRequest>),
TurnSteer(CodexTurnSteerEventRequest),
UsageLimitBanner(UsageLimitBannerEventRequest),
PluginUsed(CodexPluginUsedEventRequest),
PluginInstalled(CodexPluginEventRequest),
PluginUninstalled(CodexPluginEventRequest),
@@ -68,6 +71,21 @@ pub(crate) enum TrackEventRequest {
PluginDisabled(CodexPluginEventRequest),
}
#[derive(Serialize)]
pub(crate) struct UsageLimitBannerEventRequest {
pub(crate) event_type: &'static str,
pub(crate) event_params: UsageLimitBannerEventParams,
}
#[derive(Serialize)]
pub(crate) struct UsageLimitBannerEventParams {
pub(crate) platform: &'static str,
pub(crate) banner_type: &'static str,
pub(crate) limit_reason: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) cta_action: Option<&'static str>,
}
#[derive(Serialize)]
pub(crate) struct SkillInvocationEventRequest {
pub(crate) event_type: &'static str,
@@ -571,6 +589,43 @@ pub(crate) fn plugin_state_event_type(state: PluginState) -> &'static str {
}
}
pub(crate) fn usage_limit_banner_event_request(
action: UsageLimitBannerAction,
banner_type: UsageLimitBannerType,
) -> UsageLimitBannerEventRequest {
let limit_reason = match banner_type {
UsageLimitBannerType::WorkspaceMemberCreditsDepleted => "credits",
UsageLimitBannerType::WorkspaceMemberUsageLimitReached => "usage_limit",
};
let banner_type_param = match banner_type {
UsageLimitBannerType::WorkspaceMemberCreditsDepleted => "workspace_member_credits_depleted",
UsageLimitBannerType::WorkspaceMemberUsageLimitReached => {
"workspace_member_usage_limit_reached"
}
};
let cta_action = match banner_type {
UsageLimitBannerType::WorkspaceMemberCreditsDepleted => "notify_owner",
UsageLimitBannerType::WorkspaceMemberUsageLimitReached => "request_increase",
};
let cta_action = match action {
UsageLimitBannerAction::Shown => None,
UsageLimitBannerAction::CtaClicked => Some(cta_action),
};
UsageLimitBannerEventRequest {
event_type: match action {
UsageLimitBannerAction::Shown => "codex_usage_limit_banner_shown",
UsageLimitBannerAction::CtaClicked => "codex_usage_limit_banner_cta_clicked",
},
event_params: UsageLimitBannerEventParams {
platform: "codex_cli",
banner_type: banner_type_param,
limit_reason,
cta_action,
},
}
}
pub(crate) fn codex_app_metadata(
tracking: &TrackEventsContext,
app: AppInvocation,

View File

@@ -28,6 +28,7 @@ use crate::events::plugin_state_event_type;
use crate::events::subagent_parent_thread_id;
use crate::events::subagent_source_name;
use crate::events::subagent_thread_started_event_request;
use crate::events::usage_limit_banner_event_request;
use crate::facts::AnalyticsFact;
use crate::facts::AnalyticsJsonRpcError;
use crate::facts::AppMentionedInput;
@@ -51,6 +52,7 @@ use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ClientResponse;
use codex_app_server_protocol::CodexErrorInfo;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::ProductAnalyticsEvent;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::TurnSteerResponse;
@@ -176,7 +178,7 @@ impl AnalyticsReducer {
request_id,
request,
} => {
self.ingest_request(connection_id, request_id, *request);
self.ingest_request(connection_id, request_id, *request, out);
}
AnalyticsFact::Response {
connection_id,
@@ -309,6 +311,7 @@ impl AnalyticsReducer {
connection_id: u64,
request_id: RequestId,
request: ClientRequest,
out: &mut Vec<TrackEventRequest>,
) {
match request {
ClientRequest::TurnStart { params, .. } => {
@@ -331,6 +334,18 @@ impl AnalyticsReducer {
}),
);
}
ClientRequest::TrackProductAnalyticsEvent { params, .. } => {
let event = match params.event {
ProductAnalyticsEvent::UsageLimitBanner {
action,
banner_type,
} => TrackEventRequest::UsageLimitBanner(usage_limit_banner_event_request(
action,
banner_type,
)),
};
out.push(event);
}
_ => {}
}
}

View File

@@ -2111,6 +2111,34 @@
],
"type": "object"
},
"ProductAnalyticsEvent": {
"oneOf": [
{
"properties": {
"action": {
"$ref": "#/definitions/UsageLimitBannerAction"
},
"bannerType": {
"$ref": "#/definitions/UsageLimitBannerType"
},
"type": {
"enum": [
"usageLimitBanner"
],
"title": "UsageLimitBannerProductAnalyticsEventType",
"type": "string"
}
},
"required": [
"action",
"bannerType",
"type"
],
"title": "UsageLimitBannerProductAnalyticsEvent",
"type": "object"
}
]
},
"RealtimeOutputModality": {
"enum": [
"text",
@@ -4067,6 +4095,17 @@
],
"type": "object"
},
"TrackProductAnalyticsEventParams": {
"properties": {
"event": {
"$ref": "#/definitions/ProductAnalyticsEvent"
}
},
"required": [
"event"
],
"type": "object"
},
"TurnEnvironmentParams": {
"properties": {
"cwd": {
@@ -4250,6 +4289,20 @@
],
"type": "object"
},
"UsageLimitBannerAction": {
"enum": [
"shown",
"cta_clicked"
],
"type": "string"
},
"UsageLimitBannerType": {
"enum": [
"workspace_member_credits_depleted",
"workspace_member_usage_limit_reached"
],
"type": "string"
},
"UserInput": {
"oneOf": [
{
@@ -5792,6 +5845,30 @@
"title": "Account/sendAddCreditsNudgeEmailRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"analytics/productEvent/track"
],
"title": "Analytics/productEvent/trackRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/TrackProductAnalyticsEventParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Analytics/productEvent/trackRequest",
"type": "object"
},
{
"properties": {
"id": {

View File

@@ -1575,6 +1575,30 @@
"title": "Account/sendAddCreditsNudgeEmailRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"analytics/productEvent/track"
],
"title": "Analytics/productEvent/trackRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/TrackProductAnalyticsEventParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Analytics/productEvent/trackRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -12024,6 +12048,34 @@
],
"type": "object"
},
"ProductAnalyticsEvent": {
"oneOf": [
{
"properties": {
"action": {
"$ref": "#/definitions/v2/UsageLimitBannerAction"
},
"bannerType": {
"$ref": "#/definitions/v2/UsageLimitBannerType"
},
"type": {
"enum": [
"usageLimitBanner"
],
"title": "UsageLimitBannerProductAnalyticsEventType",
"type": "string"
}
},
"required": [
"action",
"bannerType",
"type"
],
"title": "UsageLimitBannerProductAnalyticsEvent",
"type": "object"
}
]
},
"ProfileV2": {
"additionalProperties": true,
"properties": {
@@ -16878,6 +16930,24 @@
},
"type": "object"
},
"TrackProductAnalyticsEventParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"event": {
"$ref": "#/definitions/v2/ProductAnalyticsEvent"
}
},
"required": [
"event"
],
"title": "TrackProductAnalyticsEventParams",
"type": "object"
},
"TrackProductAnalyticsEventResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "TrackProductAnalyticsEventResponse",
"type": "object"
},
"Turn": {
"properties": {
"completedAt": {
@@ -17302,6 +17372,20 @@
"title": "TurnSteerResponse",
"type": "object"
},
"UsageLimitBannerAction": {
"enum": [
"shown",
"cta_clicked"
],
"type": "string"
},
"UsageLimitBannerType": {
"enum": [
"workspace_member_credits_depleted",
"workspace_member_usage_limit_reached"
],
"type": "string"
},
"UserInput": {
"oneOf": [
{

View File

@@ -2281,6 +2281,30 @@
"title": "Account/sendAddCreditsNudgeEmailRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"analytics/productEvent/track"
],
"title": "Analytics/productEvent/trackRequestMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/TrackProductAnalyticsEventParams"
}
},
"required": [
"id",
"method",
"params"
],
"title": "Analytics/productEvent/trackRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -8698,6 +8722,34 @@
],
"type": "object"
},
"ProductAnalyticsEvent": {
"oneOf": [
{
"properties": {
"action": {
"$ref": "#/definitions/UsageLimitBannerAction"
},
"bannerType": {
"$ref": "#/definitions/UsageLimitBannerType"
},
"type": {
"enum": [
"usageLimitBanner"
],
"title": "UsageLimitBannerProductAnalyticsEventType",
"type": "string"
}
},
"required": [
"action",
"bannerType",
"type"
],
"title": "UsageLimitBannerProductAnalyticsEvent",
"type": "object"
}
]
},
"ProfileV2": {
"additionalProperties": true,
"properties": {
@@ -14764,6 +14816,24 @@
},
"type": "object"
},
"TrackProductAnalyticsEventParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"event": {
"$ref": "#/definitions/ProductAnalyticsEvent"
}
},
"required": [
"event"
],
"title": "TrackProductAnalyticsEventParams",
"type": "object"
},
"TrackProductAnalyticsEventResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "TrackProductAnalyticsEventResponse",
"type": "object"
},
"Turn": {
"properties": {
"completedAt": {
@@ -15188,6 +15258,20 @@
"title": "TurnSteerResponse",
"type": "object"
},
"UsageLimitBannerAction": {
"enum": [
"shown",
"cta_clicked"
],
"type": "string"
},
"UsageLimitBannerType": {
"enum": [
"workspace_member_credits_depleted",
"workspace_member_usage_limit_reached"
],
"type": "string"
},
"UserInput": {
"oneOf": [
{

View File

@@ -0,0 +1,57 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ProductAnalyticsEvent": {
"oneOf": [
{
"properties": {
"action": {
"$ref": "#/definitions/UsageLimitBannerAction"
},
"bannerType": {
"$ref": "#/definitions/UsageLimitBannerType"
},
"type": {
"enum": [
"usageLimitBanner"
],
"title": "UsageLimitBannerProductAnalyticsEventType",
"type": "string"
}
},
"required": [
"action",
"bannerType",
"type"
],
"title": "UsageLimitBannerProductAnalyticsEvent",
"type": "object"
}
]
},
"UsageLimitBannerAction": {
"enum": [
"shown",
"cta_clicked"
],
"type": "string"
},
"UsageLimitBannerType": {
"enum": [
"workspace_member_credits_depleted",
"workspace_member_usage_limit_reached"
],
"type": "string"
}
},
"properties": {
"event": {
"$ref": "#/definitions/ProductAnalyticsEvent"
}
},
"required": [
"event"
],
"title": "TrackProductAnalyticsEventParams",
"type": "object"
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "TrackProductAnalyticsEventResponse",
"type": "object"
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UsageLimitBannerAction } from "./UsageLimitBannerAction";
import type { UsageLimitBannerType } from "./UsageLimitBannerType";
export type ProductAnalyticsEvent = { "type": "usageLimitBanner", action: UsageLimitBannerAction, bannerType: UsageLimitBannerType, };

View File

@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProductAnalyticsEvent } from "./ProductAnalyticsEvent";
export type TrackProductAnalyticsEventParams = { event: ProductAnalyticsEvent, };

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type TrackProductAnalyticsEventResponse = Record<string, never>;

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UsageLimitBannerAction = "shown" | "cta_clicked";

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UsageLimitBannerType = "workspace_member_credits_depleted" | "workspace_member_usage_limit_reached";

View File

@@ -271,6 +271,7 @@ export type { PluginSummary } from "./PluginSummary";
export type { PluginUninstallParams } from "./PluginUninstallParams";
export type { PluginUninstallResponse } from "./PluginUninstallResponse";
export type { PluginsMigration } from "./PluginsMigration";
export type { ProductAnalyticsEvent } from "./ProductAnalyticsEvent";
export type { ProfileV2 } from "./ProfileV2";
export type { RateLimitReachedType } from "./RateLimitReachedType";
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
@@ -386,6 +387,8 @@ export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams";
export type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion";
export type { ToolRequestUserInputResponse } from "./ToolRequestUserInputResponse";
export type { ToolsV2 } from "./ToolsV2";
export type { TrackProductAnalyticsEventParams } from "./TrackProductAnalyticsEventParams";
export type { TrackProductAnalyticsEventResponse } from "./TrackProductAnalyticsEventResponse";
export type { Turn } from "./Turn";
export type { TurnCompletedNotification } from "./TurnCompletedNotification";
export type { TurnDiffUpdatedNotification } from "./TurnDiffUpdatedNotification";
@@ -402,6 +405,8 @@ export type { TurnStartedNotification } from "./TurnStartedNotification";
export type { TurnStatus } from "./TurnStatus";
export type { TurnSteerParams } from "./TurnSteerParams";
export type { TurnSteerResponse } from "./TurnSteerResponse";
export type { UsageLimitBannerAction } from "./UsageLimitBannerAction";
export type { UsageLimitBannerType } from "./UsageLimitBannerType";
export type { UserInput } from "./UserInput";
export type { WarningNotification } from "./WarningNotification";
export type { WebSearchAction } from "./WebSearchAction";

View File

@@ -573,6 +573,11 @@ client_request_definitions! {
response: v2::SendAddCreditsNudgeEmailResponse,
},
TrackProductAnalyticsEvent => "analytics/productEvent/track" {
params: v2::TrackProductAnalyticsEventParams,
response: v2::TrackProductAnalyticsEventResponse,
},
FeedbackUpload => "feedback/upload" {
params: v2::FeedbackUploadParams,
response: v2::FeedbackUploadResponse,
@@ -1462,6 +1467,36 @@ mod tests {
Ok(())
}
#[test]
fn serialize_track_product_analytics_event() -> Result<()> {
let request = ClientRequest::TrackProductAnalyticsEvent {
request_id: RequestId::Integer(2),
params: v2::TrackProductAnalyticsEventParams {
event: v2::ProductAnalyticsEvent::UsageLimitBanner {
action: v2::UsageLimitBannerAction::CtaClicked,
banner_type: v2::UsageLimitBannerType::WorkspaceMemberUsageLimitReached,
},
},
};
assert_eq!(request.id(), &RequestId::Integer(2));
assert_eq!(request.method(), "analytics/productEvent/track");
assert_eq!(
json!({
"method": "analytics/productEvent/track",
"id": 2,
"params": {
"event": {
"type": "usageLimitBanner",
"action": "cta_clicked",
"bannerType": "workspace_member_usage_limit_reached",
},
},
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_client_response() -> Result<()> {
let cwd = absolute_path("/tmp");

View File

@@ -2248,6 +2248,41 @@ pub struct SendAddCreditsNudgeEmailParams {
pub credit_type: AddCreditsNudgeCreditType,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct TrackProductAnalyticsEventParams {
pub event: ProductAnalyticsEvent,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(export_to = "v2/", tag = "type", rename_all = "camelCase")]
pub enum ProductAnalyticsEvent {
UsageLimitBanner {
action: UsageLimitBannerAction,
#[serde(rename = "bannerType")]
#[ts(rename = "bannerType")]
banner_type: UsageLimitBannerType,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/", rename_all = "snake_case")]
pub enum UsageLimitBannerAction {
Shown,
CtaClicked,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/", rename_all = "snake_case")]
pub enum UsageLimitBannerType {
WorkspaceMemberCreditsDepleted,
WorkspaceMemberUsageLimitReached,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/", rename_all = "snake_case")]
@@ -2263,6 +2298,11 @@ pub struct SendAddCreditsNudgeEmailResponse {
pub status: AddCreditsNudgeEmailStatus,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct TrackProductAnalyticsEventResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/", rename_all = "snake_case")]

View File

@@ -1671,6 +1671,19 @@ Field notes:
Use `creditType: "credits"` when workspace credits are depleted, or `creditType: "usage_limit"` when the workspace usage limit has been reached. If the owner was already notified recently, the response status is `cooldown_active`.
## Product analytics endpoints
- `analytics/productEvent/track` — record a typed client-side product analytics event.
### Track a product analytics event
```json
{ "method": "analytics/productEvent/track", "id": 9, "params": { "event": { "type": "usageLimitBanner", "action": "shown", "bannerType": "workspace_member_usage_limit_reached" } } }
{ "id": 9, "result": {} }
```
The current `usageLimitBanner` product event records when the CLI workspace-owner nudge prompt is shown or its CTA is clicked. Use `action: "shown"` when the banner is displayed, and `action: "cta_clicked"` when the user confirms it. The current workspace-owner nudge variants are `workspace_member_credits_depleted` and `workspace_member_usage_limit_reached`.
## Experimental API Opt-in
Some app-server methods and fields are intentionally gated behind an experimental capability with no backwards-compatible guarantees. This lets clients choose between:

View File

@@ -211,6 +211,7 @@ use codex_app_server_protocol::ThreadUnarchivedNotification;
use codex_app_server_protocol::ThreadUnsubscribeParams;
use codex_app_server_protocol::ThreadUnsubscribeResponse;
use codex_app_server_protocol::ThreadUnsubscribeStatus;
use codex_app_server_protocol::TrackProductAnalyticsEventResponse;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnError;
use codex_app_server_protocol::TurnInterruptParams;
@@ -1281,6 +1282,17 @@ impl CodexMessageProcessor {
self.send_add_credits_nudge_email(to_connection_request_id(request_id), params)
.await;
}
ClientRequest::TrackProductAnalyticsEvent {
request_id,
params: _,
} => {
self.outgoing
.send_response(
to_connection_request_id(request_id),
TrackProductAnalyticsEventResponse {},
)
.await;
}
ClientRequest::FeedbackUpload { request_id, params } => {
self.upload_feedback(to_connection_request_id(request_id), params)
.await;

View File

@@ -715,7 +715,8 @@ impl MessageProcessor {
}
let connection_id = connection_request_id.connection_id;
if let ClientRequest::TurnStart { request_id, .. }
| ClientRequest::TurnSteer { request_id, .. } = &codex_request
| ClientRequest::TurnSteer { request_id, .. }
| ClientRequest::TrackProductAnalyticsEvent { request_id, .. } = &codex_request
{
self.analytics_events_client.track_request(
connection_id.0,