Skip to content

Commit 82c3a34

Browse files
authored
Button and api route for uploading logs (#1221)
* button and api route for uploading logs * use env for webhook url * limit to 1mb * cleanup
1 parent 302ffe1 commit 82c3a34

File tree

8 files changed

+208
-50
lines changed

8 files changed

+208
-50
lines changed

apps/desktop/src-tauri/src/lib.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod flags;
1313
mod frame_ws;
1414
mod general_settings;
1515
mod hotkeys;
16+
mod logging;
1617
mod notifications;
1718
mod permissions;
1819
mod platform;
@@ -110,24 +111,17 @@ pub enum RecordingState {
110111
Active(InProgressRecording),
111112
}
112113

113-
#[derive(specta::Type, Serialize)]
114-
#[serde(rename_all = "camelCase")]
115114
pub struct App {
116115
#[deprecated = "can be removed when native camera preview is ready"]
117116
camera_ws_port: u16,
118-
#[serde(skip)]
119117
camera_preview: CameraPreviewManager,
120-
#[serde(skip)]
121118
handle: AppHandle,
122-
#[serde(skip)]
123119
recording_state: RecordingState,
124-
#[serde(skip)]
125120
recording_logging_handle: LoggingHandle,
126-
#[serde(skip)]
127121
mic_feed: ActorRef<feeds::microphone::MicrophoneFeed>,
128-
#[serde(skip)]
129122
camera_feed: ActorRef<feeds::camera::CameraFeed>,
130123
server_url: String,
124+
logs_dir: PathBuf,
131125
}
132126

133127
#[derive(specta::Type, Serialize, Deserialize, Clone, Debug)]
@@ -1938,7 +1932,7 @@ pub type DynLoggingLayer = Box<dyn tracing_subscriber::Layer<FilteredRegistry> +
19381932
type LoggingHandle = tracing_subscriber::reload::Handle<Option<DynLoggingLayer>, FilteredRegistry>;
19391933

19401934
#[cfg_attr(mobile, tauri::mobile_entry_point)]
1941-
pub async fn run(recording_logging_handle: LoggingHandle) {
1935+
pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
19421936
ffmpeg::init()
19431937
.map_err(|e| {
19441938
error!("Failed to initialize ffmpeg: {e}");
@@ -2256,6 +2250,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
22562250
.map(|v| v.server_url.clone())
22572251
})
22582252
.unwrap_or_else(|| "https://cap.so".to_string()),
2253+
logs_dir: logs_dir.clone(),
22592254
})));
22602255

22612256
app.manage(Arc::new(RwLock::new(
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use crate::{ArcLock, web_api::ManagerExt};
2+
use std::{fs, path::PathBuf};
3+
use tauri::{AppHandle, Manager};
4+
5+
async fn get_latest_log_file(app: &AppHandle) -> Option<PathBuf> {
6+
let logs_dir = app
7+
.state::<ArcLock<crate::App>>()
8+
.read()
9+
.await
10+
.logs_dir
11+
.clone();
12+
13+
let entries = fs::read_dir(&logs_dir).ok()?;
14+
let mut log_files: Vec<_> = entries
15+
.filter_map(|entry| {
16+
let entry = entry.ok()?;
17+
let path = entry.path();
18+
if path.is_file() && path.file_name()?.to_str()?.contains("cap-desktop.log") {
19+
let metadata = fs::metadata(&path).ok()?;
20+
let modified = metadata.modified().ok()?;
21+
Some((path, modified))
22+
} else {
23+
None
24+
}
25+
})
26+
.collect();
27+
28+
log_files.sort_by(|a, b| b.1.cmp(&a.1));
29+
log_files.first().map(|(path, _)| path.clone())
30+
}
31+
32+
pub async fn upload_log_file(app: &AppHandle) -> Result<(), String> {
33+
let log_file = get_latest_log_file(app).await.ok_or("No log file found")?;
34+
35+
let metadata =
36+
fs::metadata(&log_file).map_err(|e| format!("Failed to read log file metadata: {}", e))?;
37+
let file_size = metadata.len();
38+
39+
const MAX_SIZE: u64 = 1 * 1024 * 1024;
40+
41+
let log_content = if file_size > MAX_SIZE {
42+
let content =
43+
fs::read_to_string(&log_file).map_err(|e| format!("Failed to read log file: {}", e))?;
44+
45+
let header = format!(
46+
"⚠️ Log file truncated (original size: {} bytes, showing last ~1MB)\n\n",
47+
file_size
48+
);
49+
let max_content_size = (MAX_SIZE as usize) - header.len();
50+
51+
if content.len() > max_content_size {
52+
let start_pos = content.len() - max_content_size;
53+
let truncated = &content[start_pos..];
54+
if let Some(newline_pos) = truncated.find('\n') {
55+
format!("{}{}", header, &truncated[newline_pos + 1..])
56+
} else {
57+
format!("{}{}", header, truncated)
58+
}
59+
} else {
60+
content
61+
}
62+
} else {
63+
fs::read_to_string(&log_file).map_err(|e| format!("Failed to read log file: {}", e))?
64+
};
65+
66+
let form = reqwest::multipart::Form::new()
67+
.text("log", log_content)
68+
.text("os", std::env::consts::OS)
69+
.text("version", env!("CARGO_PKG_VERSION"));
70+
71+
let response = app
72+
.api_request("/api/desktop/logs", |client, url| {
73+
client.post(url).multipart(form)
74+
})
75+
.await
76+
.map_err(|e| format!("Failed to upload logs: {}", e))?;
77+
78+
if !response.status().is_success() {
79+
return Err(format!("Upload failed with status: {}", response.status()));
80+
}
81+
82+
Ok(())
83+
}

apps/desktop/src-tauri/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,5 @@ fn main() {
134134
.enable_all()
135135
.build()
136136
.expect("Failed to build multi threaded tokio runtime")
137-
.block_on(cap_desktop_lib::run(handle));
137+
.block_on(cap_desktop_lib::run(handle, logs_dir));
138138
}

apps/desktop/src-tauri/src/thumbnails/mod.rs

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ pub async fn collect_displays_with_thumbnails() -> Result<Vec<CaptureDisplayWith
102102
pub async fn collect_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithThumbnail>, String> {
103103
let windows = list_windows();
104104

105-
debug!(window_count = windows.len(), "Collecting window thumbnails");
106105
let mut results = Vec::new();
107106
for (capture_window, window) in windows {
108107
let thumbnail = capture_window_thumbnail(&window).await;
@@ -117,22 +116,6 @@ pub async fn collect_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithTh
117116
}
118117
});
119118

120-
if thumbnail.is_none() {
121-
warn!(
122-
window_id = ?capture_window.id,
123-
window_name = %capture_window.name,
124-
owner_name = %capture_window.owner_name,
125-
"Window thumbnail capture returned None",
126-
);
127-
} else {
128-
debug!(
129-
window_id = ?capture_window.id,
130-
window_name = %capture_window.name,
131-
owner_name = %capture_window.owner_name,
132-
"Captured window thumbnail",
133-
);
134-
}
135-
136119
results.push(CaptureWindowWithThumbnail {
137120
id: capture_window.id,
138121
name: capture_window.name,
@@ -145,7 +128,5 @@ pub async fn collect_windows_with_thumbnails() -> Result<Vec<CaptureWindowWithTh
145128
});
146129
}
147130

148-
info!(windows = results.len(), "Collected window thumbnail data");
149-
150131
Ok(results)
151132
}

apps/desktop/src-tauri/src/tray.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use tauri::{
1515
menu::{Menu, MenuItem},
1616
tray::TrayIconBuilder,
1717
};
18+
use tauri_plugin_dialog::DialogExt;
1819
use tauri_specta::Event;
1920

2021
pub enum TrayItem {
@@ -23,6 +24,7 @@ pub enum TrayItem {
2324
PreviousRecordings,
2425
PreviousScreenshots,
2526
OpenSettings,
27+
UploadLogs,
2628
Quit,
2729
}
2830

@@ -34,6 +36,7 @@ impl From<TrayItem> for MenuId {
3436
TrayItem::PreviousRecordings => "previous_recordings",
3537
TrayItem::PreviousScreenshots => "previous_screenshots",
3638
TrayItem::OpenSettings => "open_settings",
39+
TrayItem::UploadLogs => "upload_logs",
3740
TrayItem::Quit => "quit",
3841
}
3942
.into()
@@ -50,6 +53,7 @@ impl TryFrom<MenuId> for TrayItem {
5053
"previous_recordings" => Ok(TrayItem::PreviousRecordings),
5154
"previous_screenshots" => Ok(TrayItem::PreviousScreenshots),
5255
"open_settings" => Ok(TrayItem::OpenSettings),
56+
"upload_logs" => Ok(TrayItem::UploadLogs),
5357
"quit" => Ok(TrayItem::Quit),
5458
value => Err(format!("Invalid tray item id {value}")),
5559
}
@@ -78,6 +82,7 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
7882
)?,
7983
&MenuItem::with_id(app, TrayItem::OpenSettings, "Settings", true, None::<&str>)?,
8084
&PredefinedMenuItem::separator(app)?,
85+
&MenuItem::with_id(app, TrayItem::UploadLogs, "Upload Logs", true, None::<&str>)?,
8186
&MenuItem::with_id(
8287
app,
8388
"version",
@@ -130,6 +135,20 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
130135
async move { ShowCapWindow::Settings { page: None }.show(&app).await },
131136
);
132137
}
138+
Ok(TrayItem::UploadLogs) => {
139+
let app = app.clone();
140+
tokio::spawn(async move {
141+
match crate::logging::upload_log_file(&app).await {
142+
Ok(_) => {
143+
tracing::info!("Successfully uploaded logs");
144+
}
145+
Err(e) => {
146+
tracing::error!("Failed to upload logs: {e:#}");
147+
app.dialog().message("Failed to upload logs").show(|_| {});
148+
}
149+
}
150+
});
151+
}
133152
Ok(TrayItem::Quit) => {
134153
app.exit(0);
135154
}

apps/desktop/src-tauri/src/web_api.rs

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,31 +39,35 @@ impl From<String> for AuthedApiError {
3939
}
4040
}
4141

42+
fn apply_env_headers(req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
43+
let mut req = req.header("X-Cap-Desktop-Version", env!("CARGO_PKG_VERSION"));
44+
45+
if let Ok(s) = std::env::var("VITE_VERCEL_AUTOMATION_BYPASS_SECRET") {
46+
req = req.header("x-vercel-protection-bypass", s);
47+
}
48+
49+
req
50+
}
51+
4252
async fn do_authed_request(
4353
auth: &AuthStore,
4454
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
4555
url: String,
4656
) -> Result<reqwest::Response, reqwest::Error> {
4757
let client = reqwest::Client::new();
4858

49-
let mut req = build(client, url)
50-
.header(
51-
"Authorization",
52-
format!(
53-
"Bearer {}",
54-
match &auth.secret {
55-
AuthSecret::ApiKey { api_key } => api_key,
56-
AuthSecret::Session { token, .. } => token,
57-
}
58-
),
59-
)
60-
.header("X-Cap-Desktop-Version", env!("CARGO_PKG_VERSION"));
61-
62-
if let Ok(s) = std::env::var("VITE_VERCEL_AUTOMATION_BYPASS_SECRET") {
63-
req = req.header("x-vercel-protection-bypass", s);
64-
}
59+
let req = build(client, url).header(
60+
"Authorization",
61+
format!(
62+
"Bearer {}",
63+
match &auth.secret {
64+
AuthSecret::ApiKey { api_key } => api_key,
65+
AuthSecret::Session { token, .. } => token,
66+
}
67+
),
68+
);
6569

66-
req.send().await
70+
apply_env_headers(req).send().await
6771
}
6872

6973
pub trait ManagerExt<R: Runtime>: Manager<R> {
@@ -73,6 +77,12 @@ pub trait ManagerExt<R: Runtime>: Manager<R> {
7377
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
7478
) -> Result<reqwest::Response, AuthedApiError>;
7579

80+
async fn api_request(
81+
&self,
82+
path: impl Into<String>,
83+
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
84+
) -> Result<reqwest::Response, reqwest::Error>;
85+
7686
async fn make_app_url(&self, pathname: impl AsRef<str>) -> String;
7787
}
7888

@@ -99,6 +109,17 @@ impl<T: Manager<R> + Emitter<R>, R: Runtime> ManagerExt<R> for T {
99109
Ok(response)
100110
}
101111

112+
async fn api_request(
113+
&self,
114+
path: impl Into<String>,
115+
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
116+
) -> Result<reqwest::Response, reqwest::Error> {
117+
let url = self.make_app_url(path.into()).await;
118+
let client = reqwest::Client::new();
119+
120+
apply_env_headers(build(client, url)).send().await
121+
}
122+
102123
async fn make_app_url(&self, pathname: impl AsRef<str>) -> String {
103124
let app_state = self.state::<ArcLock<crate::App>>();
104125
let server_url = &app_state.read().await.server_url;

0 commit comments

Comments
 (0)