1use 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 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}