From ace8049ab6d19c5e108ea698f940d82d3a5a7174 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:00:42 +0200 Subject: [PATCH 01/22] implement web analytics pipeline --- crates/re_analytics/src/lib.rs | 81 ++++++++++++++++++ crates/re_analytics/src/native/config.rs | 2 - crates/re_analytics/src/native/pipeline.rs | 5 -- crates/re_analytics/src/native/sink.rs | 95 +--------------------- crates/re_analytics/src/web/config.rs | 49 ++++++++--- crates/re_analytics/src/web/mod.rs | 1 - crates/re_analytics/src/web/pipeline.rs | 78 ++++++++++++++---- crates/re_analytics/src/web/sink.rs | 2 - 8 files changed, 182 insertions(+), 131 deletions(-) delete mode 100644 crates/re_analytics/src/web/sink.rs diff --git a/crates/re_analytics/src/lib.rs b/crates/re_analytics/src/lib.rs index cabe6863fdd2..91bad842c299 100644 --- a/crates/re_analytics/src/lib.rs +++ b/crates/re_analytics/src/lib.rs @@ -288,3 +288,84 @@ impl Analytics { } } } + +const PUBLIC_POSTHOG_PROJECT_KEY: &str = "phc_sgKidIE4WYYFSJHd8LEYY1UZqASpnfQKeMqlJfSXwqg"; + +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +enum PostHogEvent<'a> { + Capture(PostHogCaptureEvent<'a>), + Identify(PostHogIdentifyEvent<'a>), +} + +impl<'a> PostHogEvent<'a> { + fn from_event(analytics_id: &'a str, session_id: &'a str, event: &'a Event) -> Self { + let properties = event.props.iter().map(|(name, value)| { + ( + name.as_ref(), + match value { + &Property::Integer(v) => v.into(), + &Property::Float(v) => v.into(), + Property::String(v) => v.as_str().into(), + &Property::Bool(v) => v.into(), + }, + ) + }); + + match event.kind { + crate::EventKind::Append => Self::Capture(PostHogCaptureEvent { + timestamp: event.time_utc, + event: event.name.as_ref(), + distinct_id: analytics_id, + properties: properties + .chain([("session_id", session_id.into())]) + .collect(), + }), + crate::EventKind::Update => Self::Identify(PostHogIdentifyEvent { + timestamp: event.time_utc, + event: "$identify", + distinct_id: analytics_id, + properties: [("session_id", session_id.into())].into(), + set: properties.collect(), + }), + } + } +} + +// See https://posthog.com/docs/api/post-only-endpoints#capture. +#[derive(Debug, serde::Serialize)] +struct PostHogCaptureEvent<'a> { + #[serde(with = "::time::serde::rfc3339")] + timestamp: OffsetDateTime, + event: &'a str, + distinct_id: &'a str, + properties: HashMap<&'a str, serde_json::Value>, +} + +// See https://posthog.com/docs/api/post-only-endpoints#identify. +#[derive(Debug, serde::Serialize)] +struct PostHogIdentifyEvent<'a> { + #[serde(with = "::time::serde::rfc3339")] + timestamp: OffsetDateTime, + event: &'a str, + distinct_id: &'a str, + properties: HashMap<&'a str, serde_json::Value>, + #[serde(rename = "$set")] + set: HashMap<&'a str, serde_json::Value>, +} + +// See https://posthog.com/docs/api/post-only-endpoints#batch-events. +#[derive(Debug, serde::Serialize)] +struct PostHogBatch<'a> { + api_key: &'static str, + batch: &'a [PostHogEvent<'a>], +} + +impl<'a> PostHogBatch<'a> { + fn from_events(events: &'a [PostHogEvent<'a>]) -> Self { + Self { + api_key: PUBLIC_POSTHOG_PROJECT_KEY, + batch: events, + } + } +} diff --git a/crates/re_analytics/src/native/config.rs b/crates/re_analytics/src/native/config.rs index e6d4da69dea0..6163e0e742a2 100644 --- a/crates/re_analytics/src/native/config.rs +++ b/crates/re_analytics/src/native/config.rs @@ -10,8 +10,6 @@ use uuid::Uuid; use crate::Property; -// --- - #[derive(thiserror::Error, Debug)] pub enum ConfigError { #[error("Couldn't compute config location")] diff --git a/crates/re_analytics/src/native/pipeline.rs b/crates/re_analytics/src/native/pipeline.rs index eaa95adcede1..5cfe919363f4 100644 --- a/crates/re_analytics/src/native/pipeline.rs +++ b/crates/re_analytics/src/native/pipeline.rs @@ -14,11 +14,6 @@ use super::sink::PostHogSink; use super::AbortSignal; use crate::{Config, Event}; -// TODO(cmc): abstract away the concept of a `Pipeline` behind an actual trait when comes the time -// to support more than just PostHog. - -// --- - #[derive(thiserror::Error, Debug)] pub enum PipelineError { #[error(transparent)] diff --git a/crates/re_analytics/src/native/sink.rs b/crates/re_analytics/src/native/sink.rs index a30cb7968e11..17c0dfb43914 100644 --- a/crates/re_analytics/src/native/sink.rs +++ b/crates/re_analytics/src/native/sink.rs @@ -1,17 +1,7 @@ -use std::collections::HashMap; use std::sync::Arc; -use time::OffsetDateTime; - use super::AbortSignal; -use crate::{Event, Property}; - -// TODO(cmc): abstract away the concept of a `Sink` behind an actual trait when comes the time to -// support more than just PostHog. - -const PUBLIC_POSTHOG_PROJECT_KEY: &str = "phc_sgKidIE4WYYFSJHd8LEYY1UZqASpnfQKeMqlJfSXwqg"; - -// --- +use crate::{Event, PostHogBatch, PostHogEvent}; #[derive(Debug, Clone)] struct Url(String); @@ -78,86 +68,3 @@ impl PostHogSink { ehttp::fetch(ehttp::Request::post(Self::URL, json.into_bytes()), on_done); } } - -// --- - -// TODO(cmc): support PostHog's view event - -#[derive(Debug, serde::Serialize)] -#[serde(untagged)] -enum PostHogEvent<'a> { - Capture(PostHogCaptureEvent<'a>), - Identify(PostHogIdentifyEvent<'a>), -} - -impl<'a> PostHogEvent<'a> { - fn from_event(analytics_id: &'a str, session_id: &'a str, event: &'a Event) -> Self { - let properties = event.props.iter().map(|(name, value)| { - ( - name.as_ref(), - match value { - &Property::Integer(v) => v.into(), - &Property::Float(v) => v.into(), - Property::String(v) => v.as_str().into(), - &Property::Bool(v) => v.into(), - }, - ) - }); - - match event.kind { - crate::EventKind::Append => Self::Capture(PostHogCaptureEvent { - timestamp: event.time_utc, - event: event.name.as_ref(), - distinct_id: analytics_id, - properties: properties - .chain([("session_id", session_id.into())]) - .collect(), - }), - crate::EventKind::Update => Self::Identify(PostHogIdentifyEvent { - timestamp: event.time_utc, - event: "$identify", - distinct_id: analytics_id, - properties: [("session_id", session_id.into())].into(), - set: properties.collect(), - }), - } - } -} - -// See https://posthog.com/docs/api/post-only-endpoints#capture. -#[derive(Debug, serde::Serialize)] -struct PostHogCaptureEvent<'a> { - #[serde(with = "::time::serde::rfc3339")] - timestamp: OffsetDateTime, - event: &'a str, - distinct_id: &'a str, - properties: HashMap<&'a str, serde_json::Value>, -} - -// See https://posthog.com/docs/api/post-only-endpoints#identify. -#[derive(Debug, serde::Serialize)] -struct PostHogIdentifyEvent<'a> { - #[serde(with = "::time::serde::rfc3339")] - timestamp: OffsetDateTime, - event: &'a str, - distinct_id: &'a str, - properties: HashMap<&'a str, serde_json::Value>, - #[serde(rename = "$set")] - set: HashMap<&'a str, serde_json::Value>, -} - -// See https://posthog.com/docs/api/post-only-endpoints#batch-events. -#[derive(Debug, serde::Serialize)] -struct PostHogBatch<'a> { - api_key: &'static str, - batch: &'a [PostHogEvent<'a>], -} - -impl<'a> PostHogBatch<'a> { - fn from_events(events: &'a [PostHogEvent<'a>]) -> Self { - Self { - api_key: PUBLIC_POSTHOG_PROJECT_KEY, - batch: events, - } - } -} diff --git a/crates/re_analytics/src/web/config.rs b/crates/re_analytics/src/web/config.rs index abce44ed5721..b71c7c61771d 100644 --- a/crates/re_analytics/src/web/config.rs +++ b/crates/re_analytics/src/web/config.rs @@ -3,15 +3,17 @@ use std::collections::HashMap; use uuid::Uuid; +use web_sys::Storage; use crate::Property; -// --- - #[derive(thiserror::Error, Debug)] pub enum ConfigError { - #[error("Couldn't compute config location")] - UnknownLocation, + #[error("failed to get localStorage")] + NoStorage, + + #[error("{0}")] + Storage(String), #[error(transparent)] Io(#[from] std::io::Error), @@ -20,34 +22,55 @@ pub enum ConfigError { Serde(#[from] serde_json::Error), } -// NOTE: all the `rename` clauses are to avoid a potential catastrophe :) #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Config { - #[serde(rename = "analytics_enabled")] - pub analytics_enabled: bool, - // NOTE: not a UUID on purpose, it is sometimes useful to use handcrafted IDs. - #[serde(rename = "analytics_id")] + #[serde(rename = "i")] pub analytics_id: String, /// A unique ID for this session. #[serde(skip, default = "::uuid::Uuid::new_v4")] pub session_id: Uuid, - #[serde(rename = "metadata", default)] + #[serde(rename = "m", default)] pub metadata: HashMap, } +fn get_local_storage() -> Result { + let window = web_sys::window().ok_or(ConfigError::NoStorage)?; + let Ok(Some(storage)) = window.local_storage() else { return Err(ConfigError::NoStorage) }; + Ok(storage) +} + impl Config { + const STORAGE_KEY: &str = "rerun_config"; + pub fn load() -> Result { - todo!("web support") + let storage = get_local_storage()?; + let value = storage + .get_item(Self::STORAGE_KEY) + .map_err(|_| ConfigError::Storage(format!("failed to get `{}`", Self::STORAGE_KEY)))?; + match value { + Some(value) => Ok(serde_json::from_str(&value)?), + None => Ok(Config { + analytics_id: Uuid::new_v4().to_string(), + session_id: Uuid::new_v4(), + metadata: HashMap::new(), + }), + } } pub fn save(&self) -> Result<(), ConfigError> { - todo!("web support") + let storage = get_local_storage()?; + let string = serde_json::to_string(self)?; + storage + .set_item(Self::STORAGE_KEY, &string) + .map_err(|_| ConfigError::Storage(format!("failed to set `{}`", Self::STORAGE_KEY))) } pub fn is_first_run(&self) -> bool { - todo!("web support") + // no first-run opt-out for web + + false } } diff --git a/crates/re_analytics/src/web/mod.rs b/crates/re_analytics/src/web/mod.rs index b0de016cfa56..2f4da65e81f1 100644 --- a/crates/re_analytics/src/web/mod.rs +++ b/crates/re_analytics/src/web/mod.rs @@ -1,6 +1,5 @@ mod config; mod pipeline; -mod sink; pub use config::{Config, ConfigError}; pub use pipeline::{Pipeline, PipelineError}; diff --git a/crates/re_analytics/src/web/pipeline.rs b/crates/re_analytics/src/web/pipeline.rs index 203b52ab978b..a3ebed128f0e 100644 --- a/crates/re_analytics/src/web/pipeline.rs +++ b/crates/re_analytics/src/web/pipeline.rs @@ -4,15 +4,11 @@ clippy::unused_self )] +use std::sync::Arc; use std::time::Duration; -use super::sink::PostHogSink; -use crate::{Config, Event}; - -// TODO(cmc): abstract away the concept of a `Pipeline` behind an actual trait when comes the time -// to support more than just PostHog. - -// --- +use crate::PostHogBatch; +use crate::{Config, Event, PostHogEvent}; #[derive(thiserror::Error, Debug)] pub enum PipelineError { @@ -23,17 +19,71 @@ pub enum PipelineError { Serde(#[from] serde_json::Error), } -/// An eventual, at-least-once(-ish) event pipeline, backed by a write-ahead log on the local disk. +/// WASM event pipeline. /// -/// Flushing of the WAL is entirely left up to the OS page cache, hance the -ish. +/// Unlike the native pipeline, this one is not backed by a WAL. All events are immediately sent as they are recorded. #[derive(Debug)] -pub struct Pipeline {} +pub struct Pipeline { + analytics_id: Arc, + session_id: Arc, +} impl Pipeline { - pub(crate) fn new(_config: &Config, _tick: Duration) -> Result, PipelineError> { - let _sink = PostHogSink::default(); - Ok(None) + // NOTE: different from the native URL, this one is _specifically_ for web. + const URL: &str = "https://tel.rerun.io/api/pog"; + + pub(crate) fn new(config: &Config, _tick: Duration) -> Result, PipelineError> { + Ok(Some(Pipeline { + analytics_id: config.analytics_id.as_str().into(), + session_id: config.session_id.to_string().into(), + })) } - pub fn record(&self, _event: Event) {} + pub fn record(&self, event: Event) { + // send all events immediately, ignore all errors + + let analytics_id = self.analytics_id.clone(); + let session_id = self.session_id.clone(); + + let events = [PostHogEvent::from_event( + &self.analytics_id, + &self.session_id, + &event, + )]; + let batch = PostHogBatch::from_events(&events); + let json = match serde_json::to_string_pretty(&batch) { + Ok(json) => json, + Err(err) => { + re_log::debug_once!("failed to send event: {err}"); + return; + } + }; + re_log::trace!("Sending analytics: {json}"); + ehttp::fetch( + ehttp::Request::post(Self::URL, json.into_bytes()), + move |result| match result { + Ok(response) => { + if !response.ok { + re_log::debug_once!( + "Failed to send analytics down the sink: HTTP request failed: {} {} {}", + response.status, + response.status_text, + response.text().unwrap_or("") + ); + return; + } + + re_log::trace!( + ?response, + %analytics_id, + %session_id, + "events successfully flushed" + ); + } + Err(err) => { + re_log::debug_once!("Failed to send analytics down the sink: {err}"); + } + }, + ); + } } diff --git a/crates/re_analytics/src/web/sink.rs b/crates/re_analytics/src/web/sink.rs deleted file mode 100644 index 3b7b6d472eb0..000000000000 --- a/crates/re_analytics/src/web/sink.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[derive(Default, Debug, Clone)] -pub struct PostHogSink {} From be7570ce9335d429acf387943e504bb14f4b972d Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:30:05 +0200 Subject: [PATCH 02/22] enable analytics in the web viewer --- crates/re_analytics/src/cli.rs | 6 ++-- crates/re_analytics/src/lib.rs | 18 ++++++++++ crates/re_analytics/src/native/config.rs | 42 ++++++++++++++-------- crates/re_analytics/src/web/config.rs | 26 +++++++++----- crates/re_viewer/src/viewer_analytics.rs | 45 +++++++----------------- 5 files changed, 78 insertions(+), 59 deletions(-) diff --git a/crates/re_analytics/src/cli.rs b/crates/re_analytics/src/cli.rs index 7055563dcd92..75d8f6462e5b 100644 --- a/crates/re_analytics/src/cli.rs +++ b/crates/re_analytics/src/cli.rs @@ -20,7 +20,7 @@ pub enum CliError { } pub fn clear() -> Result<(), CliError> { - let config = Config::load()?; + let config = Config::load_or_default()?; fn delete_dir(dir: &Path) -> Result<(), CliError> { eprint!("Are you sure you want to delete directory {dir:?}? [y/N]: ",); @@ -50,13 +50,13 @@ pub fn clear() -> Result<(), CliError> { } pub fn set(props: impl IntoIterator) -> Result<(), CliError> { - let mut config = Config::load()?; + let mut config = Config::load_or_default()?; config.opt_in_metadata.extend(props); config.save().map_err(Into::into) } pub fn opt(enabled: bool) -> Result<(), CliError> { - let mut config = Config::load()?; + let mut config = Config::load_or_default()?; config.analytics_enabled = enabled; config.save()?; diff --git a/crates/re_analytics/src/lib.rs b/crates/re_analytics/src/lib.rs index 91bad842c299..af0cf811334a 100644 --- a/crates/re_analytics/src/lib.rs +++ b/crates/re_analytics/src/lib.rs @@ -234,6 +234,24 @@ pub struct Analytics { impl Analytics { pub fn new(tick: Duration) -> Result { let config = Config::load()?; + + #[cfg(target_arch = "wasm32")] + let config = match config { + Some(config) => config, + None => { + // the config doesnt exist in local storage yet, save it + let config = Config::default(); + config.save()?; + config + } + }; + + #[cfg(not(target_arch = "wasm32"))] + let config = match config { + Some(config) => config, + None => Config::new()?, + }; + re_log::trace!(?config, ?tick, "loaded analytics config"); if config.is_first_run() { diff --git a/crates/re_analytics/src/native/config.rs b/crates/re_analytics/src/native/config.rs index 6163e0e742a2..bfafc356aee3 100644 --- a/crates/re_analytics/src/native/config.rs +++ b/crates/re_analytics/src/native/config.rs @@ -59,28 +59,40 @@ pub struct Config { } impl Config { - pub fn load() -> Result { + pub fn new() -> Result { let dirs = Self::project_dirs()?; let config_path = dirs.config_dir().join("analytics.json"); let data_path = dirs.data_local_dir().join("analytics"); - let config = match File::open(&config_path) { + Ok(Self { + analytics_id: Uuid::new_v4().to_string(), + analytics_enabled: true, + opt_in_metadata: Default::default(), + session_id: Uuid::new_v4(), + is_first_run: true, + config_file_path: config_path, + data_dir_path: data_path, + }) + } + + pub fn load_or_default() -> Result { + match Self::load()? { + Some(config) => Ok(config), + None => Config::new(), + } + } + + pub fn load() -> Result, ConfigError> { + let dirs = Self::project_dirs()?; + let config_path = dirs.config_dir().join("analytics.json"); + match File::open(&config_path) { Ok(file) => { let reader = BufReader::new(file); - serde_json::from_reader(reader)? + let config = serde_json::from_reader(reader)?; + Ok(Some(config)) } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config { - analytics_id: Uuid::new_v4().to_string(), - analytics_enabled: true, - opt_in_metadata: Default::default(), - session_id: Uuid::new_v4(), - is_first_run: true, - config_file_path: config_path, - data_dir_path: data_path, - }, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(err) => return Err(ConfigError::Io(err)), - }; - - Ok(config) + } } pub fn save(&self) -> Result<(), ConfigError> { diff --git a/crates/re_analytics/src/web/config.rs b/crates/re_analytics/src/web/config.rs index b71c7c61771d..9d6e59a55f0c 100644 --- a/crates/re_analytics/src/web/config.rs +++ b/crates/re_analytics/src/web/config.rs @@ -33,7 +33,7 @@ pub struct Config { pub session_id: Uuid, #[serde(rename = "m", default)] - pub metadata: HashMap, + pub opt_in_metadata: HashMap, } fn get_local_storage() -> Result { @@ -45,18 +45,18 @@ fn get_local_storage() -> Result { impl Config { const STORAGE_KEY: &str = "rerun_config"; - pub fn load() -> Result { + pub fn new() -> Result { + Ok(Self::default()) + } + + pub fn load() -> Result, ConfigError> { let storage = get_local_storage()?; let value = storage .get_item(Self::STORAGE_KEY) .map_err(|_| ConfigError::Storage(format!("failed to get `{}`", Self::STORAGE_KEY)))?; match value { - Some(value) => Ok(serde_json::from_str(&value)?), - None => Ok(Config { - analytics_id: Uuid::new_v4().to_string(), - session_id: Uuid::new_v4(), - metadata: HashMap::new(), - }), + Some(value) => Ok(Some(serde_json::from_str(&value)?)), + None => Ok(None), } } @@ -74,3 +74,13 @@ impl Config { false } } + +impl Default for Config { + fn default() -> Self { + Self { + analytics_id: Uuid::new_v4().to_string(), + session_id: Uuid::new_v4(), + opt_in_metadata: HashMap::new(), + } + } +} diff --git a/crates/re_viewer/src/viewer_analytics.rs b/crates/re_viewer/src/viewer_analytics.rs index 647e41a393e0..affdba5f17e0 100644 --- a/crates/re_viewer/src/viewer_analytics.rs +++ b/crates/re_viewer/src/viewer_analytics.rs @@ -9,21 +9,18 @@ //! //! DO NOT MOVE THIS FILE without updating all the docs pointing to it! -#[cfg(all(not(target_arch = "wasm32"), feature = "analytics"))] use re_analytics::{Analytics, Event, Property}; - -#[cfg(all(not(target_arch = "wasm32"), feature = "analytics"))] use re_log_types::StoreSource; pub struct ViewerAnalytics { // NOTE: Optional because it is possible to have the `analytics` feature flag enabled // while at the same time opting-out of analytics at run-time. - #[cfg(all(not(target_arch = "wasm32"), feature = "analytics"))] + #[cfg(feature = "analytics")] analytics: Option, } impl ViewerAnalytics { - #[cfg(all(not(target_arch = "wasm32"), feature = "analytics"))] + #[cfg(feature = "analytics")] pub fn new() -> Self { let analytics = match Analytics::new(std::time::Duration::from_secs(2)) { Ok(analytics) => Some(analytics), @@ -36,12 +33,7 @@ impl ViewerAnalytics { Self { analytics } } - #[cfg(not(all(not(target_arch = "wasm32"), feature = "analytics")))] - pub fn new() -> Self { - Self {} - } - - #[cfg(all(not(target_arch = "wasm32"), feature = "analytics"))] + #[cfg(feature = "analytics")] fn record(&self, event: Event) { if let Some(analytics) = &self.analytics { analytics.record(event); @@ -49,7 +41,7 @@ impl ViewerAnalytics { } /// Register a property that will be included in all append-events. - #[cfg(all(not(target_arch = "wasm32"), feature = "analytics"))] + #[cfg(feature = "analytics")] fn register(&mut self, name: &'static str, property: impl Into) { if let Some(analytics) = &mut self.analytics { analytics.register_append_property(name, property); @@ -57,7 +49,7 @@ impl ViewerAnalytics { } /// Deregister a property. - #[cfg(all(not(target_arch = "wasm32"), feature = "analytics"))] + #[cfg(feature = "analytics")] fn deregister(&mut self, name: &'static str) { if let Some(analytics) = &mut self.analytics { analytics.deregister_append_property(name); @@ -67,8 +59,11 @@ impl ViewerAnalytics { // ---------------------------------------------------------------------------- +// TODO(jan): add URL (iff domain is `rerun.io`) +// TODO(jan): make sure analytics knows the event comes from web + /// Here follows all the analytics collected by the Rerun Viewer. -#[cfg(all(not(target_arch = "wasm32"), feature = "analytics"))] +#[cfg(feature = "analytics")] impl ViewerAnalytics { /// When the viewer is first started pub fn on_viewer_started( @@ -87,7 +82,7 @@ impl ViewerAnalytics { }; self.register("app_env", app_env_str); - #[cfg(all(not(target_arch = "wasm32"), feature = "analytics"))] + #[cfg(feature = "analytics")] if let Some(analytics) = &self.analytics { let mut event = Event::update("update_metadata").with_build_info(build_info); @@ -118,8 +113,8 @@ impl ViewerAnalytics { // In practice this is the email of Rerun employees // who register their emails with `rerun analytics email`. // This is how we filter out employees from actual users! - for (name, value) in analytics.config().opt_in_metadata.clone() { - event = event.with_prop(name, value); + for (name, value) in analytics.config().opt_in_metadata.iter() { + event = event.with_prop(name.clone(), value.clone()); } analytics.record(event); @@ -218,19 +213,3 @@ impl ViewerAnalytics { self.record(Event::append("open_recording")); } } - -// ---------------------------------------------------------------------------- - -// When analytics are disabled: -#[cfg(not(all(not(target_arch = "wasm32"), feature = "analytics")))] -impl ViewerAnalytics { - #[allow(clippy::unused_self)] - pub fn on_viewer_started( - &mut self, - _build_info: &re_build_info::BuildInfo, - _app_env: &crate::AppEnvironment, - ) { - } - #[allow(clippy::unused_self)] - pub fn on_open_recording(&mut self, _store_db: &re_data_store::StoreDb) {} -} From 541bea604a4bab58cd6ec3d992d6c226fba7249a Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:46:42 +0200 Subject: [PATCH 03/22] fix build without `analytics` feature --- crates/re_viewer/src/viewer_analytics.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/re_viewer/src/viewer_analytics.rs b/crates/re_viewer/src/viewer_analytics.rs index affdba5f17e0..cb4872acb9a1 100644 --- a/crates/re_viewer/src/viewer_analytics.rs +++ b/crates/re_viewer/src/viewer_analytics.rs @@ -9,6 +9,7 @@ //! //! DO NOT MOVE THIS FILE without updating all the docs pointing to it! +#[cfg(feature = "analytics")] use re_analytics::{Analytics, Event, Property}; use re_log_types::StoreSource; @@ -213,3 +214,19 @@ impl ViewerAnalytics { self.record(Event::append("open_recording")); } } + +#[cfg(not(feature = "analytics"))] +impl ViewerAnalytics { + pub fn new() -> Self { + Self {} + } + + pub fn on_viewer_started( + &mut self, + _build_info: &re_build_info::BuildInfo, + _app_env: &crate::AppEnvironment, + ) { + } + + pub fn on_open_recording(&mut self, store_db: &re_data_store::StoreDb) {} +} From 8c1ef8fa48d9a81c9cd7d89b0ecaf9522af5bf6d Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:48:17 +0200 Subject: [PATCH 04/22] `WASM` -> `Wasm` --- crates/re_analytics/src/web/pipeline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/re_analytics/src/web/pipeline.rs b/crates/re_analytics/src/web/pipeline.rs index a3ebed128f0e..0d4b38ec9c13 100644 --- a/crates/re_analytics/src/web/pipeline.rs +++ b/crates/re_analytics/src/web/pipeline.rs @@ -19,7 +19,7 @@ pub enum PipelineError { Serde(#[from] serde_json::Error), } -/// WASM event pipeline. +/// Wasm event pipeline. /// /// Unlike the native pipeline, this one is not backed by a WAL. All events are immediately sent as they are recorded. #[derive(Debug)] From bc10af258eab17c6fcf840a7f7422750319b6563 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:52:43 +0200 Subject: [PATCH 05/22] fix wasm lints --- crates/re_analytics/src/lib.rs | 15 +++++++-------- crates/re_analytics/src/web/config.rs | 5 ++++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/re_analytics/src/lib.rs b/crates/re_analytics/src/lib.rs index af0cf811334a..633f1de1da3f 100644 --- a/crates/re_analytics/src/lib.rs +++ b/crates/re_analytics/src/lib.rs @@ -236,14 +236,13 @@ impl Analytics { let config = Config::load()?; #[cfg(target_arch = "wasm32")] - let config = match config { - Some(config) => config, - None => { - // the config doesnt exist in local storage yet, save it - let config = Config::default(); - config.save()?; - config - } + let config = if let Some(config) = config { + config + } else { + // the config doesnt exist in local storage yet, save it + let config = Config::default(); + config.save()?; + config }; #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/re_analytics/src/web/config.rs b/crates/re_analytics/src/web/config.rs index 9d6e59a55f0c..002e0a6c4410 100644 --- a/crates/re_analytics/src/web/config.rs +++ b/crates/re_analytics/src/web/config.rs @@ -38,13 +38,16 @@ pub struct Config { fn get_local_storage() -> Result { let window = web_sys::window().ok_or(ConfigError::NoStorage)?; - let Ok(Some(storage)) = window.local_storage() else { return Err(ConfigError::NoStorage) }; + let Ok(Some(storage)) = window.local_storage() else { + return Err(ConfigError::NoStorage); + }; Ok(storage) } impl Config { const STORAGE_KEY: &str = "rerun_config"; + #[allow(clippy::unnecessary_wraps, clippy::map_err_ignore)] pub fn new() -> Result { Ok(Self::default()) } From 49a4425fa4d96c72ba7b197bdfd0a21681f0fdd8 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Thu, 31 Aug 2023 14:00:01 +0200 Subject: [PATCH 06/22] fix more lints --- crates/re_analytics/src/native/config.rs | 6 +++--- crates/re_analytics/src/web/config.rs | 4 +++- crates/re_viewer/src/viewer_analytics.rs | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/re_analytics/src/native/config.rs b/crates/re_analytics/src/native/config.rs index bfafc356aee3..4a00e1d807f7 100644 --- a/crates/re_analytics/src/native/config.rs +++ b/crates/re_analytics/src/native/config.rs @@ -84,14 +84,14 @@ impl Config { pub fn load() -> Result, ConfigError> { let dirs = Self::project_dirs()?; let config_path = dirs.config_dir().join("analytics.json"); - match File::open(&config_path) { + match File::open(config_path) { Ok(file) => { let reader = BufReader::new(file); let config = serde_json::from_reader(reader)?; Ok(Some(config)) } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(err) => return Err(ConfigError::Io(err)), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(ConfigError::Io(err)), } } diff --git a/crates/re_analytics/src/web/config.rs b/crates/re_analytics/src/web/config.rs index 002e0a6c4410..76cf613e798f 100644 --- a/crates/re_analytics/src/web/config.rs +++ b/crates/re_analytics/src/web/config.rs @@ -47,11 +47,12 @@ fn get_local_storage() -> Result { impl Config { const STORAGE_KEY: &str = "rerun_config"; - #[allow(clippy::unnecessary_wraps, clippy::map_err_ignore)] + #[allow(clippy::unnecessary_wraps)] pub fn new() -> Result { Ok(Self::default()) } + #[allow(clippy::map_err_ignore)] pub fn load() -> Result, ConfigError> { let storage = get_local_storage()?; let value = storage @@ -63,6 +64,7 @@ impl Config { } } + #[allow(clippy::map_err_ignore)] pub fn save(&self) -> Result<(), ConfigError> { let storage = get_local_storage()?; let string = serde_json::to_string(self)?; diff --git a/crates/re_viewer/src/viewer_analytics.rs b/crates/re_viewer/src/viewer_analytics.rs index cb4872acb9a1..7aee83a4e449 100644 --- a/crates/re_viewer/src/viewer_analytics.rs +++ b/crates/re_viewer/src/viewer_analytics.rs @@ -114,7 +114,7 @@ impl ViewerAnalytics { // In practice this is the email of Rerun employees // who register their emails with `rerun analytics email`. // This is how we filter out employees from actual users! - for (name, value) in analytics.config().opt_in_metadata.iter() { + for (name, value) in &analytics.config().opt_in_metadata { event = event.with_prop(name.clone(), value.clone()); } From 23720ed871d5f330629f2faa6dd00d17dc131701 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:22:57 +0200 Subject: [PATCH 07/22] make working with wasm less annoying --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9fdafaab266f..3f5a32139b4a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -80,5 +80,6 @@ "rerun_py/pyproject.toml" ], "cmake.buildDirectory": "${workspaceRoot}/build/", - "C_Cpp.autoAddFileAssociations": false + "C_Cpp.autoAddFileAssociations": false, + "rust-analyzer.showUnlinkedFileNotification": false } From e7fda642432cf3f18699d17b35916fbeb5d0f3db Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:23:26 +0200 Subject: [PATCH 08/22] disable analytics if we're in a notebook --- crates/re_viewer/src/app.rs | 6 +++++- crates/re_viewer/src/viewer_analytics.rs | 13 +++++++------ crates/re_viewer/src/web.rs | 21 +++++++++++++++------ crates/rerun/src/run.rs | 1 + rerun_py/rerun_sdk/rerun/recording.py | 2 +- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index ef5fcbc4daee..6bb7c4453481 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -40,6 +40,9 @@ pub struct StartupOptions { pub persist_state: bool, + /// Whether or not the app is running in the context of a Jupyter Notebook. + pub is_in_notebook: bool, + /// Take a screenshot of the app and quit. /// We use this to generate screenshots of our exmples. #[cfg(not(target_arch = "wasm32"))] @@ -57,6 +60,7 @@ impl Default for StartupOptions { Self { memory_limit: re_memory::MemoryLimit::default(), persist_state: true, + is_in_notebook: false, #[cfg(not(target_arch = "wasm32"))] screenshot_to_path_then_quit: None, @@ -168,7 +172,7 @@ impl App { AppState::default() }; - let mut analytics = ViewerAnalytics::new(); + let mut analytics = ViewerAnalytics::new(startup_options.is_in_notebook); analytics.on_viewer_started(&build_info, app_env); let mut space_view_class_registry = SpaceViewClassRegistry::default(); diff --git a/crates/re_viewer/src/viewer_analytics.rs b/crates/re_viewer/src/viewer_analytics.rs index 7aee83a4e449..367972323f42 100644 --- a/crates/re_viewer/src/viewer_analytics.rs +++ b/crates/re_viewer/src/viewer_analytics.rs @@ -20,9 +20,13 @@ pub struct ViewerAnalytics { analytics: Option, } +#[cfg(feature = "analytics")] impl ViewerAnalytics { - #[cfg(feature = "analytics")] - pub fn new() -> Self { + pub fn new(disabled: bool) -> Self { + if disabled { + return Self { analytics: None }; + } + let analytics = match Analytics::new(std::time::Duration::from_secs(2)) { Ok(analytics) => Some(analytics), Err(err) => { @@ -34,7 +38,6 @@ impl ViewerAnalytics { Self { analytics } } - #[cfg(feature = "analytics")] fn record(&self, event: Event) { if let Some(analytics) = &self.analytics { analytics.record(event); @@ -42,7 +45,6 @@ impl ViewerAnalytics { } /// Register a property that will be included in all append-events. - #[cfg(feature = "analytics")] fn register(&mut self, name: &'static str, property: impl Into) { if let Some(analytics) = &mut self.analytics { analytics.register_append_property(name, property); @@ -50,7 +52,6 @@ impl ViewerAnalytics { } /// Deregister a property. - #[cfg(feature = "analytics")] fn deregister(&mut self, name: &'static str) { if let Some(analytics) = &mut self.analytics { analytics.deregister_append_property(name); @@ -217,7 +218,7 @@ impl ViewerAnalytics { #[cfg(not(feature = "analytics"))] impl ViewerAnalytics { - pub fn new() -> Self { + pub fn new(_disabled: bool) -> Self { Self {} } diff --git a/crates/re_viewer/src/web.rs b/crates/re_viewer/src/web.rs index af9cde06d580..079e85864a9a 100644 --- a/crates/re_viewer/src/web.rs +++ b/crates/re_viewer/src/web.rs @@ -80,13 +80,13 @@ impl WebHandle { fn create_app(cc: &eframe::CreationContext<'_>, url: Option) -> crate::App { let build_info = re_build_info::build_info!(); let app_env = crate::AppEnvironment::Web; - let persist_state = get_persist_state(&cc.integration_info); let startup_options = crate::StartupOptions { memory_limit: re_memory::MemoryLimit { // On wasm32 we only have 4GB of memory to play around with. limit: Some(2_500_000_000), }, - persist_state, + persist_state: get_persist_state(&cc.integration_info), + is_in_notebook: is_in_notebook(&cc.integration_info), skip_welcome_screen: false, }; let re_ui = crate::customize_eframe(cc); @@ -194,22 +194,31 @@ fn get_url(info: &eframe::IntegrationInfo) -> String { } } +fn is_in_notebook(info: &eframe::IntegrationInfo) -> bool { + get_query_bool(info, "notebook", false) +} + fn get_persist_state(info: &eframe::IntegrationInfo) -> bool { + get_query_bool(info, "persist", true) +} + +fn get_query_bool(info: &eframe::IntegrationInfo, key: &str, default: bool) -> bool { + let default_int = if default { 1 } else { 0 }; match info .web_info .location .query_map - .get("persist") + .get(key) .map(String::as_str) { Some("0") => false, Some("1") => true, Some(other) => { re_log::warn!( - "Unexpected value for 'persist' query: {other:?}. Expected either '0' or '1'. Defaulting to '1'." + "Unexpected value for '{key}' query: {other:?}. Expected either '0' or '1'. Defaulting to '{default_int}'." ); - true + default } - _ => true, + _ => default, } } diff --git a/crates/rerun/src/run.rs b/crates/rerun/src/run.rs index 811e47e3205f..89ca789fa90b 100644 --- a/crates/rerun/src/run.rs +++ b/crates/rerun/src/run.rs @@ -419,6 +419,7 @@ async fn run_impl( .unwrap_or_else(|err| panic!("Bad --memory-limit: {err}")) }), persist_state: args.persist_state, + is_in_notebook: false, screenshot_to_path_then_quit: args.screenshot_to.clone(), skip_welcome_screen: args.skip_welcome_screen, diff --git a/rerun_py/rerun_sdk/rerun/recording.py b/rerun_py/rerun_sdk/rerun/recording.py index 5ff52055f38b..d3224007ce5e 100644 --- a/rerun_py/rerun_sdk/rerun/recording.py +++ b/rerun_py/rerun_sdk/rerun/recording.py @@ -99,7 +99,7 @@ def as_html( }}())); """ From ae66e5bbf28ece048fbb4093b77e1de177c6d43b Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:57:21 +0200 Subject: [PATCH 09/22] expose `set_email` from wasm --- crates/re_analytics/src/lib.rs | 8 +++- crates/re_analytics/src/web/config.rs | 7 ++++ crates/re_viewer/src/web.rs | 8 ++++ scripts/ci/demo_assets/static/index.js | 56 +++++++++++++------------- web_viewer/index.html | 2 + web_viewer/index_bundled.html | 2 + 6 files changed, 53 insertions(+), 30 deletions(-) diff --git a/crates/re_analytics/src/lib.rs b/crates/re_analytics/src/lib.rs index 633f1de1da3f..da6617199fbf 100644 --- a/crates/re_analytics/src/lib.rs +++ b/crates/re_analytics/src/lib.rs @@ -11,12 +11,16 @@ #[cfg(not(target_arch = "wasm32"))] mod native; #[cfg(not(target_arch = "wasm32"))] -use native::{Config, ConfigError, Pipeline, PipelineError}; +pub use native::{Config, ConfigError}; +#[cfg(not(target_arch = "wasm32"))] +use native::{Pipeline, PipelineError}; #[cfg(target_arch = "wasm32")] mod web; #[cfg(target_arch = "wasm32")] -use web::{Config, ConfigError, Pipeline, PipelineError}; +pub use web::{Config, ConfigError}; +#[cfg(target_arch = "wasm32")] +use web::{Pipeline, PipelineError}; #[cfg(not(target_arch = "wasm32"))] pub mod cli; diff --git a/crates/re_analytics/src/web/config.rs b/crates/re_analytics/src/web/config.rs index 76cf613e798f..a9dfa1508761 100644 --- a/crates/re_analytics/src/web/config.rs +++ b/crates/re_analytics/src/web/config.rs @@ -64,6 +64,13 @@ impl Config { } } + pub fn load_or_default() -> Result { + match Self::load()? { + Some(config) => Ok(config), + None => Config::new(), + } + } + #[allow(clippy::map_err_ignore)] pub fn save(&self) -> Result<(), ConfigError> { let storage = get_local_storage()?; diff --git a/crates/re_viewer/src/web.rs b/crates/re_viewer/src/web.rs index 079e85864a9a..37ced0d76187 100644 --- a/crates/re_viewer/src/web.rs +++ b/crates/re_viewer/src/web.rs @@ -143,6 +143,14 @@ fn create_app(cc: &eframe::CreationContext<'_>, url: Option) -> crate::A app } +#[cfg(feature = "analytics")] +#[wasm_bindgen] +pub fn set_email(email: String) { + let mut config = re_analytics::Config::load().unwrap().unwrap_or_default(); + config.opt_in_metadata.insert("email".into(), email.into()); + config.save().unwrap() +} + #[wasm_bindgen] pub fn is_webgpu_build() -> bool { !cfg!(feature = "webgl") diff --git a/scripts/ci/demo_assets/static/index.js b/scripts/ci/demo_assets/static/index.js index e1b9bf4ef8e1..01dde6e0d29b 100644 --- a/scripts/ci/demo_assets/static/index.js +++ b/scripts/ci/demo_assets/static/index.js @@ -36,7 +36,7 @@ if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(naviga

Try anyways

`); - document.querySelector('#try_anyways').addEventListener('click', function (event) { + document.querySelector("#try_anyways").addEventListener("click", function (event) { event.preventDefault(); load_wasm(); }); @@ -56,12 +56,12 @@ function load_wasm() {

`; - const status_element = document.getElementById('status'); + const status_element = document.getElementById("status"); function progress({ loaded, total_bytes }) { if (total_bytes != null) { - status_element.innerHTML = Math.round(Math.min(loaded / total_bytes * 100, 100)) + '%'; + status_element.innerHTML = Math.round(Math.min((loaded / total_bytes) * 100, 100)) + "%"; } else { - status_element.innerHTML = (loaded / (1024 * 1024)).toFixed(1) + 'MiB' + status_element.innerHTML = (loaded / (1024 * 1024)).toFixed(1) + "MiB"; } } @@ -71,55 +71,57 @@ function load_wasm() { }, 1500); async function wasm_with_progress() { - const response = await fetch('./re_viewer_bg.wasm'); + const response = await fetch("./re_viewer_bg.wasm"); // Use the uncompressed size var content_length; var content_multiplier = 1; // If the content is gzip encoded, try to get the uncompressed size. - if (response.headers.get('content-encoding') == 'gzip') { - content_length = response.headers.get('x-goog-meta-uncompressed-size'); + if (response.headers.get("content-encoding") == "gzip") { + content_length = response.headers.get("x-goog-meta-uncompressed-size"); // If the uncompressed size wasn't found 3 seems to be a very good approximation if (content_length == null) { - content_length = response.headers.get('content-length'); + content_length = response.headers.get("content-length"); content_multiplier = 3; } } else { - content_length = response.headers.get('content-length'); + content_length = response.headers.get("content-length"); } const total_bytes = parseInt(content_length, 10) * content_multiplier; let loaded = 0; - const res = new Response(new ReadableStream({ - async start(controller) { - const reader = response.body.getReader(); - for (; ;) { - const { done, value } = await reader.read(); - if (done) break; - loaded += value.byteLength; - progress({ loaded, total_bytes }) - controller.enqueue(value); - } - controller.close(); - }, - })); + const res = new Response( + new ReadableStream({ + async start(controller) { + const reader = response.body.getReader(); + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + loaded += value.byteLength; + progress({ loaded, total_bytes }); + controller.enqueue(value); + } + controller.close(); + }, + }) + ); const wasm = await res.blob(); // Don't fade in the progress bar if we haven't hit it already. clearTimeout(timeoutId); - wasm_bindgen(URL.createObjectURL(wasm)) - .then(on_wasm_loaded) - .catch(on_wasm_error); + wasm_bindgen(URL.createObjectURL(wasm)).then(on_wasm_loaded).catch(on_wasm_error); } wasm_with_progress(); } function on_wasm_loaded() { + window.set_email = (value) => wasm_bindgen.set_email(value); + // WebGPU version is currently only supported on browsers with WebGPU support, there is no dynamic fallback to WebGL. - if (wasm_bindgen.is_webgpu_build() && typeof navigator.gpu === 'undefined') { + if (wasm_bindgen.is_webgpu_build() && typeof navigator.gpu === "undefined") { console.debug("`navigator.gpu` is undefined. This indicates lack of WebGPU support."); show_center_html(`

@@ -174,8 +176,6 @@ function on_app_started(handle) { hide_center_html(); show_canvas(); - - if (window.location !== window.parent.location) { window.parent.postMessage("READY", "*"); } diff --git a/web_viewer/index.html b/web_viewer/index.html index cc140fe1f0d6..aab8ea13ac31 100644 --- a/web_viewer/index.html +++ b/web_viewer/index.html @@ -280,6 +280,8 @@ function on_wasm_loaded() { + window.set_email = (value) => wasm_bindgen.set_email(value); + // WebGPU version is currently only supported on browsers with WebGPU support, there is no dynamic fallback to WebGL. if (wasm_bindgen.is_webgpu_build() && typeof navigator.gpu === 'undefined') { console.debug("`navigator.gpu` is undefined. This indicates lack of WebGPU support."); diff --git a/web_viewer/index_bundled.html b/web_viewer/index_bundled.html index b034819c531f..26fd463e44f7 100644 --- a/web_viewer/index_bundled.html +++ b/web_viewer/index_bundled.html @@ -277,6 +277,8 @@ } function on_wasm_loaded() { + window.set_email = (value) => wasm_bindgen.set_email(value); + // WebGPU version is currently only supported on browsers with WebGPU support, there is no dynamic fallback to WebGL. if (wasm_bindgen.is_webgpu_build() && typeof navigator.gpu === 'undefined') { console.debug("`navigator.gpu` is undefined. This indicates lack of WebGPU support."); From b390c719a980e86ada2df1116ef278af1ada3f77 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:04:04 +0200 Subject: [PATCH 10/22] fix lint --- crates/re_viewer/src/web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/re_viewer/src/web.rs b/crates/re_viewer/src/web.rs index 37ced0d76187..17f23670ca6e 100644 --- a/crates/re_viewer/src/web.rs +++ b/crates/re_viewer/src/web.rs @@ -211,7 +211,7 @@ fn get_persist_state(info: &eframe::IntegrationInfo) -> bool { } fn get_query_bool(info: &eframe::IntegrationInfo, key: &str, default: bool) -> bool { - let default_int = if default { 1 } else { 0 }; + let default_int = i32::from(default); match info .web_info .location From 984253326663146b63868f417b974a97fe8d8565 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:04:49 +0200 Subject: [PATCH 11/22] fix the same lint differently --- crates/re_viewer/src/web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/re_viewer/src/web.rs b/crates/re_viewer/src/web.rs index 17f23670ca6e..38b6217417ed 100644 --- a/crates/re_viewer/src/web.rs +++ b/crates/re_viewer/src/web.rs @@ -211,7 +211,7 @@ fn get_persist_state(info: &eframe::IntegrationInfo) -> bool { } fn get_query_bool(info: &eframe::IntegrationInfo, key: &str, default: bool) -> bool { - let default_int = i32::from(default); + let default_int = default as i32; match info .web_info .location From 6bf93c56fac064fea26cb4558c32922969787645 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:21:08 +0200 Subject: [PATCH 12/22] set url in analytics when running on web --- crates/re_viewer/src/app.rs | 13 ++++++++++++- crates/re_viewer/src/viewer_analytics.rs | 20 +++++++++++++++++--- crates/re_viewer/src/web.rs | 5 +++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 8381325d966a..49fb82747b24 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -43,6 +43,10 @@ pub struct StartupOptions { /// Whether or not the app is running in the context of a Jupyter Notebook. pub is_in_notebook: bool, + /// Set to identify the web page the viewer is running on. + #[cfg(target_arch = "wasm32")] + pub analytics_url: Option, + /// Take a screenshot of the app and quit. /// We use this to generate screenshots of our exmples. #[cfg(not(target_arch = "wasm32"))] @@ -62,6 +66,9 @@ impl Default for StartupOptions { persist_state: true, is_in_notebook: false, + #[cfg(target_arch = "wasm32")] + analytics_url: None, + #[cfg(not(target_arch = "wasm32"))] screenshot_to_path_then_quit: None, @@ -172,7 +179,11 @@ impl App { AppState::default() }; - let mut analytics = ViewerAnalytics::new(startup_options.is_in_notebook); + let mut analytics = ViewerAnalytics::new( + startup_options.is_in_notebook, + #[cfg(target_arch = "wasm32")] + startup_options.analytics_url.clone(), + ); analytics.on_viewer_started(&build_info, app_env); let mut space_view_class_registry = SpaceViewClassRegistry::default(); diff --git a/crates/re_viewer/src/viewer_analytics.rs b/crates/re_viewer/src/viewer_analytics.rs index 367972323f42..62a3b682f654 100644 --- a/crates/re_viewer/src/viewer_analytics.rs +++ b/crates/re_viewer/src/viewer_analytics.rs @@ -22,7 +22,11 @@ pub struct ViewerAnalytics { #[cfg(feature = "analytics")] impl ViewerAnalytics { - pub fn new(disabled: bool) -> Self { + #[allow(unused_mut, clippy::let_and_return)] + pub fn new( + disabled: bool, + #[cfg(target_arch = "wasm32")] analytics_url: Option, + ) -> Self { if disabled { return Self { analytics: None }; } @@ -35,7 +39,14 @@ impl ViewerAnalytics { } }; - Self { analytics } + let mut analytics = Self { analytics }; + + #[cfg(target_arch = "wasm32")] + if let Some(url) = analytics_url { + analytics.register("url", url) + } + + analytics } fn record(&self, event: Event) { @@ -218,7 +229,10 @@ impl ViewerAnalytics { #[cfg(not(feature = "analytics"))] impl ViewerAnalytics { - pub fn new(_disabled: bool) -> Self { + pub fn new( + _disabled: bool, + #[cfg(target_arch = "wasm32")] _analytics_url: Option, + ) -> Self { Self {} } diff --git a/crates/re_viewer/src/web.rs b/crates/re_viewer/src/web.rs index 38b6217417ed..15cfdfd03467 100644 --- a/crates/re_viewer/src/web.rs +++ b/crates/re_viewer/src/web.rs @@ -85,6 +85,7 @@ fn create_app(cc: &eframe::CreationContext<'_>, url: Option) -> crate::A // On wasm32 we only have 4GB of memory to play around with. limit: Some(2_500_000_000), }, + analytics_url: Some(get_location_url(&cc.integration_info)), persist_state: get_persist_state(&cc.integration_info), is_in_notebook: is_in_notebook(&cc.integration_info), skip_welcome_screen: false, @@ -202,6 +203,10 @@ fn get_url(info: &eframe::IntegrationInfo) -> String { } } +fn get_location_url(info: &eframe::IntegrationInfo) -> String { + info.web_info.location.url.clone() +} + fn is_in_notebook(info: &eframe::IntegrationInfo) -> bool { get_query_bool(info, "notebook", false) } From 0cd858fabb1fcb6654bc0509a5407e112bfeb7ed Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 10:25:07 +0200 Subject: [PATCH 13/22] fix lints --- crates/re_viewer/src/viewer_analytics.rs | 2 +- crates/re_viewer/src/web.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/re_viewer/src/viewer_analytics.rs b/crates/re_viewer/src/viewer_analytics.rs index 62a3b682f654..d1d45426eebf 100644 --- a/crates/re_viewer/src/viewer_analytics.rs +++ b/crates/re_viewer/src/viewer_analytics.rs @@ -43,7 +43,7 @@ impl ViewerAnalytics { #[cfg(target_arch = "wasm32")] if let Some(url) = analytics_url { - analytics.register("url", url) + analytics.register("url", url); } analytics diff --git a/crates/re_viewer/src/web.rs b/crates/re_viewer/src/web.rs index 15cfdfd03467..6c79aba08c13 100644 --- a/crates/re_viewer/src/web.rs +++ b/crates/re_viewer/src/web.rs @@ -149,7 +149,7 @@ fn create_app(cc: &eframe::CreationContext<'_>, url: Option) -> crate::A pub fn set_email(email: String) { let mut config = re_analytics::Config::load().unwrap().unwrap_or_default(); config.opt_in_metadata.insert("email".into(), email.into()); - config.save().unwrap() + config.save().unwrap(); } #[wasm_bindgen] From 0e6406d01dbe944783344af90cd179998fbd4ead Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:56:50 +0200 Subject: [PATCH 14/22] explain project key --- crates/re_analytics/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/re_analytics/src/lib.rs b/crates/re_analytics/src/lib.rs index da6617199fbf..2dedeb81cb87 100644 --- a/crates/re_analytics/src/lib.rs +++ b/crates/re_analytics/src/lib.rs @@ -310,6 +310,9 @@ impl Analytics { } } +/// The "public" API key can be obtained at . +/// Make sure you are logged in to the right organization and have the correct project open. +/// Unfortunately that stuff is client-side routed, and there's no way to link directly to the right place. const PUBLIC_POSTHOG_PROJECT_KEY: &str = "phc_sgKidIE4WYYFSJHd8LEYY1UZqASpnfQKeMqlJfSXwqg"; #[derive(Debug, serde::Serialize)] From 717a2a0809b433d25a1799baaf9315479d6c1799 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:59:59 +0200 Subject: [PATCH 15/22] move posthog events into their own module --- crates/re_analytics/src/event.rs | 88 ++++++++++++++++++++++++++++++++ crates/re_analytics/src/lib.rs | 87 ++----------------------------- 2 files changed, 91 insertions(+), 84 deletions(-) create mode 100644 crates/re_analytics/src/event.rs diff --git a/crates/re_analytics/src/event.rs b/crates/re_analytics/src/event.rs new file mode 100644 index 000000000000..2b19dd875508 --- /dev/null +++ b/crates/re_analytics/src/event.rs @@ -0,0 +1,88 @@ +use crate::{Event, Property}; +use std::collections::HashMap; + +use time::OffsetDateTime; + +/// The "public" API key can be obtained at . +/// Make sure you are logged in to the right organization and have the correct project open. +/// Unfortunately that stuff is client-side routed, and there's no way to link directly to the right place. +pub const PUBLIC_POSTHOG_PROJECT_KEY: &str = "phc_sgKidIE4WYYFSJHd8LEYY1UZqASpnfQKeMqlJfSXwqg"; + +#[derive(Debug, serde::Serialize)] +#[serde(untagged)] +pub enum PostHogEvent<'a> { + Capture(PostHogCaptureEvent<'a>), + Identify(PostHogIdentifyEvent<'a>), +} + +impl<'a> PostHogEvent<'a> { + pub fn from_event(analytics_id: &'a str, session_id: &'a str, event: &'a Event) -> Self { + let properties = event.props.iter().map(|(name, value)| { + ( + name.as_ref(), + match value { + &Property::Integer(v) => v.into(), + &Property::Float(v) => v.into(), + Property::String(v) => v.as_str().into(), + &Property::Bool(v) => v.into(), + }, + ) + }); + + match event.kind { + crate::EventKind::Append => Self::Capture(PostHogCaptureEvent { + timestamp: event.time_utc, + event: event.name.as_ref(), + distinct_id: analytics_id, + properties: properties + .chain([("session_id", session_id.into())]) + .collect(), + }), + crate::EventKind::Update => Self::Identify(PostHogIdentifyEvent { + timestamp: event.time_utc, + event: "$identify", + distinct_id: analytics_id, + properties: [("session_id", session_id.into())].into(), + set: properties.collect(), + }), + } + } +} + +// See https://posthog.com/docs/api/post-only-endpoints#capture. +#[derive(Debug, serde::Serialize)] +pub struct PostHogCaptureEvent<'a> { + #[serde(with = "::time::serde::rfc3339")] + timestamp: OffsetDateTime, + event: &'a str, + distinct_id: &'a str, + properties: HashMap<&'a str, serde_json::Value>, +} + +// See https://posthog.com/docs/api/post-only-endpoints#identify. +#[derive(Debug, serde::Serialize)] +pub struct PostHogIdentifyEvent<'a> { + #[serde(with = "::time::serde::rfc3339")] + timestamp: OffsetDateTime, + event: &'a str, + distinct_id: &'a str, + properties: HashMap<&'a str, serde_json::Value>, + #[serde(rename = "$set")] + set: HashMap<&'a str, serde_json::Value>, +} + +// See https://posthog.com/docs/api/post-only-endpoints#batch-events. +#[derive(Debug, serde::Serialize)] +pub struct PostHogBatch<'a> { + api_key: &'static str, + batch: &'a [PostHogEvent<'a>], +} + +impl<'a> PostHogBatch<'a> { + pub fn from_events(events: &'a [PostHogEvent<'a>]) -> Self { + Self { + api_key: PUBLIC_POSTHOG_PROJECT_KEY, + batch: events, + } + } +} diff --git a/crates/re_analytics/src/lib.rs b/crates/re_analytics/src/lib.rs index 2dedeb81cb87..1f799fb7317a 100644 --- a/crates/re_analytics/src/lib.rs +++ b/crates/re_analytics/src/lib.rs @@ -25,6 +25,9 @@ use web::{Pipeline, PipelineError}; #[cfg(not(target_arch = "wasm32"))] pub mod cli; +mod event; +use event::{PostHogBatch, PostHogEvent}; + // ---------------------------------------------------------------------------- use std::borrow::Cow; @@ -309,87 +312,3 @@ impl Analytics { } } } - -/// The "public" API key can be obtained at . -/// Make sure you are logged in to the right organization and have the correct project open. -/// Unfortunately that stuff is client-side routed, and there's no way to link directly to the right place. -const PUBLIC_POSTHOG_PROJECT_KEY: &str = "phc_sgKidIE4WYYFSJHd8LEYY1UZqASpnfQKeMqlJfSXwqg"; - -#[derive(Debug, serde::Serialize)] -#[serde(untagged)] -enum PostHogEvent<'a> { - Capture(PostHogCaptureEvent<'a>), - Identify(PostHogIdentifyEvent<'a>), -} - -impl<'a> PostHogEvent<'a> { - fn from_event(analytics_id: &'a str, session_id: &'a str, event: &'a Event) -> Self { - let properties = event.props.iter().map(|(name, value)| { - ( - name.as_ref(), - match value { - &Property::Integer(v) => v.into(), - &Property::Float(v) => v.into(), - Property::String(v) => v.as_str().into(), - &Property::Bool(v) => v.into(), - }, - ) - }); - - match event.kind { - crate::EventKind::Append => Self::Capture(PostHogCaptureEvent { - timestamp: event.time_utc, - event: event.name.as_ref(), - distinct_id: analytics_id, - properties: properties - .chain([("session_id", session_id.into())]) - .collect(), - }), - crate::EventKind::Update => Self::Identify(PostHogIdentifyEvent { - timestamp: event.time_utc, - event: "$identify", - distinct_id: analytics_id, - properties: [("session_id", session_id.into())].into(), - set: properties.collect(), - }), - } - } -} - -// See https://posthog.com/docs/api/post-only-endpoints#capture. -#[derive(Debug, serde::Serialize)] -struct PostHogCaptureEvent<'a> { - #[serde(with = "::time::serde::rfc3339")] - timestamp: OffsetDateTime, - event: &'a str, - distinct_id: &'a str, - properties: HashMap<&'a str, serde_json::Value>, -} - -// See https://posthog.com/docs/api/post-only-endpoints#identify. -#[derive(Debug, serde::Serialize)] -struct PostHogIdentifyEvent<'a> { - #[serde(with = "::time::serde::rfc3339")] - timestamp: OffsetDateTime, - event: &'a str, - distinct_id: &'a str, - properties: HashMap<&'a str, serde_json::Value>, - #[serde(rename = "$set")] - set: HashMap<&'a str, serde_json::Value>, -} - -// See https://posthog.com/docs/api/post-only-endpoints#batch-events. -#[derive(Debug, serde::Serialize)] -struct PostHogBatch<'a> { - api_key: &'static str, - batch: &'a [PostHogEvent<'a>], -} - -impl<'a> PostHogBatch<'a> { - fn from_events(events: &'a [PostHogEvent<'a>]) -> Self { - Self { - api_key: PUBLIC_POSTHOG_PROJECT_KEY, - batch: events, - } - } -} From 54a5d9339c1820429cf9f1ef56ddc08127a61497 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:03:06 +0200 Subject: [PATCH 16/22] unshorten analytics config keys --- crates/re_analytics/src/web/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/re_analytics/src/web/config.rs b/crates/re_analytics/src/web/config.rs index a9dfa1508761..665911e5de14 100644 --- a/crates/re_analytics/src/web/config.rs +++ b/crates/re_analytics/src/web/config.rs @@ -25,14 +25,14 @@ pub enum ConfigError { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Config { // NOTE: not a UUID on purpose, it is sometimes useful to use handcrafted IDs. - #[serde(rename = "i")] + #[serde(rename = "analytics_id")] pub analytics_id: String, /// A unique ID for this session. #[serde(skip, default = "::uuid::Uuid::new_v4")] pub session_id: Uuid, - #[serde(rename = "m", default)] + #[serde(rename = "metadata", default)] pub opt_in_metadata: HashMap, } From ace0a61f4c88dda7fff5d137eeabae7a3351f97d Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:16:24 +0200 Subject: [PATCH 17/22] warn if config failed to load --- crates/re_analytics/src/lib.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/re_analytics/src/lib.rs b/crates/re_analytics/src/lib.rs index 1f799fb7317a..ab50dc4cc9f7 100644 --- a/crates/re_analytics/src/lib.rs +++ b/crates/re_analytics/src/lib.rs @@ -240,7 +240,18 @@ pub struct Analytics { impl Analytics { pub fn new(tick: Duration) -> Result { - let config = Config::load()?; + let config = match Config::load() { + Ok(config) => config, + + #[allow(unused_variables)] + Err(err) => { + // NOTE: This will cause the first run disclaimer to show up again on native, + // and analytics will be disabled for the rest of the session. + #[cfg(not(target_arch = "wasm32"))] + re_log::warn!("failed to load analytics config file: {err}"); + None + } + }; #[cfg(target_arch = "wasm32")] let config = if let Some(config) = config { From 1b7679e82a2ecb99064a26afef9d7e9a775c6afc Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:27:04 +0200 Subject: [PATCH 18/22] refactor analytics config load --- crates/re_analytics/src/lib.rs | 67 +++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/crates/re_analytics/src/lib.rs b/crates/re_analytics/src/lib.rs index ab50dc4cc9f7..8308ed01fd01 100644 --- a/crates/re_analytics/src/lib.rs +++ b/crates/re_analytics/src/lib.rs @@ -238,47 +238,54 @@ pub struct Analytics { event_id: AtomicU64, } -impl Analytics { - pub fn new(tick: Duration) -> Result { - let config = match Config::load() { - Ok(config) => config, - - #[allow(unused_variables)] - Err(err) => { - // NOTE: This will cause the first run disclaimer to show up again on native, - // and analytics will be disabled for the rest of the session. - #[cfg(not(target_arch = "wasm32"))] - re_log::warn!("failed to load analytics config file: {err}"); - None - } - }; +fn load_config() -> Result { + let config = match Config::load() { + Ok(config) => config, + #[allow(unused_variables)] + Err(err) => { + // NOTE: This will cause the first run disclaimer to show up again on native, + // and analytics will be disabled for the rest of the session. + #[cfg(not(target_arch = "wasm32"))] + re_log::warn!("failed to load analytics config file: {err}"); + None + } + }; - #[cfg(target_arch = "wasm32")] - let config = if let Some(config) = config { - config - } else { - // the config doesnt exist in local storage yet, save it - let config = Config::default(); - config.save()?; - config - }; + if let Some(config) = config { + re_log::trace!(?config, "loaded analytics config"); - #[cfg(not(target_arch = "wasm32"))] - let config = match config { - Some(config) => config, - None => Config::new()?, - }; + Ok(config) + } else { + re_log::trace!(?config, "initialized analytics config"); - re_log::trace!(?config, ?tick, "loaded analytics config"); + // NOTE: If this fails, we give up, because we can't produce + // a config on native any other way. + let config = Config::new()?; + #[cfg(not(target_arch = "wasm32"))] if config.is_first_run() { eprintln!("{DISCLAIMER}"); config.save()?; - re_log::trace!(?config, ?tick, "saved analytics config"); + re_log::trace!(?config, "saved analytics config"); } + #[cfg(target_arch = "wasm32")] + { + // always save the config on web, without printing a disclaimer. + config.save()?; + re_log::trace!(?config, "saved analytics config"); + } + + Ok(config) + } +} + +impl Analytics { + pub fn new(tick: Duration) -> Result { + let config = load_config()?; let pipeline = Pipeline::new(&config, tick)?; + re_log::trace!("initialized analytics pipeline"); Ok(Self { config, From 1f581356719c6f1cc63fe8f53837638ba4a41990 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:30:43 +0200 Subject: [PATCH 19/22] use `{:?}` instead of manually quoting --- crates/re_analytics/src/web/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/re_analytics/src/web/config.rs b/crates/re_analytics/src/web/config.rs index 665911e5de14..45315b0742c4 100644 --- a/crates/re_analytics/src/web/config.rs +++ b/crates/re_analytics/src/web/config.rs @@ -57,7 +57,7 @@ impl Config { let storage = get_local_storage()?; let value = storage .get_item(Self::STORAGE_KEY) - .map_err(|_| ConfigError::Storage(format!("failed to get `{}`", Self::STORAGE_KEY)))?; + .map_err(|_| ConfigError::Storage(format!("failed to get {:?}", Self::STORAGE_KEY)))?; match value { Some(value) => Ok(Some(serde_json::from_str(&value)?)), None => Ok(None), @@ -77,7 +77,7 @@ impl Config { let string = serde_json::to_string(self)?; storage .set_item(Self::STORAGE_KEY, &string) - .map_err(|_| ConfigError::Storage(format!("failed to set `{}`", Self::STORAGE_KEY))) + .map_err(|_| ConfigError::Storage(format!("failed to set {:?}", Self::STORAGE_KEY))) } pub fn is_first_run(&self) -> bool { From 0c00f5823c5ea24da35c4dec3234eb5beb5b26d7 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:50:20 +0200 Subject: [PATCH 20/22] only set url property if we're on a `rerun.io` domain --- crates/re_analytics/src/lib.rs | 1 + crates/re_viewer/src/app.rs | 10 +++------- crates/re_viewer/src/viewer_analytics.rs | 22 ++++++++++------------ crates/re_viewer/src/web.rs | 6 +----- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/crates/re_analytics/src/lib.rs b/crates/re_analytics/src/lib.rs index 8308ed01fd01..20c984cde4f8 100644 --- a/crates/re_analytics/src/lib.rs +++ b/crates/re_analytics/src/lib.rs @@ -192,6 +192,7 @@ impl From<&str> for Property { // --- +#[cfg(not(target_arch = "wasm32"))] const DISCLAIMER: &str = " Welcome to Rerun! diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 49fb82747b24..d82c5b16aae4 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -45,7 +45,7 @@ pub struct StartupOptions { /// Set to identify the web page the viewer is running on. #[cfg(target_arch = "wasm32")] - pub analytics_url: Option, + pub location: Option, /// Take a screenshot of the app and quit. /// We use this to generate screenshots of our exmples. @@ -67,7 +67,7 @@ impl Default for StartupOptions { is_in_notebook: false, #[cfg(target_arch = "wasm32")] - analytics_url: None, + location: None, #[cfg(not(target_arch = "wasm32"))] screenshot_to_path_then_quit: None, @@ -179,11 +179,7 @@ impl App { AppState::default() }; - let mut analytics = ViewerAnalytics::new( - startup_options.is_in_notebook, - #[cfg(target_arch = "wasm32")] - startup_options.analytics_url.clone(), - ); + let mut analytics = ViewerAnalytics::new(&startup_options); analytics.on_viewer_started(&build_info, app_env); let mut space_view_class_registry = SpaceViewClassRegistry::default(); diff --git a/crates/re_viewer/src/viewer_analytics.rs b/crates/re_viewer/src/viewer_analytics.rs index d1d45426eebf..29c761d4e9fb 100644 --- a/crates/re_viewer/src/viewer_analytics.rs +++ b/crates/re_viewer/src/viewer_analytics.rs @@ -13,6 +13,8 @@ use re_analytics::{Analytics, Event, Property}; use re_log_types::StoreSource; +use crate::StartupOptions; + pub struct ViewerAnalytics { // NOTE: Optional because it is possible to have the `analytics` feature flag enabled // while at the same time opting-out of analytics at run-time. @@ -23,11 +25,8 @@ pub struct ViewerAnalytics { #[cfg(feature = "analytics")] impl ViewerAnalytics { #[allow(unused_mut, clippy::let_and_return)] - pub fn new( - disabled: bool, - #[cfg(target_arch = "wasm32")] analytics_url: Option, - ) -> Self { - if disabled { + pub fn new(startup_options: &StartupOptions) -> Self { + if startup_options.is_in_notebook { return Self { analytics: None }; } @@ -42,8 +41,10 @@ impl ViewerAnalytics { let mut analytics = Self { analytics }; #[cfg(target_arch = "wasm32")] - if let Some(url) = analytics_url { - analytics.register("url", url); + if let Some(location) = startup_options.location.as_ref() { + if location.hostname.contains("rerun.io") { + analytics.register("url", location.url.clone()); + } } analytics @@ -229,10 +230,7 @@ impl ViewerAnalytics { #[cfg(not(feature = "analytics"))] impl ViewerAnalytics { - pub fn new( - _disabled: bool, - #[cfg(target_arch = "wasm32")] _analytics_url: Option, - ) -> Self { + pub fn new(_startup_options: &StartupOptions) -> Self { Self {} } @@ -243,5 +241,5 @@ impl ViewerAnalytics { ) { } - pub fn on_open_recording(&mut self, store_db: &re_data_store::StoreDb) {} + pub fn on_open_recording(&mut self, _store_db: &re_data_store::StoreDb) {} } diff --git a/crates/re_viewer/src/web.rs b/crates/re_viewer/src/web.rs index 6c79aba08c13..f0823cf4a05e 100644 --- a/crates/re_viewer/src/web.rs +++ b/crates/re_viewer/src/web.rs @@ -85,7 +85,7 @@ fn create_app(cc: &eframe::CreationContext<'_>, url: Option) -> crate::A // On wasm32 we only have 4GB of memory to play around with. limit: Some(2_500_000_000), }, - analytics_url: Some(get_location_url(&cc.integration_info)), + location: Some(cc.integration_info.web_info.location.clone()), persist_state: get_persist_state(&cc.integration_info), is_in_notebook: is_in_notebook(&cc.integration_info), skip_welcome_screen: false, @@ -203,10 +203,6 @@ fn get_url(info: &eframe::IntegrationInfo) -> String { } } -fn get_location_url(info: &eframe::IntegrationInfo) -> String { - info.web_info.location.url.clone() -} - fn is_in_notebook(info: &eframe::IntegrationInfo) -> bool { get_query_bool(info, "notebook", false) } From efb538b39f3c934d0d8176ed773cc6ea5119786e Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:50:32 +0200 Subject: [PATCH 21/22] document `set_email` --- crates/re_viewer/src/web.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/re_viewer/src/web.rs b/crates/re_viewer/src/web.rs index f0823cf4a05e..22d4ae3d2fe8 100644 --- a/crates/re_viewer/src/web.rs +++ b/crates/re_viewer/src/web.rs @@ -144,6 +144,11 @@ fn create_app(cc: &eframe::CreationContext<'_>, url: Option) -> crate::A app } +/// Used to set the "email" property in the analytics config, +/// in the same way as `rerun analytics email YOURNAME@rerun.io`. +/// +/// This one just panics when it fails, as it's only ever really run +/// by rerun employees manually in `app.rerun.io` and `demo.rerun.io`. #[cfg(feature = "analytics")] #[wasm_bindgen] pub fn set_email(email: String) { From f03ef1a8af8cbe2e37541f105a4b292da61ef448 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Mon, 4 Sep 2023 08:51:10 +0200 Subject: [PATCH 22/22] add comments + stricter hostname check --- crates/re_viewer/src/viewer_analytics.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/re_viewer/src/viewer_analytics.rs b/crates/re_viewer/src/viewer_analytics.rs index 29c761d4e9fb..a9fe1c7bf791 100644 --- a/crates/re_viewer/src/viewer_analytics.rs +++ b/crates/re_viewer/src/viewer_analytics.rs @@ -26,6 +26,8 @@ pub struct ViewerAnalytics { impl ViewerAnalytics { #[allow(unused_mut, clippy::let_and_return)] pub fn new(startup_options: &StartupOptions) -> Self { + // We only want to have analytics on `*.rerun.io`, + // so we early-out if we detect we're running in a notebook. if startup_options.is_in_notebook { return Self { analytics: None }; } @@ -40,9 +42,10 @@ impl ViewerAnalytics { let mut analytics = Self { analytics }; + // We only want to send `url` if we're on a `rerun.io` domain. #[cfg(target_arch = "wasm32")] if let Some(location) = startup_options.location.as_ref() { - if location.hostname.contains("rerun.io") { + if location.hostname == "rerun.io" || location.hostname.ends_with(".rerun.io") { analytics.register("url", location.url.clone()); } }