Followup feedback (#5663)
- Added files to be uploaded - Refactored - Updated title
This commit is contained in:
@@ -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(())
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user