Files
galaxy/app/src/code_review/telemetry_event.rs
T

623 lines
24 KiB
Rust

use std::fmt::Display;
use std::time::Duration;
use galaxy_core::telemetry::{EnablementState, TelemetryEvent, TelemetryEventDesc};
use serde::Serialize;
use serde_json::json;
use serde_with::SerializeDisplay;
use strum_macros::{EnumDiscriminants, EnumIter};
use crate::code_review::diff_state::{BackendOrigin, DiffMode, DiffOperation};
use crate::features::FeatureFlag;
use crate::server::telemetry::CLIAgentType;
use crate::view_components::find::FindDirection;
/// Identifies which git button the user clicked in the code review header.
/// Each variant maps to one of the primary action button / dropdown items.
#[derive(Clone, Copy, Debug, Serialize)]
pub enum GitButtonKind {
#[serde(rename = "commit")]
Commit,
#[serde(rename = "push")]
Push,
#[serde(rename = "publish")]
Publish,
#[serde(rename = "create_pr")]
CreatePr,
#[serde(rename = "view_pr")]
ViewPr,
}
/// Identifies which git operation actually ran when a `GitDialog` completed.
/// Distinguishes commit-dialog chained intents (e.g. commit-and-push) from
/// standalone push/publish/create-PR dialogs so analytics can tell the user
/// flows apart.
#[derive(Clone, Copy, Debug, Serialize)]
pub enum GitOperationKind {
/// Commit dialog with the commit-only intent.
#[serde(rename = "commit_only")]
CommitOnly,
/// Commit dialog with the commit-and-push intent.
#[serde(rename = "commit_and_push")]
CommitAndPush,
/// Commit dialog with the commit-and-create-PR intent.
#[serde(rename = "commit_and_create_pr")]
CommitAndCreatePr,
/// Standalone push dialog.
#[serde(rename = "push")]
Push,
/// Standalone publish dialog (push that also sets upstream).
#[serde(rename = "publish")]
Publish,
/// Standalone create-PR dialog.
#[serde(rename = "create_pr")]
CreatePr,
}
/// Terminal status of a `GitDialog`. Captures both async-op outcomes and
/// pre-confirmation user cancels in a single enum.
#[derive(Clone, Copy, Debug, Serialize)]
pub enum GitDialogStatus {
/// User confirmed the dialog and the underlying git operation succeeded.
#[serde(rename = "succeeded")]
Succeeded,
/// User confirmed the dialog and the underlying git operation failed.
#[serde(rename = "failed")]
Failed,
/// User cancelled the dialog (ESC / close button / cancel button) before
/// the async op ran.
#[serde(rename = "cancelled")]
Cancelled,
}
/// Entry points for opening the code review pane.
#[derive(Clone, Copy, Debug, SerializeDisplay, Default)]
pub enum CodeReviewPaneEntrypoint {
/// Opened via the git diff chip (git changes button in AI control panel).
GitDiffChip,
/// Opened via the "View changes" button when Agent mode is done running.
AgentModeCompleted,
/// Opened via the "Review changes" button when Agent mode is running.
AgentModeRunning,
/// Opened via the "/code-review" slash command.
SlashCommand,
/// Opened by the agent tool call.
InvokedByAgent,
// Force opened when user accepted first diff of a conversation
ForceOpened,
// Opened via the agent mode diff header
CodeDiffHeader,
// Opened via the pane header
PaneHeader,
// Opened via the code mode v2 right panel button
RightPanel,
/// Opened via the CLI agent view footer (e.g., Claude Code).
CLIAgentView,
/// Opened via other means (unknown entry point).
#[default]
Other,
}
impl Display for CodeReviewPaneEntrypoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::GitDiffChip => write!(f, "git_diff_chip"),
Self::AgentModeCompleted => write!(f, "agent_mode_completed"),
Self::AgentModeRunning => write!(f, "agent_mode_running"),
Self::SlashCommand => write!(f, "slash_command"),
Self::InvokedByAgent => write!(f, "invoked_by_agent"),
Self::ForceOpened => write!(f, "force_opened"),
Self::CodeDiffHeader => write!(f, "agent_mode_diff_header"),
Self::PaneHeader => write!(f, "pane_header"),
Self::RightPanel => write!(f, "right_panel"),
Self::CLIAgentView => write!(f, "cli_agent_view"),
Self::Other => write!(f, "other"),
}
}
}
/// Origin of an "Add to context" action.
#[derive(Clone, Copy, Debug, Serialize)]
pub enum AddToContextOrigin {
/// User selected text and added it to context.
#[serde(rename = "selected_text")]
SelectedText,
/// User clicked the gutter to add a line/hunk to context.
#[serde(rename = "gutter")]
Gutter,
/// User clicked the "Add diff set as context" button in code review header.
#[serde(rename = "code_review_header")]
#[allow(unused)]
CodeReviewHeader,
}
/// Where code review content was sent after the user action.
#[derive(Clone, Copy, Debug, Serialize)]
pub enum CodeReviewContextDestination {
/// Written directly to the terminal PTY for an active CLI agent.
#[serde(rename = "pty")]
Pty,
/// Inserted into the Warp AI input buffer as plain text.
#[serde(rename = "agent_input")]
AgentInput,
/// Registered as an AI attachment and referenced from the input.
#[serde(rename = "agent_attachment")]
AgentAttachment,
/// Inserted into the active command buffer while a command is running.
#[serde(rename = "active_command_buffer")]
ActiveCommandBuffer,
/// Submitted as an inline code review request through the Warp AI path.
#[serde(rename = "agent_review")]
AgentReview,
/// Inserted into CLI agent rich input.
#[serde(rename = "rich_input")]
RichInput,
}
/// Scope of a diff set attachment initiated from code review.
#[derive(Clone, Copy, Debug, Serialize)]
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
pub enum DiffSetContextScope {
/// Attach the full diff set for the current review.
#[serde(rename = "all")]
All,
/// Attach the diff set for a single file.
#[serde(rename = "file")]
File,
}
/// Pane state change for minimize/maximize events.
#[derive(Clone, Copy, Debug, Serialize)]
pub enum PaneStateChange {
/// Pane was minimized.
#[serde(rename = "minimized")]
Minimized,
/// Pane was maximized.
#[serde(rename = "maximized")]
Maximized,
}
/// Telemetry events associated with the code review pane.
#[derive(Serialize, Debug, EnumDiscriminants)]
#[strum_discriminants(derive(EnumIter))]
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
pub enum CodeReviewTelemetryEvent {
/// Emitted when the code review pane is opened.
PaneOpened {
is_local: Option<bool>,
entrypoint: CodeReviewPaneEntrypoint,
is_code_mode_v2: bool,
/// The CLI agent type if opened from a CLI agent footer (e.g., Claude Code).
cli_agent: Option<CLIAgentType>,
},
/// Emitted when a user adds content to AI context from code review.
AddToContext {
is_local: Option<bool>,
origin: AddToContextOrigin,
destination: CodeReviewContextDestination,
diff_set_scope: Option<DiffSetContextScope>,
},
/// Emitted when a user clicks the revert hunk button.
RevertHunkClicked { is_local: Option<bool> },
/// Emitted when a file is saved in the code review pane.
FileSaved { is_local: Option<bool> },
/// Emitted when the code review pane is minimized or maximized.
PaneStateChanged {
is_local: Option<bool>,
state_change: PaneStateChange,
},
/// Emitted when the diff base is changed (e.g., from uncommitted to main branch).
BaseChanged {
is_local: Option<bool>,
/// The new diff mode.
mode: DiffMode,
},
/// Failure when we are calculating the diff metadata.
LoadMetadataFailed {
backend_origin: BackendOrigin,
mode: DiffMode,
error: String,
},
/// Failure when we are loading the actual diff content. Shared across
/// file-invalidation, full diff load, and remote diff paths; the
/// `operation` field distinguishes which one produced the failure.
LoadDiffFailed {
backend_origin: BackendOrigin,
operation: DiffOperation,
mode: DiffMode,
error: String,
/// Time elapsed between when the tracked diff load was requested and
/// when this failure was observed. `None` if no tracked load was in
/// flight (e.g. a background invalidation error).
load_duration: Option<Duration>,
},
/// Emitted when a full diff load completes successfully.
DiffLoadCompleted {
is_local: Option<bool>,
mode: DiffMode,
file_count: usize,
files_changed: usize,
total_additions: usize,
total_deletions: usize,
/// Time elapsed between when the diff load was requested and when the
/// diffs were ready. `None` if the load was not initiated through a
/// tracked entry point (e.g. background refresh).
load_duration: Option<Duration>,
},
/// Emitted when the code review find bar is opened or closed.
FindBarToggled {
is_local: Option<bool>,
/// Whether the find bar is now open.
is_open: bool,
},
/// Emitted when search mode settings are changed.
FindBarModeChanged {
is_local: Option<bool>,
/// Whether case-sensitive search is enabled.
case_sensitive: bool,
/// Whether regex search is enabled.
regex: bool,
},
/// Emitted when the user navigates to the next or previous match.
FindNavigated {
is_local: Option<bool>,
/// Direction of navigation.
direction: FindDirection,
},
/// Emitted when the inline comment editor is opened in the code review pane.
CommentEditorOpened { is_local: Option<bool> },
/// Emitted when a new comment is added to the inline review.
CommentAdded { is_local: Option<bool> },
/// Emitted when an existing comment is edited.
CommentEdited { is_local: Option<bool> },
/// Emitted when a comment is deleted from the inline review.
CommentDeleted {
is_local: Option<bool>,
is_imported: bool,
},
/// Emitted when the bottom comment list panel is expanded.
CommentListExpanded {
is_local: Option<bool>,
/// Number of comments currently in the list.
comment_count: usize,
},
/// Emitted when the user submits an inline review to the agent.
ReviewSubmitted {
is_local: Option<bool>,
/// Number of comments in the submitted review.
comment_count: usize,
/// Number of unique files with comments.
file_count: usize,
/// Where the review was submitted.
destination: CodeReviewContextDestination,
},
/// Emitted when a comment in the list view is clicked to jump to its location.
CommentListItemClicked { is_local: Option<bool> },
/// Emitted when one or more comments fail to be precisely relocated after code changes.
CommentRelocationFailed {
is_local: Option<bool>,
/// Number of comments that could not be matched to an exact line and had to fall back.
fallback_count: usize,
},
/// Emitted when one or more comments are resolved.
CommentResolved {
/// Number of comments resolved by this operation.
resolved_count: usize,
},
/// Emitted when the agent's insert_code_review_comments tool call is received and processed.
CommentsReceived {
is_local: Option<bool>,
/// Number of raw InsertReviewComment items from the tool call.
raw_count: usize,
/// Number of successfully converted PendingImportedReviewComments.
converted_count: usize,
/// Number of AttachedReviewComments after thread flattening.
thread_count: usize,
},
/// Emitted after newly-imported comments are relocated against editor lines.
CommentsAttached {
is_local: Option<bool>,
/// Number of non-outdated imported comments after relocation.
active_count: usize,
/// Number of outdated imported comments after relocation.
outdated_count: usize,
},
/// Emitted when a user clicks a git operation button in the code review
/// header (primary button or dropdown item).
GitButtonTriggered {
is_local: Option<bool>,
button: GitButtonKind,
},
/// Emitted when a git dialog reaches a terminal state — either the async
/// op succeeded / failed, or the user cancelled before confirming.
GitDialogCompleted {
is_local: Option<bool>,
/// The git operation that ran or would have run (e.g. `commit_and_push`
/// for the commit dialog with that chained intent).
operation: GitOperationKind,
/// Whether the dialog succeeded, failed, or was cancelled.
status: GitDialogStatus,
/// Raw error string when `status == Failed`, `None` otherwise.
error: Option<String>,
},
}
impl TelemetryEvent for CodeReviewTelemetryEvent {
fn name(&self) -> &'static str {
CodeReviewTelemetryEventDiscriminants::from(self).name()
}
fn payload(&self) -> Option<serde_json::Value> {
match self {
CodeReviewTelemetryEvent::PaneOpened {
is_local,
entrypoint,
is_code_mode_v2,
cli_agent,
} => Some(json!({
"is_local": is_local,
"entrypoint": entrypoint,
"is_code_mode_v2": is_code_mode_v2,
"agent_name": cli_agent,
})),
CodeReviewTelemetryEvent::AddToContext {
is_local,
origin,
destination,
diff_set_scope,
} => Some(json!({
"is_local": is_local,
"origin": origin,
"destination": destination,
"diff_set_scope": diff_set_scope,
})),
CodeReviewTelemetryEvent::RevertHunkClicked { is_local } => {
Some(json!({ "is_local": is_local }))
}
CodeReviewTelemetryEvent::FileSaved { is_local } => {
Some(json!({ "is_local": is_local }))
}
CodeReviewTelemetryEvent::PaneStateChanged {
is_local,
state_change,
} => Some(json!({ "is_local": is_local, "state_change": state_change })),
CodeReviewTelemetryEvent::BaseChanged { is_local, mode } => {
Some(json!({ "is_local": is_local, "mode": mode }))
}
CodeReviewTelemetryEvent::LoadMetadataFailed {
backend_origin,
mode,
error,
} => Some(json!({
"backend_origin": backend_origin,
"mode": mode,
"error": error,
})),
CodeReviewTelemetryEvent::LoadDiffFailed {
backend_origin,
operation,
mode,
error,
load_duration,
} => Some(json!({
"backend_origin": backend_origin,
"operation": operation,
"mode": mode,
"error": error,
"load_duration": load_duration,
})),
CodeReviewTelemetryEvent::DiffLoadCompleted {
is_local,
mode,
file_count,
files_changed,
total_additions,
total_deletions,
load_duration,
} => Some(json!({
"is_local": is_local,
"mode": mode,
"file_count": file_count,
"files_changed": files_changed,
"total_additions": total_additions,
"total_deletions": total_deletions,
"load_duration": load_duration,
})),
CodeReviewTelemetryEvent::FindBarToggled { is_local, is_open } => {
Some(json!({ "is_local": is_local, "is_open": is_open }))
}
CodeReviewTelemetryEvent::FindBarModeChanged {
is_local,
case_sensitive,
regex,
} => Some(json!({
"is_local": is_local,
"case_sensitive": case_sensitive,
"regex": regex,
})),
CodeReviewTelemetryEvent::FindNavigated {
is_local,
direction,
} => Some(json!({ "is_local": is_local, "direction": direction })),
CodeReviewTelemetryEvent::CommentEditorOpened { is_local } => {
Some(json!({ "is_local": is_local }))
}
CodeReviewTelemetryEvent::CommentAdded { is_local } => {
Some(json!({ "is_local": is_local }))
}
CodeReviewTelemetryEvent::CommentEdited { is_local } => {
Some(json!({ "is_local": is_local }))
}
CodeReviewTelemetryEvent::CommentDeleted {
is_local,
is_imported,
} => Some(json!({ "is_local": is_local, "is_imported": is_imported })),
CodeReviewTelemetryEvent::CommentListExpanded {
is_local,
comment_count,
} => Some(json!({ "is_local": is_local, "comment_count": comment_count })),
CodeReviewTelemetryEvent::ReviewSubmitted {
is_local,
comment_count,
file_count,
destination,
} => Some(json!({
"is_local": is_local,
"comment_count": comment_count,
"file_count": file_count,
"destination": destination,
})),
CodeReviewTelemetryEvent::CommentListItemClicked { is_local } => {
Some(json!({ "is_local": is_local }))
}
CodeReviewTelemetryEvent::CommentRelocationFailed {
is_local,
fallback_count,
} => Some(json!({ "is_local": is_local, "fallback_count": fallback_count })),
CodeReviewTelemetryEvent::CommentResolved { resolved_count } => {
Some(json!({ "resolved_count": resolved_count }))
}
CodeReviewTelemetryEvent::CommentsReceived {
is_local,
raw_count,
converted_count,
thread_count,
} => Some(json!({
"is_local": is_local,
"raw_count": raw_count,
"converted_count": converted_count,
"thread_count": thread_count,
})),
CodeReviewTelemetryEvent::CommentsAttached {
is_local,
active_count,
outdated_count,
} => Some(json!({
"is_local": is_local,
"active_count": active_count,
"outdated_count": outdated_count,
})),
CodeReviewTelemetryEvent::GitButtonTriggered { is_local, button } => {
Some(json!({ "is_local": is_local, "button": button }))
}
CodeReviewTelemetryEvent::GitDialogCompleted {
is_local,
operation,
status,
error,
} => Some(json!({
"is_local": is_local,
"operation": operation,
"status": status,
"error": error,
})),
}
}
fn description(&self) -> &'static str {
CodeReviewTelemetryEventDiscriminants::from(self).description()
}
fn enablement_state(&self) -> EnablementState {
CodeReviewTelemetryEventDiscriminants::from(self).enablement_state()
}
fn contains_ugc(&self) -> bool {
CodeReviewTelemetryEventDiscriminants::from(self).contains_ugc()
}
fn event_descs() -> impl Iterator<Item = Box<dyn TelemetryEventDesc>> {
galaxy_core::telemetry::enum_events::<Self>()
}
}
impl CodeReviewTelemetryEventDiscriminants {
pub fn contains_ugc(&self) -> bool {
false
}
}
impl TelemetryEventDesc for CodeReviewTelemetryEventDiscriminants {
fn name(&self) -> &'static str {
match self {
Self::PaneOpened => "CodeReview.PaneOpened",
Self::AddToContext => "CodeReview.AddToContext",
Self::RevertHunkClicked => "CodeReview.RevertHunkClicked",
Self::FileSaved => "CodeReview.FileSaved",
Self::PaneStateChanged => "CodeReview.PaneStateChanged",
Self::BaseChanged => "CodeReview.BaseChanged",
Self::LoadMetadataFailed => "CodeReview.LoadMetadataFailed",
Self::LoadDiffFailed => "CodeReview.LoadDiffFailed",
Self::DiffLoadCompleted => "CodeReview.DiffLoadCompleted",
Self::FindBarToggled => "CodeReview.FindBarToggled",
Self::FindBarModeChanged => "CodeReview.FindBarModeChanged",
Self::FindNavigated => "CodeReview.FindNavigated",
Self::CommentEditorOpened => "CodeReview.CommentEditorOpened",
Self::CommentAdded => "CodeReview.CommentAdded",
Self::CommentEdited => "CodeReview.CommentEdited",
Self::CommentDeleted => "CodeReview.CommentDeleted",
Self::CommentListExpanded => "CodeReview.CommentListExpanded",
Self::ReviewSubmitted => "CodeReview.ReviewSubmitted",
Self::CommentListItemClicked => "CodeReview.CommentListItemClicked",
Self::CommentRelocationFailed => "CodeReview.CommentRelocationFailed",
Self::CommentResolved => "CodeReview.CommentResolved",
Self::CommentsReceived => "CodeReview.CommentsReceived",
Self::CommentsAttached => "CodeReview.CommentsAttached",
Self::GitButtonTriggered => "CodeReview.GitButtonTriggered",
Self::GitDialogCompleted => "CodeReview.GitDialogCompleted",
}
}
fn description(&self) -> &'static str {
match self {
Self::PaneOpened => "Code review pane opened",
Self::AddToContext => "Content added to AI context from code review",
Self::RevertHunkClicked => "Revert hunk button clicked",
Self::FileSaved => "File saved in code review pane",
Self::PaneStateChanged => "Code review pane minimized or maximized",
Self::BaseChanged => "Diff base changed in code review",
Self::LoadMetadataFailed => "Failure when calculating diff metadata",
Self::LoadDiffFailed => "Failure when loading diff content",
Self::DiffLoadCompleted => "Diff content loaded successfully",
Self::FindBarToggled => "Code review find bar opened or closed",
Self::FindBarModeChanged => "Search mode changed in code review find bar",
Self::FindNavigated => "Navigated to next or previous match in code review find bar",
Self::CommentEditorOpened => "Inline code review comment editor opened",
Self::CommentAdded => "Inline code review comment added",
Self::CommentEdited => "Inline code review comment edited",
Self::CommentDeleted => "Inline code review comment deleted",
Self::CommentListExpanded => "Inline code review comment list expanded",
Self::ReviewSubmitted => "Inline code review submitted to agent",
Self::CommentListItemClicked => "Inline code review comment list item clicked",
Self::CommentRelocationFailed => {
"Inline code review comment relocation fell back to approximate line"
}
Self::CommentResolved => "Inline code review comment resolved",
Self::CommentsReceived => {
"Agent insert_code_review_comments tool call received and processed"
}
Self::CommentsAttached => "Newly-imported comments relocated against editor lines",
Self::GitButtonTriggered => {
"User clicked a git operation button in the code review header"
}
Self::GitDialogCompleted => {
"Git operation dialog reached a terminal state (succeeded, failed, or cancelled)"
}
}
}
fn enablement_state(&self) -> EnablementState {
match self {
Self::CommentsReceived | Self::CommentsAttached => {
EnablementState::Flag(FeatureFlag::PRCommentsV2)
}
Self::GitButtonTriggered | Self::GitDialogCompleted => {
EnablementState::Flag(FeatureFlag::GitOperationsInCodeReview)
}
_ => EnablementState::Always,
}
}
}
galaxy_core::register_telemetry_event!(CodeReviewTelemetryEvent);