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 os(&self) -> Result<OsInfo>;
31
32 fn is_desktop(&self) -> Result<bool>;
33
34 async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
35
36 async fn get_client_state(&self) -> Result<JValue>;
37
38 async fn screenshot(&self) -> Result<String>;
40
41 async fn upload_screenshot(&self, coredump_id: &Uuid, zoo_client: &Client) -> Result<String> {
43 let screenshot = self.screenshot().await?;
44 let cleaned = screenshot.trim_start_matches("data:image/png;base64,");
45
46 let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
48 let links = zoo_client
50 .meta()
51 .create_debug_uploads(vec![kittycad::types::multipart::Attachment {
52 name: "".to_string(),
53 filepath: Some(format!(r#"modeling-app/coredump-{coredump_id}-screenshot.png"#).into()),
54 content_type: Some("image/png".to_string()),
55 data,
56 }])
57 .await
58 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
59
60 if links.is_empty() {
61 anyhow::bail!("Failed to upload screenshot");
62 }
63
64 Ok(links[0].clone())
65 }
66
67 async fn dump(&self) -> Result<CoreDumpInfo> {
69 let mut zoo_client = kittycad::Client::new(self.token()?);
71 zoo_client.set_base_url(&self.base_api_url()?);
72
73 let coredump_id = uuid::Uuid::new_v4();
74 let client_state = self.get_client_state().await?;
75 let webrtc_stats = self.get_webrtc_stats().await?;
76 let os = self.os()?;
77 let screenshot_url = self.upload_screenshot(&coredump_id, &zoo_client).await?;
78
79 let mut core_dump_info = CoreDumpInfo {
80 id: coredump_id,
81 version: self.version()?,
82 git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
83 timestamp: chrono::Utc::now(),
84 desktop: self.is_desktop()?,
85 kcl_code: self.kcl_code()?,
86 os,
87 webrtc_stats,
88 github_issue_url: None,
89 client_state,
90 };
91
92 let data = serde_json::to_vec_pretty(&core_dump_info)?;
94
95 let links = zoo_client
97 .meta()
98 .create_debug_uploads(vec![kittycad::types::multipart::Attachment {
99 name: "".to_string(),
100 filepath: Some(format!(r#"modeling-app/coredump-{coredump_id}.json"#).into()),
101 content_type: Some("application/json".to_string()),
102 data,
103 }])
104 .await
105 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
106
107 if links.is_empty() {
108 anyhow::bail!("Failed to upload coredump");
109 }
110
111 let coredump_url = &links[0];
112
113 core_dump_info.set_github_issue_url(&screenshot_url, coredump_url, &coredump_id)?;
114
115 Ok(core_dump_info)
116 }
117}
118
119#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
122#[ts(export)]
123#[serde(rename_all = "snake_case")]
124pub struct CoreDumpInfo {
125 pub id: Uuid,
127 pub version: String,
129 pub git_rev: String,
131 #[ts(type = "string")]
133 pub timestamp: chrono::DateTime<chrono::Utc>,
134 pub desktop: bool,
136 pub os: OsInfo,
138 pub webrtc_stats: WebrtcStats,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub github_issue_url: Option<String>,
143 pub kcl_code: String,
145 pub client_state: JValue,
147}
148
149impl CoreDumpInfo {
150 pub fn set_github_issue_url(&mut self, screenshot_url: &str, coredump_url: &str, coredump_id: &Uuid) -> Result<()> {
152 let coredump_filename = Path::new(coredump_url).file_name().unwrap().to_str().unwrap();
153 let desktop_or_browser_label = if self.desktop { "desktop-app" } else { "browser" };
154 let labels = ["coredump", "bug", desktop_or_browser_label];
155 let mut body = format!(
156 r#"[Add a title above and insert a description of the issue here]
157
158
159
160> _Note: If you are capturing from a browser there is limited support for screenshots, only captures the modeling scene.
161 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._
162
163<details>
164<summary><b>Core Dump</b></summary>
165
166[{coredump_filename}]({coredump_url})
167
168Reference ID: {coredump_id}
169</details>
170"#
171 );
172
173 if !self.kcl_code.trim().is_empty() {
175 body.push_str(&format!(
176 r#"
177<details>
178<summary><b>KCL Code</b></summary>
179
180```kcl
181{}
182```
183</details>
184"#,
185 self.kcl_code
186 ));
187 }
188
189 let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect();
190
191 self.github_issue_url = Some(format!(
195 r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#,
196 "KittyCAD",
197 "modeling-app",
198 urlencoded,
199 labels.join(",")
200 ));
201
202 Ok(())
203 }
204}
205
206#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
208#[ts(export)]
209#[serde(rename_all = "snake_case")]
210pub struct OsInfo {
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub platform: Option<String>,
214 #[serde(skip_serializing_if = "Option::is_none")]
216 pub arch: Option<String>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub version: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub browser: Option<String>,
223}
224
225#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
227#[ts(export)]
228#[serde(rename_all = "snake_case")]
229pub struct WebrtcStats {
230 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub packets_lost: Option<u32>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub frames_received: Option<u32>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub frame_width: Option<f32>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub frame_height: Option<f32>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub frame_rate: Option<f32>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub key_frames_decoded: Option<u32>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
250 pub frames_dropped: Option<u32>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub pause_count: Option<u32>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub total_pauses_duration: Option<f32>,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub freeze_count: Option<u32>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub total_freezes_duration: Option<f32>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub pli_count: Option<u32>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub jitter: Option<f32>,
269}