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;
15use serde::Serialize;
16use serde_json::Value as JValue;
18use uuid::Uuid;
19
20#[async_trait::async_trait(?Send)]
21pub trait CoreDump: Clone {
22 fn token(&self) -> Result<String>;
24
25 fn base_api_url(&self) -> Result<String>;
26
27 fn version(&self) -> Result<String>;
28
29 fn kcl_code(&self) -> Result<String>;
30
31 fn os(&self) -> Result<OsInfo>;
32
33 fn is_desktop(&self) -> Result<bool>;
34
35 async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
36
37 async fn get_client_state(&self) -> Result<JValue>;
38
39 async fn screenshot(&self) -> Result<String>;
41
42 async fn upload_screenshot(&self, coredump_id: &Uuid, zoo_client: &Client) -> Result<String> {
44 let screenshot = self.screenshot().await?;
45 let cleaned = screenshot.trim_start_matches("data:image/png;base64,");
46
47 let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
49 let links = zoo_client
51 .meta()
52 .create_debug_uploads(vec![kittycad::types::multipart::Attachment {
53 name: "".to_string(),
54 filepath: Some(format!(r#"modeling-app/coredump-{coredump_id}-screenshot.png"#).into()),
55 content_type: Some("image/png".to_string()),
56 data,
57 }])
58 .await
59 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
60
61 if links.is_empty() {
62 anyhow::bail!("Failed to upload screenshot");
63 }
64
65 Ok(links[0].clone())
66 }
67
68 async fn dump(&self) -> Result<CoreDumpInfo> {
70 let mut zoo_client = kittycad::Client::new(self.token()?);
72 zoo_client.set_base_url(&self.base_api_url()?);
73
74 let coredump_id = uuid::Uuid::new_v4();
75 let client_state = self.get_client_state().await?;
76 let webrtc_stats = self.get_webrtc_stats().await?;
77 let os = self.os()?;
78 let screenshot_url = self.upload_screenshot(&coredump_id, &zoo_client).await?;
79
80 let mut core_dump_info = CoreDumpInfo {
81 id: coredump_id,
82 version: self.version()?,
83 git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
84 timestamp: chrono::Utc::now(),
85 desktop: self.is_desktop()?,
86 kcl_code: self.kcl_code()?,
87 os,
88 webrtc_stats,
89 github_issue_url: None,
90 client_state,
91 };
92
93 let data = serde_json::to_vec_pretty(&core_dump_info)?;
95
96 let links = zoo_client
98 .meta()
99 .create_debug_uploads(vec![kittycad::types::multipart::Attachment {
100 name: "".to_string(),
101 filepath: Some(format!(r#"modeling-app/coredump-{coredump_id}.json"#).into()),
102 content_type: Some("application/json".to_string()),
103 data,
104 }])
105 .await
106 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
107
108 if links.is_empty() {
109 anyhow::bail!("Failed to upload coredump");
110 }
111
112 let coredump_url = &links[0];
113
114 core_dump_info.set_github_issue_url(&screenshot_url, coredump_url, &coredump_id)?;
115
116 Ok(core_dump_info)
117 }
118}
119
120#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
123#[ts(export)]
124#[serde(rename_all = "snake_case")]
125pub struct CoreDumpInfo {
126 pub id: Uuid,
128 pub version: String,
130 pub git_rev: String,
132 #[ts(type = "string")]
134 pub timestamp: chrono::DateTime<chrono::Utc>,
135 pub desktop: bool,
137 pub os: OsInfo,
139 pub webrtc_stats: WebrtcStats,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub github_issue_url: Option<String>,
144 pub kcl_code: String,
146 pub client_state: JValue,
148}
149
150impl CoreDumpInfo {
151 pub fn set_github_issue_url(&mut self, screenshot_url: &str, coredump_url: &str, coredump_id: &Uuid) -> Result<()> {
153 let coredump_filename = Path::new(coredump_url).file_name().unwrap().to_str().unwrap();
154 let desktop_or_browser_label = if self.desktop { "desktop-app" } else { "browser" };
155 let labels = ["coredump", "bug", desktop_or_browser_label];
156 let mut body = format!(
157 r#"[Add a title above and insert a description of the issue here]
158
159
160
161> _Note: If you are capturing from a browser there is limited support for screenshots, only captures the modeling scene.
162 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._
163
164<details>
165<summary><b>Core Dump</b></summary>
166
167[{coredump_filename}]({coredump_url})
168
169Reference ID: {coredump_id}
170</details>
171"#
172 );
173
174 if !self.kcl_code.trim().is_empty() {
176 body.push_str(&format!(
177 r#"
178<details>
179<summary><b>KCL Code</b></summary>
180
181```kcl
182{}
183```
184</details>
185"#,
186 self.kcl_code
187 ));
188 }
189
190 let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect();
191
192 self.github_issue_url = Some(format!(
196 r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#,
197 "KittyCAD",
198 "modeling-app",
199 urlencoded,
200 labels.join(",")
201 ));
202
203 Ok(())
204 }
205}
206
207#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
209#[ts(export)]
210#[serde(rename_all = "snake_case")]
211pub struct OsInfo {
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub platform: Option<String>,
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub arch: Option<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub version: Option<String>,
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub browser: Option<String>,
224}
225
226#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
228#[ts(export)]
229#[serde(rename_all = "snake_case")]
230pub struct WebrtcStats {
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub packets_lost: Option<u32>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub frames_received: Option<u32>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub frame_width: Option<f32>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub frame_height: Option<f32>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub frame_rate: Option<f32>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub key_frames_decoded: Option<u32>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub frames_dropped: Option<u32>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
254 pub pause_count: Option<u32>,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub total_pauses_duration: Option<f32>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
260 pub freeze_count: Option<u32>,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub total_freezes_duration: Option<f32>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
266 pub pli_count: Option<u32>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
269 pub jitter: Option<f32>,
270}