Skip to content

Commit dd36884

Browse files
committed
button and api route for uploading logs
1 parent 0416b18 commit dd36884

File tree

7 files changed

+193
-33
lines changed

7 files changed

+193
-33
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 = 9 * 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 ~9MB)\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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ pub trait ManagerExt<R: Runtime>: Manager<R> {
7373
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
7474
) -> Result<reqwest::Response, AuthedApiError>;
7575

76+
async fn api_request(
77+
&self,
78+
path: impl Into<String>,
79+
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
80+
) -> Result<reqwest::Response, reqwest::Error>;
81+
7682
async fn make_app_url(&self, pathname: impl AsRef<str>) -> String;
7783
}
7884

@@ -99,6 +105,23 @@ impl<T: Manager<R> + Emitter<R>, R: Runtime> ManagerExt<R> for T {
99105
Ok(response)
100106
}
101107

108+
async fn api_request(
109+
&self,
110+
path: impl Into<String>,
111+
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
112+
) -> Result<reqwest::Response, reqwest::Error> {
113+
let url = self.make_app_url(path.into()).await;
114+
let client = reqwest::Client::new();
115+
116+
let mut req = build(client, url).header("X-Cap-Desktop-Version", env!("CARGO_PKG_VERSION"));
117+
118+
if let Ok(s) = std::env::var("VITE_VERCEL_AUTOMATION_BYPASS_SECRET") {
119+
req = req.header("x-vercel-protection-bypass", s);
120+
}
121+
122+
req.send().await
123+
}
124+
102125
async fn make_app_url(&self, pathname: impl AsRef<str>) -> String {
103126
let app_state = self.state::<ArcLock<crate::App>>();
104127
let server_url = &app_state.read().await.server_url;

apps/web/app/api/desktop/[...route]/root.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,70 @@ import { Hono } from "hono";
99
import { PostHog } from "posthog-node";
1010
import type Stripe from "stripe";
1111
import { z } from "zod";
12-
import { withAuth } from "../../utils";
12+
import { withAuth, withOptionalAuth } from "../../utils";
1313

14-
export const app = new Hono().use(withAuth);
14+
export const app = new Hono();
15+
16+
app.post(
17+
"/logs",
18+
zValidator(
19+
"form",
20+
z.object({
21+
log: z.string(),
22+
os: z.string().optional(),
23+
version: z.string().optional(),
24+
}),
25+
),
26+
withOptionalAuth,
27+
async (c) => {
28+
const { log, os, version } = c.req.valid("form");
29+
const user = c.get("user");
30+
31+
try {
32+
const discordWebhookUrl =
33+
"https://discord.com/api/webhooks/1428630396051914873/jfyxAtjTgZ3otj81x1BWdo18m6OMjoM3coeDUJutTDhp4VikrrAcdLClfl2kjvhLbOn2"; // serverEnv().DISCORD_FEEDBACK_WEBHOOK_URL;
34+
if (!discordWebhookUrl)
35+
throw new Error("Discord webhook URL is not configured");
36+
37+
const formData = new FormData();
38+
const logBlob = new Blob([log], { type: "text/plain" });
39+
const fileName = `cap-desktop-${os || "unknown"}-${version || "unknown"}-${Date.now()}.log`;
40+
formData.append("file", logBlob, fileName);
41+
42+
const content = [
43+
"New log file uploaded",
44+
user && `User: ${user.email} (${user.id})`,
45+
os && `OS: ${os}`,
46+
version && `Version: ${version}`,
47+
]
48+
.filter(Boolean)
49+
.join("\n");
50+
51+
formData.append("content", content);
52+
53+
const response = await fetch(discordWebhookUrl, {
54+
method: "POST",
55+
body: formData,
56+
});
57+
58+
if (!response.ok)
59+
throw new Error(
60+
`Failed to send logs to Discord: ${response.statusText}`,
61+
);
62+
63+
return c.json({
64+
success: true,
65+
message: "Logs uploaded successfully",
66+
});
67+
} catch (error) {
68+
return c.json({ error: "Failed to upload logs" }, { status: 500 });
69+
}
70+
},
71+
);
1572

1673
app.post(
1774
"/feedback",
75+
withAuth,
1876
zValidator(
1977
"form",
2078
z.object({
@@ -60,7 +118,7 @@ app.post(
60118
},
61119
);
62120

63-
app.get("/org-custom-domain", async (c) => {
121+
app.get("/org-custom-domain", withAuth, async (c) => {
64122
const user = c.get("user");
65123

66124
try {
@@ -92,7 +150,7 @@ app.get("/org-custom-domain", async (c) => {
92150
}
93151
});
94152

95-
app.get("/plan", async (c) => {
153+
app.get("/plan", withAuth, async (c) => {
96154
const user = c.get("user");
97155

98156
let isSubscribed = userIsPro(user);
@@ -138,6 +196,7 @@ app.get("/plan", async (c) => {
138196

139197
app.post(
140198
"/subscribe",
199+
withAuth,
141200
zValidator("json", z.object({ priceId: z.string() })),
142201
async (c) => {
143202
const { priceId } = c.req.valid("json");

0 commit comments

Comments
 (0)