From 164a884d5abafa660c0612cbcbee160233f77cfd Mon Sep 17 00:00:00 2001 From: Gagan Trivedi Date: Fri, 31 Oct 2025 13:25:41 +0530 Subject: [PATCH] feat: add User-Agent header to all outbound HTTP requests Add explicit User-Agent header to all HTTP requests made by the SDK. The header follows the format: flagsmith-rust-sdk/ Implementation details: - Created get_user_agent() function that reads version from CARGO_PKG_VERSION at compile time using option_env! macro - Falls back to "flagsmith-rust-sdk/unknown" if version unavailable - User-Agent header is added to default headers in Flagsmith client constructor - Header is automatically included in both API requests and analytics calls Testing: - Added test_get_user_agent_format() to verify the function returns correct format and actual version (not "unknown") during cargo test - Added test_user_agent_header_is_set() to verify the header is sent in HTTP requests using httpmock --- src/flagsmith/mod.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/flagsmith/mod.rs b/src/flagsmith/mod.rs index e91635d..2a9f4ac 100644 --- a/src/flagsmith/mod.rs +++ b/src/flagsmith/mod.rs @@ -23,6 +23,12 @@ pub mod offline_handler; const DEFAULT_API_URL: &str = "https://edge.api.flagsmith.com/api/v1/"; +// Get the SDK version from Cargo.toml at compile time, or default to "unknown" +fn get_user_agent() -> String { + let version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"); + format!("flagsmith-rust-sdk/{}", version) +} + pub struct FlagsmithOptions { pub api_url: String, pub custom_headers: HeaderMap, @@ -75,6 +81,10 @@ impl Flagsmith { header::HeaderValue::from_str(&environment_key).unwrap(), ); headers.insert("Content-Type", "application/json".parse().unwrap()); + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_str(&get_user_agent()).unwrap(), + ); let timeout = Duration::from_secs(flagsmith_options.request_timeout_seconds); let client = reqwest::blocking::Client::builder() .default_headers(headers.clone()) @@ -510,6 +520,28 @@ mod tests { implements_send_and_sync::(); } + #[test] + fn test_get_user_agent_format() { + // When + let user_agent = get_user_agent(); + + // Then + assert!(user_agent.starts_with("flagsmith-rust-sdk/")); + + // Extract version part after the slash + let version = user_agent.strip_prefix("flagsmith-rust-sdk/").unwrap(); + + // During cargo test, CARGO_PKG_VERSION is always set, so we should never get "unknown" + assert_ne!(version, "unknown", "Version should not be 'unknown' during cargo test"); + + // Version should contain numbers (semantic versioning: e.g., "2.0.0") + assert!( + version.chars().any(|c| c.is_numeric()), + "Version should contain numbers, got: {}", + version + ); + } + #[test] fn polling_thread_updates_environment_on_start() { // Given @@ -603,4 +635,38 @@ mod tests { assert_eq!(flags.unwrap().get_feature_value_as_string("some_feature").unwrap().to_owned(), "some-value"); assert_eq!(identity_flags.unwrap().get_feature_value_as_string("some_feature").unwrap().to_owned(), "some-overridden-value"); } + + #[test] + fn test_user_agent_header_is_set() { + // Given + let environment_key = "ser.test_environment_key"; + let response_body: serde_json::Value = serde_json::from_str(ENVIRONMENT_JSON).unwrap(); + let expected_user_agent = get_user_agent(); + + let mock_server = MockServer::start(); + let api_mock = mock_server.mock(|when, then| { + when.method(GET) + .path("/api/v1/environment-document/") + .header("X-Environment-Key", environment_key) + .header("User-Agent", expected_user_agent.as_str()); + then.status(200).json_body(response_body); + }); + + let url = mock_server.url("/api/v1/"); + + let flagsmith_options = FlagsmithOptions { + api_url: url, + enable_local_evaluation: true, + ..Default::default() + }; + + // When + let _flagsmith = Flagsmith::new(environment_key.to_string(), flagsmith_options); + + // let's wait for the thread to make the request + thread::sleep(std::time::Duration::from_millis(50)); + + // Then + api_mock.assert(); + } }