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