Skip to content

Commit 1245dfa

Browse files
committed
feat(ads-client): add telemetry with tracing
1 parent 12f6cc0 commit 1245dfa

File tree

8 files changed

+207
-81
lines changed

8 files changed

+207
-81
lines changed

Cargo.lock

Lines changed: 49 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/ads-client/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ default = []
1212
chrono = "0.4"
1313
context_id = { path = "../context_id" }
1414
error-support = { path = "../support/error" }
15+
once_cell = "1.5"
1516
parking_lot = "0.12"
1617
rusqlite = { version = "0.37.0", features = [
1718
"functions",
@@ -21,13 +22,14 @@ rusqlite = { version = "0.37.0", features = [
2122
] }
2223
serde = "1"
2324
serde_json = "1"
25+
sql-support = { path = "../support/sql" }
2426
thiserror = "2"
25-
once_cell = "1.5"
27+
tracing = "0.1"
28+
tracing-subscriber = { version = "0.3" }
2629
uniffi = { version = "0.29.0" }
2730
url = { version = "2", features = ["serde"] }
2831
uuid = { version = "1.3", features = ["v4"] }
2932
viaduct = { path = "../viaduct" }
30-
sql-support = { path = "../support/sql" }
3133

3234
[dev-dependencies]
3335
mockall = "0.12"

components/ads-client/src/client/ad_response.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize};
1010
use std::collections::{HashMap, HashSet};
1111
use url::Url;
1212

13+
use tracing::error;
14+
1315
#[derive(Debug, Deserialize, PartialEq, uniffi::Record, Serialize)]
1416
pub struct AdResponse {
1517
#[serde(deserialize_with = "AdResponse::deserialize_ad_response", flatten)]
@@ -33,12 +35,7 @@ impl AdResponse {
3335
if let Ok(ad) = serde_json::from_value::<MozAd>(item) {
3436
ads.push(ad);
3537
} else {
36-
#[cfg(not(test))]
37-
{
38-
use crate::instrument::{emit_telemetry_event, TelemetryEvent};
39-
// TODO: improve the telemetry event (should we include the invalid URL?)
40-
let _ = emit_telemetry_event(Some(TelemetryEvent::InvalidUrlError));
41-
}
38+
error!(target: "ads_client::telemetry", "InvalidUrlError");
4239
}
4340
}
4441
if !ads.is_empty() {

components/ads-client/src/error.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
44
*/
55

6-
use error_support::{error, ErrorHandling, GetErrorHandling};
6+
use error_support::{ErrorHandling, GetErrorHandling};
77
use viaduct::Response;
88

99
pub type AdsClientApiResult<T> = std::result::Result<T, AdsClientApiError>;
@@ -92,21 +92,6 @@ pub enum FetchAdsError {
9292
HTTPError(#[from] HTTPError),
9393
}
9494

95-
#[derive(Debug, thiserror::Error)]
96-
pub enum EmitTelemetryError {
97-
#[error("URL parse error: {0}")]
98-
UrlParse(#[from] url::ParseError),
99-
100-
#[error("Error sending request: {0}")]
101-
Request(#[from] viaduct::ViaductError),
102-
103-
#[error("JSON error: {0}")]
104-
Json(#[from] serde_json::Error),
105-
106-
#[error("Could not fetch ads, MARS responded with: {0}")]
107-
HTTPError(#[from] HTTPError),
108-
}
109-
11095
#[derive(Debug, thiserror::Error)]
11196
pub enum CallbackRequestError {
11297
#[error("Could not fetch ads, MARS responded with: {0}")]

components/ads-client/src/instrument.rs

Lines changed: 120 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,141 @@
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
44
*/
55

6-
use std::sync::LazyLock;
7-
8-
use crate::error::{ComponentError, EmitTelemetryError};
9-
use parking_lot::RwLock;
10-
use serde::{Deserialize, Serialize};
6+
use once_cell::sync::Lazy;
7+
use serde_json::json;
8+
use tracing::field::{Field, Visit};
9+
use tracing_subscriber::layer::Context;
10+
use tracing_subscriber::Layer;
1111
use url::Url;
1212
use viaduct::Request;
1313

14-
static DEFAULT_TELEMETRY_ENDPOINT: &str = "https://ads.mozilla.org/v1/log";
15-
static TELEMETRY_ENDPONT: LazyLock<RwLock<String>> =
16-
LazyLock::new(|| RwLock::new(DEFAULT_TELEMETRY_ENDPOINT.to_string()));
14+
static TELEMETRY_ENDPOINT: Lazy<Url> = Lazy::new(|| {
15+
Url::parse("https://ads.mozilla.org/v1/log")
16+
.expect("hardcoded telemetry endpoint URL must be valid")
17+
});
1718

18-
fn get_telemetry_endpoint() -> String {
19-
TELEMETRY_ENDPONT.read().clone()
19+
pub fn telemetry_layer<S>() -> impl Layer<S>
20+
where
21+
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
22+
{
23+
TelemetryLayer {
24+
endpoint: TELEMETRY_ENDPOINT.clone(),
25+
}
26+
.with_filter(TelemetryFilter)
2027
}
2128

22-
#[derive(Debug, Deserialize, Serialize)]
23-
#[serde(rename_all = "snake_case")]
24-
pub enum TelemetryEvent {
25-
Init,
26-
RenderError,
27-
AdLoadError,
28-
FetchError,
29-
InvalidUrlError,
29+
struct TelemetryLayer {
30+
endpoint: Url,
3031
}
3132

32-
pub trait TrackError<T, ComponentError> {
33-
fn emit_telemetry_if_error(self) -> Self;
34-
}
33+
impl<S> Layer<S> for TelemetryLayer
34+
where
35+
S: tracing::Subscriber,
36+
{
37+
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
38+
let mut visitor = EventVisitor::default();
39+
event.record(&mut visitor);
40+
41+
let event_message = visitor
42+
.fields
43+
.get("message")
44+
.unwrap_or_default()
45+
.as_str()
46+
.unwrap_or_default();
3547

36-
impl<T> TrackError<T, ComponentError> for Result<T, ComponentError> {
37-
/// Attempts to emit a telemetry event if the Error type can map to an event type.
38-
fn emit_telemetry_if_error(self) -> Self {
39-
if let Err(ref err) = self {
40-
let error_type = map_error_to_event_type(err);
41-
let _ = emit_telemetry_event(error_type);
48+
let mut url = self.endpoint.clone();
49+
url.set_query(Some(&format!("event={event_message}")));
50+
51+
if let Err(e) = Request::get(url).send() {
52+
eprintln!("[TELEMETRY] Failed to send event: {}", e);
4253
}
43-
self
4454
}
4555
}
4656

47-
fn map_error_to_event_type(err: &ComponentError) -> Option<TelemetryEvent> {
48-
match err {
49-
ComponentError::RequestAds(_) => Some(TelemetryEvent::FetchError),
50-
ComponentError::RecordImpression(_) => Some(TelemetryEvent::InvalidUrlError),
51-
ComponentError::RecordClick(_) => Some(TelemetryEvent::InvalidUrlError),
52-
ComponentError::ReportAd(_) => Some(TelemetryEvent::InvalidUrlError),
57+
struct TelemetryFilter;
58+
59+
impl<S> tracing_subscriber::layer::Filter<S> for TelemetryFilter
60+
where
61+
S: tracing::Subscriber,
62+
{
63+
fn enabled(
64+
&self,
65+
meta: &tracing::Metadata<'_>,
66+
_cx: &tracing_subscriber::layer::Context<'_, S>,
67+
) -> bool {
68+
meta.target() == "ads_client::telemetry"
69+
}
70+
}
71+
72+
#[derive(Default)]
73+
struct EventVisitor {
74+
fields: serde_json::Map<String, serde_json::Value>,
75+
}
76+
77+
impl Visit for EventVisitor {
78+
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
79+
self.fields.insert(
80+
field.name().to_string(),
81+
serde_json::Value::String(format!("{:?}", value)),
82+
);
83+
}
84+
85+
fn record_str(&mut self, field: &Field, value: &str) {
86+
self.fields.insert(field.name().to_string(), json!(value));
87+
}
88+
89+
fn record_i64(&mut self, field: &Field, value: i64) {
90+
self.fields.insert(field.name().to_string(), json!(value));
91+
}
92+
93+
fn record_u64(&mut self, field: &Field, value: u64) {
94+
self.fields.insert(field.name().to_string(), json!(value));
95+
}
96+
97+
fn record_bool(&mut self, field: &Field, value: bool) {
98+
self.fields.insert(field.name().to_string(), json!(value));
5399
}
54100
}
55101

56-
pub fn emit_telemetry_event(event_type: Option<TelemetryEvent>) -> Result<(), EmitTelemetryError> {
57-
let endpoint = get_telemetry_endpoint();
58-
let mut url = Url::parse(&endpoint)?;
59-
if let Some(event) = event_type {
60-
let event_string = serde_json::to_string(&event)?;
61-
url.set_query(Some(&format!("event={}", event_string)));
62-
Request::get(url).send()?;
102+
#[cfg(test)]
103+
mod tests {
104+
use super::*;
105+
use mockito::mock;
106+
use tracing::error;
107+
use tracing_subscriber::prelude::*;
108+
109+
#[test]
110+
fn test_telemetry_layer() {
111+
let subscriber = tracing_subscriber::registry::Registry::default().with(telemetry_layer());
112+
tracing::subscriber::with_default(subscriber, || {});
113+
}
114+
115+
#[test]
116+
fn test_telemetry_sends_to_mock_server() {
117+
viaduct_dev::init_backend_dev();
118+
119+
let mock_server_url = mockito::server_url();
120+
let telemetry_url = Url::parse(&format!("{}/v1/log", mock_server_url)).unwrap();
121+
122+
let mock_endpoint = mock("GET", "/v1/log")
123+
.with_status(200)
124+
.match_query(mockito::Matcher::Regex(
125+
r#"event=test%20telemetry%20error"#.to_string(),
126+
))
127+
.expect(1)
128+
.create();
129+
130+
let telemetry_layer = TelemetryLayer {
131+
endpoint: telemetry_url,
132+
}
133+
.with_filter(TelemetryFilter);
134+
let subscriber = tracing_subscriber::registry::Registry::default().with(telemetry_layer);
135+
136+
tracing::subscriber::with_default(subscriber, || {
137+
error!(target: "ads_client::telemetry", message = "test telemetry error");
138+
error!(target: "ads_client::not_telemetry", message = "non-telemetry event");
139+
});
140+
141+
mock_endpoint.assert();
63142
}
64-
Ok(())
65143
}

0 commit comments

Comments
 (0)