remote_settings/
telemetry.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use std::{fmt, sync::Arc};
6
7use crate::error::Error;
8
9/// Remote Settings sync status.
10#[derive(Debug, PartialEq, uniffi::Enum)]
11pub enum SyncStatus {
12    /// Sync completed and new data was stored.
13    Success,
14    /// Local data is already up to date, no new data was stored.
15    UpToDate,
16    /// A network-level error occurred (connection refused, timeout, bad HTTP status, ...)
17    NetworkError,
18    /// The server asked the client to back off.
19    BackoffError,
20    /// Content signature verification failed.
21    SignatureError,
22    /// Server error (5xx status)
23    ServerError,
24    /// An unknown error occurred.
25    UnknownError,
26}
27
28impl fmt::Display for SyncStatus {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        let s = match self {
31            SyncStatus::Success => "success",
32            SyncStatus::UpToDate => "up_to_date",
33            SyncStatus::NetworkError => "network_error",
34            SyncStatus::BackoffError => "backoff_error",
35            SyncStatus::SignatureError => "signature_error",
36            SyncStatus::ServerError => "server_error",
37            SyncStatus::UnknownError => "unknown_error",
38        };
39        f.write_str(s)
40    }
41}
42
43#[derive(Debug, PartialEq, uniffi::Record, Default)]
44pub struct UptakeEventExtras {
45    /// Main sync status.
46    pub value: Option<String>,
47    /// Source of the sync (eg. "settings-changes-monitoring", "main/{collection}", ...)
48    pub source: Option<String>,
49    /// Age of the data in milliseconds, if available.
50    pub age: Option<String>,
51    /// Trigger that caused the sync (eg. "manual", "startup", "scheduled", ...) if available.
52    pub trigger: Option<String>,
53    /// Timestamp received from the server, if available.
54    pub timestamp: Option<String>,
55    /// Duration of the sync operation in milliseconds, if available.
56    pub duration: Option<String>,
57    /// The name of the error that occurred, if available.
58    pub error_name: Option<String>,
59}
60
61/// Trait implemented by consumers to record Remote Settings metrics with Glean.
62///
63/// Consumers should implement this trait and pass it to
64/// [crate::RemoteSettingsService::set_telemetry].
65///
66/// Consumers implement the trait like this (Kotlin example):
67/// ```kotlin
68/// /* Import the UniFFI-generated bindings */
69/// import mozilla.appservices.remote_settings.RemoteSettingsTelemetry
70/// import mozilla.appservices.remote_settings.UptakeEventExtras
71/// /* Import the Glean-generated bindings */
72/// import org.mozilla.appservices.remote_settings.GleanMetrics.RemoteSettings as RSMetrics
73///
74/// class GleanTelemetry : RemoteSettingsTelemetry {
75///     override fun report_uptake(eventExtras: UptakeEventExtras) {
76///         RSMetrics.uptakeRemotesettings.record(eventExtras)
77///     }
78/// }
79///
80/// service.setTelemetry(GleanTelemetry())
81/// ```
82#[uniffi::export(with_foreign)]
83pub trait RemoteSettingsTelemetry: Send + Sync {
84    /// Report uptake event.
85    fn report_uptake(&self, extras: UptakeEventExtras);
86}
87
88struct NoopRemoteSettingsTelemetry;
89
90impl RemoteSettingsTelemetry for NoopRemoteSettingsTelemetry {
91    fn report_uptake(&self, _extras: UptakeEventExtras) {}
92}
93
94/// Wrapper around [RemoteSettingsTelemetry] used internally.
95#[derive(Clone)]
96pub struct RemoteSettingsTelemetryWrapper {
97    inner: Arc<dyn RemoteSettingsTelemetry>,
98}
99
100impl RemoteSettingsTelemetryWrapper {
101    pub fn new(inner: Arc<dyn RemoteSettingsTelemetry>) -> Self {
102        Self { inner }
103    }
104
105    pub fn noop() -> Self {
106        Self {
107            inner: Arc::new(NoopRemoteSettingsTelemetry),
108        }
109    }
110
111    pub fn report_uptake_success(&self, source: &str, duration: Option<u64>) {
112        self.inner.report_uptake(UptakeEventExtras {
113            value: Some(SyncStatus::Success.to_string()),
114            source: Some(source.to_string()),
115            age: None,
116            trigger: None,
117            timestamp: None,
118            duration: duration.map(|d| d.to_string()),
119            error_name: None,
120        });
121    }
122
123    pub fn report_uptake_up_to_date(&self, source: &str, duration: Option<u64>) {
124        self.inner.report_uptake(UptakeEventExtras {
125            value: Some(SyncStatus::UpToDate.to_string()),
126            source: Some(source.to_string()),
127            age: None,
128            trigger: None,
129            timestamp: None,
130            duration: duration.map(|d| d.to_string()),
131            error_name: None,
132        });
133    }
134
135    pub fn report_uptake_error(&self, error: &Error, source: &str) {
136        // This is a bit hacky and naive, but it allows us to get the original
137        // error type without needing to add too much machinery to our error types.
138        // This mimics what we do in the desktop client:
139        // https://searchfox.org/firefox-main/rev/26c440c6196eb0b4/services/settings/RemoteSettingsClient.sys.mjs#965
140        let error_name = format!("{error:?}")
141            .split(&['{', '('])
142            .next()
143            .unwrap_or("")
144            .trim()
145            .to_string();
146        self.inner.report_uptake(UptakeEventExtras {
147            value: Some(error_to_status(error).to_string()),
148            source: Some(source.to_string()),
149            age: None,
150            trigger: None,
151            timestamp: None,
152            duration: None,
153            error_name: Some(error_name),
154        });
155    }
156}
157
158fn error_to_status(error: &Error) -> SyncStatus {
159    match error {
160        Error::RequestError(viaduct::ViaductError::NetworkError(_))
161        | Error::ResponseError { .. } => SyncStatus::NetworkError,
162        Error::BackoffError(_) => SyncStatus::BackoffError,
163        #[cfg(feature = "signatures")]
164        Error::IncompleteSignatureDataError(_) => SyncStatus::SignatureError,
165        #[cfg(feature = "signatures")]
166        Error::SignatureError(_) => SyncStatus::SignatureError,
167        _ => SyncStatus::UnknownError,
168    }
169}