Skip to content

Commit 7353ca8

Browse files
Adds anonymous telemetry data for tracking usage and interactions within app
Signed-off-by: Cole Gentry <peapod2007@gmail.com>
1 parent 4bca3ec commit 7353ca8

13 files changed

Lines changed: 996 additions & 10 deletions

File tree

Cargo.lock

Lines changed: 604 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ anyhow = "1.0"
6666
tracing = "0.1"
6767
tracing-subscriber = "0.3"
6868

69+
# Analytics
70+
posthog-rs = "0.3"
71+
72+
# UUID generation for anonymous user IDs
73+
uuid = { version = "1.0", features = ["v4"] }
74+
6975
# Windows-specific: embed icon and manifest
7076
[target.'cfg(windows)'.build-dependencies]
7177
winresource = "0.1"

src/analytics.rs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
//! Analytics module for UltraLog using PostHog.
2+
//!
3+
//! This module provides anonymous usage analytics to help improve UltraLog.
4+
//! All data is anonymous - we only track feature usage, not personal information.
5+
6+
use posthog_rs::Event;
7+
use std::sync::OnceLock;
8+
use uuid::Uuid;
9+
10+
/// PostHog API key for UltraLog analytics
11+
const POSTHOG_API_KEY: &str = "phc_jrZkZhkhoHXknLz7djnuBR8s4tl9mZnR00UAWVl2GHO";
12+
13+
/// Application version for tracking
14+
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
15+
16+
/// Global distinct ID for this session
17+
static DISTINCT_ID: OnceLock<String> = OnceLock::new();
18+
19+
/// Flag to track if global client is initialized
20+
static INITIALIZED: OnceLock<bool> = OnceLock::new();
21+
22+
/// Get or generate the session's distinct ID
23+
fn get_distinct_id() -> &'static str {
24+
DISTINCT_ID.get_or_init(|| Uuid::new_v4().to_string())
25+
}
26+
27+
/// Initialize the global PostHog client (call once at startup)
28+
fn ensure_initialized() {
29+
INITIALIZED.get_or_init(|| {
30+
// Initialize the global PostHog client using builder pattern
31+
if let Ok(options) = posthog_rs::ClientOptionsBuilder::default()
32+
.api_key(POSTHOG_API_KEY.to_string())
33+
.build()
34+
{
35+
let _ = posthog_rs::init_global(options);
36+
}
37+
true
38+
});
39+
}
40+
41+
/// Get the current platform as a string
42+
fn get_platform() -> &'static str {
43+
#[cfg(target_os = "windows")]
44+
return "windows";
45+
46+
#[cfg(target_os = "macos")]
47+
return "macos";
48+
49+
#[cfg(target_os = "linux")]
50+
return "linux";
51+
52+
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
53+
return "unknown";
54+
}
55+
56+
/// Create a base event with common properties
57+
fn create_event(event_name: &str) -> Event {
58+
let mut event = Event::new(event_name, get_distinct_id());
59+
60+
// Add common properties
61+
let _ = event.insert_prop("app_version", APP_VERSION);
62+
let _ = event.insert_prop("platform", get_platform());
63+
64+
event
65+
}
66+
67+
/// Capture an event using the global client (fire and forget - errors are silently ignored)
68+
fn capture_event(event: Event) {
69+
ensure_initialized();
70+
71+
// Spawn in background thread to avoid blocking UI
72+
std::thread::spawn(move || {
73+
let _ = posthog_rs::capture(event);
74+
});
75+
}
76+
77+
// ============================================================================
78+
// Public Analytics Functions
79+
// ============================================================================
80+
81+
/// Track application startup
82+
pub fn track_app_started() {
83+
let event = create_event("app_started");
84+
capture_event(event);
85+
}
86+
87+
/// Track when a log file is loaded
88+
pub fn track_file_loaded(ecu_type: &str, file_size_bytes: u64) {
89+
let mut event = create_event("file_loaded");
90+
91+
let _ = event.insert_prop("ecu_type", ecu_type);
92+
let _ = event.insert_prop("file_size_kb", file_size_bytes / 1024);
93+
94+
capture_event(event);
95+
}
96+
97+
/// Track when a channel is selected
98+
pub fn track_channel_selected(channel_count: usize) {
99+
let mut event = create_event("channel_selected");
100+
101+
let _ = event.insert_prop("total_channels", channel_count);
102+
103+
capture_event(event);
104+
}
105+
106+
/// Track chart export (PNG or PDF)
107+
pub fn track_export(format: &str) {
108+
let mut event = create_event("chart_exported");
109+
110+
let _ = event.insert_prop("format", format);
111+
112+
capture_event(event);
113+
}
114+
115+
/// Track tool/view switch
116+
pub fn track_tool_switched(tool_name: &str) {
117+
let mut event = create_event("tool_switched");
118+
119+
let _ = event.insert_prop("tool", tool_name);
120+
121+
capture_event(event);
122+
}
123+
124+
/// Track playback usage
125+
pub fn track_playback_started(speed: f64) {
126+
let mut event = create_event("playback_started");
127+
128+
let _ = event.insert_prop("speed", speed);
129+
130+
capture_event(event);
131+
}
132+
133+
/// Track unit preference changes
134+
#[allow(dead_code)]
135+
pub fn track_unit_changed(unit_category: &str, new_unit: &str) {
136+
let mut event = create_event("unit_changed");
137+
138+
let _ = event.insert_prop("category", unit_category);
139+
let _ = event.insert_prop("new_unit", new_unit);
140+
141+
capture_event(event);
142+
}
143+
144+
/// Track update check
145+
pub fn track_update_checked(update_available: bool) {
146+
let mut event = create_event("update_checked");
147+
148+
let _ = event.insert_prop("update_available", update_available);
149+
150+
capture_event(event);
151+
}
152+
153+
/// Track colorblind mode toggle
154+
pub fn track_colorblind_mode_toggled(enabled: bool) {
155+
let mut event = create_event("colorblind_mode_toggled");
156+
157+
let _ = event.insert_prop("enabled", enabled);
158+
159+
capture_event(event);
160+
}
161+
162+
/// Track file format detection errors (helps prioritize new format support)
163+
#[allow(dead_code)]
164+
pub fn track_file_format_error(error_type: &str) {
165+
let mut event = create_event("file_format_error");
166+
167+
let _ = event.insert_prop("error_type", error_type);
168+
169+
capture_event(event);
170+
}

src/app.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::path::PathBuf;
1111
use std::sync::mpsc::{channel, Receiver, Sender};
1212
use std::thread;
1313

14+
use crate::analytics;
1415
use crate::parsers::{EcuMaster, EcuType, Haltech, Parseable, RomRaider, Speeduino};
1516
use crate::state::{
1617
ActiveTool, CacheKey, LoadResult, LoadedFile, LoadingState, ScatterPlotConfig,
@@ -423,6 +424,13 @@ impl UltraLogApp {
423424
let file_index = self.files.len();
424425
let file_name = file.name.clone();
425426

427+
// Track file load for analytics
428+
let ecu_type_str = format!("{:?}", file.ecu_type);
429+
let file_size = std::fs::metadata(&file.path)
430+
.map(|m| m.len())
431+
.unwrap_or(0);
432+
analytics::track_file_loaded(&ecu_type_str, file_size);
433+
426434
// Compute time range for this file
427435
let times = file.log.get_times_as_f64();
428436
let file_time_range =
@@ -703,6 +711,9 @@ impl UltraLogApp {
703711
channel,
704712
color_index,
705713
});
714+
715+
// Track channel selection for analytics
716+
analytics::track_channel_selected(self.tabs[tab_idx].selected_channels.len());
706717
}
707718

708719
/// Remove a channel from the active tab's selection
@@ -934,9 +945,11 @@ impl UltraLogApp {
934945
UpdateCheckResult::UpdateAvailable(info) => {
935946
self.update_state = UpdateState::UpdateAvailable(info);
936947
self.show_update_dialog = true;
948+
analytics::track_update_checked(true);
937949
}
938950
UpdateCheckResult::UpToDate => {
939951
self.update_state = UpdateState::Idle;
952+
analytics::track_update_checked(false);
940953
// Only show toast for manual checks (not startup)
941954
if self.startup_check_done {
942955
self.show_toast_success("You're running the latest version");

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//! - [`units`] - Unit preference types and conversion utilities
1212
//! - [`normalize`] - Field name normalization for standardizing channel names
1313
//! - [`updater`] - Auto-update functionality for checking and downloading updates
14+
//! - [`analytics`] - Anonymous usage analytics via PostHog
1415
//! - [`ui`] - User interface components
1516
//! - `sidebar` - File list and view options
1617
//! - `channels` - Channel selection and display
@@ -20,6 +21,7 @@
2021
//! - `toast` - Toast notification system
2122
//! - `icons` - Custom icon drawing utilities
2223
24+
pub mod analytics;
2325
pub mod app;
2426
pub mod normalize;
2527
pub mod parsers;

src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ fn main() -> eframe::Result<()> {
6969
// Initialize logging
7070
tracing_subscriber::fmt::init();
7171

72+
// Track app startup for analytics
73+
ultralog::analytics::track_app_started();
74+
7275
// Load platform-specific app icon
7376
let icon = load_app_icon();
7477

src/ui/export.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::io::BufWriter;
77
// Use fully qualified path to disambiguate from printpdf's image module
88
use ::image::{Rgba, RgbaImage};
99

10+
use crate::analytics;
1011
use crate::app::UltraLogApp;
1112
use crate::normalize::normalize_channel_name_with_custom;
1213

@@ -24,7 +25,10 @@ impl UltraLogApp {
2425

2526
// Create a simple chart representation as image
2627
match self.render_chart_to_png(&path) {
27-
Ok(_) => self.show_toast_success("Chart exported as PNG"),
28+
Ok(_) => {
29+
analytics::track_export("png");
30+
self.show_toast_success("Chart exported as PNG");
31+
}
2832
Err(e) => self.show_toast_error(&format!("Export failed: {}", e)),
2933
}
3034
}
@@ -41,7 +45,10 @@ impl UltraLogApp {
4145
};
4246

4347
match self.render_chart_to_pdf(&path) {
44-
Ok(_) => self.show_toast_success("Chart exported as PDF"),
48+
Ok(_) => {
49+
analytics::track_export("pdf");
50+
self.show_toast_success("Chart exported as PDF");
51+
}
4552
Err(e) => self.show_toast_error(&format!("Export failed: {}", e)),
4653
}
4754
}

src/ui/menu.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use eframe::egui;
44

5+
use crate::analytics;
56
use crate::app::UltraLogApp;
67
use crate::state::LoadingState;
78
use crate::units::{
@@ -90,10 +91,14 @@ impl UltraLogApp {
9091
}
9192

9293
// Color Blind Mode toggle
94+
let old_color_blind_mode = self.color_blind_mode;
9395
if ui
9496
.checkbox(&mut self.color_blind_mode, "👁 Color Blind Mode")
9597
.clicked()
9698
{
99+
if self.color_blind_mode != old_color_blind_mode {
100+
analytics::track_colorblind_mode_toggled(self.color_blind_mode);
101+
}
97102
ui.close();
98103
}
99104

src/ui/timeline.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use eframe::egui;
44

5+
use crate::analytics;
56
use crate::app::UltraLogApp;
67

78
impl UltraLogApp {
@@ -80,6 +81,8 @@ impl UltraLogApp {
8081
if ui.add(play_button).clicked() {
8182
self.is_playing = !self.is_playing;
8283
if self.is_playing {
84+
// Track playback start for analytics
85+
analytics::track_playback_started(self.playback_speed);
8386
// Reset frame time when starting playback
8487
self.last_frame_time = Some(std::time::Instant::now());
8588
// Initialize cursor if not set

src/ui/tool_switcher.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use eframe::egui;
77

8+
use crate::analytics;
89
use crate::app::UltraLogApp;
910
use crate::state::ActiveTool;
1011

@@ -54,6 +55,7 @@ impl UltraLogApp {
5455

5556
if response.clicked() {
5657
self.active_tool = tool;
58+
analytics::track_tool_switched(tool.name());
5759
}
5860
if response.hovered() {
5961
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);

0 commit comments

Comments
 (0)