Files
oak-gpui/crates/zed/src/reliability.rs
T
Conrad Irwin a39ba03bcc Use metrics-id for sentry user id when we have it (#42931)
This should make it easier to correlate Sentry reports with user reports
and
github issues (for users who have diagnostics enabled)

Release Notes:

- N/A
2025-11-17 23:44:59 +00:00

335 lines
11 KiB
Rust

use anyhow::{Context as _, Result};
use client::{Client, telemetry::MINIDUMP_ENDPOINT};
use futures::AsyncReadExt;
use gpui::{App, AppContext as _, SerializedThreadTaskTimings};
use http_client::{self, HttpClient};
use log::info;
use project::Project;
use proto::{CrashReport, GetCrashFilesResponse};
use reqwest::multipart::{Form, Part};
use smol::stream::StreamExt;
use std::{ffi::OsStr, fs, sync::Arc, thread::ThreadId, time::Duration};
use util::ResultExt;
use crate::STARTUP_TIME;
pub fn init(client: Arc<Client>, cx: &mut App) {
monitor_hangs(cx);
if client.telemetry().diagnostics_enabled() {
let client = client.clone();
cx.background_spawn(async move {
upload_previous_minidumps(client).await.warn_on_err();
})
.detach()
}
cx.observe_new(move |project: &mut Project, _, cx| {
let client = client.clone();
let Some(remote_client) = project.remote_client() else {
return;
};
remote_client.update(cx, |remote_client, cx| {
if !client.telemetry().diagnostics_enabled() {
return;
}
let request = remote_client
.proto_client()
.request(proto::GetCrashFiles {});
cx.background_spawn(async move {
let GetCrashFilesResponse { crashes } = request.await?;
let Some(endpoint) = MINIDUMP_ENDPOINT.as_ref() else {
return Ok(());
};
for CrashReport {
metadata,
minidump_contents,
} in crashes
{
if let Some(metadata) = serde_json::from_str(&metadata).log_err() {
upload_minidump(client.clone(), endpoint, minidump_contents, &metadata)
.await
.log_err();
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
})
})
.detach();
}
fn monitor_hangs(cx: &App) {
let main_thread_id = std::thread::current().id();
let foreground_executor = cx.foreground_executor();
let background_executor = cx.background_executor();
// 3 seconds hang
let (mut tx, mut rx) = futures::channel::mpsc::channel(3);
foreground_executor
.spawn(async move { while (rx.next().await).is_some() {} })
.detach();
background_executor
.spawn({
let background_executor = background_executor.clone();
async move {
let mut hang_time = None;
let mut hanging = false;
loop {
background_executor.timer(Duration::from_secs(1)).await;
match tx.try_send(()) {
Ok(_) => {
hang_time = None;
hanging = false;
continue;
}
Err(e) => {
let is_full = e.into_send_error().is_full();
if is_full && !hanging {
hanging = true;
hang_time = Some(chrono::Local::now());
}
if is_full {
save_hang_trace(
main_thread_id,
&background_executor,
hang_time.unwrap(),
);
}
}
}
}
}
})
.detach();
}
fn save_hang_trace(
main_thread_id: ThreadId,
background_executor: &gpui::BackgroundExecutor,
hang_time: chrono::DateTime<chrono::Local>,
) {
let thread_timings = background_executor.dispatcher.get_all_timings();
let thread_timings = thread_timings
.into_iter()
.map(|mut timings| {
if timings.thread_id == main_thread_id {
timings.thread_name = Some("main".to_string());
}
SerializedThreadTaskTimings::convert(*STARTUP_TIME.get().unwrap(), timings)
})
.collect::<Vec<_>>();
let trace_path = paths::hang_traces_dir().join(&format!(
"hang-{}.miniprof",
hang_time.format("%Y-%m-%d_%H-%M-%S")
));
let Some(timings) = serde_json::to_string(&thread_timings)
.context("hang timings serialization")
.log_err()
else {
return;
};
std::fs::write(&trace_path, timings)
.context("hang trace file writing")
.log_err();
info!(
"hang detected, trace file saved at: {}",
trace_path.display()
);
}
pub async fn upload_previous_minidumps(client: Arc<Client>) -> anyhow::Result<()> {
let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else {
log::warn!("Minidump endpoint not set");
return Ok(());
};
let mut children = smol::fs::read_dir(paths::logs_dir()).await?;
while let Some(child) = children.next().await {
let child = child?;
let child_path = child.path();
if child_path.extension() != Some(OsStr::new("dmp")) {
continue;
}
let mut json_path = child_path.clone();
json_path.set_extension("json");
if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?)
&& upload_minidump(
client.clone(),
minidump_endpoint,
smol::fs::read(&child_path)
.await
.context("Failed to read minidump")?,
&metadata,
)
.await
.log_err()
.is_some()
{
fs::remove_file(child_path).ok();
fs::remove_file(json_path).ok();
}
}
Ok(())
}
async fn upload_minidump(
client: Arc<Client>,
endpoint: &str,
minidump: Vec<u8>,
metadata: &crashes::CrashInfo,
) -> Result<()> {
let mut form = Form::new()
.part(
"upload_file_minidump",
Part::bytes(minidump)
.file_name("minidump.dmp")
.mime_str("application/octet-stream")?,
)
.text(
"sentry[tags][channel]",
metadata.init.release_channel.clone(),
)
.text("sentry[tags][version]", metadata.init.zed_version.clone())
.text("sentry[tags][binary]", metadata.init.binary.clone())
.text("sentry[release]", metadata.init.commit_sha.clone())
.text("platform", "rust");
let mut panic_message = "".to_owned();
if let Some(panic_info) = metadata.panic.as_ref() {
panic_message = panic_info.message.clone();
form = form
.text("sentry[logentry][formatted]", panic_info.message.clone())
.text("span", panic_info.span.clone());
}
if let Some(minidump_error) = metadata.minidump_error.clone() {
form = form.text("minidump_error", minidump_error);
}
if let Some(id) = client.telemetry().metrics_id() {
form = form.text("sentry[user][id]", id.to_string());
form = form.text(
"sentry[user][is_staff]",
if client.telemetry().is_staff().unwrap_or_default() {
"true"
} else {
"false"
},
);
} else if let Some(id) = client.telemetry().installation_id() {
form = form.text("sentry[user][id]", format!("installation-{}", id))
}
::telemetry::event!(
"Minidump Uploaded",
panic_message = panic_message,
crashed_version = metadata.init.zed_version.clone(),
commit_sha = metadata.init.commit_sha.clone(),
);
let gpu_count = metadata.gpus.len();
for (index, gpu) in metadata.gpus.iter().cloned().enumerate() {
let system_specs::GpuInfo {
device_name,
device_pci_id,
vendor_name,
vendor_pci_id,
driver_version,
driver_name,
} = gpu;
let num = if gpu_count == 1 && metadata.active_gpu.is_none() {
String::new()
} else {
index.to_string()
};
let name = format!("gpu{num}");
let root = format!("sentry[contexts][{name}]");
form = form
.text(
format!("{root}[Description]"),
"A GPU found on the users system. May or may not be the GPU Zed is running on",
)
.text(format!("{root}[type]"), "gpu")
.text(format!("{root}[name]"), device_name.unwrap_or(name))
.text(format!("{root}[id]"), format!("{:#06x}", device_pci_id))
.text(
format!("{root}[vendor_id]"),
format!("{:#06x}", vendor_pci_id),
)
.text_if_some(format!("{root}[vendor_name]"), vendor_name)
.text_if_some(format!("{root}[driver_version]"), driver_version)
.text_if_some(format!("{root}[driver_name]"), driver_name);
}
if let Some(active_gpu) = metadata.active_gpu.clone() {
form = form
.text(
"sentry[contexts][Active_GPU][Description]",
"The GPU Zed is running on",
)
.text("sentry[contexts][Active_GPU][type]", "gpu")
.text("sentry[contexts][Active_GPU][name]", active_gpu.device_name)
.text(
"sentry[contexts][Active_GPU][driver_version]",
active_gpu.driver_info,
)
.text(
"sentry[contexts][Active_GPU][driver_name]",
active_gpu.driver_name,
)
.text(
"sentry[contexts][Active_GPU][is_software_emulated]",
active_gpu.is_software_emulated.to_string(),
);
}
// TODO: feature-flag-context, and more of device-context like screen resolution, available ram, device model, etc
let mut response_text = String::new();
let mut response = client
.http_client()
.send_multipart_form(endpoint, form)
.await?;
response
.body_mut()
.read_to_string(&mut response_text)
.await?;
if !response.status().is_success() {
anyhow::bail!("failed to upload minidump: {response_text}");
}
log::info!("Uploaded minidump. event id: {response_text}");
Ok(())
}
trait FormExt {
fn text_if_some(
self,
label: impl Into<std::borrow::Cow<'static, str>>,
value: Option<impl Into<std::borrow::Cow<'static, str>>>,
) -> Self;
}
impl FormExt for Form {
fn text_if_some(
self,
label: impl Into<std::borrow::Cow<'static, str>>,
value: Option<impl Into<std::borrow::Cow<'static, str>>>,
) -> Self {
match value {
Some(value) => self.text(label.into(), value.into()),
None => self,
}
}
}