Send crash reports to Slack automatically (#4111)

This uploads apples crash reports to our servers when telemetry is
enabled.

Rather than jumping via the website, it uploads to collab directly. (I'd
like
to update the panic handler to do this too to make it possible to work
on that end-to-end without zed.dev running).

Release Notes:

- Added reporting of crashes when telemetry is enabled
This commit is contained in:
Conrad Irwin 2024-01-18 15:30:39 -07:00 committed by GitHub
commit 827a8dade5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 173 additions and 82 deletions

View File

@ -52,6 +52,7 @@ pub use user::*;
lazy_static! {
pub static ref ZED_SERVER_URL: String =
std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string());
pub static ref ZED_RPC_URL: Option<String> = std::env::var("ZED_RPC_URL").ok();
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
@ -933,6 +934,10 @@ impl Client {
http: Arc<dyn HttpClient>,
release_channel: Option<ReleaseChannel>,
) -> Result<Url> {
if let Some(url) = &*ZED_RPC_URL {
return Url::parse(url).context("invalid rpc url");
}
let mut url = format!("{}/rpc", *ZED_SERVER_URL);
if let Some(preview_param) =
release_channel.and_then(|channel| channel.release_query_param())
@ -941,14 +946,6 @@ impl Client {
url += preview_param;
}
let response = http.get(&url, Default::default(), false).await?;
// Normally, ZED_SERVER_URL is set to the URL of zed.dev website.
// The website's /rpc endpoint redirects to a collab server's /rpc endpoint,
// which requires authorization via an HTTP header.
//
// For testing purposes, ZED_SERVER_URL can also set to the direct URL of
// of a collab server. In that case, a request to the /rpc endpoint will
// return an 'unauthorized' response.
let collab_url = if response.status().is_redirection() {
response
.headers()
@ -957,8 +954,6 @@ impl Client {
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string()
} else if response.status() == StatusCode::UNAUTHORIZED {
url
} else {
Err(anyhow!(
"unexpected /rpc response status {}",

View File

@ -15,6 +15,8 @@ lazy_static::lazy_static! {
pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot");
pub static ref DEFAULT_PRETTIER_DIR: PathBuf = HOME.join("Library/Application Support/Zed/prettier");
pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db");
pub static ref CRASHES_DIR: PathBuf = HOME.join("Library/Logs/DiagnosticReports");
pub static ref CRASHES_RETIRED_DIR: PathBuf = HOME.join("Library/Logs/DiagnosticReports/Retired");
pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json");
pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json");
pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt");

View File

@ -43,7 +43,8 @@ use util::{
async_maybe,
channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL},
http::{self, HttpClient},
paths, ResultExt,
paths::{self, CRASHES_DIR, CRASHES_RETIRED_DIR},
ResultExt,
};
use uuid::Uuid;
use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
@ -229,14 +230,14 @@ fn main() {
initialize_workspace(app_state.clone(), cx);
if stdout_is_a_pty() {
upload_panics_and_crashes(http.clone(), cx);
cx.activate(true);
let urls = collect_url_args();
if !urls.is_empty() {
listener.open_urls(&urls)
}
} else {
upload_previous_panics(http.clone(), cx);
upload_panics_and_crashes(http.clone(), cx);
// TODO Development mode that forces the CLI mode usually runs Zed binary as is instead
// of an *app, hence gets no specific callbacks run. Emulate them here, if needed.
if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
@ -543,7 +544,22 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
let mut backtrace = backtrace
.frames()
.iter()
.filter_map(|frame| Some(format!("{:#}", frame.symbols().first()?.name()?)))
.flat_map(|frame| {
frame.symbols().iter().filter_map(|symbol| {
let name = symbol.name()?;
let addr = symbol.addr()? as usize;
let position = if let (Some(path), Some(lineno)) = (
symbol.filename().and_then(|path| path.file_name()),
symbol.lineno(),
) {
format!("{}:{}", path.to_string_lossy(), lineno)
} else {
"?".to_string()
};
Some(format!("{:} ({:#x}) at {}", name, addr, position))
})
})
.collect::<Vec<_>>();
// Strip out leading stack frames for rust panic-handling.
@ -599,77 +615,154 @@ fn init_panic_hook(app: &App, installation_id: Option<String>, session_id: Strin
}));
}
fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
fn upload_panics_and_crashes(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
let telemetry_settings = *client::TelemetrySettings::get_global(cx);
cx.background_executor()
.spawn(async move {
let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL);
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("panic")) {
continue;
}
let filename = if let Some(filename) = child_path.file_name() {
filename.to_string_lossy()
} else {
continue;
};
if !filename.starts_with("zed") {
continue;
}
if telemetry_settings.diagnostics {
let panic_file_content = smol::fs::read_to_string(&child_path)
.await
.context("error reading panic file")?;
let panic = serde_json::from_str(&panic_file_content)
.ok()
.or_else(|| {
panic_file_content
.lines()
.next()
.and_then(|line| serde_json::from_str(line).ok())
})
.unwrap_or_else(|| {
log::error!(
"failed to deserialize panic file {:?}",
panic_file_content
);
None
});
if let Some(panic) = panic {
let body = serde_json::to_string(&PanicRequest {
panic,
token: client::ZED_SECRET_CLIENT_TOKEN.into(),
})
.unwrap();
let request = Request::post(&panic_report_url)
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("Content-Type", "application/json")
.body(body.into())?;
let response = http.send(request).await.context("error sending panic")?;
if !response.status().is_success() {
log::error!("Error uploading panic to server: {}", response.status());
}
}
}
// We've done what we can, delete the file
std::fs::remove_file(child_path)
.context("error removing panic")
.log_err();
}
Ok::<_, anyhow::Error>(())
upload_previous_panics(http.clone(), telemetry_settings)
.await
.log_err();
upload_previous_crashes(http, telemetry_settings)
.await
.log_err()
})
.detach_and_log_err(cx);
.detach()
}
/// upload panics to us (via zed.dev)
async fn upload_previous_panics(
http: Arc<dyn HttpClient>,
telemetry_settings: client::TelemetrySettings,
) -> Result<()> {
let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL);
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("panic")) {
continue;
}
let filename = if let Some(filename) = child_path.file_name() {
filename.to_string_lossy()
} else {
continue;
};
if !filename.starts_with("zed") {
continue;
}
if telemetry_settings.diagnostics {
let panic_file_content = smol::fs::read_to_string(&child_path)
.await
.context("error reading panic file")?;
let panic = serde_json::from_str(&panic_file_content)
.ok()
.or_else(|| {
panic_file_content
.lines()
.next()
.and_then(|line| serde_json::from_str(line).ok())
})
.unwrap_or_else(|| {
log::error!("failed to deserialize panic file {:?}", panic_file_content);
None
});
if let Some(panic) = panic {
let body = serde_json::to_string(&PanicRequest {
panic,
token: client::ZED_SECRET_CLIENT_TOKEN.into(),
})
.unwrap();
let request = Request::post(&panic_report_url)
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("Content-Type", "application/json")
.body(body.into())?;
let response = http.send(request).await.context("error sending panic")?;
if !response.status().is_success() {
log::error!("Error uploading panic to server: {}", response.status());
}
}
}
// We've done what we can, delete the file
std::fs::remove_file(child_path)
.context("error removing panic")
.log_err();
}
Ok::<_, anyhow::Error>(())
}
static LAST_CRASH_UPLOADED: &'static str = "LAST_CRASH_UPLOADED";
/// upload crashes from apple's diagnostic reports to our server.
/// (only if telemetry is enabled)
async fn upload_previous_crashes(
http: Arc<dyn HttpClient>,
telemetry_settings: client::TelemetrySettings,
) -> Result<()> {
if !telemetry_settings.diagnostics {
return Ok(());
}
let last_uploaded = KEY_VALUE_STORE
.read_kvp(LAST_CRASH_UPLOADED)?
.unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this.
let mut uploaded = last_uploaded.clone();
let crash_report_url = format!("{}/api/crash", &*client::ZED_SERVER_URL);
for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] {
let mut children = smol::fs::read_dir(&dir).await?;
while let Some(child) = children.next().await {
let child = child?;
let Some(filename) = child
.path()
.file_name()
.map(|f| f.to_string_lossy().to_lowercase())
else {
continue;
};
if !filename.starts_with("zed-") || !filename.ends_with(".ips") {
continue;
}
if filename <= last_uploaded {
continue;
}
let body = smol::fs::read_to_string(&child.path())
.await
.context("error reading crash file")?;
let request = Request::post(&crash_report_url)
.redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("Content-Type", "text/plain")
.header(
"Authorization",
format!("token {}", client::ZED_SECRET_CLIENT_TOKEN),
)
.body(body.into())?;
let response = http.send(request).await.context("error sending crash")?;
if !response.status().is_success() {
log::error!("Error uploading crash to server: {}", response.status());
}
if uploaded < filename {
uploaded = filename.clone();
KEY_VALUE_STORE
.write_kvp(LAST_CRASH_UPLOADED.to_string(), filename)
.await?;
}
}
}
Ok(())
}
async fn load_login_shell_environment() -> Result<()> {

View File

@ -116,7 +116,8 @@ setTimeout(() => {
ZED_WINDOW_POSITION: positions[i],
ZED_STATELESS: "1",
ZED_ALWAYS_ACTIVE: "1",
ZED_SERVER_URL: "http://localhost:8080",
ZED_SERVER_URL: "http://localhost:3000",
ZED_RPC_URL: "http://localhost:8080/rpc",
ZED_ADMIN_API_TOKEN: "secret",
ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`,
PATH: process.env.PATH,