Followup feedback (#5663)

- Added files to be uploaded
- Refactored
- Updated title
This commit is contained in:
Ahmed Ibrahim
2025-10-24 23:07:40 -07:00
committed by GitHub
parent 71f838389b
commit 88abbf58ce
4 changed files with 69 additions and 104 deletions

View File

@@ -167,14 +167,13 @@ impl CodexLogSnapshot {
Ok(path) Ok(path)
} }
/// Uploads feedback to Sentry with both the in-memory Codex logs and an optional /// Upload feedback to Sentry with optional attachments.
/// rollout file attached. Also records metadata such as classification, pub fn upload_feedback(
/// reason (free-form note), and CLI version as Sentry tags or message.
pub fn upload_feedback_with_rollout(
&self, &self,
classification: &str, classification: &str,
reason: Option<&str>, reason: Option<&str>,
cli_version: &str, cli_version: &str,
include_logs: bool,
rollout_path: Option<&std::path::Path>, rollout_path: Option<&std::path::Path>,
) -> Result<()> { ) -> Result<()> {
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -194,129 +193,73 @@ impl CodexLogSnapshot {
// Build Sentry client // Build Sentry client
let client = Client::from_config(ClientOptions { let client = Client::from_config(ClientOptions {
dsn: Some(Dsn::from_str(SENTRY_DSN).map_err(|e| anyhow!("invalid DSN: {}", e))?), dsn: Some(Dsn::from_str(SENTRY_DSN).map_err(|e| anyhow!("invalid DSN: {e}"))?),
transport: Some(Arc::new(DefaultTransportFactory {})), transport: Some(Arc::new(DefaultTransportFactory {})),
..Default::default() ..Default::default()
}); });
// Tags: thread id, classification, cli_version
let mut tags = BTreeMap::from([ let mut tags = BTreeMap::from([
(String::from("thread_id"), self.thread_id.to_string()), (String::from("thread_id"), self.thread_id.to_string()),
(String::from("classification"), classification.to_string()), (String::from("classification"), classification.to_string()),
(String::from("cli_version"), cli_version.to_string()), (String::from("cli_version"), cli_version.to_string()),
]); ]);
// Reason (freeform) include entire note as a tag; keep title in message.
if let Some(r) = reason { if let Some(r) = reason {
tags.insert(String::from("reason"), r.to_string()); tags.insert(String::from("reason"), r.to_string());
} }
// Elevate level for error-like classifications
let level = match classification { let level = match classification {
"bug" | "bad_result" => Level::Error, "bug" | "bad_result" => Level::Error,
_ => Level::Info, _ => Level::Info,
}; };
let mut envelope = Envelope::new(); let mut envelope = Envelope::new();
// Title is the message in Sentry: "[Classification]: Codex session <thread_id>"
let title = format!( let title = format!(
"[{}]: Codex session {}", "[{}]: Codex session {}",
display_classification(classification), display_classification(classification),
self.thread_id self.thread_id
); );
let event = Event {
let mut event = Event {
level, level,
message: Some(title), message: Some(title.clone()),
tags, tags,
..Default::default() ..Default::default()
}; };
if let Some(r) = reason {
use sentry::protocol::Exception;
use sentry::protocol::Values;
event.exception = Values::from(vec![Exception {
ty: title.clone(),
value: Some(r.to_string()),
..Default::default()
}]);
}
envelope.add_item(EnvelopeItem::Event(event)); envelope.add_item(EnvelopeItem::Event(event));
// Attachment 1: Codex logs snapshot if include_logs {
envelope.add_item(EnvelopeItem::Attachment(Attachment {
buffer: self.bytes.clone(),
filename: String::from("codex-logs.log"),
content_type: Some("text/plain".to_string()),
ty: None,
}));
// Attachment 2: rollout file (if provided and readable)
if let Some((path, data)) = rollout_path.and_then(|p| fs::read(p).ok().map(|d| (p, d))) {
// Name the file by suffix so users can spot it.
let fname = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "rollout.jsonl".to_string());
envelope.add_item(EnvelopeItem::Attachment(Attachment { envelope.add_item(EnvelopeItem::Attachment(Attachment {
buffer: data, buffer: self.bytes.clone(),
filename: fname, filename: String::from("codex-logs.log"),
content_type: Some("application/jsonl".to_string()), content_type: Some("text/plain".to_string()),
ty: None, ty: None,
})); }));
} }
client.send_envelope(envelope); if let Some((path, data)) = rollout_path.and_then(|p| fs::read(p).ok().map(|d| (p, d))) {
client.flush(Some(Duration::from_secs(UPLOAD_TIMEOUT_SECS))); let fname = path
.file_name()
Ok(()) .map(|s| s.to_string_lossy().to_string())
} .unwrap_or_else(|| "rollout.jsonl".to_string());
let content_type = "text/plain".to_string();
/// Upload a metadata-only feedback event (no attachments). Includes classification, envelope.add_item(EnvelopeItem::Attachment(Attachment {
/// optional reason, CLI version and thread ID as tags. buffer: data,
pub fn upload_feedback_metadata_only( filename: fname,
&self, content_type: Some(content_type),
classification: &str, ty: None,
reason: Option<&str>, }));
cli_version: &str,
) -> Result<()> {
use std::collections::BTreeMap;
use std::str::FromStr;
use std::sync::Arc;
use sentry::Client;
use sentry::ClientOptions;
use sentry::protocol::Envelope;
use sentry::protocol::EnvelopeItem;
use sentry::protocol::Event;
use sentry::protocol::Level;
use sentry::transports::DefaultTransportFactory;
use sentry::types::Dsn;
let client = Client::from_config(ClientOptions {
dsn: Some(Dsn::from_str(SENTRY_DSN).map_err(|e| anyhow!("invalid DSN: {}", e))?),
transport: Some(Arc::new(DefaultTransportFactory {})),
..Default::default()
});
let mut tags = BTreeMap::from([
(String::from("thread_id"), self.thread_id.to_string()),
(String::from("classification"), classification.to_string()),
(String::from("cli_version"), cli_version.to_string()),
]);
if let Some(r) = reason {
tags.insert(String::from("reason"), r.to_string());
} }
let level = match classification {
"bug" | "bad_result" => Level::Error,
_ => Level::Info,
};
let mut envelope = Envelope::new();
// Title is the message in Sentry: "[Classification]: Codex session <thread_id>"
let title = format!(
"[{}]: Codex session {}",
display_classification(classification),
self.thread_id
);
let event = Event {
level,
message: Some(title),
tags,
..Default::default()
};
envelope.add_item(EnvelopeItem::Event(event));
client.send_envelope(envelope); client.send_envelope(envelope);
client.flush(Some(Duration::from_secs(UPLOAD_TIMEOUT_SECS))); client.flush(Some(Duration::from_secs(UPLOAD_TIMEOUT_SECS)));
Ok(()) Ok(())

View File

@@ -76,17 +76,17 @@ impl FeedbackNoteView {
let cli_version = crate::version::CODEX_CLI_VERSION; let cli_version = crate::version::CODEX_CLI_VERSION;
let mut thread_id = self.snapshot.thread_id.clone(); let mut thread_id = self.snapshot.thread_id.clone();
let result = if self.include_logs { let result = self.snapshot.upload_feedback(
self.snapshot.upload_feedback_with_rollout( classification,
classification, reason_opt,
reason_opt, cli_version,
cli_version, self.include_logs,
rollout_path_ref, if self.include_logs {
) rollout_path_ref
} else { } else {
self.snapshot None
.upload_feedback_metadata_only(classification, reason_opt, cli_version) },
}; );
match result { match result {
Ok(()) => { Ok(()) => {
@@ -380,6 +380,7 @@ fn make_feedback_item(
pub(crate) fn feedback_upload_consent_params( pub(crate) fn feedback_upload_consent_params(
app_event_tx: AppEventSender, app_event_tx: AppEventSender,
category: FeedbackCategory, category: FeedbackCategory,
rollout_path: Option<std::path::PathBuf>,
) -> super::SelectionViewParams { ) -> super::SelectionViewParams {
use super::popup_consts::standard_popup_hint_line; use super::popup_consts::standard_popup_hint_line;
let yes_action: super::SelectionAction = Box::new({ let yes_action: super::SelectionAction = Box::new({
@@ -404,8 +405,20 @@ pub(crate) fn feedback_upload_consent_params(
} }
}); });
// Build header listing files that would be sent if user consents.
let mut header_lines: Vec<Box<dyn crate::render::renderable::Renderable>> = vec![
Line::from("Upload logs?".bold()).into(),
Line::from("").into(),
Line::from("The following files will be sent:".dim()).into(),
Line::from(vec!["".into(), "codex-logs.log".into()]).into(),
];
if let Some(path) = rollout_path.as_deref()
&& let Some(name) = path.file_name().map(|s| s.to_string_lossy().to_string())
{
header_lines.push(Line::from(vec!["".into(), name.into()]).into());
}
super::SelectionViewParams { super::SelectionViewParams {
title: Some("Upload logs?".to_string()),
footer_hint: Some(standard_popup_hint_line()), footer_hint: Some(standard_popup_hint_line()),
items: vec![ items: vec![
super::SelectionItem { super::SelectionItem {
@@ -426,6 +439,9 @@ pub(crate) fn feedback_upload_consent_params(
..Default::default() ..Default::default()
}, },
], ],
header: Box::new(crate::render::renderable::ColumnRenderable::with(
header_lines,
)),
..Default::default() ..Default::default()
} }
} }

View File

@@ -370,8 +370,11 @@ impl ChatWidget {
} }
pub(crate) fn open_feedback_consent(&mut self, category: crate::app_event::FeedbackCategory) { pub(crate) fn open_feedback_consent(&mut self, category: crate::app_event::FeedbackCategory) {
let params = let params = crate::bottom_pane::feedback_upload_consent_params(
crate::bottom_pane::feedback_upload_consent_params(self.app_event_tx.clone(), category); self.app_event_tx.clone(),
category,
self.current_rollout_path.clone(),
);
self.bottom_pane.show_selection_view(params); self.bottom_pane.show_selection_view(params);
self.request_redraw(); self.request_redraw();
} }

View File

@@ -4,6 +4,9 @@ expression: popup
--- ---
Upload logs? Upload logs?
The following files will be sent:
• codex-logs.log
1. Yes Share the current Codex session logs with the team for 1. Yes Share the current Codex session logs with the team for
troubleshooting. troubleshooting.
2. No 2. No