firebase_rs_sdk/analytics/
config.rs1#[cfg(not(target_arch = "wasm32"))]
2use std::time::Duration;
3
4use reqwest::Client;
5use serde::Deserialize;
6
7use crate::analytics::error::{
8 config_fetch_failed, internal_error, missing_measurement_id, AnalyticsResult,
9};
10use crate::app::FirebaseApp;
11
12#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct DynamicConfig {
15 measurement_id: String,
16 app_id: Option<String>,
17}
18
19impl DynamicConfig {
20 pub fn new(measurement_id: impl Into<String>, app_id: Option<String>) -> Self {
21 Self {
22 measurement_id: measurement_id.into(),
23 app_id,
24 }
25 }
26
27 pub fn measurement_id(&self) -> &str {
28 &self.measurement_id
29 }
30
31 pub fn app_id(&self) -> Option<&str> {
32 self.app_id.as_deref()
33 }
34}
35
36pub(crate) fn from_app_options(app: &FirebaseApp) -> Option<DynamicConfig> {
38 let options = app.options();
39 options
40 .measurement_id
41 .as_ref()
42 .map(|mid| DynamicConfig::new(mid.clone(), options.app_id.clone()))
43}
44
45pub(crate) async fn fetch_dynamic_config(app: &FirebaseApp) -> AnalyticsResult<DynamicConfig> {
48 let options = app.options();
49 let app_id = options.app_id.clone().ok_or_else(|| {
50 missing_measurement_id(
51 "Firebase options are missing `app_id`; unable to fetch analytics configuration",
52 )
53 })?;
54
55 let api_key = options.api_key.clone().ok_or_else(|| {
56 missing_measurement_id(
57 "Firebase options are missing `api_key`; unable to fetch analytics configuration",
58 )
59 })?;
60
61 let url = dynamic_config_url(&app_id);
62
63 #[cfg(not(target_arch = "wasm32"))]
64 let client = Client::builder()
65 .timeout(Duration::from_secs(10))
66 .build()
67 .map_err(|err| internal_error(format!("failed to build HTTP client: {err}")))?;
68
69 #[cfg(target_arch = "wasm32")]
70 let client = Client::builder()
71 .build()
72 .map_err(|err| internal_error(format!("failed to build HTTP client: {err}")))?;
73
74 let response = client
75 .get(url)
76 .header("x-goog-api-key", api_key)
77 .header("Accept", "application/json")
78 .send()
79 .await
80 .map_err(|err| config_fetch_failed(format!("failed to fetch analytics config: {err}")))?;
81
82 if !response.status().is_success() {
83 let status = response.status();
84 let body = response
85 .text()
86 .await
87 .unwrap_or_else(|_| "<unavailable response body>".to_string());
88 return Err(config_fetch_failed(format!(
89 "analytics config request failed with status {status}: {body}"
90 )));
91 }
92
93 let parsed: RemoteConfigResponse = response
94 .json()
95 .await
96 .map_err(|err| config_fetch_failed(format!("invalid analytics config response: {err}")))?;
97
98 let measurement_id = match parsed.measurement_id {
99 Some(value) => value,
100 None => {
101 return Err(config_fetch_failed(
102 "remote analytics config response did not include a measurement ID",
103 ))
104 }
105 };
106
107 Ok(DynamicConfig::new(measurement_id, parsed.app_id))
108}
109
110fn dynamic_config_url(app_id: &str) -> String {
111 if let Ok(template) = std::env::var("FIREBASE_ANALYTICS_CONFIG_URL") {
112 return template.replace("{app-id}", app_id);
113 }
114
115 format!(
116 "https://firebase.googleapis.com/v1alpha/projects/-/apps/{}/webConfig",
117 app_id
118 )
119}
120
121#[derive(Deserialize)]
122struct RemoteConfigResponse {
123 #[serde(rename = "measurementId")]
124 measurement_id: Option<String>,
125 #[serde(rename = "appId")]
126 app_id: Option<String>,
127}