firebase_rs_sdk/analytics/
transport.rs1use std::collections::BTreeMap;
2#[cfg(target_arch = "wasm32")]
3#[allow(unused_imports)]
4use std::time::Duration;
5#[cfg(not(target_arch = "wasm32"))]
6use std::time::Duration;
7
8use reqwest::Client;
9use reqwest::StatusCode;
10use serde::Serialize;
11
12use crate::analytics::error::{internal_error, invalid_argument, network_error, AnalyticsResult};
13
14#[derive(Clone, Debug)]
16pub struct MeasurementProtocolConfig {
17 measurement_id: String,
18 api_secret: String,
19 endpoint: MeasurementProtocolEndpoint,
20 timeout: Duration,
21}
22
23impl MeasurementProtocolConfig {
24 pub fn new(measurement_id: impl Into<String>, api_secret: impl Into<String>) -> Self {
25 Self {
26 measurement_id: measurement_id.into(),
27 api_secret: api_secret.into(),
28 endpoint: MeasurementProtocolEndpoint::Collect,
29 timeout: Duration::from_secs(10),
30 }
31 }
32
33 pub fn with_endpoint(mut self, endpoint: MeasurementProtocolEndpoint) -> Self {
34 self.endpoint = endpoint;
35 self
36 }
37
38 pub fn with_timeout(mut self, timeout: Duration) -> Self {
39 self.timeout = timeout;
40 self
41 }
42
43 #[cfg(not(target_arch = "wasm32"))]
44 pub(crate) fn timeout(&self) -> Duration {
45 self.timeout
46 }
47
48 pub(crate) fn measurement_id(&self) -> &str {
49 &self.measurement_id
50 }
51
52 pub(crate) fn api_secret(&self) -> &str {
53 &self.api_secret
54 }
55}
56
57#[derive(Clone, Debug)]
59pub enum MeasurementProtocolEndpoint {
60 Collect,
62 DebugCollect,
64 Custom(String),
66}
67
68impl MeasurementProtocolEndpoint {
69 fn as_str(&self) -> &str {
70 match self {
71 MeasurementProtocolEndpoint::Collect => "https://www.google-analytics.com/mp/collect",
72 MeasurementProtocolEndpoint::DebugCollect => {
73 "https://www.google-analytics.com/debug/mp/collect"
74 }
75 MeasurementProtocolEndpoint::Custom(url) => url,
76 }
77 }
78}
79
80#[derive(Clone, Debug)]
81pub struct MeasurementProtocolDispatcher {
82 client: Client,
83 config: MeasurementProtocolConfig,
84}
85
86impl MeasurementProtocolDispatcher {
87 pub fn new(config: MeasurementProtocolConfig) -> AnalyticsResult<Self> {
89 if config.measurement_id().trim().is_empty() {
90 return Err(invalid_argument(
91 "measurement protocol measurement_id must not be empty",
92 ));
93 }
94 if config.api_secret().trim().is_empty() {
95 return Err(invalid_argument(
96 "measurement protocol api_secret must not be empty",
97 ));
98 }
99 #[cfg(not(target_arch = "wasm32"))]
100 let client = Client::builder()
101 .timeout(config.timeout())
102 .build()
103 .map_err(|err| internal_error(format!("failed to build HTTP client: {err}")))?;
104
105 #[cfg(target_arch = "wasm32")]
106 let client = Client::builder()
107 .build()
108 .map_err(|err| internal_error(format!("failed to build HTTP client: {err}")))?;
109
110 Ok(Self { client, config })
111 }
112
113 pub async fn send_event(
118 &self,
119 client_id: &str,
120 event_name: &str,
121 params: &BTreeMap<String, String>,
122 ) -> AnalyticsResult<()> {
123 let payload = MeasurementPayloadOwned {
124 client_id: client_id.to_owned(),
125 events: vec![MeasurementEventOwned {
126 name: event_name.to_owned(),
127 params: params.clone(),
128 }],
129 };
130
131 let body = serde_json::to_vec(&payload).map_err(|err| {
132 internal_error(format!("failed to serialise analytics payload: {err}"))
133 })?;
134
135 let endpoint = self.config.endpoint.as_str().to_owned();
136 let measurement_id = self.config.measurement_id().to_owned();
137 let api_secret = self.config.api_secret().to_owned();
138 let client = self.client.clone();
139
140 let response = client
141 .post(&endpoint)
142 .query(&[
143 ("measurement_id", measurement_id.as_str()),
144 ("api_secret", api_secret.as_str()),
145 ])
146 .header("content-type", "application/json")
147 .body(body)
148 .send()
149 .await
150 .map_err(|err| network_error(format!("failed to send analytics event: {err}")))?;
151
152 if response.status().is_success() {
153 return Ok(());
154 }
155
156 let status = response.status();
157 let body = response
158 .text()
159 .await
160 .unwrap_or_else(|_| "<unavailable response body>".to_string());
161
162 let message = match status {
163 StatusCode::BAD_REQUEST => {
164 format!("measurement protocol rejected the event (400). Response: {body}")
165 }
166 _ => format!(
167 "measurement protocol request failed with status {status}. Response: {body}"
168 ),
169 };
170
171 Err(network_error(message))
172 }
173
174 pub fn config(&self) -> &MeasurementProtocolConfig {
175 &self.config
176 }
177}
178
179#[derive(Serialize)]
180struct MeasurementPayloadOwned {
181 client_id: String,
182 events: Vec<MeasurementEventOwned>,
183}
184
185#[derive(Serialize)]
186struct MeasurementEventOwned {
187 name: String,
188 #[serde(rename = "params")]
189 params: BTreeMap<String, String>,
190}