summaryrefslogtreecommitdiffstats
path: root/toolkit/components/glean/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/glean/src
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/glean/src')
-rw-r--r--toolkit/components/glean/src/init/mod.rs368
-rw-r--r--toolkit/components/glean/src/init/upload_pref.rs99
-rw-r--r--toolkit/components/glean/src/init/user_activity.rs129
-rw-r--r--toolkit/components/glean/src/init/viaduct_uploader.rs73
-rw-r--r--toolkit/components/glean/src/lib.rs210
5 files changed, 879 insertions, 0 deletions
diff --git a/toolkit/components/glean/src/init/mod.rs b/toolkit/components/glean/src/init/mod.rs
new file mode 100644
index 0000000000..f430cd7384
--- /dev/null
+++ b/toolkit/components/glean/src/init/mod.rs
@@ -0,0 +1,368 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use std::env;
+use std::ffi::CString;
+use std::ops::DerefMut;
+use std::path::PathBuf;
+
+use firefox_on_glean::{metrics, pings};
+use nserror::{nsresult, NS_ERROR_FAILURE};
+use nsstring::{nsACString, nsCString, nsString};
+use xpcom::interfaces::{
+ mozILocaleService, nsIFile, nsIPrefService, nsIProperties, nsIXULAppInfo, nsIXULRuntime,
+};
+use xpcom::{RefPtr, XpCom};
+
+use glean::{ClientInfoMetrics, Configuration};
+
+#[cfg(not(target_os = "android"))]
+mod upload_pref;
+#[cfg(not(target_os = "android"))]
+mod user_activity;
+mod viaduct_uploader;
+
+#[cfg(not(target_os = "android"))]
+use upload_pref::UploadPrefObserver;
+#[cfg(not(target_os = "android"))]
+use user_activity::UserActivityObserver;
+use viaduct_uploader::ViaductUploader;
+
+/// Project FOG's entry point.
+///
+/// This assembles client information and the Glean configuration and then initializes the global
+/// Glean instance.
+#[cfg(not(target_os = "android"))]
+#[no_mangle]
+pub extern "C" fn fog_init(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> nsresult {
+ let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled");
+ let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0;
+ let uploader = Some(Box::new(ViaductUploader) as Box<dyn glean::net::PingUploader>);
+
+ fog_init_internal(
+ data_path_override,
+ app_id_override,
+ upload_enabled || recording_enabled,
+ uploader,
+ )
+ .into()
+}
+
+/// Project FOG's entry point on Android.
+///
+/// This assembles client information and the Glean configuration and then initializes the global
+/// Glean instance.
+/// It always enables upload and set no uploader.
+/// This should only be called in test scenarios.
+/// In normal use Glean should be initialized and controlled by the Glean Kotlin SDK.
+#[cfg(target_os = "android")]
+#[no_mangle]
+pub extern "C" fn fog_init(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> nsresult {
+ // On Android always enable Glean upload.
+ let upload_enabled = true;
+ // Don't set up an uploader.
+ let uploader = None;
+
+ fog_init_internal(
+ data_path_override,
+ app_id_override,
+ upload_enabled,
+ uploader,
+ )
+ .into()
+}
+
+fn fog_init_internal(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+ upload_enabled: bool,
+ uploader: Option<Box<dyn glean::net::PingUploader>>,
+) -> Result<(), nsresult> {
+ metrics::fog::initialization.start();
+
+ log::debug!("Initializing FOG.");
+
+ setup_observers()?;
+
+ let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?;
+
+ conf.upload_enabled = upload_enabled;
+ conf.uploader = uploader;
+
+ // If we're operating in automation without any specific source tags to set,
+ // set the tag "automation" so any pings that escape don't clutter the tables.
+ // See https://mozilla.github.io/glean/book/user/debugging/index.html#enabling-debugging-features-through-environment-variables
+ if env::var("MOZ_AUTOMATION").is_ok() && env::var("GLEAN_SOURCE_TAGS").is_err() {
+ log::info!("In automation, setting 'automation' source tag.");
+ glean::set_source_tags(vec!["automation".to_string()]);
+ log::info!("In automation, disabling MPS to avoid 4am issues.");
+ conf.use_core_mps = false;
+ }
+
+ log::debug!("Configuration: {:#?}", conf);
+
+ // Register all custom pings before we initialize.
+ pings::register_pings(Some(&conf.application_id));
+
+ glean::initialize(conf, client_info);
+
+ metrics::fog::initialization.stop();
+
+ Ok(())
+}
+
+fn build_configuration(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> Result<(Configuration, ClientInfoMetrics), nsresult> {
+ let data_path_str = if data_path_override.is_empty() {
+ get_data_path()?
+ } else {
+ data_path_override.to_utf8().to_string()
+ };
+ let data_path = PathBuf::from(&data_path_str);
+
+ let (app_build, app_display_version, channel, locale) = get_app_info()?;
+
+ let client_info = ClientInfoMetrics {
+ app_build,
+ app_display_version,
+ channel: Some(channel),
+ locale: Some(locale),
+ };
+ log::debug!("Client Info: {:#?}", client_info);
+
+ const SERVER: &str = "https://incoming.telemetry.mozilla.org";
+ let localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port");
+ let server = if localhost_port > 0 {
+ format!("http://localhost:{}", localhost_port)
+ } else {
+ String::from(SERVER)
+ };
+
+ let application_id = if app_id_override.is_empty() {
+ "firefox.desktop".to_string()
+ } else {
+ app_id_override.to_utf8().to_string()
+ };
+
+ extern "C" {
+ fn FOG_MaxPingLimit() -> u32;
+ fn FOG_EventTimestampsEnabled() -> bool;
+ }
+
+ // SAFETY NOTE: Safe because it returns a primitive by value.
+ let pings_per_interval = unsafe { FOG_MaxPingLimit() };
+ metrics::fog::max_pings_per_minute.set(pings_per_interval.into());
+
+ // SAFETY NOTE: Safe because it returns a primitive by value.
+ let enable_event_timestamps = unsafe { FOG_EventTimestampsEnabled() };
+
+ let rate_limit = Some(glean::PingRateLimit {
+ seconds_per_interval: 60,
+ pings_per_interval,
+ });
+
+ let configuration = Configuration {
+ upload_enabled: false,
+ data_path,
+ application_id,
+ max_events: None,
+ delay_ping_lifetime_io: true,
+ server_endpoint: Some(server),
+ uploader: None,
+ use_core_mps: true,
+ trim_data_to_registered_pings: true,
+ log_level: None,
+ rate_limit,
+ enable_event_timestamps,
+ experimentation_id: None,
+ };
+
+ Ok((configuration, client_info))
+}
+
+#[cfg(not(target_os = "android"))]
+fn setup_observers() -> Result<(), nsresult> {
+ if let Err(e) = UploadPrefObserver::begin_observing() {
+ log::error!(
+ "Could not observe data upload pref. Abandoning FOG init due to {:?}",
+ e
+ );
+ return Err(e);
+ }
+
+ if let Err(e) = UserActivityObserver::begin_observing() {
+ log::error!(
+ "Could not observe user activity. Abandoning FOG init due to {:?}",
+ e
+ );
+ return Err(e);
+ }
+
+ Ok(())
+}
+
+#[cfg(target_os = "android")]
+fn setup_observers() -> Result<(), nsresult> {
+ // No observers are set up on Android.
+ Ok(())
+}
+
+/// Construct and return the data_path from the profile dir, or return an error.
+fn get_data_path() -> Result<String, nsresult> {
+ let dir_svc: RefPtr<nsIProperties> = match xpcom::components::Directory::service() {
+ Ok(ds) => ds,
+ _ => return Err(NS_ERROR_FAILURE),
+ };
+ let mut profile_dir = xpcom::GetterAddrefs::<nsIFile>::new();
+ unsafe {
+ dir_svc
+ .Get(
+ cstr!("ProfD").as_ptr(),
+ &nsIFile::IID,
+ profile_dir.void_ptr(),
+ )
+ .to_result()?;
+ }
+ let profile_dir = profile_dir.refptr().ok_or(NS_ERROR_FAILURE)?;
+ let mut profile_path = nsString::new();
+ unsafe {
+ (*profile_dir).GetPath(&mut *profile_path).to_result()?;
+ }
+ let profile_path = String::from_utf16(&profile_path[..]).map_err(|_| NS_ERROR_FAILURE)?;
+ let data_path = profile_path + "/datareporting/glean";
+ Ok(data_path)
+}
+
+/// Return a tuple of the build_id, app version, build channel, and locale.
+/// If the XUL Runtime isn't a XULAppInfo (e.g. in xpcshell),
+/// build_id ad app_version will be "unknown".
+/// Other problems result in an error being returned instead.
+fn get_app_info() -> Result<(String, String, String, String), nsresult> {
+ let xul: RefPtr<nsIXULRuntime> =
+ xpcom::components::XULRuntime::service().map_err(|_| NS_ERROR_FAILURE)?;
+
+ let pref_service: RefPtr<nsIPrefService> =
+ xpcom::components::Preferences::service().map_err(|_| NS_ERROR_FAILURE)?;
+ let locale_service: RefPtr<mozILocaleService> =
+ xpcom::components::Locale::service().map_err(|_| NS_ERROR_FAILURE)?;
+ let branch = xpcom::getter_addrefs(|p| {
+ // Safe because:
+ // * `null` is explicitly allowed per documentation
+ // * `p` is a valid outparam guaranteed by `getter_addrefs`
+ unsafe { pref_service.GetDefaultBranch(std::ptr::null(), p) }
+ })?;
+ let pref_name = CString::new("app.update.channel").map_err(|_| NS_ERROR_FAILURE)?;
+ let mut channel = nsCString::new();
+ // Safe because:
+ // * `branch` is non-null (otherwise `getter_addrefs` would've been `Err`
+ // * `pref_name` exists so a pointer to it is valid for the life of the function
+ // * `channel` exists so a pointer to it is valid, and it can be written to
+ unsafe {
+ if (*branch)
+ .GetCharPref(pref_name.as_ptr(), channel.deref_mut() as *mut nsACString)
+ .to_result()
+ .is_err()
+ {
+ channel = "unknown".into();
+ }
+ }
+
+ let app_info = match xul.query_interface::<nsIXULAppInfo>() {
+ Some(ai) => ai,
+ // In e.g. xpcshell the XULRuntime isn't XULAppInfo.
+ // We still want to return sensible values so tests don't explode.
+ _ => {
+ return Ok((
+ "unknown".to_owned(),
+ "unknown".to_owned(),
+ channel.to_string(),
+ "unknown".to_owned(),
+ ))
+ }
+ };
+
+ let mut build_id = nsCString::new();
+ unsafe {
+ app_info.GetAppBuildID(&mut *build_id).to_result()?;
+ }
+
+ let mut version = nsCString::new();
+ unsafe {
+ app_info.GetVersion(&mut *version).to_result()?;
+ }
+
+ let mut locale = nsCString::new();
+ unsafe {
+ locale_service
+ .GetAppLocaleAsBCP47(&mut *locale)
+ .to_result()?;
+ }
+
+ Ok((
+ build_id.to_string(),
+ version.to_string(),
+ channel.to_string(),
+ locale.to_string(),
+ ))
+}
+
+/// **TEST-ONLY METHOD**
+/// Resets FOG and the underlying Glean SDK, clearing stores.
+#[no_mangle]
+pub extern "C" fn fog_test_reset(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> nsresult {
+ fog_test_reset_internal(data_path_override, app_id_override).into()
+}
+
+// Split out into its own function so I could use `?`
+#[cfg(not(target_os = "android"))]
+fn fog_test_reset_internal(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> Result<(), nsresult> {
+ let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?;
+
+ let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled");
+ let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0;
+ conf.upload_enabled = upload_enabled || recording_enabled;
+
+ // Don't accidentally send "main" pings during tests.
+ conf.use_core_mps = false;
+
+ // I'd prefer to reuse the uploader, but it gets moved into Glean so we build anew.
+ conf.uploader = Some(Box::new(ViaductUploader) as Box<dyn glean::net::PingUploader>);
+
+ glean::test_reset_glean(conf, client_info, true);
+ Ok(())
+}
+
+#[cfg(target_os = "android")]
+fn fog_test_reset_internal(
+ data_path_override: &nsACString,
+ app_id_override: &nsACString,
+) -> Result<(), nsresult> {
+ let (mut conf, client_info) = build_configuration(data_path_override, app_id_override)?;
+
+ // On Android always enable Glean upload.
+ conf.upload_enabled = true;
+
+ // Don't accidentally send "main" pings during tests.
+ conf.use_core_mps = false;
+
+ // Same as before, would prefer to reuse, but it gets moved into Glean so we build anew.
+ conf.uploader = Some(Box::new(ViaductUploader) as Box<dyn glean::net::PingUploader>);
+
+ glean::test_reset_glean(conf, client_info, true);
+ Ok(())
+}
diff --git a/toolkit/components/glean/src/init/upload_pref.rs b/toolkit/components/glean/src/init/upload_pref.rs
new file mode 100644
index 0000000000..737230c16c
--- /dev/null
+++ b/toolkit/components/glean/src/init/upload_pref.rs
@@ -0,0 +1,99 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use std::ffi::CStr;
+use std::os::raw::c_char;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use nsstring::{nsACString, nsCStr};
+use xpcom::{
+ interfaces::{nsIPrefBranch, nsISupports},
+ RefPtr,
+};
+
+/// Whether the current value of the localhost testing pref is permitting
+/// metric recording (even if upload is disabled).
+static RECORDING_ENABLED: AtomicBool = AtomicBool::new(false);
+
+// Partially cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1192
+#[xpcom(implement(nsIObserver), atomic)]
+pub(crate) struct UploadPrefObserver {}
+
+#[allow(non_snake_case)]
+impl UploadPrefObserver {
+ pub(crate) fn begin_observing() -> Result<(), nsresult> {
+ // Ensure we begin with the correct current value of RECORDING_ENABLED.
+ let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0;
+ RECORDING_ENABLED.store(recording_enabled, Ordering::SeqCst);
+
+ // SAFETY: Everything here is self-contained.
+ //
+ // * We allocate the pref observer, created by the xpcom macro
+ // * We query the pref service and bail out if it doesn't exist.
+ // * We create a nsCStr from a static string.
+ // * We control all input to `AddObserverImpl`
+ unsafe {
+ let pref_obs = Self::allocate(InitUploadPrefObserver {});
+ let pref_branch: RefPtr<nsIPrefBranch> =
+ xpcom::components::Preferences::service().map_err(|_| NS_ERROR_FAILURE)?;
+ let pref_nscstr =
+ &nsCStr::from("datareporting.healthreport.uploadEnabled") as &nsACString;
+ (*pref_branch)
+ .AddObserverImpl(pref_nscstr, pref_obs.coerce(), false)
+ .to_result()?;
+ let pref_nscstr = &nsCStr::from("telemetry.fog.test.localhost_port") as &nsACString;
+ (*pref_branch)
+ .AddObserverImpl(pref_nscstr, pref_obs.coerce(), false)
+ .to_result()?;
+ }
+
+ Ok(())
+ }
+
+ unsafe fn Observe(
+ &self,
+ _subject: *const nsISupports,
+ topic: *const c_char,
+ pref_name: *const u16,
+ ) -> nserror::nsresult {
+ let topic = CStr::from_ptr(topic).to_str().unwrap();
+ // Conversion utf16 to utf8 is messy.
+ // We should only ever observe changes to one of the two prefs we want,
+ // but just to be on the safe side let's assert.
+
+ // cargo-culted from https://searchfox.org/mozilla-central/rev/598e50d2c3cd81cd616654f16af811adceb08f9f/security/manager/ssl/cert_storage/src/lib.rs#1606-1612
+ // (with a little transformation)
+ let len = (0..).take_while(|&i| *pref_name.offset(i) != 0).count(); // find NUL.
+ let slice = std::slice::from_raw_parts(pref_name, len);
+ let pref_name = match String::from_utf16(slice) {
+ Ok(name) => name,
+ Err(_) => return NS_ERROR_FAILURE,
+ };
+ log::info!("Observed {:?}, {:?}", topic, pref_name);
+ debug_assert!(topic == "nsPref:changed");
+ debug_assert!(
+ pref_name == "datareporting.healthreport.uploadEnabled"
+ || pref_name == "telemetry.fog.test.localhost_port"
+ );
+
+ let upload_enabled = static_prefs::pref!("datareporting.healthreport.uploadEnabled");
+ let recording_enabled = static_prefs::pref!("telemetry.fog.test.localhost_port") < 0;
+ log::info!(
+ "New upload_enabled {}, recording_enabled {}",
+ upload_enabled,
+ recording_enabled
+ );
+ if RECORDING_ENABLED.load(Ordering::SeqCst) && !recording_enabled {
+ // Whenever the test pref goes from permitting recording to forbidding it,
+ // ensure Glean is told to wipe the stores.
+ // This may send a "deletion-request" ping for a client_id that's never sent
+ // any other pings.
+ glean::set_upload_enabled(false);
+ }
+ RECORDING_ENABLED.store(recording_enabled, Ordering::SeqCst);
+ glean::set_upload_enabled(upload_enabled || recording_enabled);
+ NS_OK
+ }
+}
diff --git a/toolkit/components/glean/src/init/user_activity.rs b/toolkit/components/glean/src/init/user_activity.rs
new file mode 100644
index 0000000000..dbafeca4e8
--- /dev/null
+++ b/toolkit/components/glean/src/init/user_activity.rs
@@ -0,0 +1,129 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use std::ffi::CStr;
+use std::os::raw::c_char;
+use std::sync::{
+ atomic::{AtomicBool, Ordering},
+ RwLock,
+};
+use std::time::{Duration, Instant};
+
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use xpcom::{
+ interfaces::{nsIObserverService, nsISupports},
+ RefPtr,
+};
+
+// Partially cargo-culted from UploadPrefObserver.
+#[xpcom(implement(nsIObserver), atomic)]
+pub(crate) struct UserActivityObserver {
+ last_edge: RwLock<Instant>,
+ was_active: AtomicBool,
+}
+
+/// Listens to Firefox Desktop's `user-interaction-(in)active` topics,
+/// debouncing them before calling into the Glean SDK Client Activity API.
+/// See
+/// [the docs](https://firefox-source-docs.mozilla.org/toolkit/components/glean/builtin_pings.html)
+/// for more info.
+#[allow(non_snake_case)]
+impl UserActivityObserver {
+ pub(crate) fn begin_observing() -> Result<(), nsresult> {
+ // First and foremost, even if we can't get the ObserverService,
+ // init always means client activity.
+ glean::handle_client_active();
+
+ // SAFETY: Everything here is self-contained.
+ //
+ // * We allocate the activity observer, created by the xpcom macro
+ // * We create cstr from a static string.
+ // * We control all input to `AddObserver`
+ unsafe {
+ let activity_obs = Self::allocate(InitUserActivityObserver {
+ last_edge: RwLock::new(Instant::now()),
+ was_active: AtomicBool::new(false),
+ });
+ let obs_service: RefPtr<nsIObserverService> =
+ xpcom::components::Observer::service().map_err(|_| NS_ERROR_FAILURE)?;
+ let rv = obs_service.AddObserver(
+ activity_obs.coerce(),
+ cstr!("user-interaction-active").as_ptr(),
+ false,
+ );
+ if !rv.succeeded() {
+ return Err(rv);
+ }
+ let rv = obs_service.AddObserver(
+ activity_obs.coerce(),
+ cstr!("user-interaction-inactive").as_ptr(),
+ false,
+ );
+ if !rv.succeeded() {
+ return Err(rv);
+ }
+ }
+ Ok(())
+ }
+
+ unsafe fn Observe(
+ &self,
+ _subject: *const nsISupports,
+ topic: *const c_char,
+ _data: *const u16,
+ ) -> nserror::nsresult {
+ match CStr::from_ptr(topic).to_str() {
+ Ok("user-interaction-active") => self.handle_active(),
+ Ok("user-interaction-inactive") => self.handle_inactive(),
+ _ => NS_OK,
+ }
+ }
+
+ fn handle_active(&self) -> nserror::nsresult {
+ let was_active = self.was_active.swap(true, Ordering::SeqCst);
+ if !was_active {
+ let inactivity = self
+ .last_edge
+ .read()
+ .expect("Edge lock poisoned.")
+ .elapsed();
+ // We only care after a certain period of inactivity (default 20min).
+ let limit = static_prefs::pref!("telemetry.fog.test.inactivity_limit");
+ if inactivity >= Duration::from_secs(limit.into()) {
+ log::info!(
+ "User triggers core activity after {}s!",
+ inactivity.as_secs()
+ );
+ glean::handle_client_active();
+ }
+ let mut edge = self.last_edge.write().expect("Edge lock poisoned.");
+ *edge = Instant::now();
+ }
+ NS_OK
+ }
+
+ fn handle_inactive(&self) -> nserror::nsresult {
+ let was_active = self.was_active.swap(false, Ordering::SeqCst);
+ // This is actually always so. Inactivity is only notified once.
+ if was_active {
+ let activity = self
+ .last_edge
+ .read()
+ .expect("Edge lock poisoned.")
+ .elapsed();
+ // We only care after a certain period of activity (default 2min).
+ let limit = static_prefs::pref!("telemetry.fog.test.activity_limit");
+ if activity >= Duration::from_secs(limit.into()) {
+ log::info!(
+ "User triggers core inactivity after {}s!",
+ activity.as_secs()
+ );
+ glean::handle_client_inactive();
+ }
+ let mut edge = self.last_edge.write().expect("Edge lock poisoned.");
+ *edge = Instant::now();
+ }
+ NS_OK
+ }
+}
diff --git a/toolkit/components/glean/src/init/viaduct_uploader.rs b/toolkit/components/glean/src/init/viaduct_uploader.rs
new file mode 100644
index 0000000000..d9ce4e0488
--- /dev/null
+++ b/toolkit/components/glean/src/init/viaduct_uploader.rs
@@ -0,0 +1,73 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use glean::net::{PingUploader, UploadResult};
+use url::Url;
+use viaduct::{Error::*, Request};
+
+extern "C" {
+ fn FOG_TooLateToSend() -> bool;
+}
+
+/// An uploader that uses [Viaduct](https://github.com/mozilla/application-services/tree/main/components/viaduct).
+#[derive(Debug)]
+pub(crate) struct ViaductUploader;
+
+impl PingUploader for ViaductUploader {
+ /// Uploads a ping to a server.
+ ///
+ /// # Arguments
+ ///
+ /// * `url` - the URL path to upload the data to.
+ /// * `body` - the serialized text data to send.
+ /// * `headers` - a vector of tuples containing the headers to send with
+ /// the request, i.e. (Name, Value).
+ fn upload(&self, url: String, body: Vec<u8>, headers: Vec<(String, String)>) -> UploadResult {
+ log::trace!("FOG Ping Uploader uploading to {}", url);
+ let url_clone = url.clone();
+ let result: std::result::Result<UploadResult, viaduct::Error> = (move || {
+ // SAFETY NOTE: Safe because it returns a primitive by value.
+ if unsafe { FOG_TooLateToSend() } {
+ log::trace!("Attempted to send ping too late into shutdown.");
+ return Ok(UploadResult::done());
+ }
+ let debug_tagged = headers.iter().any(|(name, _)| name == "X-Debug-ID");
+ let localhost_port = static_prefs::pref!("telemetry.fog.test.localhost_port");
+ if localhost_port < 0
+ || (localhost_port == 0 && !debug_tagged && cfg!(feature = "disable_upload"))
+ {
+ log::info!("FOG Ping uploader faking success");
+ return Ok(UploadResult::http_status(200));
+ }
+ let parsed_url = Url::parse(&url_clone)?;
+
+ log::info!("FOG Ping uploader uploading to {:?}", parsed_url);
+
+ let mut req = Request::post(parsed_url.clone()).body(body.clone());
+ for (header_key, header_value) in &headers {
+ req = req.header(header_key.to_owned(), header_value)?;
+ }
+
+ log::trace!("FOG Ping Uploader sending ping to {}", parsed_url);
+ let res = req.send()?;
+ Ok(UploadResult::http_status(res.status as i32))
+ })();
+ log::trace!(
+ "FOG Ping Uploader completed uploading to {} (Result {:?})",
+ url,
+ result
+ );
+ match result {
+ Ok(result) => result,
+ Err(NonTlsUrl | UrlError(_)) => UploadResult::unrecoverable_failure(),
+ Err(
+ RequestHeaderError(_)
+ | BackendError(_)
+ | NetworkError(_)
+ | BackendNotInitialized
+ | SetBackendError,
+ ) => UploadResult::recoverable_failure(),
+ }
+ }
+}
diff --git a/toolkit/components/glean/src/lib.rs b/toolkit/components/glean/src/lib.rs
new file mode 100644
index 0000000000..79f3258bb7
--- /dev/null
+++ b/toolkit/components/glean/src/lib.rs
@@ -0,0 +1,210 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! Firefox on Glean (FOG) is the name of the layer that integrates the [Glean SDK][glean-sdk] into Firefox Desktop.
+//! It is currently being designed and implemented.
+//!
+//! The [Glean SDK][glean-sdk] is a data collection library built by Mozilla for use in its products.
+//! Like [Telemetry][telemetry], it can be used to
+//! (in accordance with our [Privacy Policy][privacy-policy])
+//! send anonymous usage statistics to Mozilla in order to make better decisions.
+//!
+//! Documentation can be found online in the [Firefox Source Docs][docs].
+//!
+//! [glean-sdk]: https://github.com/mozilla/glean/
+//! [book-of-glean]: https://mozilla.github.io/glean/book/index.html
+//! [privacy-policy]: https://www.mozilla.org/privacy/
+//! [docs]: https://firefox-source-docs.mozilla.org/toolkit/components/glean/
+
+use firefox_on_glean::{ipc, metrics, pings};
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use nsstring::{nsACString, nsCString};
+use thin_vec::ThinVec;
+
+#[macro_use]
+extern crate cstr;
+#[cfg_attr(not(target_os = "android"), macro_use)]
+extern crate xpcom;
+
+mod init;
+
+pub use init::fog_init;
+
+#[no_mangle]
+pub extern "C" fn fog_shutdown() {
+ glean::shutdown();
+}
+
+#[no_mangle]
+pub extern "C" fn fog_register_pings() {
+ pings::register_pings(None);
+}
+
+static mut PENDING_BUF: Vec<u8> = Vec::new();
+
+// IPC serialization/deserialization methods
+// Crucially important that the first two not be called on multiple threads.
+
+/// Only safe if only called on a single thread (the same single thread you call
+/// fog_give_ipc_buf on).
+#[no_mangle]
+pub unsafe extern "C" fn fog_serialize_ipc_buf() -> usize {
+ if let Some(buf) = ipc::take_buf() {
+ PENDING_BUF = buf;
+ PENDING_BUF.len()
+ } else {
+ PENDING_BUF = vec![];
+ 0
+ }
+}
+
+/// Only safe if called on a single thread (the same single thread you call
+/// fog_serialize_ipc_buf on), and if buf points to an allocated buffer of at
+/// least buf_len bytes.
+#[no_mangle]
+pub unsafe extern "C" fn fog_give_ipc_buf(buf: *mut u8, buf_len: usize) -> usize {
+ let pending_len = PENDING_BUF.len();
+ if buf.is_null() || buf_len < pending_len {
+ return 0;
+ }
+ std::ptr::copy_nonoverlapping(PENDING_BUF.as_ptr(), buf, pending_len);
+ PENDING_BUF = Vec::new();
+ pending_len
+}
+
+/// Only safe if buf points to an allocated buffer of at least buf_len bytes.
+/// No ownership is transfered to Rust by this method: caller owns the memory at
+/// buf before and after this call.
+#[no_mangle]
+pub unsafe extern "C" fn fog_use_ipc_buf(buf: *const u8, buf_len: usize) {
+ let slice = std::slice::from_raw_parts(buf, buf_len);
+ let res = ipc::replay_from_buf(slice);
+ if res.is_err() {
+ log::warn!("Unable to replay ipc buffer. This will result in data loss.");
+ metrics::fog_ipc::replay_failures.add(1);
+ }
+}
+
+/// Sets the debug tag for pings assembled in the future.
+/// Returns an error result if the provided value is not a valid tag.
+#[no_mangle]
+pub extern "C" fn fog_set_debug_view_tag(value: &nsACString) -> nsresult {
+ let result = glean::set_debug_view_tag(&value.to_string());
+ if result {
+ return NS_OK;
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+}
+
+/// Submits a ping by name.
+#[no_mangle]
+pub extern "C" fn fog_submit_ping(ping_name: &nsACString) -> nsresult {
+ glean::submit_ping_by_name(&ping_name.to_string(), None);
+ NS_OK
+}
+
+/// Turns ping logging on or off.
+/// Returns an error if the logging failed to be configured.
+#[no_mangle]
+pub extern "C" fn fog_set_log_pings(value: bool) -> nsresult {
+ glean::set_log_pings(value);
+ NS_OK
+}
+
+/// Flushes ping-lifetime data to the db when delay_ping_lifetime_io is true.
+#[no_mangle]
+pub extern "C" fn fog_persist_ping_lifetime_data() -> nsresult {
+ glean::persist_ping_lifetime_data();
+ NS_OK
+}
+
+/// Indicate that an experiment is running.
+/// Glean will add an experiment annotation which is sent with pings.
+/// This information is not persisted between runs.
+///
+/// See [`glean_core::Glean::set_experiment_active`].
+#[no_mangle]
+pub extern "C" fn fog_set_experiment_active(
+ experiment_id: &nsACString,
+ branch: &nsACString,
+ extra_keys: &ThinVec<nsCString>,
+ extra_values: &ThinVec<nsCString>,
+) {
+ assert_eq!(
+ extra_keys.len(),
+ extra_values.len(),
+ "Experiment extra keys and values differ in length."
+ );
+ let extra = if extra_keys.len() == 0 {
+ None
+ } else {
+ Some(
+ extra_keys
+ .iter()
+ .zip(extra_values.iter())
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect(),
+ )
+ };
+ glean::set_experiment_active(experiment_id.to_string(), branch.to_string(), extra);
+}
+
+/// Indicate that an experiment is no longer running.
+///
+/// See [`glean_core::Glean::set_experiment_inactive`].
+#[no_mangle]
+pub extern "C" fn fog_set_experiment_inactive(experiment_id: &nsACString) {
+ glean::set_experiment_inactive(experiment_id.to_string());
+}
+
+/// TEST ONLY FUNCTION
+///
+/// Returns true if the identified experiment is active.
+#[no_mangle]
+pub extern "C" fn fog_test_is_experiment_active(experiment_id: &nsACString) -> bool {
+ glean::test_is_experiment_active(experiment_id.to_string())
+}
+
+/// TEST ONLY FUNCTION
+///
+/// Fills `branch`, `extra_keys`, and `extra_values` with the identified experiment's data.
+/// Panics if the identified experiment isn't active.
+#[no_mangle]
+pub extern "C" fn fog_test_get_experiment_data(
+ experiment_id: &nsACString,
+ branch: &mut nsACString,
+ extra_keys: &mut ThinVec<nsCString>,
+ extra_values: &mut ThinVec<nsCString>,
+) {
+ let data = glean::test_get_experiment_data(experiment_id.to_string());
+ if let Some(data) = data {
+ branch.assign(&data.branch);
+ if let Some(extra) = data.extra {
+ let (data_keys, data_values): (Vec<_>, Vec<_>) = extra.iter().unzip();
+ extra_keys.extend(data_keys.into_iter().map(|key| key.into()));
+ extra_values.extend(data_values.into_iter().map(|value| value.into()));
+ }
+ }
+}
+
+/// Sets the remote feature configuration.
+///
+/// See [`glean_core::Glean::set_metrics_disabled_config`].
+#[no_mangle]
+pub extern "C" fn fog_set_metrics_feature_config(config_json: &nsACString) {
+ // Normalize null and empty strings to a stringified empty map
+ if config_json == "null" || config_json.is_empty() {
+ glean::glean_set_metrics_enabled_config("{}".to_owned());
+ }
+ glean::glean_set_metrics_enabled_config(config_json.to_string());
+}
+
+/// Performs Glean tasks when client state changes to inactive
+///
+/// See [`glean_core::Glean::handle_client_inactive`].
+#[no_mangle]
+pub extern "C" fn fog_internal_glean_handle_client_inactive() {
+ glean::handle_client_inactive();
+}