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