1#![allow(dead_code)]
3
4#[cfg(not(target_arch = "wasm32"))]
5pub mod local;
6#[cfg(target_arch = "wasm32")]
7pub mod wasm;
8
9use std::path::Path;
10
11use anyhow::Result;
12use base64::Engine;
13use kittycad::Client;
14use serde::{Deserialize, Serialize};
15use serde_json::Value as JValue;
17use uuid::Uuid;
18
19#[async_trait::async_trait(?Send)]
20pub trait CoreDump: Clone {
21 fn token(&self) -> Result<String>;
23
24 fn base_api_url(&self) -> Result<String>;
25
26 fn version(&self) -> Result<String>;
27
28 fn kcl_code(&self) -> Result<String>;
29
30 fn pool(&self) -> Result<String>;
31
32 fn os(&self) -> Result<OsInfo>;
33
34 fn is_desktop(&self) -> Result<bool>;
35
36 async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
37
38 async fn get_client_state(&self) -> Result<JValue>;
39
40 async fn screenshot(&self) -> Result<String>;
42
43 async fn upload_screenshot(&self, coredump_id: &Uuid, zoo_client: &Client) -> Result<String> {
45 let screenshot = self.screenshot().await?;
46 let cleaned = screenshot.trim_start_matches("data:image/png;base64,");
47
48 let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
50 let links = zoo_client
52 .meta()
53 .create_debug_uploads(vec![kittycad::types::multipart::Attachment {
54 name: "".to_string(),
55 filepath: Some(format!(r#"modeling-app/coredump-{coredump_id}-screenshot.png"#).into()),
56 content_type: Some("image/png".to_string()),
57 data,
58 }])
59 .await
60 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
61
62 if links.is_empty() {
63 anyhow::bail!("Failed to upload screenshot");
64 }
65
66 Ok(links[0].clone())
67 }
68
69 async fn dump(&self) -> Result<CoreDumpInfo> {
71 let mut zoo_client = kittycad::Client::new(self.token()?);
73 zoo_client.set_base_url(&self.base_api_url()?);
74
75 let coredump_id = uuid::Uuid::new_v4();
76 let client_state = self.get_client_state().await?;
77 let webrtc_stats = self.get_webrtc_stats().await?;
78 let os = self.os()?;
79 let screenshot_url = self.upload_screenshot(&coredump_id, &zoo_client).await?;
80
81 let mut core_dump_info = CoreDumpInfo {
82 id: coredump_id,
83 version: self.version()?,
84 git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
85 timestamp: chrono::Utc::now(),
86 desktop: self.is_desktop()?,
87 kcl_code: self.kcl_code()?,
88 os,
89 webrtc_stats,
90 github_issue_url: None,
91 pool: self.pool()?,
92 client_state,
93 };
94
95 let data = serde_json::to_vec_pretty(&core_dump_info)?;
97
98 let links = zoo_client
100 .meta()
101 .create_debug_uploads(vec![kittycad::types::multipart::Attachment {
102 name: "".to_string(),
103 filepath: Some(format!(r#"modeling-app/coredump-{coredump_id}.json"#).into()),
104 content_type: Some("application/json".to_string()),
105 data,
106 }])
107 .await
108 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
109
110 if links.is_empty() {
111 anyhow::bail!("Failed to upload coredump");
112 }
113
114 let coredump_url = &links[0];
115
116 core_dump_info.set_github_issue_url(&screenshot_url, coredump_url, &coredump_id)?;
117
118 Ok(core_dump_info)
119 }
120}
121
122#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
125#[ts(export)]
126#[serde(rename_all = "snake_case")]
127pub struct CoreDumpInfo {
128 pub id: Uuid,
130 pub version: String,
132 pub git_rev: String,
134 #[ts(type = "string")]
136 pub timestamp: chrono::DateTime<chrono::Utc>,
137 pub desktop: bool,
139 pub os: OsInfo,
141 pub webrtc_stats: WebrtcStats,
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub github_issue_url: Option<String>,
146 pub kcl_code: String,
148 pub pool: String,
150 pub client_state: JValue,
152}
153
154impl CoreDumpInfo {
155 pub fn set_github_issue_url(&mut self, screenshot_url: &str, coredump_url: &str, coredump_id: &Uuid) -> Result<()> {
157 let coredump_filename = Path::new(coredump_url).file_name().unwrap().to_str().unwrap();
158 let desktop_or_browser_label = if self.desktop { "desktop-app" } else { "browser" };
159 let labels = ["coredump", "bug", desktop_or_browser_label];
160 let mut body = format!(
161 r#"[Add a title above and insert a description of the issue here]
162
163
164
165> _Note: If you are capturing from a browser there is limited support for screenshots, only captures the modeling scene.
166 If you are on MacOS native screenshots may be disabled by default. To enable native screenshots add Zoo Design Studio to System Settings -> Screen & SystemAudio Recording for native screenshots._
167
168<details>
169<summary><b>Core Dump</b></summary>
170
171[{coredump_filename}]({coredump_url})
172
173Reference ID: {coredump_id}
174</details>
175"#
176 );
177
178 if !self.kcl_code.trim().is_empty() {
180 body.push_str(&format!(
181 r#"
182<details>
183<summary><b>KCL Code</b></summary>
184
185```kcl
186{}
187```
188</details>
189"#,
190 self.kcl_code
191 ));
192 }
193
194 let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect();
195
196 self.github_issue_url = Some(format!(
200 r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#,
201 "KittyCAD",
202 "modeling-app",
203 urlencoded,
204 labels.join(",")
205 ));
206
207 Ok(())
208 }
209}
210
211#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
213#[ts(export)]
214#[serde(rename_all = "snake_case")]
215pub struct OsInfo {
216 #[serde(skip_serializing_if = "Option::is_none")]
218 pub platform: Option<String>,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub arch: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub version: Option<String>,
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub browser: Option<String>,
228}
229
230#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
232#[ts(export)]
233#[serde(rename_all = "snake_case")]
234pub struct WebrtcStats {
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub packets_lost: Option<u32>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub frames_received: Option<u32>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub frame_width: Option<f32>,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub frame_height: Option<f32>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub frame_rate: Option<f32>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub key_frames_decoded: Option<u32>,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub frames_dropped: Option<u32>,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
258 pub pause_count: Option<u32>,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub total_pauses_duration: Option<f32>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
264 pub freeze_count: Option<u32>,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub total_freezes_duration: Option<f32>,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub pli_count: Option<u32>,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
273 pub jitter: Option<f32>,
274}