firebase_rs_sdk/analytics/
config.rs

1#[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/// Minimal dynamic configuration returned by the Firebase Analytics config endpoint.
13#[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
36/// Attempts to build a dynamic config directly from the locally supplied Firebase options.
37pub(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
45/// Fetches remote dynamic configuration for the provided Firebase app using the REST endpoint
46/// `/v1alpha/projects/-/apps/{app_id}/webConfig`.
47pub(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}