Skip to main content

kcl_lib/coredump/
mod.rs

1//! Core dump related structures and functions.
2#![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;
16/// "Value" would be OK. This is imported as "JValue" throughout the rest of this crate.
17use serde_json::Value as JValue;
18use uuid::Uuid;
19
20#[async_trait::async_trait(?Send)]
21pub trait CoreDump: Clone {
22    /// Return the authentication token.
23    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    /// Return a screenshot of the app.
40    async fn screenshot(&self) -> Result<String>;
41
42    /// Get a screenshot of the app and upload it to public cloud storage.
43    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        // Base64 decode the screenshot.
48        let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
49        // Upload the screenshot.
50        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    /// Dump the app info.
69    async fn dump(&self) -> Result<CoreDumpInfo> {
70        // Create the zoo client.
71        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        // pretty-printed JSON byte vector of the coredump.
94        let data = serde_json::to_vec_pretty(&core_dump_info)?;
95
96        // Upload the coredump.
97        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/// The app info structure.
121/// The Core Dump Info structure.
122#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
123#[ts(export)]
124#[serde(rename_all = "snake_case")]
125pub struct CoreDumpInfo {
126    /// The unique id for the core dump - this helps correlate uploaded files with coredump data.
127    pub id: Uuid,
128    /// The version of the app.
129    pub version: String,
130    /// The git revision of the app.
131    pub git_rev: String,
132    /// A timestamp of the core dump.
133    #[ts(type = "string")]
134    pub timestamp: chrono::DateTime<chrono::Utc>,
135    /// If the app is running in desktop or the browser.
136    pub desktop: bool,
137    /// The os info.
138    pub os: OsInfo,
139    /// The webrtc stats.
140    pub webrtc_stats: WebrtcStats,
141    /// A GitHub issue url to report the core dump.
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub github_issue_url: Option<String>,
144    /// The kcl code the user is using.
145    pub kcl_code: String,
146    /// The client state (singletons and xstate).
147    pub client_state: JValue,
148}
149
150impl CoreDumpInfo {
151    /// Set the github issue url.
152    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![Screenshot]({screenshot_url})
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        // Add the kcl code if it exists.
175        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        // Note that `github_issue_url` is not included in the coredump file.
193        // It has already been encoded and uploaded at this point.
194        // The `github_issue_url` is used in openWindow in wasm.ts.
195        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/// The os info structure.
208#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
209#[ts(export)]
210#[serde(rename_all = "snake_case")]
211pub struct OsInfo {
212    /// The platform the app is running on.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub platform: Option<String>,
215    /// The architecture the app is running on.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub arch: Option<String>,
218    /// The kernel version.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub version: Option<String>,
221    /// Information about the browser.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub browser: Option<String>,
224}
225
226/// The webrtc stats structure.
227#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
228#[ts(export)]
229#[serde(rename_all = "snake_case")]
230pub struct WebrtcStats {
231    /// The packets lost.
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub packets_lost: Option<u32>,
234    /// The frames received.
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub frames_received: Option<u32>,
237    /// The frame width.
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub frame_width: Option<f32>,
240    /// The frame height.
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub frame_height: Option<f32>,
243    /// The frame rate.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub frame_rate: Option<f32>,
246    /// The number of key frames decoded.
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub key_frames_decoded: Option<u32>,
249    /// The number of frames dropped.
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub frames_dropped: Option<u32>,
252    /// The pause count.
253    #[serde(default, skip_serializing_if = "Option::is_none")]
254    pub pause_count: Option<u32>,
255    /// The total pauses duration.
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub total_pauses_duration: Option<f32>,
258    /// The freeze count.
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub freeze_count: Option<u32>,
261    /// The total freezes duration.
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub total_freezes_duration: Option<f32>,
264    /// The pli count.
265    #[serde(default, skip_serializing_if = "Option::is_none")]
266    pub pli_count: Option<u32>,
267    /// Packet jitter for this synchronizing source, measured in seconds.
268    #[serde(default, skip_serializing_if = "Option::is_none")]
269    pub jitter: Option<f32>,
270}