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