Implement Core Dump for modeling app state (#2653)

This commit is contained in:
Dan Shaw
2024-06-20 16:36:28 -07:00
committed by GitHub
parent 2e7bdf02cf
commit a1bcad9dfb
11 changed files with 811 additions and 34 deletions

View File

@ -5489,3 +5489,72 @@ test('Paste should not work unless an input is focused', async ({
) )
).toContain(pasteContent) ).toContain(pasteContent)
}) })
test('Core dump from keyboard commands success', async ({ page }) => {
// This test can run long if it takes a little too long to load
// the engine, plus coredump takes bit to process.
test.setTimeout(150000)
const u = await getUtils(page)
await page.addInitScript(async () => {
;(window as any).playwrightSkipFilePicker = true
localStorage.setItem(
'persistCode',
`const topAng = 25
const bottomAng = 35
const baseLen = 3.5
const baseHeight = 1
const totalHeightHalf = 2
const armThick = 0.5
const totalLen = 9.5
const part001 = startSketchOn('-XZ')
|> startProfileAt([0, 0], %)
|> yLine(baseHeight, %)
|> xLine(baseLen, %)
|> angledLineToY({
angle: topAng,
to: totalHeightHalf,
}, %, 'seg04')
|> xLineTo(totalLen, %, 'seg03')
|> yLine(-armThick, %, 'seg01')
|> angledLineThatIntersects({
angle: HALF_TURN,
offset: -armThick,
intersectTag: 'seg04'
}, %)
|> angledLineToY([segAng('seg04', %) + 180, ZERO], %)
|> angledLineToY({
angle: -bottomAng,
to: -totalHeightHalf - armThick,
}, %, 'seg02')
|> xLineTo(segEndX('seg03', %) + 0, %)
|> yLine(-segLen('seg01', %), %)
|> angledLineThatIntersects({
angle: HALF_TURN,
offset: -armThick,
intersectTag: 'seg02'
}, %)
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|> xLineTo(ZERO, %)
|> close(%)
|> extrude(4, %)`
)
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
// Start waiting for popup before clicking. Note no await.
const popupPromise = page.waitForEvent('popup')
await page.keyboard.press('Meta+Shift+.')
// after invoking coredump, a loading toast will appear
await expect(page.getByText('Starting core dump')).toBeVisible()
// Allow time for core dump processing
await page.waitForTimeout(1000)
await expect(page.getByText('Core dump completed successfully')).toBeVisible()
const popup = await popupPromise
console.log(await popup.title())
// GitHub popup will go to unlogged in page. Can't look for "New Issue" here.
await expect(popup).toHaveTitle(/GitHub /)
})

1
src-tauri/Cargo.lock generated
View File

@ -6181,6 +6181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357" checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde_json",
"thiserror", "thiserror",
"ts-rs-macros", "ts-rs-macros",
"url", "url",

View File

@ -121,7 +121,24 @@ export const ModelingMachineProvider = ({
htmlRef, htmlRef,
token token
) )
useHotkeyWrapper(['meta + shift + .'], () => coreDump(coreDumpManager, true)) useHotkeyWrapper(['meta + shift + .'], () => {
console.warn('CoreDump: Initializing core dump')
toast.promise(
coreDump(coreDumpManager, true),
{
loading: 'Starting core dump...',
success: 'Core dump completed successfully',
error: 'Error while exporting core dump',
},
{
success: {
// Note: this extended duration is especially important for Playwright e2e testing
// default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
duration: 6000,
},
}
)
})
// Settings machine setup // Settings machine setup
// const retrievedSettings = useRef( // const retrievedSettings = useRef(

View File

@ -25,7 +25,7 @@ import type { Program } from '../wasm-lib/kcl/bindings/Program'
import type { Token } from '../wasm-lib/kcl/bindings/Token' import type { Token } from '../wasm-lib/kcl/bindings/Token'
import { Coords2d } from './std/sketch' import { Coords2d } from './std/sketch'
import { fileSystemManager } from 'lang/std/fileSystemManager' import { fileSystemManager } from 'lang/std/fileSystemManager'
import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo' import { CoreDumpInfo } from 'wasm-lib/kcl/bindings/CoreDumpInfo'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow' import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes' import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
@ -335,14 +335,27 @@ export function programMemoryInit(): ProgramMemory {
export async function coreDump( export async function coreDump(
coreDumpManager: CoreDumpManager, coreDumpManager: CoreDumpManager,
openGithubIssue: boolean = false openGithubIssue: boolean = false
): Promise<AppInfo> { ): Promise<CoreDumpInfo> {
try { try {
const dump: AppInfo = await coredump(coreDumpManager) const dump: CoreDumpInfo = await coredump(coreDumpManager)
/* NOTE: this console output of the coredump should include the field
`github_issue_url` which is not in the uploaded coredump file.
`github_issue_url` is added after the file is uploaded
and is only needed for the openWindow operation which creates
a new GitHub issue for the user.
*/
if (openGithubIssue && dump.github_issue_url) { if (openGithubIssue && dump.github_issue_url) {
openWindow(dump.github_issue_url) openWindow(dump.github_issue_url)
} else {
console.error(
'github_issue_url undefined. Unable to create GitHub issue for coredump.'
)
} }
console.log('CoreDump: final coredump', dump)
console.log('CoreDump: final coredump JSON', JSON.stringify(dump))
return dump return dump
} catch (e: any) { } catch (e: any) {
console.error('CoreDump: error', e)
throw new Error(`Error getting core dump: ${e}`) throw new Error(`Error getting core dump: ${e}`)
} }
} }

View File

@ -13,8 +13,15 @@ import screenshot from 'lib/screenshot'
import React from 'react' import React from 'react'
import { VITE_KC_API_BASE_URL } from 'env' import { VITE_KC_API_BASE_URL } from 'env'
// This is a class for getting all the values from the JS world to pass to the Rust world /**
// for a core dump. * CoreDumpManager module
* - for getting all the values from the JS world to pass to the Rust world for a core dump.
* @module lib/coredump
* @class
*/
// CoreDumpManager is instantiated in ModelingMachineProvider and passed to coreDump() in wasm.ts
// The async function coreDump() handles any errors thrown in its Promise catch method and rethrows
// them to so the toast handler in ModelingMachineProvider can show the user an error message toast
export class CoreDumpManager { export class CoreDumpManager {
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
htmlRef: React.RefObject<HTMLDivElement> | null htmlRef: React.RefObject<HTMLDivElement> | null
@ -144,6 +151,293 @@ export class CoreDumpManager {
}) })
} }
// Currently just a placeholder to begin loading singleton and xstate data into
getClientState(): Promise<string> {
/**
* Deep clone a JavaScript Object
* - NOTE: this function throws on parse errors from things like circular references
* - It is also synchronous and could be more performant
* - There is a whole rabbit hole to explore here if you like.
* - This works for our use case.
* @param {object} obj - The object to clone.
*/
const deepClone = (obj: any) => JSON.parse(JSON.stringify(obj))
/**
* Check if a function is private method
*/
const isPrivateMethod = (key: string) => {
return key.length && key[0] === '_'
}
// Turn off verbose logging by default
const verboseLogging = false
/**
* Toggle verbose debug logging of step-by-step client state coredump data
*/
const debugLog = verboseLogging ? console.log : () => {}
console.warn('CoreDump: Gathering client state')
// Initialize the clientState object
let clientState = {
// singletons
engine_command_manager: {
artifact_map: {},
command_logs: [],
engine_connection: { state: { type: '' } },
default_planes: {},
scene_command_artifacts: {},
},
kcl_manager: {
ast: {},
kcl_errors: [],
},
scene_infra: {},
scene_entities_manager: {},
editor_manager: {},
// xstate
auth_machine: {},
command_bar_machine: {},
file_machine: {},
home_machine: {},
modeling_machine: {},
settings_machine: {},
}
debugLog('CoreDump: initialized clientState', clientState)
debugLog('CoreDump: globalThis.window', globalThis.window)
try {
// Singletons
// engine_command_manager
debugLog('CoreDump: engineCommandManager', this.engineCommandManager)
// artifact map - this.engineCommandManager.artifactMap
if (this.engineCommandManager?.artifactMap) {
debugLog(
'CoreDump: Engine Command Manager artifact map',
this.engineCommandManager.artifactMap
)
clientState.engine_command_manager.artifact_map = deepClone(
this.engineCommandManager.artifactMap
)
}
// command logs - this.engineCommandManager.commandLogs
if (this.engineCommandManager?.commandLogs) {
debugLog(
'CoreDump: Engine Command Manager command logs',
this.engineCommandManager.commandLogs
)
clientState.engine_command_manager.command_logs = deepClone(
this.engineCommandManager.commandLogs
)
}
// default planes - this.engineCommandManager.defaultPlanes
if (this.engineCommandManager?.defaultPlanes) {
debugLog(
'CoreDump: Engine Command Manager default planes',
this.engineCommandManager.defaultPlanes
)
clientState.engine_command_manager.default_planes = deepClone(
this.engineCommandManager.defaultPlanes
)
}
// engine connection state
if (this.engineCommandManager?.engineConnection?.state) {
debugLog(
'CoreDump: Engine Command Manager engine connection state',
this.engineCommandManager.engineConnection.state
)
clientState.engine_command_manager.engine_connection.state =
this.engineCommandManager.engineConnection.state
}
// in sequence - this.engineCommandManager.inSequence
if (this.engineCommandManager?.inSequence) {
debugLog(
'CoreDump: Engine Command Manager in sequence',
this.engineCommandManager.inSequence
)
;(clientState.engine_command_manager as any).in_sequence =
this.engineCommandManager.inSequence
}
// out sequence - this.engineCommandManager.outSequence
if (this.engineCommandManager?.outSequence) {
debugLog(
'CoreDump: Engine Command Manager out sequence',
this.engineCommandManager.outSequence
)
;(clientState.engine_command_manager as any).out_sequence =
this.engineCommandManager.outSequence
}
// scene command artifacts - this.engineCommandManager.sceneCommandArtifacts
if (this.engineCommandManager?.sceneCommandArtifacts) {
debugLog(
'CoreDump: Engine Command Manager scene command artifacts',
this.engineCommandManager.sceneCommandArtifacts
)
clientState.engine_command_manager.scene_command_artifacts = deepClone(
this.engineCommandManager.sceneCommandArtifacts
)
}
// KCL Manager - globalThis?.window?.kclManager
const kclManager = (globalThis?.window as any)?.kclManager
debugLog('CoreDump: kclManager', kclManager)
if (kclManager) {
// KCL Manager AST
debugLog('CoreDump: KCL Manager AST', kclManager?.ast)
if (kclManager?.ast) {
clientState.kcl_manager.ast = deepClone(kclManager.ast)
}
// KCL Errors
debugLog('CoreDump: KCL Errors', kclManager?.kclErrors)
if (kclManager?.kclErrors) {
clientState.kcl_manager.kcl_errors = deepClone(kclManager.kclErrors)
}
// KCL isExecuting
debugLog('CoreDump: KCL isExecuting', kclManager?.isExecuting)
if (kclManager?.isExecuting) {
;(clientState.kcl_manager as any).isExecuting = kclManager.isExecuting
}
// KCL logs
debugLog('CoreDump: KCL logs', kclManager?.logs)
if (kclManager?.logs) {
;(clientState.kcl_manager as any).logs = deepClone(kclManager.logs)
}
// KCL programMemory
debugLog('CoreDump: KCL programMemory', kclManager?.programMemory)
if (kclManager?.programMemory) {
;(clientState.kcl_manager as any).programMemory = deepClone(
kclManager.programMemory
)
}
// KCL wasmInitFailed
debugLog('CoreDump: KCL wasmInitFailed', kclManager?.wasmInitFailed)
if (kclManager?.wasmInitFailed) {
;(clientState.kcl_manager as any).wasmInitFailed =
kclManager.wasmInitFailed
}
}
// Scene Infra - globalThis?.window?.sceneInfra
const sceneInfra = (globalThis?.window as any)?.sceneInfra
debugLog('CoreDump: Scene Infra', sceneInfra)
if (sceneInfra) {
const sceneInfraSkipKeys = ['camControls']
const sceneInfraKeys = Object.keys(sceneInfra)
.sort()
.filter((entry) => {
return (
typeof sceneInfra[entry] !== 'function' &&
!sceneInfraSkipKeys.includes(entry)
)
})
debugLog('CoreDump: Scene Infra keys', sceneInfraKeys)
sceneInfraKeys.forEach((key: string) => {
debugLog('CoreDump: Scene Infra', key, sceneInfra[key])
try {
;(clientState.scene_infra as any)[key] = sceneInfra[key]
} catch (error) {
console.error(
'CoreDump: unable to parse Scene Infra ' + key + ' data due to ',
error
)
}
})
}
// Scene Entities Manager - globalThis?.window?.sceneEntitiesManager
const sceneEntitiesManager = (globalThis?.window as any)
?.sceneEntitiesManager
debugLog('CoreDump: sceneEntitiesManager', sceneEntitiesManager)
if (sceneEntitiesManager) {
// Scene Entities Manager active segments
debugLog(
'CoreDump: Scene Entities Manager active segments',
sceneEntitiesManager?.activeSegments
)
if (sceneEntitiesManager?.activeSegments) {
;(clientState.scene_entities_manager as any).activeSegments =
deepClone(sceneEntitiesManager.activeSegments)
}
}
// Editor Manager - globalThis?.window?.editorManager
const editorManager = (globalThis?.window as any)?.editorManager
debugLog('CoreDump: editorManager', editorManager)
if (editorManager) {
const editorManagerSkipKeys = ['camControls']
const editorManagerKeys = Object.keys(editorManager)
.sort()
.filter((entry) => {
return (
typeof editorManager[entry] !== 'function' &&
!isPrivateMethod(entry) &&
!editorManagerSkipKeys.includes(entry)
)
})
debugLog('CoreDump: Editor Manager keys', editorManagerKeys)
editorManagerKeys.forEach((key: string) => {
debugLog('CoreDump: Editor Manager', key, editorManager[key])
try {
;(clientState.editor_manager as any)[key] = deepClone(
editorManager[key]
)
} catch (error) {
console.error(
'CoreDump: unable to parse Editor Manager ' +
key +
' data due to ',
error
)
}
})
}
// enableMousePositionLogs - Not coredumped
// See https://github.com/KittyCAD/modeling-app/issues/2338#issuecomment-2136441998
debugLog(
'CoreDump: enableMousePositionLogs [not coredumped]',
(globalThis?.window as any)?.enableMousePositionLogs
)
// XState Machines
debugLog(
'CoreDump: xstate services',
(globalThis?.window as any)?.__xstate__?.services
)
debugLog('CoreDump: final clientState', clientState)
const clientStateJson = JSON.stringify(clientState)
debugLog('CoreDump: final clientState JSON', clientStateJson)
return Promise.resolve(clientStateJson)
} catch (error) {
console.error('CoreDump: unable to return data due to ', error)
return Promise.reject(JSON.stringify(error))
}
}
// Return a data URL (png format) of the screenshot of the current page. // Return a data URL (png format) of the screenshot of the current page.
screenshot(): Promise<string> { screenshot(): Promise<string> {
return screenshot(this.htmlRef) return screenshot(this.htmlRef)

View File

@ -3272,6 +3272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357" checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde_json",
"thiserror", "thiserror",
"ts-rs-macros", "ts-rs-macros",
"url", "url",

View File

@ -37,7 +37,7 @@ serde_json = "1.0.116"
sha2 = "0.10.8" sha2 = "0.10.8"
thiserror = "1.0.61" thiserror = "1.0.61"
toml = "0.8.14" toml = "0.8.14"
ts-rs = { version = "9.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings"] } ts-rs = { version = "9.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] } url = { version = "2.5.2", features = ["serde"] }
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] } uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] } validator = { version = "0.18.1", features = ["derive"] }

View File

@ -3,6 +3,7 @@
use anyhow::Result; use anyhow::Result;
use crate::coredump::CoreDump; use crate::coredump::CoreDump;
use serde_json::Value as JValue;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CoreDumper {} pub struct CoreDumper {}
@ -55,6 +56,10 @@ impl CoreDump for CoreDumper {
Ok(crate::coredump::WebrtcStats::default()) Ok(crate::coredump::WebrtcStats::default())
} }
async fn get_client_state(&self) -> Result<JValue> {
Ok(JValue::default())
}
async fn screenshot(&self) -> Result<String> { async fn screenshot(&self) -> Result<String> {
// Take a screenshot of the engine. // Take a screenshot of the engine.
todo!() todo!()

View File

@ -7,8 +7,13 @@ pub mod wasm;
use anyhow::Result; use anyhow::Result;
use base64::Engine; use base64::Engine;
use kittycad::Client;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// "Value" would be OK. This is imported as "JValue" throughout the rest of this crate.
use serde_json::Value as JValue;
use std::path::Path;
use uuid::Uuid;
#[async_trait::async_trait(?Send)] #[async_trait::async_trait(?Send)]
pub trait CoreDump: Clone { pub trait CoreDump: Clone {
@ -27,25 +32,24 @@ pub trait CoreDump: Clone {
async fn get_webrtc_stats(&self) -> Result<WebrtcStats>; async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
async fn get_client_state(&self) -> Result<JValue>;
/// Return a screenshot of the app. /// Return a screenshot of the app.
async fn screenshot(&self) -> Result<String>; async fn screenshot(&self) -> Result<String>;
/// Get a screenshot of the app and upload it to public cloud storage. /// Get a screenshot of the app and upload it to public cloud storage.
async fn upload_screenshot(&self) -> Result<String> { async fn upload_screenshot(&self, coredump_id: &Uuid, zoo_client: &Client) -> Result<String> {
let screenshot = self.screenshot().await?; let screenshot = self.screenshot().await?;
let cleaned = screenshot.trim_start_matches("data:image/png;base64,"); let cleaned = screenshot.trim_start_matches("data:image/png;base64,");
// Create the zoo client.
let mut zoo = kittycad::Client::new(self.token()?);
zoo.set_base_url(&self.base_api_url()?);
// Base64 decode the screenshot. // Base64 decode the screenshot.
let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?; let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
// Upload the screenshot. // Upload the screenshot.
let links = zoo let links = zoo_client
.meta() .meta()
.create_debug_uploads(vec![kittycad::types::multipart::Attachment { .create_debug_uploads(vec![kittycad::types::multipart::Attachment {
name: "".to_string(), name: "".to_string(),
filename: Some("modeling-app/core-dump-screenshot.png".to_string()), filename: Some(format!(r#"modeling-app/coredump-{coredump_id}-screenshot.png"#)),
content_type: Some("image/png".to_string()), content_type: Some("image/png".to_string()),
data, data,
}]) }])
@ -60,12 +64,19 @@ pub trait CoreDump: Clone {
} }
/// Dump the app info. /// Dump the app info.
async fn dump(&self) -> Result<AppInfo> { async fn dump(&self) -> Result<CoreDumpInfo> {
// Create the zoo client.
let mut zoo_client = kittycad::Client::new(self.token()?);
zoo_client.set_base_url(&self.base_api_url()?);
let coredump_id = uuid::Uuid::new_v4();
let client_state = self.get_client_state().await?;
let webrtc_stats = self.get_webrtc_stats().await?; let webrtc_stats = self.get_webrtc_stats().await?;
let os = self.os().await?; let os = self.os().await?;
let screenshot_url = self.upload_screenshot().await?; let screenshot_url = self.upload_screenshot(&coredump_id, &zoo_client).await?;
let mut app_info = AppInfo { let mut core_dump_info = CoreDumpInfo {
id: coredump_id,
version: self.version()?, version: self.version()?,
git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()), git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
timestamp: chrono::Utc::now(), timestamp: chrono::Utc::now(),
@ -74,18 +85,44 @@ pub trait CoreDump: Clone {
webrtc_stats, webrtc_stats,
github_issue_url: None, github_issue_url: None,
pool: self.pool()?, pool: self.pool()?,
client_state,
}; };
app_info.set_github_issue_url(&screenshot_url)?;
Ok(app_info) // pretty-printed JSON byte vector of the coredump.
let data = serde_json::to_vec_pretty(&core_dump_info)?;
// Upload the coredump.
let links = zoo_client
.meta()
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
name: "".to_string(),
filename: Some(format!(r#"modeling-app/coredump-{}.json"#, coredump_id)),
content_type: Some("application/json".to_string()),
data,
}])
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
if links.is_empty() {
anyhow::bail!("Failed to upload coredump");
}
let coredump_url = &links[0];
core_dump_info.set_github_issue_url(&screenshot_url, coredump_url, &coredump_id)?;
Ok(core_dump_info)
} }
} }
/// The app info structure. /// The app info structure.
/// The Core Dump Info structure.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct AppInfo { pub struct CoreDumpInfo {
/// The unique id for the core dump - this helps correlate uploaded files with coredump data.
pub id: Uuid,
/// The version of the app. /// The version of the app.
pub version: String, pub version: String,
/// The git revision of the app. /// The git revision of the app.
@ -95,45 +132,44 @@ pub struct AppInfo {
pub timestamp: chrono::DateTime<chrono::Utc>, pub timestamp: chrono::DateTime<chrono::Utc>,
/// If the app is running in tauri or the browser. /// If the app is running in tauri or the browser.
pub tauri: bool, pub tauri: bool,
/// The os info. /// The os info.
pub os: OsInfo, pub os: OsInfo,
/// The webrtc stats. /// The webrtc stats.
pub webrtc_stats: WebrtcStats, pub webrtc_stats: WebrtcStats,
/// A GitHub issue url to report the core dump. /// A GitHub issue url to report the core dump.
/// This gets prepoulated with all the core dump info.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub github_issue_url: Option<String>, pub github_issue_url: Option<String>,
/// Engine pool the client is connected to. /// Engine pool the client is connected to.
pub pool: String, pub pool: String,
/// The client state (singletons and xstate).
pub client_state: JValue,
} }
impl AppInfo { impl CoreDumpInfo {
/// Set the github issue url. /// Set the github issue url.
pub fn set_github_issue_url(&mut self, screenshot_url: &str) -> Result<()> { pub fn set_github_issue_url(&mut self, screenshot_url: &str, coredump_url: &str, coredump_id: &Uuid) -> Result<()> {
let coredump_filename = Path::new(coredump_url).file_name().unwrap().to_str().unwrap();
let tauri_or_browser_label = if self.tauri { "tauri" } else { "browser" }; let tauri_or_browser_label = if self.tauri { "tauri" } else { "browser" };
let labels = ["coredump", "bug", tauri_or_browser_label]; let labels = ["coredump", "bug", tauri_or_browser_label];
let body = format!( let body = format!(
r#"[Insert a description of the issue here] r#"[Add a title above and insert a description of the issue here]
![Screenshot]({}) ![Screenshot]({screenshot_url})
<details> <details>
<summary><b>Core Dump</b></summary> <summary><b>Core Dump</b></summary>
```json [{coredump_filename}]({coredump_url})
{}
``` Reference ID: {coredump_id}
</details> </details>
"#, "#
screenshot_url,
serde_json::to_string_pretty(&self)?
); );
let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect(); let urlencoded: String = form_urlencoded::byte_serialize(body.as_bytes()).collect();
// Note that `github_issue_url` is not included in the coredump file.
// It has already been encoded and uploaded at this point.
// The `github_issue_url` is used in openWindow in wasm.ts.
self.github_issue_url = Some(format!( self.github_issue_url = Some(format!(
r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#, r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#,
"KittyCAD", "KittyCAD",

View File

@ -4,6 +4,7 @@ use anyhow::Result;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use crate::{coredump::CoreDump, wasm::JsFuture}; use crate::{coredump::CoreDump, wasm::JsFuture};
use serde_json::Value as JValue;
#[wasm_bindgen(module = "/../../lib/coredump.ts")] #[wasm_bindgen(module = "/../../lib/coredump.ts")]
extern "C" { extern "C" {
@ -31,6 +32,9 @@ extern "C" {
#[wasm_bindgen(method, js_name = getWebrtcStats, catch)] #[wasm_bindgen(method, js_name = getWebrtcStats, catch)]
fn get_webrtc_stats(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>; fn get_webrtc_stats(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
#[wasm_bindgen(method, js_name = getClientState, catch)]
fn get_client_state(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
#[wasm_bindgen(method, js_name = screenshot, catch)] #[wasm_bindgen(method, js_name = screenshot, catch)]
fn screenshot(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>; fn screenshot(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
} }
@ -123,6 +127,27 @@ impl CoreDump for CoreDumper {
Ok(stats) Ok(stats)
} }
async fn get_client_state(&self) -> Result<JValue> {
let promise = self
.manager
.get_client_state()
.map_err(|e| anyhow::anyhow!("Failed to get promise from get client state: {:?}", e))?;
let value = JsFuture::from(promise)
.await
.map_err(|e| anyhow::anyhow!("Failed to get response from client state: {:?}", e))?;
// Parse the value as a string.
let s = value
.as_string()
.ok_or_else(|| anyhow::anyhow!("Failed to get string from response from client stat: `{:?}`", value))?;
let client_state: JValue =
serde_json::from_str(&s).map_err(|e| anyhow::anyhow!("Failed to parse client state: {:?}", e))?;
Ok(client_state)
}
async fn screenshot(&self) -> Result<String> { async fn screenshot(&self) -> Result<String> {
let promise = self let promise = self
.manager .manager

View File

@ -0,0 +1,316 @@
{
"version": "0.20.1",
"git_rev": "3a05211d306ca045ace2e7bf10b7f8138e1daad5",
"timestamp": "2024-05-07T20:06:34.655Z",
"tauri": false,
"os": {
"platform": "Mac OS",
"version": "10.15.7",
"browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
},
"webrtc_stats": {
"packets_lost": 0,
"frames_received": 672,
"frame_width": 1440.0,
"frame_height": 712.0,
"frame_rate": 58.0,
"key_frames_decoded": 7,
"frames_dropped": 77,
"pause_count": 0,
"total_pauses_duration": 0.0,
"freeze_count": 12,
"total_freezes_duration": 3.057,
"pli_count": 6,
"jitter": 0.011
},
"pool": "",
"client_state": {
"engine_command_manager": {
"artifact_map": {
"ac7a8c52-7437-42e6-ae2a-25c54d0b4a16": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"83a2e866-d8e1-47d9-afae-d55a64cd5b40": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "plane_set_color",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"c92d7e84-a03e-456e-aad0-3e302ae0ffb6": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"a9abb9b8-54b1-4042-8ab3-e67c7ddb4cb3": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "plane_set_color",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"bafb884d-ee93-48f5-a667-91d1bdb8178a": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"6b7f539b-c4ca-4e62-a971-147e1abaca7b": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "plane_set_color",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"6ddaaaa3-b080-4cb3-b08e-8fe277312ccc": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"f1ef28b0-49b3-45f8-852d-772648149785": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"7c532be8-f07d-456b-8643-53808df86823": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "79cc6dfc-7205-41b2-922f-0d0279f5a30d",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"cf38f826-6d15-45f4-9c64-a6e9e4909e75": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"d69e6248-9e0f-4bc8-bc6e-d995931e8886": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "plane_set_color",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"cdb44075-ac6d-4626-b321-26d022f5f7f0": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"44f81391-0f2d-49f3-92b9-7dfc8446d571": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "plane_set_color",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"7b893ecf-8887-49c3-b978-392813d5f625": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"61247d28-04ba-4768-85b0-fbc582151dbd": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "plane_set_color",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"1318f5af-76a9-4161-9f6a-d410831fd098": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"908d264c-5989-4030-b2f4-5dfb52fda71a": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
},
"0de4c253-ee21-4385-93bd-b93264f7eb60": {
"type": "result",
"range": [0, 0],
"pathToNode": [],
"commandType": "make_plane",
"data": { "type": "empty" },
"raw": {
"success": true,
"request_id": "91f1be72-4267-40a4-9923-9c5b5a0ec29c",
"resp": {
"type": "modeling",
"data": { "modeling_response": { "type": "empty" } }
}
}
}
},
"engine_connection": {
"state": {
"type": "connection-established"
}
}
},
"kcl_manager": {},
"scene_infra": {},
"auth_machine": {},
"command_bar_machine": {},
"file_machine": {},
"home_machine": {},
"modeling_machine": {},
"settings_machine": {}
}
}