diff --git a/crates/store/re_data_source/src/data_source.rs b/crates/store/re_data_source/src/data_source.rs index 5712ffa26770..353e97dac289 100644 --- a/crates/store/re_data_source/src/data_source.rs +++ b/crates/store/re_data_source/src/data_source.rs @@ -240,6 +240,92 @@ impl LogDataSource { Self::RedapProxy(uri) => Ok(re_grpc_client::stream(uri)), } } + + /// Returns analytics data for this data source. + pub fn analytics(&self) -> LogDataSourceAnalytics { + match self { + Self::RrdHttpUrl { url, .. } => { + let file_extension = std::path::Path::new(url.path()) + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.to_lowercase()); + LogDataSourceAnalytics { + source_type: "rrd_http_url", + file_extension, + file_source: None, + } + } + + #[cfg(not(target_arch = "wasm32"))] + Self::FilePath(file_src, path) => { + let file_extension = path + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.to_lowercase()); + LogDataSourceAnalytics { + source_type: "file_path", + file_extension, + file_source: Some(Self::file_source_to_analytics_str(file_src)), + } + } + + Self::FileContents(file_src, file_contents) => { + let file_extension = std::path::Path::new(&file_contents.name) + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.to_lowercase()); + LogDataSourceAnalytics { + source_type: "file_contents", + file_extension, + file_source: Some(Self::file_source_to_analytics_str(file_src)), + } + } + + #[cfg(not(target_arch = "wasm32"))] + Self::Stdin => LogDataSourceAnalytics { + source_type: "stdin", + file_extension: None, + file_source: None, + }, + + Self::RedapDatasetSegment { .. } => LogDataSourceAnalytics { + source_type: "redap_dataset_segment", + file_extension: None, + file_source: None, + }, + + Self::RedapProxy(_) => LogDataSourceAnalytics { + source_type: "redap_proxy", + file_extension: None, + file_source: None, + }, + } + } + + fn file_source_to_analytics_str(file_source: &re_log_types::FileSource) -> &'static str { + use re_log_types::FileSource; + match file_source { + FileSource::Cli => "cli", + FileSource::Uri => "uri", + FileSource::DragAndDrop { .. } => "drag_and_drop", + FileSource::FileDialog { .. } => "file_dialog", + FileSource::Sdk => "sdk", + } + } +} + +/// Analytics data extracted from a [`LogDataSource`]. +#[derive(Clone, Debug)] +pub struct LogDataSourceAnalytics { + /// The type of data source (e.g., "file", "http", ``redap_grpc``, "stdin"). + pub source_type: &'static str, + + /// The file extension if applicable (e.g., "rrd", "png", "glb"). + pub file_extension: Option, + + /// How the file was opened (e.g., "cli", ``file_dialog``, ``drag_and_drop``). + /// Only applicable for file-based sources. + pub file_source: Option<&'static str>, } // TODO(ab, andreas): This should be replaced by the use of `AsyncRuntimeHandle`. However, this diff --git a/crates/store/re_data_source/src/lib.rs b/crates/store/re_data_source/src/lib.rs index 69477fa407e6..8bfa2b979495 100644 --- a/crates/store/re_data_source/src/lib.rs +++ b/crates/store/re_data_source/src/lib.rs @@ -11,7 +11,7 @@ mod data_source; #[cfg(not(target_arch = "wasm32"))] mod load_stdin; -pub use self::data_source::LogDataSource; +pub use self::data_source::{LogDataSource, LogDataSourceAnalytics}; // ---------------------------------------------------------------------------- diff --git a/crates/utils/re_analytics/src/event.rs b/crates/utils/re_analytics/src/event.rs index 1db0f1ce5ef7..5d25eb22195c 100644 --- a/crates/utils/re_analytics/src/event.rs +++ b/crates/utils/re_analytics/src/event.rs @@ -458,6 +458,47 @@ impl Properties for SetPersonProperty { // ----------------------------------------------- +/// Tracks when a data source is loaded from the viewer. +/// +/// This is sent when a user opens a file, URL, or other data source. +pub struct LoadDataSource { + /// The type of data source being loaded (e.g., "file", "http" etc.). + pub source_type: &'static str, + + /// The file extension if applicable (e.g., "rrd", "png", "glb"). + /// None for non-file sources like stdin or gRPC streams. + pub file_extension: Option, + + /// How the file was opened (e.g., "cli", "`file_dialog`" etc.). + /// Only applicable for file-based sources. + pub file_source: Option<&'static str>, + + /// Whether the data source stream was started successfully. + pub started_successfully: bool, +} + +impl Event for LoadDataSource { + const NAME: &'static str = "load_data_source"; +} + +impl Properties for LoadDataSource { + fn serialize(self, event: &mut AnalyticsEvent) { + let Self { + source_type, + file_extension, + file_source, + started_successfully, + } = self; + + event.insert("source_type", source_type); + event.insert_opt("file_extension", file_extension); + event.insert_opt("file_source", file_source.map(|s| s.to_owned())); + event.insert("started_successfully", started_successfully); + } +} + +// ----------------------------------------------- + #[cfg(test)] mod tests { use super::*; diff --git a/crates/viewer/re_viewer/src/app.rs b/crates/viewer/re_viewer/src/app.rs index 7b5127ad0890..05caa040149f 100644 --- a/crates/viewer/re_viewer/src/app.rs +++ b/crates/viewer/re_viewer/src/app.rs @@ -1360,7 +1360,19 @@ impl App { } } - match data_source.clone().stream(&self.connection_registry) { + let stream = data_source.clone().stream(&self.connection_registry); + #[cfg(feature = "analytics")] + if let Some(analytics) = re_analytics::Analytics::global_or_init() { + let data_source_analytics = data_source.analytics(); + analytics.record(re_analytics::event::LoadDataSource { + source_type: data_source_analytics.source_type, + file_extension: data_source_analytics.file_extension, + file_source: data_source_analytics.file_source, + started_successfully: stream.is_ok(), + }); + } + + match stream { Ok(rx) => self.add_log_receiver(rx), Err(err) => { re_log::error!("Failed to open data source: {}", re_error::format(err));