Skip to main content

rpfm_telemetry/
actions.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Anonymous action telemetry, shared between the UI and the server.
12//!
13//! Tracks which actions (UI slots, server commands, ...) are used during a
14//! session and sends aggregated counts to Sentry on graceful shutdown. This
15//! helps understand which features are most used and guides development
16//! priorities.
17//!
18//! Telemetry is opt-out: the flag defaults to `true` so events captured
19//! during early startup (before settings have been loaded) aren't lost.
20//! Callers refresh it via [`set_usage_telemetry_enabled`] once they know
21//! the user's preference. While disabled, [`track_action`] still emits a
22//! log line for debugging, but no counters are kept.
23
24use std::collections::HashMap;
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::{LazyLock, RwLock};
27
28use crate::info;
29use crate::logger::{sentry, Event, Level};
30
31/// Tag key attached to usage-telemetry Sentry events so the [`crate::logger`]
32/// `before_send` hook can tell them apart from crash reports.
33pub(crate) const TELEMETRY_EVENT_TAG: &str = "rpfm.kind";
34pub(crate) const TELEMETRY_EVENT_VALUE: &str = "usage_telemetry";
35
36/// Whether usage telemetry counter updates are enabled. On by default so
37/// early-boot actions (before settings are loaded) are still counted;
38/// callers may opt out via [`set_usage_telemetry_enabled`].
39pub(crate) static USAGE_TELEMETRY_ENABLED: AtomicBool = AtomicBool::new(true);
40
41/// Global action counter. Keys are action names, values are invocation counts.
42static ACTION_COUNTS: LazyLock<RwLock<HashMap<String, u64>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
43
44/// Enables or disables usage-telemetry counter updates.
45///
46/// Callers should set this once at startup (from the `enable_usage_telemetry`
47/// setting) and refresh it whenever the setting changes. Pending counts
48/// accumulated while enabled are preserved and will still be flushed.
49pub fn set_usage_telemetry_enabled(enabled: bool) {
50    USAGE_TELEMETRY_ENABLED.store(enabled, Ordering::Relaxed);
51}
52
53/// Returns the current usage-telemetry enabled state.
54pub fn is_usage_telemetry_enabled() -> bool {
55    USAGE_TELEMETRY_ENABLED.load(Ordering::Relaxed)
56}
57
58/// Records a single action occurrence.
59///
60/// Also emits an `info!` log line for consistency with the existing logging.
61/// If telemetry is disabled, only the log line is emitted.
62pub fn track_action(action: &str) {
63    info!("Triggering `{}` By Slot", action);
64    record_action(action);
65}
66
67/// Records a single action occurrence without emitting a log line.
68///
69/// Useful for callers (like the server) that already log incoming actions
70/// elsewhere and don't want a duplicate log line. No-op when telemetry is
71/// disabled.
72pub fn record_action(action: &str) {
73    if USAGE_TELEMETRY_ENABLED.load(Ordering::Relaxed) {
74        if let Ok(mut counts) = ACTION_COUNTS.write() {
75            *counts.entry(action.to_string()).or_insert(0) += 1;
76        }
77    }
78}
79
80/// Sends accumulated action telemetry to Sentry and clears the counters.
81///
82/// Should be called once on graceful shutdown, while the Sentry guard is
83/// still alive. `source` is used as the Sentry event message (e.g.
84/// `"UI Action Telemetry"` or `"Server Action Telemetry"`), allowing UI and
85/// server events to be distinguished in Sentry.
86///
87/// The event is tagged as usage-telemetry so the logger's `before_send`
88/// filter lets it through even when crash reports are disabled.
89pub fn flush(source: &str) {
90    let counts = match ACTION_COUNTS.write() {
91        Ok(mut guard) => guard.drain().collect::<HashMap<String, u64>>(),
92        Err(_) => return,
93    };
94
95    if counts.is_empty() {
96        return;
97    }
98
99    info!("Flushing action telemetry ({} distinct actions)...", counts.len());
100
101    let mut event = Event::new();
102    event.level = Level::Info;
103    event.message = Some(source.to_string());
104    event.tags.insert(TELEMETRY_EVENT_TAG.to_string(), TELEMETRY_EVENT_VALUE.to_string());
105
106    for (action, count) in &counts {
107        event.extra.insert(
108            action.clone(),
109            sentry::protocol::Value::from(*count),
110        );
111    }
112
113    sentry::capture_event(event);
114}