Remove panic/crash reporting from collab (#39249)

Crashes have been going to Sentry since v0.201.x

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin
2025-09-30 23:10:13 -06:00
committed by GitHub
parent dd6c653fe9
commit 1659fb81e7
11 changed files with 8 additions and 943 deletions
Generated
-1
View File
@@ -3385,7 +3385,6 @@ dependencies = [
"reqwest 0.11.27",
"reqwest_client",
"rpc",
"rustc-demangle",
"scrypt",
"sea-orm",
"semantic_version",
-1
View File
@@ -620,7 +620,6 @@ runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804
"async-dispatcher-runtime",
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-demangle = "0.1.23"
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
-2
View File
@@ -20,7 +20,5 @@ LLM_DATABASE_MAX_CONNECTIONS = 5
LLM_API_SECRET = "llm-secret"
OPENAI_API_KEY = "llm-secret"
# SLACK_PANICS_WEBHOOK = ""
# RUST_LOG=info
# LOG_JSON=true
-1
View File
@@ -46,7 +46,6 @@ rand.workspace = true
reqwest = { version = "0.11", features = ["json"] }
reqwest_client.workspace = true
rpc.workspace = true
rustc-demangle.workspace = true
scrypt = "0.11"
sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
semantic_version.workspace = true
-5
View File
@@ -214,11 +214,6 @@ spec:
secretKeyRef:
name: blob-store
key: bucket
- name: SLACK_PANICS_WEBHOOK
valueFrom:
secretKeyRef:
name: slack
key: panics_webhook
- name: COMPLETE_WITH_LANGUAGE_MODEL_RATE_LIMIT_PER_HOUR
value: "1000"
- name: SUPERMAVEN_ADMIN_API_KEY
-2
View File
@@ -1,8 +1,6 @@
pub mod contributors;
pub mod events;
pub mod extensions;
pub mod ips_file;
pub mod slack;
use crate::{AppState, Error, Result, auth, db::UserId, rpc};
use anyhow::Context as _;
+8 -438
View File
@@ -1,33 +1,28 @@
use super::ips_file::IpsFile;
use crate::api::CloudflareIpCountryHeader;
use crate::{AppState, Error, Result, api::slack};
use crate::{AppState, Error, Result};
use anyhow::anyhow;
use aws_sdk_s3::primitives::ByteStream;
use axum::{
Extension, Router, TypedHeader,
body::Bytes,
headers::Header,
http::{HeaderMap, HeaderName, StatusCode},
http::{HeaderName, StatusCode},
routing::post,
};
use chrono::Duration;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::sync::{Arc, OnceLock};
use telemetry_events::{Event, EventRequestBody, Panic};
use telemetry_events::{Event, EventRequestBody};
use util::ResultExt;
use uuid::Uuid;
const CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
pub fn router() -> Router {
Router::new()
.route("/telemetry/events", post(post_events))
.route("/telemetry/crashes", post(post_crash))
.route("/telemetry/crashes", post(post_panic))
.route("/telemetry/panics", post(post_panic))
.route("/telemetry/hangs", post(post_hang))
.route("/telemetry/hangs", post(post_panic))
}
pub struct ZedChecksumHeader(Vec<u8>);
@@ -58,437 +53,12 @@ impl Header for ZedChecksumHeader {
}
}
pub async fn post_crash(
Extension(app): Extension<Arc<AppState>>,
headers: HeaderMap,
body: Bytes,
) -> Result<()> {
let report = IpsFile::parse(&body)?;
let version_threshold = SemanticVersion::new(0, 123, 0);
let bundle_id = &report.header.bundle_id;
let app_version = &report.app_version();
if bundle_id == "dev.zed.Zed-Dev" {
log::error!("Crash uploads from {} are ignored.", bundle_id);
return Ok(());
}
if app_version.is_none() || app_version.unwrap() < version_threshold {
log::error!(
"Crash uploads from {} are ignored.",
report.header.app_version
);
return Ok(());
}
let app_version = app_version.unwrap();
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
let response = blob_store_client
.head_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(report.header.incident_id.clone() + ".ips")
.send()
.await;
if response.is_ok() {
log::info!("We've already uploaded this crash");
return Ok(());
}
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(report.header.incident_id.clone() + ".ips")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
let recent_panic_on: Option<i64> = headers
.get("x-zed-panicked-on")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse().ok());
let installation_id = headers
.get("x-zed-installation-id")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_default();
let mut recent_panic = None;
if let Some(recent_panic_on) = recent_panic_on {
let crashed_at = match report.timestamp() {
Ok(t) => Some(t),
Err(e) => {
log::error!("Can't parse {}: {}", report.header.timestamp, e);
None
}
};
if crashed_at.is_some_and(|t| (t.timestamp_millis() - recent_panic_on).abs() <= 30000) {
recent_panic = headers.get("x-zed-panic").and_then(|h| h.to_str().ok());
}
}
let description = report.description(recent_panic);
let summary = report.backtrace_summary();
tracing::error!(
service = "client",
version = %report.header.app_version,
os_version = %report.header.os_version,
bundle_id = %report.header.bundle_id,
incident_id = %report.header.incident_id,
installation_id = %installation_id,
description = %description,
backtrace = %summary,
"crash report"
);
if let Some(kinesis_client) = app.kinesis_client.clone()
&& let Some(stream) = app.config.kinesis_stream.clone()
{
let properties = json!({
"app_version": report.header.app_version,
"os_version": report.header.os_version,
"os_name": "macOS",
"bundle_id": report.header.bundle_id,
"incident_id": report.header.incident_id,
"installation_id": installation_id,
"description": description,
"backtrace": summary,
});
let row = SnowflakeRow::new(
"Crash Reported",
None,
false,
Some(installation_id),
properties,
);
let data = serde_json::to_vec(&row)?;
kinesis_client
.put_record()
.stream_name(stream)
.partition_key(row.insert_id.unwrap_or_default())
.data(data.into())
.send()
.await
.log_err();
}
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
let payload = slack::WebhookBody::new(|w| {
w.add_section(|s| s.text(slack::Text::markdown(description)))
.add_section(|s| {
s.add_field(slack::Text::markdown(format!(
"*Version:*\n{} ({})",
bundle_id, app_version
)))
.add_field({
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
hostname.strip_prefix("http://").unwrap_or_default()
});
slack::Text::markdown(format!(
"*Incident:*\n<https://{}.{}/{}.ips|{}…>",
CRASH_REPORTS_BUCKET,
hostname,
report.header.incident_id,
report
.header
.incident_id
.chars()
.take(8)
.collect::<String>(),
))
})
})
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(summary)))
});
let payload_json = serde_json::to_string(&payload).map_err(|err| {
log::error!("Failed to serialize payload to JSON: {err}");
Error::Internal(anyhow!(err))
})?;
reqwest::Client::new()
.post(slack_panics_webhook)
.header("Content-Type", "application/json")
.body(payload_json)
.send()
.await
.map_err(|err| {
log::error!("Failed to send payload to Slack: {err}");
Error::Internal(anyhow!(err))
})?;
}
pub async fn post_panic() -> Result<()> {
// as of v0.201.x crash/panic reporting is now done via Sentry.
// The endpoint returns OK to avoid spurious errors for old clients.
Ok(())
}
pub async fn post_hang(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
body: Bytes,
) -> Result<()> {
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
return Err(Error::http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
if checksum != expected {
return Err(Error::http(
StatusCode::BAD_REQUEST,
"invalid checksum".into(),
))?;
}
let incident_id = Uuid::new_v4().to_string();
// dump JSON into S3 so we can get frame offsets if we need to.
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".hang.json")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
let report: telemetry_events::HangReport = serde_json::from_slice(&body).map_err(|err| {
log::error!("can't parse report json: {err}");
Error::Internal(anyhow!(err))
})?;
let mut backtrace = "Possible hang detected on main thread:".to_string();
let unknown = "<unknown>".to_string();
for frame in report.backtrace.iter() {
backtrace.push_str(&format!("\n{}", frame.symbols.first().unwrap_or(&unknown)));
}
tracing::error!(
service = "client",
version = %report.app_version.unwrap_or_default().to_string(),
os_name = %report.os_name,
os_version = report.os_version.unwrap_or_default(),
incident_id = %incident_id,
installation_id = %report.installation_id.unwrap_or_default(),
backtrace = %backtrace,
"hang report");
Ok(())
}
pub async fn post_panic(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
body: Bytes,
) -> Result<()> {
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
return Err(Error::http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
if checksum != expected {
return Err(Error::http(
StatusCode::BAD_REQUEST,
"invalid checksum".into(),
))?;
}
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
.map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
let incident_id = uuid::Uuid::new_v4().to_string();
let panic = report.panic;
if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
return Err(Error::http(
StatusCode::BAD_REQUEST,
"invalid os version".into(),
))?;
}
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
let response = blob_store_client
.head_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.send()
.await;
if response.is_ok() {
log::info!("We've already uploaded this crash");
return Ok(());
}
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
let backtrace = panic.backtrace.join("\n");
tracing::error!(
service = "client",
version = %panic.app_version,
os_name = %panic.os_name,
os_version = %panic.os_version.clone().unwrap_or_default(),
incident_id = %incident_id,
installation_id = %panic.installation_id.clone().unwrap_or_default(),
description = %panic.payload,
backtrace = %backtrace,
"panic report"
);
if let Some(kinesis_client) = app.kinesis_client.clone()
&& let Some(stream) = app.config.kinesis_stream.clone()
{
let properties = json!({
"app_version": panic.app_version,
"os_name": panic.os_name,
"os_version": panic.os_version,
"incident_id": incident_id,
"installation_id": panic.installation_id,
"description": panic.payload,
"backtrace": backtrace,
});
let row = SnowflakeRow::new(
"Panic Reported",
None,
false,
panic.installation_id.clone(),
properties,
);
let data = serde_json::to_vec(&row)?;
kinesis_client
.put_record()
.stream_name(stream)
.partition_key(row.insert_id.unwrap_or_default())
.data(data.into())
.send()
.await
.log_err();
}
if !report_to_slack(&panic) {
return Ok(());
}
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
let backtrace = if panic.backtrace.len() > 25 {
let total = panic.backtrace.len();
format!(
"{}\n and {} more",
panic
.backtrace
.iter()
.take(20)
.cloned()
.collect::<Vec<_>>()
.join("\n"),
total - 20
)
} else {
panic.backtrace.join("\n")
};
let backtrace_with_summary = panic.payload + "\n" + &backtrace;
let version = if panic.release_channel == "nightly"
&& !panic.app_version.contains("remote-server")
&& let Some(sha) = panic.app_commit_sha
{
format!("Zed Nightly {}", sha.chars().take(7).collect::<String>())
} else {
panic.app_version
};
let payload = slack::WebhookBody::new(|w| {
w.add_section(|s| s.text(slack::Text::markdown("Panic request".to_string())))
.add_section(|s| {
s.add_field(slack::Text::markdown(format!("*Version:*\n {version} ",)))
.add_field({
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
hostname.strip_prefix("http://").unwrap_or_default()
});
slack::Text::markdown(format!(
"*{} {}:*\n<https://{}.{}/{}.json|{}…>",
panic.os_name,
panic.os_version.unwrap_or_default(),
CRASH_REPORTS_BUCKET,
hostname,
incident_id,
incident_id.chars().take(8).collect::<String>(),
))
})
})
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace_with_summary)))
});
let payload_json = serde_json::to_string(&payload).map_err(|err| {
log::error!("Failed to serialize payload to JSON: {err}");
Error::Internal(anyhow!(err))
})?;
reqwest::Client::new()
.post(slack_panics_webhook)
.header("Content-Type", "application/json")
.body(payload_json)
.send()
.await
.map_err(|err| {
log::error!("Failed to send payload to Slack: {err}");
Error::Internal(anyhow!(err))
})?;
}
Ok(())
}
fn report_to_slack(panic: &Panic) -> bool {
// Panics on macOS should make their way to Slack as a crash report,
// so we don't need to send them a second time via this channel.
if panic.os_name == "macOS" {
return false;
}
if panic.payload.contains("ERROR_SURFACE_LOST_KHR") {
return false;
}
if panic.payload.contains("ERROR_INITIALIZATION_FAILED") {
return false;
}
if panic
.payload
.contains("GPU has crashed, and no debug information is available")
{
return false;
}
true
}
pub async fn post_events(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
-346
View File
@@ -1,346 +0,0 @@
use anyhow::Context as _;
use collections::HashMap;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug)]
pub struct IpsFile {
pub header: Header,
pub body: Body,
}
impl IpsFile {
pub fn parse(bytes: &[u8]) -> anyhow::Result<IpsFile> {
let mut split = bytes.splitn(2, |&b| b == b'\n');
let header_bytes = split.next().context("No header found")?;
let header: Header = serde_json::from_slice(header_bytes).context("parsing header")?;
let body_bytes = split.next().context("No body found")?;
let body: Body = serde_json::from_slice(body_bytes).context("parsing body")?;
Ok(IpsFile { header, body })
}
pub fn faulting_thread(&self) -> Option<&Thread> {
self.body.threads.get(self.body.faulting_thread? as usize)
}
pub fn app_version(&self) -> Option<SemanticVersion> {
self.header.app_version.parse().ok()
}
pub fn timestamp(&self) -> anyhow::Result<chrono::DateTime<chrono::FixedOffset>> {
chrono::DateTime::parse_from_str(&self.header.timestamp, "%Y-%m-%d %H:%M:%S%.f %#z")
.map_err(|e| anyhow::anyhow!(e))
}
pub fn description(&self, panic: Option<&str>) -> String {
let mut desc = if self.body.termination.indicator == "Abort trap: 6" {
match panic {
Some(panic_message) => format!("Panic `{}`", panic_message),
None => "Crash `Abort trap: 6` (possible panic)".into(),
}
} else if let Some(msg) = &self.body.exception.message {
format!("Exception `{}`", msg)
} else {
format!("Crash `{}`", self.body.termination.indicator)
};
if let Some(thread) = self.faulting_thread() {
if let Some(queue) = thread.queue.as_ref() {
desc += &format!(
" on thread {} ({})",
self.body.faulting_thread.unwrap_or_default(),
queue
);
} else {
desc += &format!(
" on thread {} ({})",
self.body.faulting_thread.unwrap_or_default(),
thread.name.clone().unwrap_or_default()
);
}
}
desc
}
pub fn backtrace_summary(&self) -> String {
if let Some(thread) = self.faulting_thread() {
let mut frames = thread
.frames
.iter()
.filter_map(|frame| {
if let Some(name) = &frame.symbol {
if self.is_ignorable_frame(name) {
return None;
}
Some(format!("{:#}", rustc_demangle::demangle(name)))
} else if let Some(image) = self.body.used_images.get(frame.image_index) {
Some(image.name.clone().unwrap_or("<unknown-image>".into()))
} else {
Some("<unknown>".into())
}
})
.collect::<Vec<_>>();
let total = frames.len();
if total > 21 {
frames = frames.into_iter().take(20).collect();
frames.push(format!(" and {} more...", total - 20))
}
frames.join("\n")
} else {
"<no backtrace available>".into()
}
}
fn is_ignorable_frame(&self, symbol: &String) -> bool {
[
"pthread_kill",
"panic",
"backtrace",
"rust_begin_unwind",
"abort",
]
.iter()
.any(|s| symbol.contains(s))
}
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Header {
pub app_name: String,
pub timestamp: String,
pub app_version: String,
pub slice_uuid: String,
pub build_version: String,
pub platform: i64,
#[serde(rename = "bundleID", default)]
pub bundle_id: String,
pub share_with_app_devs: i64,
pub is_first_party: i64,
pub bug_type: String,
pub os_version: String,
pub roots_installed: i64,
pub name: String,
pub incident_id: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Body {
pub uptime: i64,
pub proc_role: String,
pub version: i64,
#[serde(rename = "userID")]
pub user_id: i64,
pub deploy_version: i64,
pub model_code: String,
#[serde(rename = "coalitionID")]
pub coalition_id: i64,
pub os_version: OsVersion,
pub capture_time: String,
pub code_signing_monitor: i64,
pub incident: String,
pub pid: i64,
pub translated: bool,
pub cpu_type: String,
#[serde(rename = "roots_installed")]
pub roots_installed: i64,
#[serde(rename = "bug_type")]
pub bug_type: String,
pub proc_launch: String,
pub proc_start_abs_time: i64,
pub proc_exit_abs_time: i64,
pub proc_name: String,
pub proc_path: String,
pub bundle_info: BundleInfo,
pub store_info: StoreInfo,
pub parent_proc: String,
pub parent_pid: i64,
pub coalition_name: String,
pub crash_reporter_key: String,
#[serde(rename = "codeSigningID")]
pub code_signing_id: String,
#[serde(rename = "codeSigningTeamID")]
pub code_signing_team_id: String,
pub code_signing_flags: i64,
pub code_signing_validation_category: i64,
pub code_signing_trust_level: i64,
pub instruction_byte_stream: InstructionByteStream,
pub sip: String,
pub exception: Exception,
pub termination: Termination,
pub asi: Asi,
pub ext_mods: ExtMods,
pub faulting_thread: Option<i64>,
pub threads: Vec<Thread>,
pub used_images: Vec<UsedImage>,
pub shared_cache: SharedCache,
pub vm_summary: String,
pub legacy_info: LegacyInfo,
pub log_writing_signature: String,
pub trial_info: TrialInfo,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct OsVersion {
pub train: String,
pub build: String,
pub release_type: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct BundleInfo {
#[serde(rename = "CFBundleShortVersionString")]
pub cfbundle_short_version_string: String,
#[serde(rename = "CFBundleVersion")]
pub cfbundle_version: String,
#[serde(rename = "CFBundleIdentifier")]
pub cfbundle_identifier: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct StoreInfo {
pub device_identifier_for_vendor: String,
pub third_party: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct InstructionByteStream {
#[serde(rename = "beforePC")]
pub before_pc: String,
#[serde(rename = "atPC")]
pub at_pc: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Exception {
pub codes: String,
pub raw_codes: Vec<i64>,
#[serde(rename = "type")]
pub type_field: String,
pub subtype: Option<String>,
pub signal: String,
pub port: Option<i64>,
pub guard_id: Option<i64>,
pub message: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Termination {
pub flags: i64,
pub code: i64,
pub namespace: String,
pub indicator: String,
pub by_proc: String,
pub by_pid: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Asi {
#[serde(rename = "libsystem_c.dylib")]
pub libsystem_c_dylib: Vec<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ExtMods {
pub caller: ExtMod,
pub system: ExtMod,
pub targeted: ExtMod,
pub warnings: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ExtMod {
#[serde(rename = "thread_create")]
pub thread_create: i64,
#[serde(rename = "thread_set_state")]
pub thread_set_state: i64,
#[serde(rename = "task_for_pid")]
pub task_for_pid: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Thread {
pub thread_state: HashMap<String, Value>,
pub id: i64,
pub triggered: Option<bool>,
pub name: Option<String>,
pub queue: Option<String>,
pub frames: Vec<Frame>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Frame {
pub image_offset: i64,
pub symbol: Option<String>,
pub symbol_location: Option<i64>,
pub image_index: usize,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct UsedImage {
pub source: String,
pub arch: Option<String>,
pub base: i64,
#[serde(rename = "CFBundleShortVersionString")]
pub cfbundle_short_version_string: Option<String>,
#[serde(rename = "CFBundleIdentifier")]
pub cfbundle_identifier: Option<String>,
pub size: i64,
pub uuid: String,
pub path: Option<String>,
pub name: Option<String>,
#[serde(rename = "CFBundleVersion")]
pub cfbundle_version: Option<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct SharedCache {
pub base: i64,
pub size: i64,
pub uuid: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct LegacyInfo {
pub thread_triggered: ThreadTriggered,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct ThreadTriggered {
pub name: String,
pub queue: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct TrialInfo {
pub rollouts: Vec<Rollout>,
pub experiments: Vec<Value>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct Rollout {
pub rollout_id: String,
pub factor_pack_ids: HashMap<String, Value>,
pub deployment_id: i64,
}
-144
View File
@@ -1,144 +0,0 @@
use serde::{Deserialize, Serialize};
/// https://api.slack.com/reference/messaging/payload
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct WebhookBody {
text: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
blocks: Vec<Block>,
#[serde(skip_serializing_if = "Option::is_none")]
thread_ts: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
mrkdwn: Option<bool>,
}
impl WebhookBody {
pub fn new(f: impl FnOnce(Self) -> Self) -> Self {
f(Self::default())
}
pub fn add_section(mut self, build: impl FnOnce(Section) -> Section) -> Self {
self.blocks.push(Block::Section(build(Section::default())));
self
}
pub fn add_rich_text(mut self, build: impl FnOnce(RichText) -> RichText) -> Self {
self.blocks
.push(Block::RichText(build(RichText::default())));
self
}
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
/// https://api.slack.com/reference/block-kit/blocks
pub enum Block {
#[serde(rename = "section")]
Section(Section),
#[serde(rename = "rich_text")]
RichText(RichText),
// .... etc.
}
/// https://api.slack.com/reference/block-kit/blocks#section
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct Section {
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<Text>,
#[serde(skip_serializing_if = "Vec::is_empty")]
fields: Vec<Text>,
// fields, accessories...
}
impl Section {
pub fn text(mut self, text: Text) -> Self {
self.text = Some(text);
self
}
pub fn add_field(mut self, field: Text) -> Self {
self.fields.push(field);
self
}
}
/// https://api.slack.com/reference/block-kit/composition-objects#text
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Text {
#[serde(rename = "plain_text")]
PlainText { text: String, emoji: bool },
#[serde(rename = "mrkdwn")]
Markdown { text: String, verbatim: bool },
}
impl Text {
pub fn plain(s: String) -> Self {
Self::PlainText {
text: s,
emoji: true,
}
}
pub fn markdown(s: String) -> Self {
Self::Markdown {
text: s,
verbatim: false,
}
}
}
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct RichText {
elements: Vec<RichTextObject>,
}
impl RichText {
pub fn new(f: impl FnOnce(Self) -> Self) -> Self {
f(Self::default())
}
pub fn add_preformatted(
mut self,
build: impl FnOnce(RichTextPreformatted) -> RichTextPreformatted,
) -> Self {
self.elements.push(RichTextObject::Preformatted(build(
RichTextPreformatted::default(),
)));
self
}
}
/// https://api.slack.com/reference/block-kit/blocks#rich_text
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RichTextObject {
#[serde(rename = "rich_text_preformatted")]
Preformatted(RichTextPreformatted),
// etc.
}
/// https://api.slack.com/reference/block-kit/blocks#rich_text_preformatted
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct RichTextPreformatted {
#[serde(skip_serializing_if = "Vec::is_empty")]
elements: Vec<RichTextElement>,
#[serde(skip_serializing_if = "Option::is_none")]
border: Option<u8>,
}
impl RichTextPreformatted {
pub fn add_text(mut self, text: String) -> Self {
self.elements.push(RichTextElement::Text { text });
self
}
}
/// https://api.slack.com/reference/block-kit/blocks#element-types
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RichTextElement {
#[serde(rename = "text")]
Text { text: String },
// etc.
}
-2
View File
@@ -153,7 +153,6 @@ pub struct Config {
pub prediction_api_key: Option<Arc<str>>,
pub prediction_model: Option<Arc<str>>,
pub zed_client_checksum_seed: Option<String>,
pub slack_panics_webhook: Option<String>,
pub auto_join_channel_id: Option<ChannelId>,
pub supermaven_admin_api_key: Option<Arc<str>>,
}
@@ -204,7 +203,6 @@ impl Config {
prediction_api_key: None,
prediction_model: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,
migrations_path: None,
seed_path: None,
-1
View File
@@ -599,7 +599,6 @@ impl TestServer {
prediction_api_key: None,
prediction_model: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,
migrations_path: None,
seed_path: None,