From 6149edb86f75c7c110fc6de2050f85aaa1013398 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 13 Oct 2025 23:12:34 +0800 Subject: [PATCH 01/15] wip --- apps/desktop/src-tauri/src/lib.rs | 1 + apps/desktop/src-tauri/src/upload.rs | 53 ++++++++++++++++++- .../(window-chrome)/settings/recordings.tsx | 10 +++- apps/desktop/src/utils/tauri.ts | 4 ++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 58ce372540..28bcf437f7 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2006,6 +2006,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) { target_select_overlay::TargetUnderCursor, hotkeys::OnEscapePress, upload::UploadProgressEvent, + upload::UploadDebugEvent, ]) .error_handling(tauri_specta::ErrorHandlingMode::Throw) .typ::() diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index d0bb1a535d..0cbbb8a8f3 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -34,7 +34,7 @@ use tokio::{ fs::File, io::{AsyncReadExt, AsyncSeekExt, BufReader}, task::{self, JoinHandle}, - time, + time::{self, Instant}, }; use tokio_util::io::ReaderStream; use tracing::{debug, error, info}; @@ -593,6 +593,25 @@ fn retryable_client(host: String) -> reqwest::ClientBuilder { ) } +#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)] +pub struct UploadDebugEvent { + pub video_id: String, + pub upload_id: String, + pub state: UploadDebugEventState, +} + +#[derive(Serialize, Type, Debug, Clone)] +pub enum UploadDebugEventState { + Pending, + Uploading { + part_number: u32, + chunk_size: String, + total_size: String, + duration: String, + }, + Done, +} + /// Takes an incoming stream of bytes and individually uploads them to S3. /// /// Note: It's on the caller to ensure the chunks are sized correctly within S3 limits. @@ -604,6 +623,16 @@ fn multipart_uploader( ) -> impl Stream> { debug!("Initializing multipart uploader for video {video_id:?}"); + let start = Instant::now(); + + UploadDebugEvent { + video_id: video_id.clone(), + upload_id: upload_id.clone(), + state: UploadDebugEventState::Pending, + } + .emit(&app) + .ok(); + try_stream! { let mut stream = pin!(stream); let mut prev_part_number = None; @@ -638,6 +667,19 @@ fn multipart_uploader( false => Ok(()), }?; + UploadDebugEvent { + video_id: video_id.clone(), + upload_id: upload_id.clone(), + state: UploadDebugEventState::Uploading { + part_number, + chunk_size: size.to_string(), + total_size: total_size.to_string(), + duration: format!("{:?}", start.elapsed()), + }, + } + .emit(&app) + .ok(); + yield UploadedPart { etag: etag.ok_or_else(|| format!("uploader/part/{part_number}/error: ETag header not found"))?, part_number, @@ -645,6 +687,15 @@ fn multipart_uploader( total_size }; } + + debug!("Completed multipart upload for {video_id:?} in {:?}", start.elapsed()); + UploadDebugEvent { + video_id: video_id.clone(), + upload_id: upload_id.clone(), + state: UploadDebugEventState::Done, + } + .emit(&app) + .ok(); } } diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index 16f03892a9..f111a242d8 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -13,7 +13,6 @@ import { revealItemInDir } from "@tauri-apps/plugin-opener"; import * as shell from "@tauri-apps/plugin-shell"; import { cx } from "cva"; import { - createEffect, createMemo, createSignal, For, @@ -379,6 +378,7 @@ function RecordingItem(props: { + ); } @@ -410,3 +410,11 @@ function TooltipIconButton( ); } + +function StatsForNerds() { + createTauriEventListener(events.uploadDebugEvent, (e) => { + console.log(e); + }); + + return

Hello World

; +} diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index cfece4f3a0..b0db1344c3 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -302,6 +302,7 @@ requestOpenSettings: RequestOpenSettings, requestScreenCapturePrewarm: RequestScreenCapturePrewarm, requestStartRecording: RequestStartRecording, targetUnderCursor: TargetUnderCursor, +uploadDebugEvent: UploadDebugEvent, uploadProgressEvent: UploadProgressEvent }>({ audioInputLevelChange: "audio-input-level-change", @@ -325,6 +326,7 @@ requestOpenSettings: "request-open-settings", requestScreenCapturePrewarm: "request-screen-capture-prewarm", requestStartRecording: "request-start-recording", targetUnderCursor: "target-under-cursor", +uploadDebugEvent: "upload-debug-event", uploadProgressEvent: "upload-progress-event" }) @@ -466,6 +468,8 @@ export type StudioRecordingStatus = { status: "InProgress" } | { status: "Failed export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null } export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; sceneSegments?: SceneSegment[] } export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number } +export type UploadDebugEvent = { video_id: string; upload_id: string; state: UploadDebugEventState } +export type UploadDebugEventState = "Pending" | { Uploading: { part_number: number; chunk_size: string; total_size: string; duration: string } } | "Done" export type UploadMeta = { state: "MultipartUpload"; video_id: string; file_path: string; pre_created_video: VideoUploadInfo; recording_dir: string } | { state: "SinglePartUpload"; video_id: string; recording_dir: string; file_path: string; screenshot_path: string } | { state: "Failed"; error: string } | { state: "Complete" } export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload" export type UploadProgress = { progress: number } From d8b4f6ed3aa5f41460e77d8f9c6c96c91ebd5c2e Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 13 Oct 2025 23:19:11 +0800 Subject: [PATCH 02/15] human work --- apps/desktop/src-tauri/src/upload.rs | 68 +++++++++++++++++-- .../(window-chrome)/settings/recordings.tsx | 1 - apps/desktop/src/utils/tauri.ts | 4 +- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 0cbbb8a8f3..34a38fc71e 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -25,7 +25,7 @@ use std::{ path::{Path, PathBuf}, pin::pin, str::FromStr, - time::Duration, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use tauri::{AppHandle, ipc::Channel}; use tauri_plugin_clipboard_manager::ClipboardExt; @@ -597,17 +597,25 @@ fn retryable_client(host: String) -> reqwest::ClientBuilder { pub struct UploadDebugEvent { pub video_id: String, pub upload_id: String, + pub epoch: String, pub state: UploadDebugEventState, } #[derive(Serialize, Type, Debug, Clone)] pub enum UploadDebugEventState { Pending, + Presigning { + part_number: u32, + chunk_size: String, + total_size: String, + }, Uploading { part_number: u32, chunk_size: String, total_size: String, - duration: String, + }, + PendingNextChunk { + prev_part_number: u32, }, Done, } @@ -628,6 +636,11 @@ fn multipart_uploader( UploadDebugEvent { video_id: video_id.clone(), upload_id: upload_id.clone(), + epoch: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|v| v.as_millis()) + .unwrap_or_default() + .to_string(), state: UploadDebugEventState::Pending, } .emit(&app) @@ -643,10 +656,44 @@ fn multipart_uploader( let md5_sum = base64::encode(md5::compute(&chunk).0); let size = chunk.len(); + UploadDebugEvent { + video_id: video_id.clone(), + upload_id: upload_id.clone(), + epoch: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|v| v.as_millis()) + .unwrap_or_default() + .to_string(), + state: UploadDebugEventState::Presigning { + part_number, + chunk_size: size.to_string(), + total_size: total_size.to_string(), + }, + } + .emit(&app) + .ok(); + let presigned_url = api::upload_multipart_presign_part(&app, &video_id, &upload_id, part_number, &md5_sum) .await?; + UploadDebugEvent { + video_id: video_id.clone(), + upload_id: upload_id.clone(), + epoch: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|v| v.as_millis()) + .unwrap_or_default() + .to_string(), + state: UploadDebugEventState::Uploading { + part_number, + chunk_size: size.to_string(), + total_size: total_size.to_string(), + }, + } + .emit(&app) + .ok(); + let url = Uri::from_str(&presigned_url).map_err(|err| format!("uploader/part/{part_number}/invalid_url: {err:?}"))?; let resp = retryable_client(url.host().unwrap_or("").to_string()) .build() @@ -670,11 +717,13 @@ fn multipart_uploader( UploadDebugEvent { video_id: video_id.clone(), upload_id: upload_id.clone(), - state: UploadDebugEventState::Uploading { - part_number, - chunk_size: size.to_string(), - total_size: total_size.to_string(), - duration: format!("{:?}", start.elapsed()), + epoch: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|v| v.as_millis()) + .unwrap_or_default() + .to_string(), + state: UploadDebugEventState::PendingNextChunk { + prev_part_number: part_number }, } .emit(&app) @@ -692,6 +741,11 @@ fn multipart_uploader( UploadDebugEvent { video_id: video_id.clone(), upload_id: upload_id.clone(), + epoch: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|v| v.as_millis()) + .unwrap_or_default() + .to_string(), state: UploadDebugEventState::Done, } .emit(&app) diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index f111a242d8..75b14ddef2 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -378,7 +378,6 @@ function RecordingItem(props: { - ); } diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index b0db1344c3..f8648a08d6 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -468,8 +468,8 @@ export type StudioRecordingStatus = { status: "InProgress" } | { status: "Failed export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null } export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; sceneSegments?: SceneSegment[] } export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number } -export type UploadDebugEvent = { video_id: string; upload_id: string; state: UploadDebugEventState } -export type UploadDebugEventState = "Pending" | { Uploading: { part_number: number; chunk_size: string; total_size: string; duration: string } } | "Done" +export type UploadDebugEvent = { video_id: string; upload_id: string; epoch: string; state: UploadDebugEventState } +export type UploadDebugEventState = "Pending" | { Presigning: { part_number: number; chunk_size: string; total_size: string } } | { Uploading: { part_number: number; chunk_size: string; total_size: string } } | { PendingNextChunk: { prev_part_number: number } } | "Done" export type UploadMeta = { state: "MultipartUpload"; video_id: string; file_path: string; pre_created_video: VideoUploadInfo; recording_dir: string } | { state: "SinglePartUpload"; video_id: string; recording_dir: string; file_path: string; screenshot_path: string } | { state: "Failed"; error: string } | { state: "Complete" } export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload" export type UploadProgress = { progress: number } From b8d46f5da4c07b99eb58554fe86d28687c10c371 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Mon, 13 Oct 2025 23:47:23 +0800 Subject: [PATCH 03/15] Claude cooked --- .../(window-chrome)/settings/recordings.tsx | 374 +++++++++++++++--- 1 file changed, 323 insertions(+), 51 deletions(-) diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index 75b14ddef2..b0a559eded 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -1,4 +1,5 @@ import { ProgressCircle } from "@cap/ui-solid"; +import { Dialog } from "@kobalte/core/dialog"; import Tooltip from "@corvu/tooltip"; import { createMutation, @@ -29,6 +30,7 @@ import { events, type RecordingMetaWithMetadata, type UploadProgress, + type UploadDebugEvent, } from "~/utils/tauri"; type Recording = { @@ -86,6 +88,7 @@ export default function Recordings() { const [uploadProgress, setUploadProgress] = createStore< Record >({}); + const [isStatsModalOpen, setIsStatsModalOpen] = createSignal(false); const recordings = createQuery(() => recordingsQuery); createTauriEventListener(events.uploadProgressEvent, (e) => { @@ -136,12 +139,24 @@ export default function Recordings() { return (
-
-

Previous Recordings

-

- Manage your recordings and perform actions. -

+
+
+

Previous Recordings

+

+ Manage your recordings and perform actions. +

+
+
+ 0} fallback={ @@ -315,49 +330,10 @@ function RecordingItem(props: { - {(_) => { - const reupload = createMutation(() => ({ - mutationFn: () => - commands.uploadExportedVideo( - props.recording.path, - "Reupload", - new Channel((progress) => {}), - ), - })); - - return ( - <> - reupload.mutate()} - > - - - } - > - - - - - {(sharing) => ( - shell.open(sharing().link)} - > - - - )} - - - ); - }} + void; +}) { + const [debugEvents, setDebugEvents] = createStore< + Record + >({}); + + const [hoveredEvent, setHoveredEvent] = + createSignal(null); + createTauriEventListener(events.uploadDebugEvent, (e) => { - console.log(e); + const eventWithTimestamp: UploadDebugEventWithTimestamp = { + ...e, + timestamp: Date.now(), + }; + + const key = `${e.video_id}:${e.upload_id}`; + setDebugEvents(key, (prev = []) => [...prev, eventWithTimestamp]); + }); + + const allEvents = createMemo(() => { + const events = Object.values(debugEvents).flat(); + return events.sort((a, b) => a.timestamp - b.timestamp); }); - return

Hello World

; + const timeRange = createMemo(() => { + const events = allEvents(); + if (events.length === 0) return { start: 0, end: 0 }; + const start = Math.min(...events.map((e) => e.timestamp)); + const end = Math.max(...events.map((e) => e.timestamp)); + return { start, end: Math.max(end, start + 60000) }; // At least 1 minute range + }); + + const getEventColor = (state: UploadDebugEvent["state"]) => { + switch (state) { + case "Pending": + return "bg-yellow-500"; + case "Done": + return "bg-green-500"; + default: + if (typeof state === "object") { + if ("Presigning" in state) return "bg-blue-500"; + if ("Uploading" in state) return "bg-purple-500"; + if ("PendingNextChunk" in state) return "bg-orange-500"; + } + return "bg-gray-500"; + } + }; + + const formatTimestamp = (timestamp: number) => { + return new Date(timestamp).toLocaleTimeString([], { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + }; + + const getStateLabel = (state: UploadDebugEvent["state"]): string => { + if (typeof state === "string") return state; + if (typeof state === "object") { + if ("Presigning" in state) + return `Presigning Part ${state.Presigning.part_number}`; + if ("Uploading" in state) + return `Uploading Part ${state.Uploading.part_number}`; + if ("PendingNextChunk" in state) + return `Pending After Part ${state.PendingNextChunk.prev_part_number}`; + } + return "Unknown"; + }; + + const getEventDetails = (event: UploadDebugEventWithTimestamp): string => { + const state = event.state; + let details = `Video ID: ${event.video_id}\nUpload ID: ${event.upload_id}\nTime: ${formatTimestamp(event.timestamp)}\nState: ${getStateLabel(state)}`; + + if (typeof state === "object") { + if ("Presigning" in state) { + details += `\nPart: ${state.Presigning.part_number}\nChunk Size: ${state.Presigning.chunk_size}\nTotal Size: ${state.Presigning.total_size}`; + } else if ("Uploading" in state) { + details += `\nPart: ${state.Uploading.part_number}\nChunk Size: ${state.Uploading.chunk_size}\nTotal Size: ${state.Uploading.total_size}`; + } + } + + return details; + }; + + return ( + + + +
+ +
+ + Upload Debug Timeline ({allEvents().length} events) + + + ✕ + +
+ +
+ 0} + fallback={ +
+
📊
+
+

No debug events recorded yet

+

+ Start uploading a recording to see the timeline +

+
+
+ } + > +
+ {/* Timeline container */} +
+
+ {/* Time axis */} +
+
+ {formatTimestamp(timeRange().start)} + Timeline + {formatTimestamp(timeRange().end)} +
+
+ + {/* Event tracks */} +
+ + {([key, events]) => ( +
+
+ {key} +
+
+ {/* Timeline background */} +
+ {/* Grid lines */} + i, + )} + > + {(i) => ( +
+ )} + +
+ + {/* Events */} + + {(event) => { + const position = + ((event.timestamp - timeRange().start) / + (timeRange().end - timeRange().start)) * + 100; + return ( +
+ setHoveredEvent(event) + } + onMouseLeave={() => + setHoveredEvent(null) + } + /> + ); + }} + +
+
+ )} + +
+
+
+ + {/* Legend */} +
+
+ Legend: +
+
+
+
+ Pending +
+
+
+ Presigning +
+
+
+ Uploading +
+
+
+ Pending Next Chunk +
+
+
+ Done +
+
+
+
+ +
+ + {/* Hover tooltip */} + + {(event) => ( +
+ {getEventDetails(event())} +
+ )} +
+ +
+ +
+ ); +} + +function InstantModeActions(props: { + recording: Recording; + uploadProgress: number | undefined; +}) { + const reupload = createMutation(() => ({ + mutationFn: () => + commands.uploadExportedVideo( + props.recording.path, + "Reupload", + new Channel((progress) => {}), + ), + })); + + return ( + <> + reupload.mutate()} + > + +
+ } + > + + + + + {(sharing) => ( + shell.open(sharing().link)} + > + + + )} + + + ); } From 4ff3deded25ccd41876ea61b5d6f54a80939446d Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 00:04:42 +0800 Subject: [PATCH 04/15] dump UI --- .../settings/UploadStatsForNerds.tsx | 319 ++++++++++++++++++ .../(window-chrome)/settings/experimental.tsx | 20 +- .../(window-chrome)/settings/recordings.tsx | 315 ----------------- 3 files changed, 337 insertions(+), 317 deletions(-) create mode 100644 apps/desktop/src/routes/(window-chrome)/settings/UploadStatsForNerds.tsx diff --git a/apps/desktop/src/routes/(window-chrome)/settings/UploadStatsForNerds.tsx b/apps/desktop/src/routes/(window-chrome)/settings/UploadStatsForNerds.tsx new file mode 100644 index 0000000000..490fd0ed1c --- /dev/null +++ b/apps/desktop/src/routes/(window-chrome)/settings/UploadStatsForNerds.tsx @@ -0,0 +1,319 @@ +import { ProgressCircle } from "@cap/ui-solid"; +import { Dialog } from "@kobalte/core/dialog"; +import { createMutation } from "@tanstack/solid-query"; +import { Channel } from "@tauri-apps/api/core"; +import { cx } from "cva"; +import { createSignal, createMemo, Show, For } from "solid-js"; +import { createStore } from "solid-js/store"; +import { createTauriEventListener } from "~/utils/createEventListener"; +import { + commands, + events, + UploadDebugEvent, + UploadProgress, +} from "~/utils/tauri"; + +interface UploadDebugEventWithTimestamp extends UploadDebugEvent { + timestamp: number; +} + +export function UploadStatsForNerds(props: { + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const [debugEvents, setDebugEvents] = createStore< + Record + >({}); + + const [hoveredEvent, setHoveredEvent] = + createSignal(null); + + createTauriEventListener(events.uploadDebugEvent, (e) => { + console.log(e); + const eventWithTimestamp: UploadDebugEventWithTimestamp = { + ...e, + timestamp: Date.now(), + }; + + const key = `${e.video_id}:${e.upload_id}`; + setDebugEvents(key, (prev = []) => [...prev, eventWithTimestamp]); + }); + + const allEvents = createMemo(() => { + const events = Object.values(debugEvents).flat(); + return events.sort((a, b) => a.timestamp - b.timestamp); + }); + + const timeRange = createMemo(() => { + const events = allEvents(); + if (events.length === 0) return { start: 0, end: 0 }; + const start = Math.min(...events.map((e) => e.timestamp)); + const end = Math.max(...events.map((e) => e.timestamp)); + return { start, end: Math.max(end, start + 60000) }; // At least 1 minute range + }); + + const getEventColor = (state: UploadDebugEvent["state"]) => { + switch (state) { + case "Pending": + return "bg-yellow-500"; + case "Done": + return "bg-green-500"; + default: + if (typeof state === "object") { + if ("Presigning" in state) return "bg-blue-500"; + if ("Uploading" in state) return "bg-purple-500"; + if ("PendingNextChunk" in state) return "bg-orange-500"; + } + return "bg-gray-500"; + } + }; + + const formatTimestamp = (timestamp: number) => { + return new Date(timestamp).toLocaleTimeString([], { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + }; + + const getStateLabel = (state: UploadDebugEvent["state"]): string => { + if (typeof state === "string") return state; + if (typeof state === "object") { + if ("Presigning" in state) + return `Presigning Part ${state.Presigning.part_number}`; + if ("Uploading" in state) + return `Uploading Part ${state.Uploading.part_number}`; + if ("PendingNextChunk" in state) + return `Pending After Part ${state.PendingNextChunk.prev_part_number}`; + } + return "Unknown"; + }; + + const getEventDetails = (event: UploadDebugEventWithTimestamp): string => { + const state = event.state; + let details = `Video ID: ${event.video_id}\nUpload ID: ${event.upload_id}\nTime: ${formatTimestamp(event.timestamp)}\nState: ${getStateLabel(state)}`; + + if (typeof state === "object") { + if ("Presigning" in state) { + details += `\nPart: ${state.Presigning.part_number}\nChunk Size: ${state.Presigning.chunk_size}\nTotal Size: ${state.Presigning.total_size}`; + } else if ("Uploading" in state) { + details += `\nPart: ${state.Uploading.part_number}\nChunk Size: ${state.Uploading.chunk_size}\nTotal Size: ${state.Uploading.total_size}`; + } + } + + return details; + }; + + return ( + + + +
+ +
+ + Upload Debug Timeline ({allEvents().length} events) + + + ✕ + +
+ +
+ 0} + fallback={ +
+
📊
+
+

No debug events recorded yet

+

+ Start uploading a recording to see the timeline +

+
+
+ } + > +
+ {/* Timeline container */} +
+
+ {/* Time axis */} +
+
+ {formatTimestamp(timeRange().start)} + Timeline + {formatTimestamp(timeRange().end)} +
+
+ + {/* Event tracks */} +
+ + {([key, events]) => ( +
+
+ {key} +
+
+ {/* Timeline background */} +
+ {/* Grid lines */} + i, + )} + > + {(i) => ( +
+ )} + +
+ + {/* Events */} + + {(event) => { + const position = + ((event.timestamp - timeRange().start) / + (timeRange().end - timeRange().start)) * + 100; + return ( +
+ setHoveredEvent(event) + } + onMouseLeave={() => + setHoveredEvent(null) + } + /> + ); + }} + +
+
+ )} + +
+
+
+ + {/* Legend */} +
+
+ Legend: +
+
+
+
+ Pending +
+
+
+ Presigning +
+
+
+ Uploading +
+
+
+ Pending Next Chunk +
+
+
+ Done +
+
+
+
+ +
+ + {/* Hover tooltip */} + + {(event) => ( +
+ {getEventDetails(event())} +
+ )} +
+ +
+ +
+ ); +} + +function InstantModeActions(props: { + recording: Recording; + uploadProgress: number | undefined; +}) { + const reupload = createMutation(() => ({ + mutationFn: () => + commands.uploadExportedVideo( + props.recording.path, + "Reupload", + new Channel((progress) => {}), + ), + })); + + return ( + <> + reupload.mutate()} + > + + + } + > + + + + + {(sharing) => ( + shell.open(sharing().link)} + > + + + )} + + + ); +} diff --git a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx index 114453a984..cef17d35f5 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx @@ -1,9 +1,11 @@ -import { createResource, Show } from "solid-js"; +import { createResource, createSignal, Show } from "solid-js"; import { createStore } from "solid-js/store"; import { generalSettingsStore } from "~/store"; import type { GeneralSettingsStore } from "~/utils/tauri"; -import { ToggleSetting } from "./Setting"; +import { Setting, ToggleSetting } from "./Setting"; +import { UploadStatsForNerds } from "./UploadStatsForNerds"; +import { Button } from "@cap/ui-solid"; export default function ExperimentalSettings() { const [store] = createResource(() => generalSettingsStore.get()); @@ -29,6 +31,7 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { enableNewUploader: false, }, ); + const [isStatsModalOpen, setIsStatsModalOpen] = createSignal(false); const handleChange = async ( key: K, @@ -110,6 +113,19 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { ); }} /> + + + + +
diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index b0a559eded..1aab29fe13 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -88,7 +88,6 @@ export default function Recordings() { const [uploadProgress, setUploadProgress] = createStore< Record >({}); - const [isStatsModalOpen, setIsStatsModalOpen] = createSignal(false); const recordings = createQuery(() => recordingsQuery); createTauriEventListener(events.uploadProgressEvent, (e) => { @@ -146,17 +145,7 @@ export default function Recordings() { Manage your recordings and perform actions.

- - 0} fallback={ @@ -385,307 +374,3 @@ function TooltipIconButton( ); } - -interface UploadDebugEventWithTimestamp extends UploadDebugEvent { - timestamp: number; -} - -function StatsForNerds(props: { - open: boolean; - onOpenChange: (open: boolean) => void; -}) { - const [debugEvents, setDebugEvents] = createStore< - Record - >({}); - - const [hoveredEvent, setHoveredEvent] = - createSignal(null); - - createTauriEventListener(events.uploadDebugEvent, (e) => { - const eventWithTimestamp: UploadDebugEventWithTimestamp = { - ...e, - timestamp: Date.now(), - }; - - const key = `${e.video_id}:${e.upload_id}`; - setDebugEvents(key, (prev = []) => [...prev, eventWithTimestamp]); - }); - - const allEvents = createMemo(() => { - const events = Object.values(debugEvents).flat(); - return events.sort((a, b) => a.timestamp - b.timestamp); - }); - - const timeRange = createMemo(() => { - const events = allEvents(); - if (events.length === 0) return { start: 0, end: 0 }; - const start = Math.min(...events.map((e) => e.timestamp)); - const end = Math.max(...events.map((e) => e.timestamp)); - return { start, end: Math.max(end, start + 60000) }; // At least 1 minute range - }); - - const getEventColor = (state: UploadDebugEvent["state"]) => { - switch (state) { - case "Pending": - return "bg-yellow-500"; - case "Done": - return "bg-green-500"; - default: - if (typeof state === "object") { - if ("Presigning" in state) return "bg-blue-500"; - if ("Uploading" in state) return "bg-purple-500"; - if ("PendingNextChunk" in state) return "bg-orange-500"; - } - return "bg-gray-500"; - } - }; - - const formatTimestamp = (timestamp: number) => { - return new Date(timestamp).toLocaleTimeString([], { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - }; - - const getStateLabel = (state: UploadDebugEvent["state"]): string => { - if (typeof state === "string") return state; - if (typeof state === "object") { - if ("Presigning" in state) - return `Presigning Part ${state.Presigning.part_number}`; - if ("Uploading" in state) - return `Uploading Part ${state.Uploading.part_number}`; - if ("PendingNextChunk" in state) - return `Pending After Part ${state.PendingNextChunk.prev_part_number}`; - } - return "Unknown"; - }; - - const getEventDetails = (event: UploadDebugEventWithTimestamp): string => { - const state = event.state; - let details = `Video ID: ${event.video_id}\nUpload ID: ${event.upload_id}\nTime: ${formatTimestamp(event.timestamp)}\nState: ${getStateLabel(state)}`; - - if (typeof state === "object") { - if ("Presigning" in state) { - details += `\nPart: ${state.Presigning.part_number}\nChunk Size: ${state.Presigning.chunk_size}\nTotal Size: ${state.Presigning.total_size}`; - } else if ("Uploading" in state) { - details += `\nPart: ${state.Uploading.part_number}\nChunk Size: ${state.Uploading.chunk_size}\nTotal Size: ${state.Uploading.total_size}`; - } - } - - return details; - }; - - return ( - - - -
- -
- - Upload Debug Timeline ({allEvents().length} events) - - - ✕ - -
- -
- 0} - fallback={ -
-
📊
-
-

No debug events recorded yet

-

- Start uploading a recording to see the timeline -

-
-
- } - > -
- {/* Timeline container */} -
-
- {/* Time axis */} -
-
- {formatTimestamp(timeRange().start)} - Timeline - {formatTimestamp(timeRange().end)} -
-
- - {/* Event tracks */} -
- - {([key, events]) => ( -
-
- {key} -
-
- {/* Timeline background */} -
- {/* Grid lines */} - i, - )} - > - {(i) => ( -
- )} - -
- - {/* Events */} - - {(event) => { - const position = - ((event.timestamp - timeRange().start) / - (timeRange().end - timeRange().start)) * - 100; - return ( -
- setHoveredEvent(event) - } - onMouseLeave={() => - setHoveredEvent(null) - } - /> - ); - }} - -
-
- )} - -
-
-
- - {/* Legend */} -
-
- Legend: -
-
-
-
- Pending -
-
-
- Presigning -
-
-
- Uploading -
-
-
- Pending Next Chunk -
-
-
- Done -
-
-
-
- -
- - {/* Hover tooltip */} - - {(event) => ( -
- {getEventDetails(event())} -
- )} -
- -
- -
- ); -} - -function InstantModeActions(props: { - recording: Recording; - uploadProgress: number | undefined; -}) { - const reupload = createMutation(() => ({ - mutationFn: () => - commands.uploadExportedVideo( - props.recording.path, - "Reupload", - new Channel((progress) => {}), - ), - })); - - return ( - <> - reupload.mutate()} - > - - - } - > - - - - - {(sharing) => ( - shell.open(sharing().link)} - > - - - )} - - - ); -} From 730e16085e502a378571e4909979b9c33e127958 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 13:52:51 +0800 Subject: [PATCH 05/15] otel-wip --- Cargo.lock | 172 +++++++++++++++++++++++++++++ apps/desktop/src-tauri/Cargo.toml | 6 + apps/desktop/src-tauri/src/lib.rs | 15 ++- apps/desktop/src-tauri/src/main.rs | 25 ++++- 4 files changed, 216 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d8dd1581f..24160e4e21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1213,6 +1213,9 @@ dependencies = [ "nix 0.29.0", "objc", "objc2-app-kit", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "png 0.17.16", "rand 0.8.5", "relative-path", @@ -1257,6 +1260,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-appender", + "tracing-opentelemetry", "tracing-subscriber", "uuid", "wgpu", @@ -6039,6 +6043,80 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +dependencies = [ + "async-trait", + "bytes", + "http 1.3.1", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +dependencies = [ + "http 1.3.1", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror 2.0.16", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.2", + "thiserror 2.0.16", + "tokio", + "tokio-stream", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -6367,6 +6445,26 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -6651,6 +6749,29 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "psl-types" version = "2.0.11" @@ -9721,6 +9842,38 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "http 1.3.1", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project", + "sync_wrapper", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.2" @@ -9823,6 +9976,25 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6e5658463dd88089aba75c7791e1d3120633b1bfde22478b28f625a9bb1b8e" +dependencies = [ + "js-sys", + "opentelemetry", + "opentelemetry_sdk", + "rustversion", + "smallvec", + "thiserror 2.0.16", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-subscriber" version = "0.3.20" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 7f9c67d7b1..bdd36b74cd 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -133,3 +133,9 @@ windows-sys = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { version = "0.29.0", features = ["fs"] } + +# [target.'cfg(debug_assertions)'.dependencies] +tracing-opentelemetry = "0.32.0" +opentelemetry = "0.31.0" +opentelemetry-otlp = "0.31.0" #{ version = , features = ["http-proto", "reqwest-client"] } +opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio", "trace"] } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index d110abd7d4..702a97ece0 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -87,6 +87,7 @@ use tauri_specta::Event; use tokio::sync::Mutex; use tokio::sync::{RwLock, oneshot}; use tracing::{error, trace, warn}; +use tracing_subscriber::Layer; use upload::{create_or_get_video, upload_image, upload_video}; use web_api::AuthedApiError; use web_api::ManagerExt as WebManagerExt; @@ -1886,11 +1887,23 @@ async fn update_auth_plan(app: AppHandle) { AuthStore::update_auth_plan(&app).await.ok(); } -pub type FilteredRegistry = tracing_subscriber::layer::Layered< +type FilteredRegistryInner = tracing_subscriber::layer::Layered< tracing_subscriber::filter::FilterFn bool>, tracing_subscriber::Registry, >; +#[cfg(debug_assertions)] +pub type FilteredRegistry = tracing_subscriber::layer::Layered< + tracing_opentelemetry::OpenTelemetryLayer< + // FilteredRegistryInner, + tracing_subscriber::filter::FilterFn bool>, + opentelemetry_sdk::trace::SdkTracerProvider, + >, + tracing_subscriber::Registry, +>; +#[cfg(not(debug_assertions))] +pub type FilteredRegistry = FilteredRegistryInner; + pub type DynLoggingLayer = Box + Send + Sync>; type LoggingHandle = tracing_subscriber::reload::Handle, FilteredRegistry>; diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 9dcaab06d9..43baab5a15 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -77,6 +77,29 @@ fn main() { (|v| v.target().starts_with("cap_")) as fn(&tracing::Metadata) -> bool, )); + // #[cfg(debug_assertions)] + let (registry, _tracer) = { + use opentelemetry::trace::TracerProvider; + use opentelemetry_otlp::WithExportConfig; + use tracing_subscriber::Layer; + + let tracer = opentelemetry_sdk::trace::SdkTracerProvider::builder() + .with_batch_exporter( + opentelemetry_otlp::SpanExporter::builder() + .with_http() + .with_protocol(opentelemetry_otlp::Protocol::HttpJson) + .build() + .unwrap(), + ) + .build(); + + opentelemetry::global::set_tracer_provider(tracer.clone()); + ( + registry.with(tracing_opentelemetry::layer().with_tracer(tracer.tracer("cap-desktop"))), + tracer, + ) + }; + registry .with(layer) .with( @@ -104,5 +127,5 @@ fn main() { .enable_all() .build() .expect("Failed to build multi threaded tokio runtime") - .block_on(cap_desktop_lib::run(handle)); + .block_on(cap_desktop_lib::run(handle.boxed())); } From df5c16e2b82895de96ad4731d71682e7579c1457 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 14 Oct 2025 14:14:51 +0800 Subject: [PATCH 06/15] use runtime cfg for desktop otel --- apps/desktop/src-tauri/src/lib.rs | 14 +------------- apps/desktop/src-tauri/src/main.rs | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 702a97ece0..795763b7b9 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1887,23 +1887,11 @@ async fn update_auth_plan(app: AppHandle) { AuthStore::update_auth_plan(&app).await.ok(); } -type FilteredRegistryInner = tracing_subscriber::layer::Layered< +type FilteredRegistry = tracing_subscriber::layer::Layered< tracing_subscriber::filter::FilterFn bool>, tracing_subscriber::Registry, >; -#[cfg(debug_assertions)] -pub type FilteredRegistry = tracing_subscriber::layer::Layered< - tracing_opentelemetry::OpenTelemetryLayer< - // FilteredRegistryInner, - tracing_subscriber::filter::FilterFn bool>, - opentelemetry_sdk::trace::SdkTracerProvider, - >, - tracing_subscriber::Registry, ->; -#[cfg(not(debug_assertions))] -pub type FilteredRegistry = FilteredRegistryInner; - pub type DynLoggingLayer = Box + Send + Sync>; type LoggingHandle = tracing_subscriber::reload::Handle, FilteredRegistry>; diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 43baab5a15..6fbb5e2a5e 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -47,7 +47,7 @@ fn main() { (sentry_client, _guard) }); - let (layer, handle) = tracing_subscriber::reload::Layer::new(None::); + let (reload_layer, handle) = tracing_subscriber::reload::Layer::new(None::); let logs_dir = { #[cfg(target_os = "macos")] @@ -73,12 +73,7 @@ fn main() { let file_appender = tracing_appender::rolling::daily(&logs_dir, "cap-desktop.log"); let (non_blocking, _logger_guard) = tracing_appender::non_blocking(file_appender); - let registry = tracing_subscriber::registry().with(tracing_subscriber::filter::filter_fn( - (|v| v.target().starts_with("cap_")) as fn(&tracing::Metadata) -> bool, - )); - - // #[cfg(debug_assertions)] - let (registry, _tracer) = { + let (otel_layer, _tracer) = if cfg!(debug_assertions) { use opentelemetry::trace::TracerProvider; use opentelemetry_otlp::WithExportConfig; use tracing_subscriber::Layer; @@ -93,15 +88,22 @@ fn main() { ) .build(); + let layer = tracing_opentelemetry::layer() + .with_tracer(tracer.tracer("cap-desktop")) + .boxed(); + opentelemetry::global::set_tracer_provider(tracer.clone()); - ( - registry.with(tracing_opentelemetry::layer().with_tracer(tracer.tracer("cap-desktop"))), - tracer, - ) + (Some(layer), Some(tracer)) + } else { + (None, None) }; - registry - .with(layer) + tracing_subscriber::registry() + .with(tracing_subscriber::filter::filter_fn( + (|v| v.target().starts_with("cap_")) as fn(&tracing::Metadata) -> bool, + )) + .with(reload_layer) + .with(otel_layer) .with( tracing_subscriber::fmt::layer() .with_ansi(true) @@ -127,5 +129,5 @@ fn main() { .enable_all() .build() .expect("Failed to build multi threaded tokio runtime") - .block_on(cap_desktop_lib::run(handle.boxed())); + .block_on(cap_desktop_lib::run(handle)); } From afe757d5f488ea987e171e1642947f7e51e2f553 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 14:31:00 +0800 Subject: [PATCH 07/15] include service nm,e --- apps/desktop/src-tauri/src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 6fbb5e2a5e..097261ede1 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -86,6 +86,11 @@ fn main() { .build() .unwrap(), ) + .with_resource( + opentelemetry_sdk::Resource::builder() + .with_service_name("cap-desktop") + .build(), + ) .build(); let layer = tracing_opentelemetry::layer() From c4835c5b2033ff912e5a1008fc6fe008f0d44b6d Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 15:12:05 +0800 Subject: [PATCH 08/15] tracing all the things --- Cargo.lock | 13 + apps/desktop/src-tauri/Cargo.toml | 1 + apps/desktop/src-tauri/src/api.rs | 12 +- apps/desktop/src-tauri/src/captions.rs | 10 + apps/desktop/src-tauri/src/export.rs | 4 +- apps/desktop/src-tauri/src/fake_window.rs | 3 + .../desktop/src-tauri/src/general_settings.rs | 3 +- apps/desktop/src-tauri/src/hotkeys.rs | 4 +- apps/desktop/src-tauri/src/lib.rs | 57 +++- apps/desktop/src-tauri/src/permissions.rs | 4 +- apps/desktop/src-tauri/src/platform/mod.rs | 2 + apps/desktop/src-tauri/src/recording.rs | 7 + .../src-tauri/src/target_select_overlay.rs | 7 +- apps/desktop/src-tauri/src/tracing_utils.rs | 245 ++++++++++++++++++ apps/desktop/src-tauri/src/upload.rs | 127 ++------- apps/desktop/src-tauri/src/windows.rs | 7 +- apps/desktop/src/utils/tauri.ts | 4 - 17 files changed, 388 insertions(+), 122 deletions(-) create mode 100644 apps/desktop/src-tauri/src/tracing_utils.rs diff --git a/Cargo.lock b/Cargo.lock index 24160e4e21..46435d97d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1260,6 +1260,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-appender", + "tracing-futures", "tracing-opentelemetry", "tracing-subscriber", "uuid", @@ -9965,6 +9966,18 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "futures", + "futures-task", + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.2.0" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index bdd36b74cd..1fa839b662 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -107,6 +107,7 @@ tauri-plugin-sentry = "0.5.0" thiserror.workspace = true bytes = "1.10.1" async-stream = "0.3.6" +tracing-futures = { version = "0.2.5", features = ["futures-03"] } [target.'cfg(target_os = "macos")'.dependencies] core-graphics = "0.24.0" diff --git a/apps/desktop/src-tauri/src/api.rs b/apps/desktop/src-tauri/src/api.rs index fdfab9c4c2..d60dbd9492 100644 --- a/apps/desktop/src-tauri/src/api.rs +++ b/apps/desktop/src-tauri/src/api.rs @@ -4,9 +4,11 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use tauri::AppHandle; +use tracing::instrument; use crate::web_api::{AuthedApiError, ManagerExt}; +#[instrument] pub async fn upload_multipart_initiate( app: &AppHandle, video_id: &str, @@ -44,6 +46,7 @@ pub async fn upload_multipart_initiate( .map(|data| data.upload_id) } +#[instrument] pub async fn upload_multipart_presign_part( app: &AppHandle, video_id: &str, @@ -86,7 +89,7 @@ pub async fn upload_multipart_presign_part( .map(|data| data.presigned_url) } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct UploadedPart { pub part_number: u32, @@ -107,6 +110,7 @@ pub struct S3VideoMeta { pub fps: Option, } +#[instrument] pub async fn upload_multipart_complete( app: &AppHandle, video_id: &str, @@ -158,7 +162,7 @@ pub async fn upload_multipart_complete( .map(|data| data.location) } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "lowercase")] pub enum PresignedS3PutRequestMethod { #[allow(unused)] @@ -166,7 +170,7 @@ pub enum PresignedS3PutRequestMethod { Put, } -#[derive(Serialize)] +#[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct PresignedS3PutRequest { pub video_id: String, @@ -176,6 +180,7 @@ pub struct PresignedS3PutRequest { pub meta: Option, } +#[instrument(skip())] pub async fn upload_signed( app: &AppHandle, body: PresignedS3PutRequest, @@ -213,6 +218,7 @@ pub async fn upload_signed( .map(|data| data.presigned_put_data.url) } +#[instrument] pub async fn desktop_video_progress( app: &AppHandle, video_id: &str, diff --git a/apps/desktop/src-tauri/src/captions.rs b/apps/desktop/src-tauri/src/captions.rs index b00d3a21b8..ee8a89d58a 100644 --- a/apps/desktop/src-tauri/src/captions.rs +++ b/apps/desktop/src-tauri/src/captions.rs @@ -16,6 +16,7 @@ use tauri::{AppHandle, Emitter, Manager, Window}; use tempfile::tempdir; use tokio::io::AsyncWriteExt; use tokio::sync::Mutex; +use tracing::instrument; use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters}; // Re-export caption types from cap_project @@ -48,6 +49,7 @@ const WHISPER_SAMPLE_RATE: u32 = 16000; /// Function to handle creating directories for the model #[tauri::command] #[specta::specta] +#[instrument] pub async fn create_dir(path: String, _recursive: bool) -> Result<(), String> { std::fs::create_dir_all(path).map_err(|e| format!("Failed to create directory: {e}")) } @@ -55,6 +57,7 @@ pub async fn create_dir(path: String, _recursive: bool) -> Result<(), String> { /// Function to save the model file #[tauri::command] #[specta::specta] +#[instrument] pub async fn save_model_file(path: String, data: Vec) -> Result<(), String> { std::fs::write(&path, &data).map_err(|e| format!("Failed to write model file: {e}")) } @@ -650,6 +653,7 @@ fn process_with_whisper( /// Function to transcribe audio from a video file using Whisper #[tauri::command] #[specta::specta] +#[instrument] pub async fn transcribe_audio( video_path: String, model_path: String, @@ -712,6 +716,7 @@ pub async fn transcribe_audio( /// Function to save caption data to a file #[tauri::command] #[specta::specta] +#[instrument] pub async fn save_captions( video_id: String, captions: CaptionData, @@ -966,6 +971,7 @@ pub fn parse_captions_json(json: &str) -> Result Result { Ok(std::path::Path::new(&model_path).exists()) } @@ -1141,6 +1149,7 @@ pub async fn check_model_exists(model_path: String) -> Result { /// Function to delete a downloaded model #[tauri::command] #[specta::specta] +#[instrument] pub async fn delete_whisper_model(model_path: String) -> Result<(), String> { if !std::path::Path::new(&model_path).exists() { return Err(format!("Model file not found: {model_path}")); @@ -1185,6 +1194,7 @@ fn format_srt_time(seconds: f64) -> String { /// Export captions to an SRT file #[tauri::command] #[specta::specta] +#[instrument] pub async fn export_captions_srt( video_id: String, app: AppHandle, diff --git a/apps/desktop/src-tauri/src/export.rs b/apps/desktop/src-tauri/src/export.rs index 2170bacdf8..3196d7bed0 100644 --- a/apps/desktop/src-tauri/src/export.rs +++ b/apps/desktop/src-tauri/src/export.rs @@ -4,7 +4,7 @@ use cap_project::{RecordingMeta, XY}; use serde::Deserialize; use specta::Type; use std::path::PathBuf; -use tracing::info; +use tracing::{info, instrument}; #[derive(Deserialize, Clone, Copy, Debug, Type)] #[serde(tag = "format")] @@ -24,6 +24,7 @@ impl ExportSettings { #[tauri::command] #[specta::specta] +#[instrument(skip(progress))] pub async fn export_video( project_path: PathBuf, progress: tauri::ipc::Channel, @@ -88,6 +89,7 @@ pub struct ExportEstimates { // This will need to be refactored at some point to be more accurate. #[tauri::command] #[specta::specta] +#[instrument] pub async fn get_export_estimates( path: PathBuf, resolution: XY, diff --git a/apps/desktop/src-tauri/src/fake_window.rs b/apps/desktop/src-tauri/src/fake_window.rs index b474b57268..93b96dd46c 100644 --- a/apps/desktop/src-tauri/src/fake_window.rs +++ b/apps/desktop/src-tauri/src/fake_window.rs @@ -2,11 +2,13 @@ use scap_targets::bounds::LogicalBounds; use std::{collections::HashMap, sync::Arc, time::Duration}; use tauri::{AppHandle, Manager, WebviewWindow}; use tokio::{sync::RwLock, time::sleep}; +use tracing::instrument; pub struct FakeWindowBounds(pub Arc>>>); #[tauri::command] #[specta::specta] +#[instrument(skip(state))] pub async fn set_fake_window_bounds( window: tauri::Window, name: String, @@ -23,6 +25,7 @@ pub async fn set_fake_window_bounds( #[tauri::command] #[specta::specta] +#[instrument(skip(state))] pub async fn remove_fake_window( window: tauri::Window, name: String, diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs index ada685f664..eba4cf1d38 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-tauri/src/general_settings.rs @@ -4,7 +4,7 @@ use serde_json::json; use specta::Type; use tauri::{AppHandle, Wry}; use tauri_plugin_store::StoreExt; -use tracing::error; +use tracing::{error, instrument}; use uuid::Uuid; #[derive(Default, Serialize, Deserialize, Type, Debug, Clone, Copy)] @@ -256,6 +256,7 @@ pub fn init(app: &AppHandle) { #[tauri::command] #[specta::specta] +#[instrument] pub fn get_default_excluded_windows() -> Vec { default_excluded_windows() } diff --git a/apps/desktop/src-tauri/src/hotkeys.rs b/apps/desktop/src-tauri/src/hotkeys.rs index 79a4a5eff3..3bda4e3b1b 100644 --- a/apps/desktop/src-tauri/src/hotkeys.rs +++ b/apps/desktop/src-tauri/src/hotkeys.rs @@ -11,8 +11,9 @@ use tauri::{AppHandle, Manager}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut}; use tauri_plugin_store::StoreExt; use tauri_specta::Event; +use tracing::instrument; -#[derive(Serialize, Deserialize, Type, PartialEq, Clone, Copy)] +#[derive(Serialize, Deserialize, Type, PartialEq, Clone, Copy, Debug)] pub struct Hotkey { #[specta(type = String)] code: Code, @@ -180,6 +181,7 @@ async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), Strin #[tauri::command(async)] #[specta::specta] +#[instrument] pub fn set_hotkey(app: AppHandle, action: HotkeyAction, hotkey: Option) -> Result<(), ()> { let global_shortcut = app.global_shortcut(); let state = app.state::(); diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 795763b7b9..8bdec576f9 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -66,6 +66,7 @@ use serde_json::json; use specta::Type; use std::{ collections::BTreeMap, + fmt, fs::File, future::Future, io::BufWriter, @@ -86,8 +87,7 @@ use tauri_specta::Event; #[cfg(target_os = "macos")] use tokio::sync::Mutex; use tokio::sync::{RwLock, oneshot}; -use tracing::{error, trace, warn}; -use tracing_subscriber::Layer; +use tracing::{error, instrument, trace, warn}; use upload::{create_or_get_video, upload_image, upload_video}; use web_api::AuthedApiError; use web_api::ManagerExt as WebManagerExt; @@ -130,6 +130,12 @@ pub struct App { server_url: String, } +impl fmt::Debug for App { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("App").finish() + } +} + #[derive(specta::Type, Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub enum VideoType { @@ -221,6 +227,7 @@ impl App { #[tauri::command] #[specta::specta] +#[instrument] async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> Result<(), String> { let mic_feed = state.read().await.mic_feed.clone(); @@ -246,6 +253,7 @@ async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> R #[tauri::command] #[specta::specta] +#[instrument] async fn set_camera_input( app_handle: AppHandle, state: MutableState<'_, App>, @@ -389,6 +397,7 @@ struct CurrentRecording { #[tauri::command] #[specta::specta] +#[instrument] async fn get_current_recording( state: MutableState<'_, App>, ) -> Result>, ()> { @@ -576,6 +585,7 @@ async fn create_screenshot( #[tauri::command] #[specta::specta] +#[instrument] async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(), String> { println!("Attempting to copy file from {src} to {dst}"); @@ -699,6 +709,7 @@ pub fn is_valid_video(path: &std::path::Path) -> bool { #[tauri::command] #[specta::specta] +#[instrument(skip(clipboard))] async fn copy_screenshot_to_clipboard( clipboard: MutableState<'_, ClipboardContext>, path: String, @@ -717,6 +728,7 @@ async fn copy_screenshot_to_clipboard( #[tauri::command] #[specta::specta] +#[instrument] async fn open_file_path(_app: AppHandle, path: PathBuf) -> Result<(), String> { let path_str = path.to_str().ok_or("Invalid path")?; @@ -775,6 +787,7 @@ impl EditorStateChanged { #[tauri::command] #[specta::specta] +#[instrument(skip(editor_instance))] async fn start_playback( editor_instance: WindowEditorInstance, fps: u32, @@ -787,6 +800,7 @@ async fn start_playback( #[tauri::command] #[specta::specta] +#[instrument(skip(editor_instance))] async fn stop_playback(editor_instance: WindowEditorInstance) -> Result<(), String> { let mut state = editor_instance.state.lock().await; @@ -809,6 +823,7 @@ struct SerializedEditorInstance { #[tauri::command] #[specta::specta] +#[instrument] async fn create_editor_instance(window: Window) -> Result { let CapWindowId::Editor { id } = CapWindowId::from_str(window.label()).unwrap() else { return Err("Invalid window".to_string()); @@ -844,12 +859,14 @@ async fn create_editor_instance(window: Window) -> Result Result { let path = editor.project_path.clone(); RecordingMeta::load_for_project(&path).map_err(|e| e.to_string()) } #[tauri::command] #[specta::specta] +#[instrument(skip(editor))] async fn set_pretty_name(editor: WindowEditorInstance, pretty_name: String) -> Result<(), String> { let mut meta = editor.meta().clone(); meta.pretty_name = pretty_name; @@ -858,6 +875,7 @@ async fn set_pretty_name(editor: WindowEditorInstance, pretty_name: String) -> R #[tauri::command] #[specta::specta] +#[instrument(skip(clipboard))] async fn copy_video_to_clipboard( app: AppHandle, clipboard: MutableState<'_, ClipboardContext>, @@ -875,6 +893,7 @@ async fn copy_video_to_clipboard( #[tauri::command] #[specta::specta] +#[instrument] async fn get_video_metadata(path: PathBuf) -> Result { let recording_meta = RecordingMeta::load_for_project(&path).map_err(|v| v.to_string())?; @@ -954,6 +973,7 @@ async fn get_video_metadata(path: PathBuf) -> Result Result, String> { @@ -1033,6 +1057,7 @@ async fn generate_zoom_segments_from_clicks( #[tauri::command] #[specta::specta] +#[instrument] async fn list_audio_devices() -> Result, ()> { if !permissions::do_permissions_check(false) .microphone @@ -1049,7 +1074,7 @@ pub struct UploadProgress { progress: f64, } -#[derive(Deserialize, Type)] +#[derive(Debug, Deserialize, Type)] pub enum UploadMode { Initial { pre_created_video: Option, @@ -1059,6 +1084,7 @@ pub enum UploadMode { #[tauri::command] #[specta::specta] +#[instrument(skip(channel))] async fn upload_exported_video( app: AppHandle, path: PathBuf, @@ -1183,6 +1209,7 @@ async fn upload_exported_video( #[tauri::command] #[specta::specta] +#[instrument(skip(clipboard))] async fn upload_screenshot( app: AppHandle, clipboard: MutableState<'_, ClipboardContext>, @@ -1232,6 +1259,7 @@ async fn upload_screenshot( #[tauri::command] #[specta::specta] +#[instrument] async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Result<(), String> { let id = uuid::Uuid::new_v4().to_string(); @@ -1360,6 +1388,7 @@ async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Resul #[tauri::command] #[specta::specta] +#[instrument] async fn save_file_dialog( app: AppHandle, file_name: String, @@ -1479,6 +1508,7 @@ pub enum FileType { #[tauri::command(async)] #[specta::specta] +#[instrument] fn get_recording_meta( path: PathBuf, _file_type: FileType, @@ -1490,6 +1520,7 @@ fn get_recording_meta( #[tauri::command] #[specta::specta] +#[instrument] fn list_recordings(app: AppHandle) -> Result, String> { let recordings_dir = recordings_path(&app); @@ -1536,6 +1567,7 @@ fn list_recordings(app: AppHandle) -> Result Result, String> { let screenshots_dir = screenshots_path(&app); @@ -1579,6 +1611,7 @@ fn list_screenshots(app: AppHandle) -> Result, Str #[tauri::command] #[specta::specta] +#[instrument] async fn check_upgraded_and_update(app: AppHandle) -> Result { println!("Checking upgraded status and updating..."); @@ -1641,6 +1674,7 @@ async fn check_upgraded_and_update(app: AppHandle) -> Result { #[tauri::command] #[specta::specta] +#[instrument] fn open_external_link(app: tauri::AppHandle, url: String) -> Result<(), String> { if let Ok(Some(settings)) = GeneralSettingsStore::get(&app) && settings.disable_auto_open_links @@ -1656,6 +1690,7 @@ fn open_external_link(app: tauri::AppHandle, url: String) -> Result<(), String> #[tauri::command] #[specta::specta] +#[instrument] async fn reset_camera_permissions(_app: AppHandle) -> Result<(), String> { #[cfg(target_os = "macos")] { @@ -1678,6 +1713,7 @@ async fn reset_camera_permissions(_app: AppHandle) -> Result<(), String> { #[tauri::command] #[specta::specta] +#[instrument] async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), ()> { #[cfg(debug_assertions)] let bundle_id = "com.apple.Terminal"; @@ -1696,12 +1732,14 @@ async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), ()> { #[tauri::command] #[specta::specta] +#[instrument] async fn is_camera_window_open(app: AppHandle) -> bool { CapWindowId::Camera.get(&app).is_some() } #[tauri::command] #[specta::specta] +#[instrument(skip(editor_instance))] async fn seek_to(editor_instance: WindowEditorInstance, frame_number: u32) -> Result<(), String> { editor_instance .modify_and_emit_state(|state| { @@ -1714,6 +1752,7 @@ async fn seek_to(editor_instance: WindowEditorInstance, frame_number: u32) -> Re #[tauri::command] #[specta::specta] +#[instrument(skip(editor_instance))] async fn get_mic_waveforms(editor_instance: WindowEditorInstance) -> Result>, String> { let mut out = Vec::new(); @@ -1730,6 +1769,7 @@ async fn get_mic_waveforms(editor_instance: WindowEditorInstance) -> Result Result>, String> { @@ -1748,6 +1788,7 @@ async fn get_system_audio_waveforms( #[tauri::command] #[specta::specta] +#[instrument(skip(editor_instance))] async fn editor_delete_project( app: tauri::AppHandle, editor_instance: WindowEditorInstance, @@ -1770,6 +1811,7 @@ async fn editor_delete_project( // keep this async otherwise opening windows may hang on windows #[tauri::command] #[specta::specta] +#[instrument] async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String> { let _ = window.show(&app).await; Ok(()) @@ -1777,12 +1819,14 @@ async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String #[tauri::command(async)] #[specta::specta] +#[instrument] fn list_fails() -> Result, ()> { Ok(cap_fail::get_state()) } #[tauri::command(async)] #[specta::specta] +#[instrument] fn set_fail(name: String, value: bool) { cap_fail::set_fail(&name, value) } @@ -1847,6 +1891,7 @@ async fn check_notification_permissions(app: AppHandle) { #[tauri::command] #[specta::specta] +#[instrument] async fn set_server_url(app: MutableState<'_, App>, server_url: String) -> Result<(), ()> { app.write().await.server_url = server_url; Ok(()) @@ -1854,6 +1899,7 @@ async fn set_server_url(app: MutableState<'_, App>, server_url: String) -> Resul #[tauri::command] #[specta::specta] +#[instrument] async fn set_camera_preview_state( app: MutableState<'_, App>, state: CameraPreviewState, @@ -1869,6 +1915,7 @@ async fn set_camera_preview_state( #[tauri::command] #[specta::specta] +#[instrument] async fn await_camera_preview_ready(app: MutableState<'_, App>) -> Result { let app = app.read().await.camera_feed.clone(); @@ -1883,6 +1930,7 @@ async fn await_camera_preview_ready(app: MutableState<'_, App>) -> Result() @@ -2649,12 +2696,14 @@ fn screenshots_path(app: &AppHandle) -> PathBuf { #[tauri::command] #[specta::specta] +#[instrument] fn global_message_dialog(app: AppHandle, message: String) { app.dialog().message(message).show(|_| {}); } #[tauri::command] #[specta::specta] +#[instrument(skip(clipboard))] async fn write_clipboard_string( clipboard: MutableState<'_, ClipboardContext>, text: String, diff --git a/apps/desktop/src-tauri/src/permissions.rs b/apps/desktop/src-tauri/src/permissions.rs index 73dc886d74..32fd857f91 100644 --- a/apps/desktop/src-tauri/src/permissions.rs +++ b/apps/desktop/src-tauri/src/permissions.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; #[cfg(target_os = "macos")] use cidre::av; +use tracing::instrument; #[cfg(target_os = "macos")] #[link(name = "ApplicationServices", kind = "framework")] @@ -11,7 +12,7 @@ unsafe extern "C" { -> bool; } -#[derive(Serialize, Deserialize, specta::Type)] +#[derive(Debug, Serialize, Deserialize, specta::Type)] #[serde(rename_all = "camelCase")] pub enum OSPermission { ScreenRecording, @@ -61,6 +62,7 @@ pub fn open_permission_settings(_permission: OSPermission) { #[tauri::command] #[specta::specta] +#[instrument] pub async fn request_permission(_permission: OSPermission) { #[cfg(target_os = "macos")] { diff --git a/apps/desktop/src-tauri/src/platform/mod.rs b/apps/desktop/src-tauri/src/platform/mod.rs index 5a03c820c7..267ec61b9c 100644 --- a/apps/desktop/src-tauri/src/platform/mod.rs +++ b/apps/desktop/src-tauri/src/platform/mod.rs @@ -8,6 +8,7 @@ pub mod macos; #[cfg(target_os = "macos")] pub use macos::*; +use tracing::instrument; #[derive(Debug, Serialize, Deserialize, Type, Default)] #[repr(isize)] @@ -29,6 +30,7 @@ pub enum HapticPerformanceTime { #[tauri::command] #[specta::specta] +#[instrument] pub fn perform_haptic_feedback( _pattern: Option, _time: Option, diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index c6d212540c..dd806fcd52 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -213,6 +213,7 @@ pub fn list_cameras() -> Vec { #[tauri::command] #[specta::specta] +#[instrument] pub async fn list_displays_with_thumbnails() -> Result, String> { tokio::task::spawn_blocking(|| { tauri::async_runtime::block_on(collect_displays_with_thumbnails()) @@ -223,6 +224,7 @@ pub async fn list_displays_with_thumbnails() -> Result Result, String> { tokio::task::spawn_blocking( || tauri::async_runtime::block_on(collect_windows_with_thumbnails()), @@ -663,6 +665,7 @@ pub async fn start_recording( #[tauri::command] #[specta::specta] +#[instrument] pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; @@ -675,6 +678,7 @@ pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String> #[tauri::command] #[specta::specta] +#[instrument] pub async fn resume_recording(state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; @@ -687,6 +691,7 @@ pub async fn resume_recording(state: MutableState<'_, App>) -> Result<(), String #[tauri::command] #[specta::specta] +#[instrument] pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; let Some(current_recording) = state.clear_current_recording() else { @@ -703,6 +708,7 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res #[tauri::command] #[specta::specta] +#[instrument] pub async fn restart_recording( app: AppHandle, state: MutableState<'_, App>, @@ -724,6 +730,7 @@ pub async fn restart_recording( #[tauri::command] #[specta::specta] +#[instrument] pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { let recording_data = { let mut app_state = state.write().await; diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index c218bc5288..1d1f9c7dc0 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -18,7 +18,7 @@ use tauri::{AppHandle, Manager, WebviewWindow}; use tauri_plugin_global_shortcut::{GlobalShortcut, GlobalShortcutExt}; use tauri_specta::Event; use tokio::task::JoinHandle; -use tracing::error; +use tracing::{error, instrument}; #[derive(tauri_specta::Event, Serialize, Type, Clone)] pub struct TargetUnderCursor { @@ -42,6 +42,7 @@ pub struct DisplayInformation { #[specta::specta] #[tauri::command] +#[instrument(skip(state))] pub async fn open_target_select_overlays( app: AppHandle, state: tauri::State<'_, WindowFocusManager>, @@ -102,6 +103,7 @@ pub async fn open_target_select_overlays( #[specta::specta] #[tauri::command] +#[instrument] pub async fn close_target_select_overlays(app: AppHandle) -> Result<(), String> { for (id, window) in app.webview_windows() { if let Ok(CapWindowId::TargetSelectOverlay { .. }) = CapWindowId::from_str(&id) { @@ -114,6 +116,7 @@ pub async fn close_target_select_overlays(app: AppHandle) -> Result<(), String> #[specta::specta] #[tauri::command] +#[instrument] pub async fn get_window_icon(window_id: &str) -> Result, String> { let window_id = window_id .parse::() @@ -127,6 +130,7 @@ pub async fn get_window_icon(window_id: &str) -> Result, String> #[specta::specta] #[tauri::command] +#[instrument] pub async fn display_information(display_id: &str) -> Result { let display_id = display_id .parse::() @@ -142,6 +146,7 @@ pub async fn display_information(display_id: &str) -> Result Result<(), String> { let window = Window::from_id(&window_id).ok_or("Window not found")?; diff --git a/apps/desktop/src-tauri/src/tracing_utils.rs b/apps/desktop/src-tauri/src/tracing_utils.rs new file mode 100644 index 0000000000..339e97719f --- /dev/null +++ b/apps/desktop/src-tauri/src/tracing_utils.rs @@ -0,0 +1,245 @@ +use std::time::Instant; +#[cfg(debug_assertions)] +use tracing::{Instrument, info_span}; +use tracing::{debug, info}; + +/// A tracing-based replacement for UploadDebugEvent that provides structured logging +/// and OpenTelemetry spans for upload debugging in debug builds. +pub struct UploadTracer { + pub video_id: String, + pub upload_id: String, + start_time: Instant, +} + +impl UploadTracer { + pub fn new(video_id: String, upload_id: String) -> Self { + debug!( + video_id = %video_id, + upload_id = %upload_id, + "Initializing upload tracer" + ); + + Self { + video_id, + upload_id, + start_time: Instant::now(), + } + } + + /// Create a span for the entire upload operation + #[cfg(debug_assertions)] + pub fn upload_span(&self, operation: F) -> R + where + F: FnOnce() -> R, + { + let span = info_span!( + "upload_operation", + video_id = %self.video_id, + upload_id = %self.upload_id, + elapsed_ms = tracing::field::Empty + ); + + let _enter = span.enter(); + let start = Instant::now(); + let result = operation(); + let elapsed = start.elapsed(); + + span.record("elapsed_ms", elapsed.as_millis()); + info!( + elapsed_ms = elapsed.as_millis(), + "Upload operation completed" + ); + + result + } + + /// For release builds, just execute the operation without tracing overhead + #[cfg(not(debug_assertions))] + pub fn upload_span(&self, operation: F) -> R + where + F: FnOnce() -> R, + { + operation() + } + + /// Create a span for presigning operations + #[cfg(debug_assertions)] + pub async fn presign_span( + &self, + part_number: u32, + chunk_size: usize, + total_size: u64, + operation: F, + ) -> R + where + F: std::future::Future, + { + let span = info_span!( + "presign_part", + video_id = %self.video_id, + upload_id = %self.upload_id, + part_number = part_number, + chunk_size = chunk_size, + total_size = total_size, + elapsed_ms = tracing::field::Empty + ); + + let start = Instant::now(); + let result = operation.instrument(span.clone()).await; + let elapsed = start.elapsed(); + + span.record("elapsed_ms", elapsed.as_millis()); + info!( + part_number = part_number, + chunk_size = chunk_size, + elapsed_ms = elapsed.as_millis(), + "Presigning completed" + ); + + result + } + + #[cfg(not(debug_assertions))] + pub async fn presign_span( + &self, + _part_number: u32, + _chunk_size: usize, + _total_size: u64, + operation: F, + ) -> R + where + F: std::future::Future, + { + operation.await + } + + /// Create a span for chunk upload operations + #[cfg(debug_assertions)] + pub async fn upload_chunk_span( + &self, + part_number: u32, + chunk_size: usize, + total_size: u64, + operation: F, + ) -> R + where + F: std::future::Future, + { + let span = info_span!( + "upload_chunk", + video_id = %self.video_id, + upload_id = %self.upload_id, + part_number = part_number, + chunk_size = chunk_size, + total_size = total_size, + elapsed_ms = tracing::field::Empty + ); + + let start = Instant::now(); + let result = operation.instrument(span.clone()).await; + let elapsed = start.elapsed(); + + span.record("elapsed_ms", elapsed.as_millis()); + info!( + part_number = part_number, + chunk_size = chunk_size, + elapsed_ms = elapsed.as_millis(), + "Chunk upload completed" + ); + + result + } + + #[cfg(not(debug_assertions))] + pub async fn upload_chunk_span( + &self, + _part_number: u32, + _chunk_size: usize, + _total_size: u64, + operation: F, + ) -> R + where + F: std::future::Future, + { + operation.await + } + + /// Log when waiting for next chunk + pub fn log_pending_next_chunk(&self, prev_part_number: u32) { + debug!( + video_id = %self.video_id, + upload_id = %self.upload_id, + prev_part_number = prev_part_number, + "Pending next chunk" + ); + } + + /// Log upload completion with total time + pub fn log_completion(&self) { + let total_elapsed = self.start_time.elapsed(); + info!( + video_id = %self.video_id, + upload_id = %self.upload_id, + total_elapsed_ms = total_elapsed.as_millis(), + total_elapsed_secs = total_elapsed.as_secs(), + "Upload completed" + ); + } + + /// Create a span for the entire multipart upload process + #[cfg(debug_assertions)] + pub async fn multipart_upload_span(&self, operation: F) -> R + where + F: std::future::Future, + { + let span = info_span!( + "multipart_upload", + video_id = %self.video_id, + upload_id = %self.upload_id, + total_elapsed_ms = tracing::field::Empty + ); + + let start = Instant::now(); + let result = operation.instrument(span.clone()).await; + let elapsed = start.elapsed(); + + span.record("total_elapsed_ms", elapsed.as_millis()); + + result + } + + #[cfg(not(debug_assertions))] + pub async fn multipart_upload_span(&self, operation: F) -> R + where + F: std::future::Future, + { + operation.await + } +} + +/// Helper macro for creating timed spans with automatic timing +#[macro_export] +macro_rules! timed_span { + ($span_name:expr, $($field:ident = $value:expr),*) => {{ + #[cfg(debug_assertions)] + { + let span = tracing::info_span!( + $span_name, + elapsed_ms = tracing::field::Empty, + $($field = $value),* + ); + let _enter = span.enter(); + let start = std::time::Instant::now(); + let result = { + // Code block will be inserted here by the caller + }; + let elapsed = start.elapsed(); + span.record("elapsed_ms", elapsed.as_millis()); + result + } + #[cfg(not(debug_assertions))] + { + // Code block will be inserted here by the caller + } + }}; +} diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 9e49987aa0..ef927923ef 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -17,16 +17,15 @@ use flume::Receiver; use futures::{Stream, StreamExt, TryStreamExt, stream}; use image::{ImageReader, codecs::jpeg::JpegEncoder}; use reqwest::StatusCode; -use sentry::types::Auth; use serde::{Deserialize, Serialize}; use specta::Type; use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, io, path::{Path, PathBuf}, pin::pin, str::FromStr, - time::{Duration, SystemTime, UNIX_EPOCH}, + time::Duration, }; use tauri::{AppHandle, ipc::Channel}; use tauri_plugin_clipboard_manager::ClipboardExt; @@ -38,7 +37,8 @@ use tokio::{ time::{self, Instant}, }; use tokio_util::io::ReaderStream; -use tracing::{debug, error, info}; +use tracing::{Span, debug, error, info, info_span, instrument}; +use tracing_futures::Instrument; pub struct UploadedItem { pub link: String, @@ -57,6 +57,7 @@ pub struct UploadProgressEvent { // a typical recommended chunk size is 5MB (AWS min part size). const CHUNK_SIZE: u64 = 5 * 1024 * 1024; // 5MB +#[instrument(skip(channel))] pub async fn upload_video( app: &AppHandle, video_id: String, @@ -169,6 +170,7 @@ async fn file_reader_stream(path: impl AsRef) -> Result<(ReaderStream Result { let input = ffmpeg::format::input(path).map_err(|e| format!("Failed to read input file: {e}"))?; @@ -301,6 +305,7 @@ pub fn build_video_meta(path: &PathBuf) -> Result { }) } +#[instrument] pub async fn compress_image(path: PathBuf) -> Result, String> { task::spawn_blocking(move || { let img = ImageReader::open(&path) @@ -456,6 +461,7 @@ pub struct Chunk { /// Creates a stream that reads chunks from a file, yielding [Chunk]'s. #[allow(unused)] +#[instrument] pub fn from_file_to_chunks(path: PathBuf) -> impl Stream> { try_stream! { let file = File::open(path).await?; @@ -475,11 +481,13 @@ pub fn from_file_to_chunks(path: PathBuf) -> impl Stream>, @@ -583,6 +591,7 @@ pub fn from_pending_file_to_chunks( } } } + .instrument(Span::current()) } fn retryable_client(host: String) -> reqwest::ClientBuilder { @@ -604,36 +613,10 @@ fn retryable_client(host: String) -> reqwest::ClientBuilder { ) } -#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)] -pub struct UploadDebugEvent { - pub video_id: String, - pub upload_id: String, - pub epoch: String, - pub state: UploadDebugEventState, -} - -#[derive(Serialize, Type, Debug, Clone)] -pub enum UploadDebugEventState { - Pending, - Presigning { - part_number: u32, - chunk_size: String, - total_size: String, - }, - Uploading { - part_number: u32, - chunk_size: String, - total_size: String, - }, - PendingNextChunk { - prev_part_number: u32, - }, - Done, -} - /// Takes an incoming stream of bytes and individually uploads them to S3. /// /// Note: It's on the caller to ensure the chunks are sized correctly within S3 limits. +#[instrument(skip(stream))] fn multipart_uploader( app: AppHandle, video_id: String, @@ -641,25 +624,12 @@ fn multipart_uploader( stream: impl Stream>, ) -> impl Stream> { debug!("Initializing multipart uploader for video {video_id:?}"); - let start = Instant::now(); - UploadDebugEvent { - video_id: video_id.clone(), - upload_id: upload_id.clone(), - epoch: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|v| v.as_millis()) - .unwrap_or_default() - .to_string(), - state: UploadDebugEventState::Pending, - } - .emit(&app) - .ok(); - try_stream! { let mut stream = pin!(stream); let mut prev_part_number = None; + while let Some(item) = stream.next().await { let Chunk { total_size, part_number, chunk } = item.map_err(|err| format!("uploader/part/{:?}/fs: {err:?}", prev_part_number.map(|p| p + 1)))?; debug!("Uploading chunk {part_number} ({} bytes) for video {video_id:?}", chunk.len()); @@ -667,43 +637,12 @@ fn multipart_uploader( let md5_sum = base64::encode(md5::compute(&chunk).0); let size = chunk.len(); - UploadDebugEvent { - video_id: video_id.clone(), - upload_id: upload_id.clone(), - epoch: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|v| v.as_millis()) - .unwrap_or_default() - .to_string(), - state: UploadDebugEventState::Presigning { - part_number, - chunk_size: size.to_string(), - total_size: total_size.to_string(), - }, - } - .emit(&app) - .ok(); - let presigned_url = api::upload_multipart_presign_part(&app, &video_id, &upload_id, part_number, &md5_sum) .await?; - UploadDebugEvent { - video_id: video_id.clone(), - upload_id: upload_id.clone(), - epoch: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|v| v.as_millis()) - .unwrap_or_default() - .to_string(), - state: UploadDebugEventState::Uploading { - part_number, - chunk_size: size.to_string(), - total_size: total_size.to_string(), - }, - } - .emit(&app) - .ok(); + // async move { + debug!("Uploading part {part_number}"); let url = Uri::from_str(&presigned_url).map_err(|err| format!("uploader/part/{part_number}/invalid_url: {err:?}"))?; let resp = retryable_client(url.host().unwrap_or("").to_string()) @@ -725,20 +664,7 @@ fn multipart_uploader( false => Ok(()), }?; - UploadDebugEvent { - video_id: video_id.clone(), - upload_id: upload_id.clone(), - epoch: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|v| v.as_millis()) - .unwrap_or_default() - .to_string(), - state: UploadDebugEventState::PendingNextChunk { - prev_part_number: part_number - }, - } - .emit(&app) - .ok(); + debug!("Completed upload of part {part_number}"); yield UploadedPart { etag: etag.ok_or_else(|| format!("uploader/part/{part_number}/error: ETag header not found"))?, @@ -746,25 +672,18 @@ fn multipart_uploader( size, total_size }; + // } + // .instrument(tracing::info_span!("s3_upload_part")) + // .await } debug!("Completed multipart upload for {video_id:?} in {:?}", start.elapsed()); - UploadDebugEvent { - video_id: video_id.clone(), - upload_id: upload_id.clone(), - epoch: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|v| v.as_millis()) - .unwrap_or_default() - .to_string(), - state: UploadDebugEventState::Done, - } - .emit(&app) - .ok(); } + .instrument(Span::current()) } /// Takes an incoming stream of bytes and streams them to an S3 object. +#[instrument(skip(stream))] pub async fn singlepart_uploader( app: AppHandle, request: PresignedS3PutRequest, diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 5a5b113974..4306b570d9 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -18,7 +18,7 @@ use tauri::{ }; use tauri_specta::Event; use tokio::sync::RwLock; -use tracing::{debug, error, warn}; +use tracing::{debug, error, instrument, warn}; use crate::{ App, ArcLock, RequestScreenCapturePrewarm, fake_window, @@ -178,7 +178,7 @@ impl CapWindowId { } } -#[derive(Clone, Type, Deserialize)] +#[derive(Debug, Clone, Type, Deserialize)] pub enum ShowCapWindow { Setup, Main { @@ -816,6 +816,7 @@ fn add_traffic_lights(window: &WebviewWindow, controls_inset: Option None, @@ -832,6 +833,7 @@ pub fn set_theme(window: tauri::Window, theme: AppTheme) { #[tauri::command] #[specta::specta] +#[instrument] pub fn position_traffic_lights(_window: tauri::Window, _controls_inset: Option<(f64, f64)>) { #[cfg(target_os = "macos")] position_traffic_lights_impl( @@ -881,6 +883,7 @@ fn should_protect_window(app: &AppHandle, window_title: &str) -> bool { #[tauri::command] #[specta::specta] +#[instrument] pub fn refresh_window_content_protection(app: AppHandle) -> Result<(), String> { for (label, window) in app.webview_windows() { if let Ok(id) = CapWindowId::from_str(&label) { diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 6b7593047e..da55ae0c0e 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -307,7 +307,6 @@ requestOpenSettings: RequestOpenSettings, requestScreenCapturePrewarm: RequestScreenCapturePrewarm, requestStartRecording: RequestStartRecording, targetUnderCursor: TargetUnderCursor, -uploadDebugEvent: UploadDebugEvent, uploadProgressEvent: UploadProgressEvent }>({ audioInputLevelChange: "audio-input-level-change", @@ -330,7 +329,6 @@ requestOpenSettings: "request-open-settings", requestScreenCapturePrewarm: "request-screen-capture-prewarm", requestStartRecording: "request-start-recording", targetUnderCursor: "target-under-cursor", -uploadDebugEvent: "upload-debug-event", uploadProgressEvent: "upload-progress-event" }) @@ -472,8 +470,6 @@ export type StudioRecordingStatus = { status: "InProgress" } | { status: "Failed export type TargetUnderCursor = { display_id: DisplayId | null; window: WindowUnderCursor | null } export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments: ZoomSegment[]; sceneSegments?: SceneSegment[] } export type TimelineSegment = { recordingSegment?: number; timescale: number; start: number; end: number } -export type UploadDebugEvent = { video_id: string; upload_id: string; epoch: string; state: UploadDebugEventState } -export type UploadDebugEventState = "Pending" | { Presigning: { part_number: number; chunk_size: string; total_size: string } } | { Uploading: { part_number: number; chunk_size: string; total_size: string } } | { PendingNextChunk: { prev_part_number: number } } | "Done" export type UploadMeta = { state: "MultipartUpload"; video_id: string; file_path: string; pre_created_video: VideoUploadInfo; recording_dir: string } | { state: "SinglePartUpload"; video_id: string; recording_dir: string; file_path: string; screenshot_path: string } | { state: "Failed"; error: string } | { state: "Complete" } export type UploadMode = { Initial: { pre_created_video: VideoUploadInfo | null } } | "Reupload" export type UploadProgress = { progress: number } From f824fad3ae80355c17cf738dee9754175cc93c3b Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 15:15:53 +0800 Subject: [PATCH 09/15] drop old UI --- apps/desktop/src-tauri/src/tracing_utils.rs | 245 -------------- .../settings/UploadStatsForNerds.tsx | 319 ------------------ .../(window-chrome)/settings/experimental.tsx | 18 +- 3 files changed, 1 insertion(+), 581 deletions(-) delete mode 100644 apps/desktop/src-tauri/src/tracing_utils.rs delete mode 100644 apps/desktop/src/routes/(window-chrome)/settings/UploadStatsForNerds.tsx diff --git a/apps/desktop/src-tauri/src/tracing_utils.rs b/apps/desktop/src-tauri/src/tracing_utils.rs deleted file mode 100644 index 339e97719f..0000000000 --- a/apps/desktop/src-tauri/src/tracing_utils.rs +++ /dev/null @@ -1,245 +0,0 @@ -use std::time::Instant; -#[cfg(debug_assertions)] -use tracing::{Instrument, info_span}; -use tracing::{debug, info}; - -/// A tracing-based replacement for UploadDebugEvent that provides structured logging -/// and OpenTelemetry spans for upload debugging in debug builds. -pub struct UploadTracer { - pub video_id: String, - pub upload_id: String, - start_time: Instant, -} - -impl UploadTracer { - pub fn new(video_id: String, upload_id: String) -> Self { - debug!( - video_id = %video_id, - upload_id = %upload_id, - "Initializing upload tracer" - ); - - Self { - video_id, - upload_id, - start_time: Instant::now(), - } - } - - /// Create a span for the entire upload operation - #[cfg(debug_assertions)] - pub fn upload_span(&self, operation: F) -> R - where - F: FnOnce() -> R, - { - let span = info_span!( - "upload_operation", - video_id = %self.video_id, - upload_id = %self.upload_id, - elapsed_ms = tracing::field::Empty - ); - - let _enter = span.enter(); - let start = Instant::now(); - let result = operation(); - let elapsed = start.elapsed(); - - span.record("elapsed_ms", elapsed.as_millis()); - info!( - elapsed_ms = elapsed.as_millis(), - "Upload operation completed" - ); - - result - } - - /// For release builds, just execute the operation without tracing overhead - #[cfg(not(debug_assertions))] - pub fn upload_span(&self, operation: F) -> R - where - F: FnOnce() -> R, - { - operation() - } - - /// Create a span for presigning operations - #[cfg(debug_assertions)] - pub async fn presign_span( - &self, - part_number: u32, - chunk_size: usize, - total_size: u64, - operation: F, - ) -> R - where - F: std::future::Future, - { - let span = info_span!( - "presign_part", - video_id = %self.video_id, - upload_id = %self.upload_id, - part_number = part_number, - chunk_size = chunk_size, - total_size = total_size, - elapsed_ms = tracing::field::Empty - ); - - let start = Instant::now(); - let result = operation.instrument(span.clone()).await; - let elapsed = start.elapsed(); - - span.record("elapsed_ms", elapsed.as_millis()); - info!( - part_number = part_number, - chunk_size = chunk_size, - elapsed_ms = elapsed.as_millis(), - "Presigning completed" - ); - - result - } - - #[cfg(not(debug_assertions))] - pub async fn presign_span( - &self, - _part_number: u32, - _chunk_size: usize, - _total_size: u64, - operation: F, - ) -> R - where - F: std::future::Future, - { - operation.await - } - - /// Create a span for chunk upload operations - #[cfg(debug_assertions)] - pub async fn upload_chunk_span( - &self, - part_number: u32, - chunk_size: usize, - total_size: u64, - operation: F, - ) -> R - where - F: std::future::Future, - { - let span = info_span!( - "upload_chunk", - video_id = %self.video_id, - upload_id = %self.upload_id, - part_number = part_number, - chunk_size = chunk_size, - total_size = total_size, - elapsed_ms = tracing::field::Empty - ); - - let start = Instant::now(); - let result = operation.instrument(span.clone()).await; - let elapsed = start.elapsed(); - - span.record("elapsed_ms", elapsed.as_millis()); - info!( - part_number = part_number, - chunk_size = chunk_size, - elapsed_ms = elapsed.as_millis(), - "Chunk upload completed" - ); - - result - } - - #[cfg(not(debug_assertions))] - pub async fn upload_chunk_span( - &self, - _part_number: u32, - _chunk_size: usize, - _total_size: u64, - operation: F, - ) -> R - where - F: std::future::Future, - { - operation.await - } - - /// Log when waiting for next chunk - pub fn log_pending_next_chunk(&self, prev_part_number: u32) { - debug!( - video_id = %self.video_id, - upload_id = %self.upload_id, - prev_part_number = prev_part_number, - "Pending next chunk" - ); - } - - /// Log upload completion with total time - pub fn log_completion(&self) { - let total_elapsed = self.start_time.elapsed(); - info!( - video_id = %self.video_id, - upload_id = %self.upload_id, - total_elapsed_ms = total_elapsed.as_millis(), - total_elapsed_secs = total_elapsed.as_secs(), - "Upload completed" - ); - } - - /// Create a span for the entire multipart upload process - #[cfg(debug_assertions)] - pub async fn multipart_upload_span(&self, operation: F) -> R - where - F: std::future::Future, - { - let span = info_span!( - "multipart_upload", - video_id = %self.video_id, - upload_id = %self.upload_id, - total_elapsed_ms = tracing::field::Empty - ); - - let start = Instant::now(); - let result = operation.instrument(span.clone()).await; - let elapsed = start.elapsed(); - - span.record("total_elapsed_ms", elapsed.as_millis()); - - result - } - - #[cfg(not(debug_assertions))] - pub async fn multipart_upload_span(&self, operation: F) -> R - where - F: std::future::Future, - { - operation.await - } -} - -/// Helper macro for creating timed spans with automatic timing -#[macro_export] -macro_rules! timed_span { - ($span_name:expr, $($field:ident = $value:expr),*) => {{ - #[cfg(debug_assertions)] - { - let span = tracing::info_span!( - $span_name, - elapsed_ms = tracing::field::Empty, - $($field = $value),* - ); - let _enter = span.enter(); - let start = std::time::Instant::now(); - let result = { - // Code block will be inserted here by the caller - }; - let elapsed = start.elapsed(); - span.record("elapsed_ms", elapsed.as_millis()); - result - } - #[cfg(not(debug_assertions))] - { - // Code block will be inserted here by the caller - } - }}; -} diff --git a/apps/desktop/src/routes/(window-chrome)/settings/UploadStatsForNerds.tsx b/apps/desktop/src/routes/(window-chrome)/settings/UploadStatsForNerds.tsx deleted file mode 100644 index 490fd0ed1c..0000000000 --- a/apps/desktop/src/routes/(window-chrome)/settings/UploadStatsForNerds.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { ProgressCircle } from "@cap/ui-solid"; -import { Dialog } from "@kobalte/core/dialog"; -import { createMutation } from "@tanstack/solid-query"; -import { Channel } from "@tauri-apps/api/core"; -import { cx } from "cva"; -import { createSignal, createMemo, Show, For } from "solid-js"; -import { createStore } from "solid-js/store"; -import { createTauriEventListener } from "~/utils/createEventListener"; -import { - commands, - events, - UploadDebugEvent, - UploadProgress, -} from "~/utils/tauri"; - -interface UploadDebugEventWithTimestamp extends UploadDebugEvent { - timestamp: number; -} - -export function UploadStatsForNerds(props: { - open: boolean; - onOpenChange: (open: boolean) => void; -}) { - const [debugEvents, setDebugEvents] = createStore< - Record - >({}); - - const [hoveredEvent, setHoveredEvent] = - createSignal(null); - - createTauriEventListener(events.uploadDebugEvent, (e) => { - console.log(e); - const eventWithTimestamp: UploadDebugEventWithTimestamp = { - ...e, - timestamp: Date.now(), - }; - - const key = `${e.video_id}:${e.upload_id}`; - setDebugEvents(key, (prev = []) => [...prev, eventWithTimestamp]); - }); - - const allEvents = createMemo(() => { - const events = Object.values(debugEvents).flat(); - return events.sort((a, b) => a.timestamp - b.timestamp); - }); - - const timeRange = createMemo(() => { - const events = allEvents(); - if (events.length === 0) return { start: 0, end: 0 }; - const start = Math.min(...events.map((e) => e.timestamp)); - const end = Math.max(...events.map((e) => e.timestamp)); - return { start, end: Math.max(end, start + 60000) }; // At least 1 minute range - }); - - const getEventColor = (state: UploadDebugEvent["state"]) => { - switch (state) { - case "Pending": - return "bg-yellow-500"; - case "Done": - return "bg-green-500"; - default: - if (typeof state === "object") { - if ("Presigning" in state) return "bg-blue-500"; - if ("Uploading" in state) return "bg-purple-500"; - if ("PendingNextChunk" in state) return "bg-orange-500"; - } - return "bg-gray-500"; - } - }; - - const formatTimestamp = (timestamp: number) => { - return new Date(timestamp).toLocaleTimeString([], { - hour12: false, - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }); - }; - - const getStateLabel = (state: UploadDebugEvent["state"]): string => { - if (typeof state === "string") return state; - if (typeof state === "object") { - if ("Presigning" in state) - return `Presigning Part ${state.Presigning.part_number}`; - if ("Uploading" in state) - return `Uploading Part ${state.Uploading.part_number}`; - if ("PendingNextChunk" in state) - return `Pending After Part ${state.PendingNextChunk.prev_part_number}`; - } - return "Unknown"; - }; - - const getEventDetails = (event: UploadDebugEventWithTimestamp): string => { - const state = event.state; - let details = `Video ID: ${event.video_id}\nUpload ID: ${event.upload_id}\nTime: ${formatTimestamp(event.timestamp)}\nState: ${getStateLabel(state)}`; - - if (typeof state === "object") { - if ("Presigning" in state) { - details += `\nPart: ${state.Presigning.part_number}\nChunk Size: ${state.Presigning.chunk_size}\nTotal Size: ${state.Presigning.total_size}`; - } else if ("Uploading" in state) { - details += `\nPart: ${state.Uploading.part_number}\nChunk Size: ${state.Uploading.chunk_size}\nTotal Size: ${state.Uploading.total_size}`; - } - } - - return details; - }; - - return ( - - - -
- -
- - Upload Debug Timeline ({allEvents().length} events) - - - ✕ - -
- -
- 0} - fallback={ -
-
📊
-
-

No debug events recorded yet

-

- Start uploading a recording to see the timeline -

-
-
- } - > -
- {/* Timeline container */} -
-
- {/* Time axis */} -
-
- {formatTimestamp(timeRange().start)} - Timeline - {formatTimestamp(timeRange().end)} -
-
- - {/* Event tracks */} -
- - {([key, events]) => ( -
-
- {key} -
-
- {/* Timeline background */} -
- {/* Grid lines */} - i, - )} - > - {(i) => ( -
- )} - -
- - {/* Events */} - - {(event) => { - const position = - ((event.timestamp - timeRange().start) / - (timeRange().end - timeRange().start)) * - 100; - return ( -
- setHoveredEvent(event) - } - onMouseLeave={() => - setHoveredEvent(null) - } - /> - ); - }} - -
-
- )} - -
-
-
- - {/* Legend */} -
-
- Legend: -
-
-
-
- Pending -
-
-
- Presigning -
-
-
- Uploading -
-
-
- Pending Next Chunk -
-
-
- Done -
-
-
-
- -
- - {/* Hover tooltip */} - - {(event) => ( -
- {getEventDetails(event())} -
- )} -
- -
- -
- ); -} - -function InstantModeActions(props: { - recording: Recording; - uploadProgress: number | undefined; -}) { - const reupload = createMutation(() => ({ - mutationFn: () => - commands.uploadExportedVideo( - props.recording.path, - "Reupload", - new Channel((progress) => {}), - ), - })); - - return ( - <> - reupload.mutate()} - > - - - } - > - - - - - {(sharing) => ( - shell.open(sharing().link)} - > - - - )} - - - ); -} diff --git a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx index cef17d35f5..a9dc595839 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx @@ -3,9 +3,7 @@ import { createStore } from "solid-js/store"; import { generalSettingsStore } from "~/store"; import type { GeneralSettingsStore } from "~/utils/tauri"; -import { Setting, ToggleSetting } from "./Setting"; -import { UploadStatsForNerds } from "./UploadStatsForNerds"; -import { Button } from "@cap/ui-solid"; +import { ToggleSetting } from "./Setting"; export default function ExperimentalSettings() { const [store] = createResource(() => generalSettingsStore.get()); @@ -31,7 +29,6 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { enableNewUploader: false, }, ); - const [isStatsModalOpen, setIsStatsModalOpen] = createSignal(false); const handleChange = async ( key: K, @@ -113,19 +110,6 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { ); }} /> - - - - - From 142cc78eef15b046365fe4dc13c6c64ed362ba06 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 15:17:19 +0800 Subject: [PATCH 10/15] reset --- .../(window-chrome)/settings/experimental.tsx | 2 +- .../(window-chrome)/settings/recordings.tsx | 62 +++++++++++++++---- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx index a9dc595839..114453a984 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx @@ -1,4 +1,4 @@ -import { createResource, createSignal, Show } from "solid-js"; +import { createResource, Show } from "solid-js"; import { createStore } from "solid-js/store"; import { generalSettingsStore } from "~/store"; diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index 3d606aa26c..9944f0ac37 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -1,5 +1,4 @@ import { ProgressCircle } from "@cap/ui-solid"; -import { Dialog } from "@kobalte/core/dialog"; import Tooltip from "@corvu/tooltip"; import { createMutation, @@ -14,6 +13,7 @@ import { revealItemInDir } from "@tauri-apps/plugin-opener"; import * as shell from "@tauri-apps/plugin-shell"; import { cx } from "cva"; import { + createEffect, createMemo, createSignal, For, @@ -30,7 +30,6 @@ import { events, type RecordingMetaWithMetadata, type UploadProgress, - type UploadDebugEvent, } from "~/utils/tauri"; type Recording = { @@ -139,13 +138,11 @@ export default function Recordings() { return (
-
-
-

Previous Recordings

-

- Manage your recordings and perform actions. -

-
+
+

Previous Recordings

+

+ Manage your recordings and perform actions. +

0} @@ -320,10 +317,49 @@ function RecordingItem(props: { - + {(_) => { + const reupload = createMutation(() => ({ + mutationFn: () => + commands.uploadExportedVideo( + props.recording.path, + "Reupload", + new Channel((progress) => {}), + ), + })); + + return ( + <> + reupload.mutate()} + > + + + } + > + + + + + {(sharing) => ( + shell.open(sharing().link)} + > + + + )} + + + ); + }} Date: Tue, 14 Oct 2025 15:25:33 +0800 Subject: [PATCH 11/15] add otel script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f201da2f1..a0cf8ce2e1 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "web": "pnpm --filter=@cap/web", "env-setup": "node scripts/env-cli.js", "check-tauri-versions": "node scripts/check-tauri-plugin-versions.js", - "clean": "find . -name node_modules -o -name .next -o -name .output -o -name .turbo -o -name dist -type d -prune | xargs rm -rf" + "clean": "find . -name node_modules -o -name .next -o -name .output -o -name .turbo -o -name dist -type d -prune | xargs rm -rf", + "run-otel": "docker run -p 9058:3000 -p 4317:4317 -p 4318:4318 --rm -it docker.io/grafana/otel-lgtm" }, "devDependencies": { "@biomejs/biome": "2.2.0", From ffa0355b6c29e288dfc73425a19a50bcde5e7701 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 15:29:05 +0800 Subject: [PATCH 12/15] fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0cf8ce2e1..fd3c5da4f0 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "env-setup": "node scripts/env-cli.js", "check-tauri-versions": "node scripts/check-tauri-plugin-versions.js", "clean": "find . -name node_modules -o -name .next -o -name .output -o -name .turbo -o -name dist -type d -prune | xargs rm -rf", - "run-otel": "docker run -p 9058:3000 -p 4317:4317 -p 4318:4318 --rm -it docker.io/grafana/otel-lgtm" + "lgtm-otel": "docker run -p 9058:3000 -p 4317:4317 -p 4318:4318 --rm -it docker.io/grafana/otel-lgtm" }, "devDependencies": { "@biomejs/biome": "2.2.0", From 7c251cd3a7ad75abe08aee4c51b4692462ff1ee2 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 15:29:31 +0800 Subject: [PATCH 13/15] fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd3c5da4f0..40d8264d0f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "env-setup": "node scripts/env-cli.js", "check-tauri-versions": "node scripts/check-tauri-plugin-versions.js", "clean": "find . -name node_modules -o -name .next -o -name .output -o -name .turbo -o -name dist -type d -prune | xargs rm -rf", - "lgtm-otel": "docker run -p 9058:3000 -p 4317:4317 -p 4318:4318 --rm -it docker.io/grafana/otel-lgtm" + "lgtm-otel": "docker run -p 3010:3000 -p 4317:4317 -p 4318:4318 --rm -it docker.io/grafana/otel-lgtm" }, "devDependencies": { "@biomejs/biome": "2.2.0", From 92f33baccafa1283d4ff3689591f78d57f3f5c54 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 15:43:27 +0800 Subject: [PATCH 14/15] dont blow up the logs --- apps/desktop/src-tauri/src/captions.rs | 16 ++--- apps/desktop/src-tauri/src/fake_window.rs | 2 +- apps/desktop/src-tauri/src/hotkeys.rs | 2 +- apps/desktop/src-tauri/src/lib.rs | 60 +++++++++---------- apps/desktop/src-tauri/src/recording.rs | 10 ++-- .../src-tauri/src/target_select_overlay.rs | 4 +- apps/desktop/src-tauri/src/windows.rs | 7 ++- 7 files changed, 48 insertions(+), 53 deletions(-) diff --git a/apps/desktop/src-tauri/src/captions.rs b/apps/desktop/src-tauri/src/captions.rs index ee8a89d58a..88e4ec457c 100644 --- a/apps/desktop/src-tauri/src/captions.rs +++ b/apps/desktop/src-tauri/src/captions.rs @@ -716,11 +716,11 @@ pub async fn transcribe_audio( /// Function to save caption data to a file #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] pub async fn save_captions( + app: AppHandle, video_id: String, captions: CaptionData, - app: AppHandle, ) -> Result<(), String> { tracing::info!("Saving captions for video_id: {}", video_id); @@ -971,10 +971,10 @@ pub fn parse_captions_json(json: &str) -> Result Result, String> { let captions_dir = app_captions_dir(&app, &video_id)?; let captions_path = captions_dir.join("captions.json"); @@ -1049,7 +1049,7 @@ impl DownloadProgress { /// Helper function to download a Whisper model from Hugging Face Hub #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(window))] pub async fn download_whisper_model( window: Window, model_name: String, @@ -1194,15 +1194,15 @@ fn format_srt_time(seconds: f64) -> String { /// Export captions to an SRT file #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] pub async fn export_captions_srt( - video_id: String, app: AppHandle, + video_id: String, ) -> Result, String> { tracing::info!("Starting SRT export for video_id: {}", video_id); // Load captions - let captions = match load_captions(video_id.clone(), app.clone()).await? { + let captions = match load_captions(app.clone(), video_id.clone()).await? { Some(c) => { tracing::info!("Found {} caption segments to export", c.segments.len()); c diff --git a/apps/desktop/src-tauri/src/fake_window.rs b/apps/desktop/src-tauri/src/fake_window.rs index 93b96dd46c..149b0952e6 100644 --- a/apps/desktop/src-tauri/src/fake_window.rs +++ b/apps/desktop/src-tauri/src/fake_window.rs @@ -25,7 +25,7 @@ pub async fn set_fake_window_bounds( #[tauri::command] #[specta::specta] -#[instrument(skip(state))] +#[instrument(skip(state, window))] pub async fn remove_fake_window( window: tauri::Window, name: String, diff --git a/apps/desktop/src-tauri/src/hotkeys.rs b/apps/desktop/src-tauri/src/hotkeys.rs index 3bda4e3b1b..14bf3d38cd 100644 --- a/apps/desktop/src-tauri/src/hotkeys.rs +++ b/apps/desktop/src-tauri/src/hotkeys.rs @@ -181,7 +181,7 @@ async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), Strin #[tauri::command(async)] #[specta::specta] -#[instrument] +#[instrument(skip(app))] pub fn set_hotkey(app: AppHandle, action: HotkeyAction, hotkey: Option) -> Result<(), ()> { let global_shortcut = app.global_shortcut(); let state = app.state::(); diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 8bdec576f9..29338b56c8 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -130,12 +130,6 @@ pub struct App { server_url: String, } -impl fmt::Debug for App { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("App").finish() - } -} - #[derive(specta::Type, Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub enum VideoType { @@ -227,7 +221,7 @@ impl App { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(state))] async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> Result<(), String> { let mic_feed = state.read().await.mic_feed.clone(); @@ -253,7 +247,7 @@ async fn set_mic_input(state: MutableState<'_, App>, label: Option) -> R #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app_handle, state))] async fn set_camera_input( app_handle: AppHandle, state: MutableState<'_, App>, @@ -397,7 +391,7 @@ struct CurrentRecording { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(state))] async fn get_current_recording( state: MutableState<'_, App>, ) -> Result>, ()> { @@ -585,7 +579,7 @@ async fn create_screenshot( #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] async fn copy_file_to_path(app: AppHandle, src: String, dst: String) -> Result<(), String> { println!("Attempting to copy file from {src} to {dst}"); @@ -728,7 +722,7 @@ async fn copy_screenshot_to_clipboard( #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(_app))] async fn open_file_path(_app: AppHandle, path: PathBuf) -> Result<(), String> { let path_str = path.to_str().ok_or("Invalid path")?; @@ -823,7 +817,7 @@ struct SerializedEditorInstance { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(window))] async fn create_editor_instance(window: Window) -> Result { let CapWindowId::Editor { id } = CapWindowId::from_str(window.label()).unwrap() else { return Err("Invalid window".to_string()); @@ -875,7 +869,7 @@ async fn set_pretty_name(editor: WindowEditorInstance, pretty_name: String) -> R #[tauri::command] #[specta::specta] -#[instrument(skip(clipboard))] +#[instrument(skip(app, clipboard))] async fn copy_video_to_clipboard( app: AppHandle, clipboard: MutableState<'_, ClipboardContext>, @@ -973,7 +967,7 @@ async fn get_video_metadata(path: PathBuf) -> Result, @@ -1259,7 +1253,7 @@ async fn upload_screenshot( #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app, _state))] async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Result<(), String> { let id = uuid::Uuid::new_v4().to_string(); @@ -1388,7 +1382,7 @@ async fn take_screenshot(app: AppHandle, _state: MutableState<'_, App>) -> Resul #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] async fn save_file_dialog( app: AppHandle, file_name: String, @@ -1520,7 +1514,7 @@ fn get_recording_meta( #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] fn list_recordings(app: AppHandle) -> Result, String> { let recordings_dir = recordings_path(&app); @@ -1567,7 +1561,7 @@ fn list_recordings(app: AppHandle) -> Result Result, String> { let screenshots_dir = screenshots_path(&app); @@ -1611,7 +1605,7 @@ fn list_screenshots(app: AppHandle) -> Result, Str #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] async fn check_upgraded_and_update(app: AppHandle) -> Result { println!("Checking upgraded status and updating..."); @@ -1674,7 +1668,7 @@ async fn check_upgraded_and_update(app: AppHandle) -> Result { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] fn open_external_link(app: tauri::AppHandle, url: String) -> Result<(), String> { if let Ok(Some(settings)) = GeneralSettingsStore::get(&app) && settings.disable_auto_open_links @@ -1690,7 +1684,7 @@ fn open_external_link(app: tauri::AppHandle, url: String) -> Result<(), String> #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(_app))] async fn reset_camera_permissions(_app: AppHandle) -> Result<(), String> { #[cfg(target_os = "macos")] { @@ -1713,7 +1707,7 @@ async fn reset_camera_permissions(_app: AppHandle) -> Result<(), String> { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(_app))] async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), ()> { #[cfg(debug_assertions)] let bundle_id = "com.apple.Terminal"; @@ -1732,7 +1726,7 @@ async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), ()> { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] async fn is_camera_window_open(app: AppHandle) -> bool { CapWindowId::Camera.get(&app).is_some() } @@ -1788,7 +1782,7 @@ async fn get_system_audio_waveforms( #[tauri::command] #[specta::specta] -#[instrument(skip(editor_instance))] +#[instrument(skip(app, editor_instance, window))] async fn editor_delete_project( app: tauri::AppHandle, editor_instance: WindowEditorInstance, @@ -1811,7 +1805,7 @@ async fn editor_delete_project( // keep this async otherwise opening windows may hang on windows #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] async fn show_window(app: AppHandle, window: ShowCapWindow) -> Result<(), String> { let _ = window.show(&app).await; Ok(()) @@ -1891,7 +1885,7 @@ async fn check_notification_permissions(app: AppHandle) { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] async fn set_server_url(app: MutableState<'_, App>, server_url: String) -> Result<(), ()> { app.write().await.server_url = server_url; Ok(()) @@ -1899,7 +1893,7 @@ async fn set_server_url(app: MutableState<'_, App>, server_url: String) -> Resul #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] async fn set_camera_preview_state( app: MutableState<'_, App>, state: CameraPreviewState, @@ -1915,7 +1909,7 @@ async fn set_camera_preview_state( #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] async fn await_camera_preview_ready(app: MutableState<'_, App>) -> Result { let app = app.read().await.camera_feed.clone(); @@ -1930,7 +1924,7 @@ async fn await_camera_preview_ready(app: MutableState<'_, App>) -> Result PathBuf { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] fn global_message_dialog(app: AppHandle, message: String) { app.dialog().message(message).show(|_| {}); } diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index dd806fcd52..a9617babf5 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -665,7 +665,7 @@ pub async fn start_recording( #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(state))] pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; @@ -678,7 +678,7 @@ pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String> #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(state))] pub async fn resume_recording(state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; @@ -691,7 +691,7 @@ pub async fn resume_recording(state: MutableState<'_, App>) -> Result<(), String #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app, state))] pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { let mut state = state.write().await; let Some(current_recording) = state.clear_current_recording() else { @@ -708,7 +708,7 @@ pub async fn stop_recording(app: AppHandle, state: MutableState<'_, App>) -> Res #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app, state))] pub async fn restart_recording( app: AppHandle, state: MutableState<'_, App>, @@ -730,7 +730,7 @@ pub async fn restart_recording( #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app, state))] pub async fn delete_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> { let recording_data = { let mut app_state = state.write().await; diff --git a/apps/desktop/src-tauri/src/target_select_overlay.rs b/apps/desktop/src-tauri/src/target_select_overlay.rs index 1d1f9c7dc0..c9aab06b1b 100644 --- a/apps/desktop/src-tauri/src/target_select_overlay.rs +++ b/apps/desktop/src-tauri/src/target_select_overlay.rs @@ -42,7 +42,7 @@ pub struct DisplayInformation { #[specta::specta] #[tauri::command] -#[instrument(skip(state))] +#[instrument(skip(app, state))] pub async fn open_target_select_overlays( app: AppHandle, state: tauri::State<'_, WindowFocusManager>, @@ -103,7 +103,7 @@ pub async fn open_target_select_overlays( #[specta::specta] #[tauri::command] -#[instrument] +#[instrument(skip(app))] pub async fn close_target_select_overlays(app: AppHandle) -> Result<(), String> { for (id, window) in app.webview_windows() { if let Ok(CapWindowId::TargetSelectOverlay { .. }) = CapWindowId::from_str(&id) { diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 4306b570d9..5245414184 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -816,7 +816,7 @@ fn add_traffic_lights(window: &WebviewWindow, controls_inset: Option None, @@ -833,7 +833,7 @@ pub fn set_theme(window: tauri::Window, theme: AppTheme) { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(_window))] pub fn position_traffic_lights(_window: tauri::Window, _controls_inset: Option<(f64, f64)>) { #[cfg(target_os = "macos")] position_traffic_lights_impl( @@ -883,7 +883,7 @@ fn should_protect_window(app: &AppHandle, window_title: &str) -> bool { #[tauri::command] #[specta::specta] -#[instrument] +#[instrument(skip(app))] pub fn refresh_window_content_protection(app: AppHandle) -> Result<(), String> { for (label, window) in app.webview_windows() { if let Ok(id) = CapWindowId::from_str(&label) { @@ -966,6 +966,7 @@ impl MonitorExt for Display { #[specta::specta] #[tauri::command(async)] +#[instrument(skip(_window))] pub fn set_window_transparent(_window: tauri::Window, _value: bool) { #[cfg(target_os = "macos")] { From 52a635b91fd89f892e166e43a98c42d6152558b8 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 14 Oct 2025 15:48:55 +0800 Subject: [PATCH 15/15] fixs --- apps/desktop/src-tauri/Cargo.toml | 11 +++++------ apps/desktop/src-tauri/src/upload.rs | 22 +++++++++------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 1fa839b662..c5f149fd78 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -108,6 +108,11 @@ thiserror.workspace = true bytes = "1.10.1" async-stream = "0.3.6" tracing-futures = { version = "0.2.5", features = ["futures-03"] } +tracing-opentelemetry = "0.32.0" +opentelemetry = "0.31.0" +opentelemetry-otlp = "0.31.0" #{ version = , features = ["http-proto", "reqwest-client"] } +opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio", "trace"] } + [target.'cfg(target_os = "macos")'.dependencies] core-graphics = "0.24.0" @@ -134,9 +139,3 @@ windows-sys = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { version = "0.29.0", features = ["fs"] } - -# [target.'cfg(debug_assertions)'.dependencies] -tracing-opentelemetry = "0.32.0" -opentelemetry = "0.31.0" -opentelemetry-otlp = "0.31.0" #{ version = , features = ["http-proto", "reqwest-client"] } -opentelemetry_sdk = { version = "0.31.0", features = ["rt-tokio", "trace"] } diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index ef927923ef..f741806ebc 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -37,7 +37,7 @@ use tokio::{ time::{self, Instant}, }; use tokio_util::io::ReaderStream; -use tracing::{Span, debug, error, info, info_span, instrument}; +use tracing::{Span, debug, error, info, instrument, trace}; use tracing_futures::Instrument; pub struct UploadedItem { @@ -57,7 +57,7 @@ pub struct UploadProgressEvent { // a typical recommended chunk size is 5MB (AWS min part size). const CHUNK_SIZE: u64 = 5 * 1024 * 1024; // 5MB -#[instrument(skip(channel))] +#[instrument(skip(app, channel))] pub async fn upload_video( app: &AppHandle, video_id: String, @@ -170,7 +170,7 @@ async fn file_reader_stream(path: impl AsRef) -> Result<(ReaderStream reqwest::ClientBuilder { /// Takes an incoming stream of bytes and individually uploads them to S3. /// /// Note: It's on the caller to ensure the chunks are sized correctly within S3 limits. -#[instrument(skip(stream))] +#[instrument(skip(app, stream))] fn multipart_uploader( app: AppHandle, video_id: String, @@ -632,7 +632,7 @@ fn multipart_uploader( while let Some(item) = stream.next().await { let Chunk { total_size, part_number, chunk } = item.map_err(|err| format!("uploader/part/{:?}/fs: {err:?}", prev_part_number.map(|p| p + 1)))?; - debug!("Uploading chunk {part_number} ({} bytes) for video {video_id:?}", chunk.len()); + trace!("Uploading chunk {part_number} ({} bytes) for video {video_id:?}", chunk.len()); prev_part_number = Some(part_number); let md5_sum = base64::encode(md5::compute(&chunk).0); let size = chunk.len(); @@ -641,8 +641,7 @@ fn multipart_uploader( api::upload_multipart_presign_part(&app, &video_id, &upload_id, part_number, &md5_sum) .await?; - // async move { - debug!("Uploading part {part_number}"); + trace!("Uploading part {part_number}"); let url = Uri::from_str(&presigned_url).map_err(|err| format!("uploader/part/{part_number}/invalid_url: {err:?}"))?; let resp = retryable_client(url.host().unwrap_or("").to_string()) @@ -664,7 +663,7 @@ fn multipart_uploader( false => Ok(()), }?; - debug!("Completed upload of part {part_number}"); + trace!("Completed upload of part {part_number}"); yield UploadedPart { etag: etag.ok_or_else(|| format!("uploader/part/{part_number}/error: ETag header not found"))?, @@ -672,9 +671,6 @@ fn multipart_uploader( size, total_size }; - // } - // .instrument(tracing::info_span!("s3_upload_part")) - // .await } debug!("Completed multipart upload for {video_id:?} in {:?}", start.elapsed()); @@ -683,7 +679,7 @@ fn multipart_uploader( } /// Takes an incoming stream of bytes and streams them to an S3 object. -#[instrument(skip(stream))] +#[instrument(skip(app, stream))] pub async fn singlepart_uploader( app: AppHandle, request: PresignedS3PutRequest,