firebase_rs_sdk/analytics/
config.rs

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