Implement Core Dump for modeling app state (#2653)
This commit is contained in:
@ -5489,3 +5489,72 @@ test('Paste should not work unless an input is focused', async ({
|
||||
)
|
||||
).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
1
src-tauri/Cargo.lock
generated
@ -6181,6 +6181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"ts-rs-macros",
|
||||
"url",
|
||||
|
@ -121,7 +121,24 @@ export const ModelingMachineProvider = ({
|
||||
htmlRef,
|
||||
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
|
||||
// const retrievedSettings = useRef(
|
||||
|
@ -25,7 +25,7 @@ import type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
import type { Token } from '../wasm-lib/kcl/bindings/Token'
|
||||
import { Coords2d } from './std/sketch'
|
||||
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 openWindow from 'lib/openWindow'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
@ -335,14 +335,27 @@ export function programMemoryInit(): ProgramMemory {
|
||||
export async function coreDump(
|
||||
coreDumpManager: CoreDumpManager,
|
||||
openGithubIssue: boolean = false
|
||||
): Promise<AppInfo> {
|
||||
): Promise<CoreDumpInfo> {
|
||||
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) {
|
||||
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
|
||||
} catch (e: any) {
|
||||
console.error('CoreDump: error', e)
|
||||
throw new Error(`Error getting core dump: ${e}`)
|
||||
}
|
||||
}
|
||||
|
@ -13,8 +13,15 @@ import screenshot from 'lib/screenshot'
|
||||
import React from 'react'
|
||||
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 {
|
||||
engineCommandManager: EngineCommandManager
|
||||
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.
|
||||
screenshot(): Promise<string> {
|
||||
return screenshot(this.htmlRef)
|
||||
|
1
src/wasm-lib/Cargo.lock
generated
1
src/wasm-lib/Cargo.lock
generated
@ -3272,6 +3272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e2dcf58e612adda9a83800731e8e4aba04d8a302b9029617b0b6e4b021d5357"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"ts-rs-macros",
|
||||
"url",
|
||||
|
@ -37,7 +37,7 @@ serde_json = "1.0.116"
|
||||
sha2 = "0.10.8"
|
||||
thiserror = "1.0.61"
|
||||
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"] }
|
||||
uuid = { version = "1.8.0", features = ["v4", "js", "serde"] }
|
||||
validator = { version = "0.18.1", features = ["derive"] }
|
||||
|
@ -3,6 +3,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::coredump::CoreDump;
|
||||
use serde_json::Value as JValue;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CoreDumper {}
|
||||
@ -55,6 +56,10 @@ impl CoreDump for CoreDumper {
|
||||
Ok(crate::coredump::WebrtcStats::default())
|
||||
}
|
||||
|
||||
async fn get_client_state(&self) -> Result<JValue> {
|
||||
Ok(JValue::default())
|
||||
}
|
||||
|
||||
async fn screenshot(&self) -> Result<String> {
|
||||
// Take a screenshot of the engine.
|
||||
todo!()
|
||||
|
@ -7,8 +7,13 @@ pub mod wasm;
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use kittycad::Client;
|
||||
use schemars::JsonSchema;
|
||||
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)]
|
||||
pub trait CoreDump: Clone {
|
||||
@ -27,25 +32,24 @@ pub trait CoreDump: Clone {
|
||||
|
||||
async fn get_webrtc_stats(&self) -> Result<WebrtcStats>;
|
||||
|
||||
async fn get_client_state(&self) -> Result<JValue>;
|
||||
|
||||
/// Return a screenshot of the app.
|
||||
async fn screenshot(&self) -> Result<String>;
|
||||
|
||||
/// 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 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.
|
||||
let data = base64::engine::general_purpose::STANDARD.decode(cleaned)?;
|
||||
// Upload the screenshot.
|
||||
let links = zoo
|
||||
let links = zoo_client
|
||||
.meta()
|
||||
.create_debug_uploads(vec![kittycad::types::multipart::Attachment {
|
||||
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()),
|
||||
data,
|
||||
}])
|
||||
@ -60,12 +64,19 @@ pub trait CoreDump: Clone {
|
||||
}
|
||||
|
||||
/// 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 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()?,
|
||||
git_rev: git_rev::try_revision_string!().map_or_else(|| "unknown".to_string(), |s| s.to_string()),
|
||||
timestamp: chrono::Utc::now(),
|
||||
@ -74,18 +85,44 @@ pub trait CoreDump: Clone {
|
||||
webrtc_stats,
|
||||
github_issue_url: None,
|
||||
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 Core Dump Info structure.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[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.
|
||||
pub version: String,
|
||||
/// The git revision of the app.
|
||||
@ -95,45 +132,44 @@ pub struct AppInfo {
|
||||
pub timestamp: chrono::DateTime<chrono::Utc>,
|
||||
/// If the app is running in tauri or the browser.
|
||||
pub tauri: bool,
|
||||
|
||||
/// The os info.
|
||||
pub os: OsInfo,
|
||||
|
||||
/// The webrtc stats.
|
||||
pub webrtc_stats: WebrtcStats,
|
||||
|
||||
/// 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")]
|
||||
pub github_issue_url: Option<String>,
|
||||
|
||||
/// Engine pool the client is connected to.
|
||||
pub pool: String,
|
||||
/// The client state (singletons and xstate).
|
||||
pub client_state: JValue,
|
||||
}
|
||||
|
||||
impl AppInfo {
|
||||
impl CoreDumpInfo {
|
||||
/// 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 labels = ["coredump", "bug", tauri_or_browser_label];
|
||||
let body = format!(
|
||||
r#"[Insert a description of the issue here]
|
||||
r#"[Add a title above and insert a description of the issue here]
|
||||
|
||||

|
||||

|
||||
|
||||
<details>
|
||||
<summary><b>Core Dump</b></summary>
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
[{coredump_filename}]({coredump_url})
|
||||
|
||||
Reference ID: {coredump_id}
|
||||
</details>
|
||||
"#,
|
||||
screenshot_url,
|
||||
serde_json::to_string_pretty(&self)?
|
||||
"#
|
||||
);
|
||||
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!(
|
||||
r#"https://github.com/{}/{}/issues/new?body={}&labels={}"#,
|
||||
"KittyCAD",
|
||||
|
@ -4,6 +4,7 @@ use anyhow::Result;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::{coredump::CoreDump, wasm::JsFuture};
|
||||
use serde_json::Value as JValue;
|
||||
|
||||
#[wasm_bindgen(module = "/../../lib/coredump.ts")]
|
||||
extern "C" {
|
||||
@ -31,6 +32,9 @@ extern "C" {
|
||||
#[wasm_bindgen(method, js_name = getWebrtcStats, catch)]
|
||||
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)]
|
||||
fn screenshot(this: &CoreDumpManager) -> Result<js_sys::Promise, js_sys::Error>;
|
||||
}
|
||||
@ -123,6 +127,27 @@ impl CoreDump for CoreDumper {
|
||||
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> {
|
||||
let promise = self
|
||||
.manager
|
||||
|
316
src/wasm-lib/tests/cordump/inputs/coredump.fixture.json
Normal file
316
src/wasm-lib/tests/cordump/inputs/coredump.fixture.json
Normal 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": {}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user