ads_client/
instrument.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*/
5
6use std::sync::LazyLock;
7
8use crate::error::{ComponentError, EmitTelemetryError};
9use parking_lot::RwLock;
10use serde::{Deserialize, Serialize};
11use url::Url;
12use viaduct::Request;
13
14static DEFAULT_TELEMETRY_ENDPOINT: &str = "https://ads.mozilla.org/v1/log";
15static TELEMETRY_ENDPONT: LazyLock<RwLock<String>> =
16    LazyLock::new(|| RwLock::new(DEFAULT_TELEMETRY_ENDPOINT.to_string()));
17
18#[cfg(test)]
19pub fn set_telemetry_endpoint(endpoint: String) {
20    let mut telemetry_endpoint_lock = TELEMETRY_ENDPONT.write();
21    *telemetry_endpoint_lock = endpoint;
22}
23
24fn get_telemetry_endpoint() -> String {
25    TELEMETRY_ENDPONT.read().clone()
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum TelemetryEvent {
31    Init,
32    RenderError,
33    AdLoadError,
34    FetchError,
35    InvalidUrlError,
36}
37
38pub trait TrackError<T, ComponentError> {
39    fn emit_telemetry_if_error(self) -> Self;
40}
41
42impl<T> TrackError<T, ComponentError> for Result<T, ComponentError> {
43    /// Attempts to emit a telemetry event if the Error type can map to an event type.
44    fn emit_telemetry_if_error(self) -> Self {
45        if let Err(ref err) = self {
46            let error_type = map_error_to_event_type(err);
47            let _ = emit_telemetry_event(error_type);
48        }
49        self
50    }
51}
52
53fn map_error_to_event_type(err: &ComponentError) -> Option<TelemetryEvent> {
54    match err {
55        ComponentError::RequestAds(_) => Some(TelemetryEvent::FetchError),
56        ComponentError::RecordImpression(_) => Some(TelemetryEvent::InvalidUrlError),
57        ComponentError::RecordClick(_) => Some(TelemetryEvent::InvalidUrlError),
58        ComponentError::ReportAd(_) => Some(TelemetryEvent::InvalidUrlError),
59    }
60}
61
62pub fn emit_telemetry_event(event_type: Option<TelemetryEvent>) -> Result<(), EmitTelemetryError> {
63    let endpoint = get_telemetry_endpoint();
64    let mut url = Url::parse(&endpoint)?;
65    if let Some(event) = event_type {
66        let event_string = serde_json::to_string(&event)?;
67        url.set_query(Some(&format!("event={}", event_string)));
68        Request::get(url).send()?;
69    }
70    Ok(())
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::error::{CallbackRequestError, ComponentError, RecordClickError};
77    use mockito::mock;
78
79    #[test]
80    fn test_emit_telemetry_emits_telemetry_for_mappable_error() {
81        viaduct_reqwest::use_reqwest_backend();
82        set_telemetry_endpoint(format!("{}{}", mockito::server_url(), "/v1/log"));
83        let mock = mock("GET", "/v1/log")
84            .match_query(mockito::Matcher::UrlEncoded(
85                "event".into(),
86                "\"invalid_url_error\"".into(),
87            ))
88            .with_status(200)
89            .expect(1)
90            .create();
91
92        let result: Result<(), ComponentError> = Err(ComponentError::RecordClick(
93            RecordClickError::CallbackRequest(CallbackRequestError::MissingCallback {
94                message: "bad url".into(),
95            }),
96        ));
97
98        let res = result.emit_telemetry_if_error();
99
100        mock.assert();
101
102        assert!(res.is_err());
103    }
104
105    #[test]
106    fn test_emit_telemetry_event_on_ok_does_nothing() {
107        viaduct_reqwest::use_reqwest_backend();
108        set_telemetry_endpoint(format!("{}{}", mockito::server_url(), "/v1/log"));
109
110        let mock = mock("GET", "/v1/log").with_status(200).expect(0).create();
111
112        let result: Result<String, ComponentError> =
113            Ok("All is good".to_string()).emit_telemetry_if_error();
114
115        mock.assert();
116        assert!(result.is_ok());
117        assert_eq!(result.unwrap(), "All is good".to_string());
118    }
119}