re_analytics/
event.rs

1// This file is linked to in a number of places, do not move/rename it without updating all the links!
2
3//! All analytics events collected by the Rerun viewer are defined in this file.
4//!
5//! Analytics can be completely disabled with `rerun analytics disable`,
6//! or by compiling rerun without the `analytics` feature flag.
7//!
8//! All collected analytics data is anonymized, stripping all personal identifiable information
9//! as well as information about user data.
10//! Read more about our analytics policy at <https://github.com/rerun-io/rerun/tree/main/crates/utils/re_analytics>.
11
12/// Records a crash caused by a panic.
13///
14/// Used in `re_crash_handler`.
15pub struct CrashPanic {
16    pub build_info: BuildInfo,
17    pub callstack: String,
18    pub message: Option<String>,
19    pub file_line: Option<String>,
20}
21
22/// Holds information about the user's environment.
23///
24/// Used in `re_viewer`.
25pub struct Identify {
26    /// Info on how the `re_viewer` crate was built.
27    pub build_info: re_build_info::BuildInfo,
28
29    // If we happen to know the Python or Rust version used on the _host machine_, i.e. the
30    // machine running the viewer, then override the versions from `build_info`.
31    //
32    // The Python/Rust versions appearing in user profiles always apply to the host
33    // environment, _not_ the environment in which the data logging is taking place!
34    pub rust_version: Option<String>,
35    pub llvm_version: Option<String>,
36    pub python_version: Option<String>,
37
38    /// Opt-in meta-data you can set via `rerun analytics`.
39    ///
40    /// For instance, Rerun employees are encouraged to set `rerun analytics email`.
41    /// For real users, this is usually empty.
42    pub opt_in_metadata: HashMap<String, Property>,
43}
44
45/// Sent when the viewer is first started.
46///
47/// Used in `re_viewer`.
48pub struct ViewerStarted {
49    /// The URL on which the web viewer is running.
50    ///
51    /// This will be used to populate `hashed_root_domain` property for all urls.
52    /// This will also populate `rerun_url` property if the url root domain is `rerun.io`.
53    pub url: Option<String>,
54
55    /// The environment in which the viewer is running.
56    pub app_env: &'static str,
57
58    /// Sparse information about the runtime environment the viewer is running in.
59    pub runtime_info: ViewerRuntimeInformation,
60}
61
62/// Some sparse information about the runtime environment the viewer is running in.
63pub struct ViewerRuntimeInformation {
64    /// Whether the viewer is started directly from within Windows Subsystem for Linux (WSL).
65    pub is_wsl: bool,
66
67    /// The wgpu graphics backend used by the viewer.
68    ///
69    /// For possible values see [`wgpu::Backend`](https://docs.rs/wgpu/latest/wgpu/enum.Backend.html).
70    pub graphics_adapter_backend: String,
71
72    /// The device tier `re_renderer` identified for the graphics adapter.
73    ///
74    /// For possible values see [`re_renderer::config::DeviceTier`](https://docs.rs/re_renderer/latest/re_renderer/config/enum.DeviceTier.html).
75    /// This is a very rough indication of the capabilities of the graphics adapter.
76    /// We do not want to send details graphics driver/capability information here since
77    /// it's too detailed (could be used for fingerprinting which we don't want) and not as useful
78    /// anyways since it's hard to learn about the typically identified capabilities.
79    pub re_renderer_device_tier: String,
80}
81
82impl Properties for ViewerRuntimeInformation {
83    fn serialize(self, event: &mut AnalyticsEvent) {
84        let Self {
85            is_wsl,
86            graphics_adapter_backend,
87            re_renderer_device_tier,
88        } = self;
89
90        event.insert("is_wsl", is_wsl);
91        event.insert("graphics_adapter_backend", graphics_adapter_backend);
92        event.insert("re_renderer_device_tier", re_renderer_device_tier);
93    }
94}
95
96/// Sent when a new recording is opened.
97///
98/// Used in `re_viewer`.
99pub struct OpenRecording {
100    /// The URL on which the web viewer is running.
101    ///
102    /// This will be used to populate `hashed_root_domain` property for all urls.
103    /// This will also populate `rerun_url` property if the url root domain is `rerun.io`.
104    pub url: Option<String>,
105
106    /// The environment in which the viewer is running.
107    pub app_env: &'static str,
108
109    pub store_info: Option<StoreInfo>,
110
111    /// How data is being loaded into the viewer.
112    pub data_source: Option<&'static str>,
113}
114
115/// Basic information about a recording's chunk store.
116pub struct StoreInfo {
117    /// Name of the application.
118    ///
119    /// In case the recording does not come from an official example, the id is hashed.
120    pub application_id: Id,
121
122    /// Name of the recording.
123    ///
124    /// In case the recording does not come from an official example, the id is hashed.
125    pub recording_id: Id,
126
127    /// Where data is being logged.
128    pub store_source: String,
129
130    /// The Rerun version that was used to encode the RRD data.
131    pub store_version: String,
132
133    // Various versions of the host environment.
134    pub rust_version: Option<String>,
135    pub llvm_version: Option<String>,
136    pub python_version: Option<String>,
137
138    // Whether or not the data is coming from one of the Rerun example applications.
139    pub app_id_starts_with_rerun_example: bool,
140}
141
142#[derive(Clone)]
143pub enum Id {
144    /// When running an example application we record the full id.
145    Official(String),
146
147    /// For user applications we hash the id.
148    Hashed(Property),
149}
150
151/// Sent when a Wasm file is served.
152///
153/// Used in `re_web_viewer_server`.
154pub struct ServeWasm;
155
156impl Event for ServeWasm {
157    const NAME: &'static str = "serve_wasm";
158}
159
160impl Properties for ServeWasm {
161    // No properties.
162}
163
164// ----------------------------------------------------------------------------
165
166use std::collections::HashMap;
167
168use re_build_info::BuildInfo;
169use url::Url;
170
171use crate::{AnalyticsEvent, Event, EventKind, Properties, Property};
172
173impl From<Id> for Property {
174    fn from(val: Id) -> Self {
175        match val {
176            Id::Official(id) => Self::String(id),
177            Id::Hashed(id) => id,
178        }
179    }
180}
181
182impl Event for Identify {
183    const NAME: &'static str = "$identify";
184
185    const KIND: EventKind = EventKind::Update;
186}
187
188impl Properties for Identify {
189    fn serialize(self, event: &mut AnalyticsEvent) {
190        let Self {
191            build_info,
192            rust_version,
193            llvm_version,
194            python_version,
195            opt_in_metadata,
196        } = self;
197
198        build_info.serialize(event);
199        event.insert_opt("rust_version", rust_version);
200        event.insert_opt("llvm_version", llvm_version);
201        event.insert_opt("python_version", python_version);
202        for (name, value) in opt_in_metadata {
203            event.insert(name, value);
204        }
205    }
206}
207
208impl Event for ViewerStarted {
209    const NAME: &'static str = "viewer_started";
210}
211
212const RERUN_DOMAINS: [&str; 1] = ["rerun.io"];
213
214/// Given a URL, extract the root domain.
215fn extract_root_domain(url: &str) -> Option<String> {
216    let parsed = Url::parse(url).ok()?;
217    let domain = parsed.domain()?;
218    let parts = domain.split('.').collect::<Vec<_>>();
219    if parts.len() >= 2 {
220        Some(parts[parts.len() - 2..].join("."))
221    } else {
222        None
223    }
224}
225
226fn add_sanitized_url_properties(event: &mut AnalyticsEvent, url: Option<String>) {
227    let Some(root_domain) = url.as_ref().and_then(|url| extract_root_domain(url)) else {
228        return;
229    };
230
231    if RERUN_DOMAINS.contains(&root_domain.as_str()) {
232        event.insert_opt("rerun_url", url);
233    }
234
235    let hashed = Property::from(root_domain).hashed();
236    event.insert("hashed_root_domain", hashed);
237}
238
239impl Properties for ViewerStarted {
240    fn serialize(self, event: &mut AnalyticsEvent) {
241        let Self {
242            url,
243            app_env,
244            runtime_info,
245        } = self;
246
247        event.insert("app_env", app_env);
248        add_sanitized_url_properties(event, url);
249        runtime_info.serialize(event);
250    }
251}
252
253impl Event for OpenRecording {
254    const NAME: &'static str = "open_recording";
255}
256
257impl Properties for OpenRecording {
258    fn serialize(self, event: &mut AnalyticsEvent) {
259        let Self {
260            url,
261            app_env,
262            store_info,
263            data_source,
264        } = self;
265
266        add_sanitized_url_properties(event, url);
267
268        event.insert("app_env", app_env);
269
270        if let Some(store_info) = store_info {
271            let StoreInfo {
272                application_id,
273                recording_id,
274                store_source,
275                store_version,
276                rust_version,
277                llvm_version,
278                python_version,
279
280                app_id_starts_with_rerun_example,
281            } = store_info;
282
283            event.insert("application_id", application_id);
284            event.insert("recording_id", recording_id);
285            event.insert("store_source", store_source);
286            event.insert("store_version", store_version);
287            event.insert_opt("rust_version", rust_version);
288            event.insert_opt("llvm_version", llvm_version);
289            event.insert_opt("python_version", python_version);
290            event.insert(
291                "app_id_starts_with_rerun_example",
292                app_id_starts_with_rerun_example,
293            );
294        }
295
296        if let Some(data_source) = data_source {
297            event.insert("data_source", data_source);
298        }
299    }
300}
301
302impl Event for CrashPanic {
303    const NAME: &'static str = "crash-panic";
304}
305
306impl Properties for CrashPanic {
307    fn serialize(self, event: &mut AnalyticsEvent) {
308        let Self {
309            build_info,
310            callstack,
311            message,
312            file_line,
313        } = self;
314
315        build_info.serialize(event);
316        event.insert("callstack", callstack);
317        event.insert_opt("message", message);
318        event.insert_opt("file_line", file_line);
319    }
320}
321
322pub struct CrashSignal {
323    pub build_info: BuildInfo,
324    pub signal: String,
325    pub callstack: String,
326}
327
328impl Event for CrashSignal {
329    const NAME: &'static str = "crash-signal";
330}
331
332impl Properties for CrashSignal {
333    fn serialize(self, event: &mut AnalyticsEvent) {
334        let Self {
335            build_info,
336            signal,
337            callstack,
338        } = self;
339
340        build_info.serialize(event);
341        event.insert("signal", signal.clone());
342        event.insert("callstack", callstack.clone());
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_root_domain() {
352        // Valid urls
353        assert_eq!(
354            extract_root_domain("https://rerun.io"),
355            Some("rerun.io".to_owned())
356        );
357        assert_eq!(
358            extract_root_domain("https://ReRun.io"),
359            Some("rerun.io".to_owned())
360        );
361        assert_eq!(
362            extract_root_domain("http://app.rerun.io"),
363            Some("rerun.io".to_owned())
364        );
365        assert_eq!(
366            extract_root_domain(
367                "https://www.rerun.io/viewer?url=https://app.rerun.io/version/0.15.1/examples/detect_and_track_objects.rrd"
368            ),
369            Some("rerun.io".to_owned())
370        );
371
372        // Local domains
373        assert_eq!(
374            extract_root_domain("http://localhost:9090/?url=rerun%2Bhttp://localhost:9877"),
375            None
376        );
377        assert_eq!(
378            extract_root_domain("http://127.0.0.1:9090/?url=rerun%2Bhttp://localhost:9877"),
379            None
380        );
381
382        // Invalid urls
383        assert_eq!(extract_root_domain("rerun.io"), None);
384        assert_eq!(extract_root_domain("https:/rerun"), None);
385        assert_eq!(extract_root_domain("https://rerun"), None);
386    }
387}