diff --git a/crates/common/src/geo.rs b/crates/common/src/geo.rs index 1f12885..f3af042 100644 --- a/crates/common/src/geo.rs +++ b/crates/common/src/geo.rs @@ -11,6 +11,76 @@ use crate::constants::{ HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE, }; +/// Geographic information extracted from a request. +/// +/// Contains all available geographic data from Fastly's geolocation service, +/// including city, country, continent, coordinates, and DMA/metro code. +#[derive(Debug, Clone, serde::Serialize)] +pub struct GeoInfo { + /// City name + pub city: String, + /// Two-letter country code (e.g., "US", "GB") + pub country: String, + /// Continent name + pub continent: String, + /// Latitude coordinate + pub latitude: f64, + /// Longitude coordinate + pub longitude: f64, + /// DMA (Designated Market Area) / metro code + pub metro_code: i64, + /// Region code + pub region: Option, +} + +impl GeoInfo { + /// Creates a new `GeoInfo` from a request by performing a geo lookup. + /// + /// This constructor performs a geo lookup based on the client's IP address and returns + /// all available geographic data in a structured format. It does not modify the request + /// or set headers. + /// + /// # Arguments + /// + /// * `req` - The request to extract geographic information from + /// + /// # Returns + /// + /// `Some(GeoInfo)` if geo data is available, `None` otherwise + /// + /// # Example + /// + /// ```ignore + /// if let Some(geo_info) = GeoInfo::from_request(&req) { + /// println!("User is in {} ({})", geo_info.city, geo_info.country); + /// println!("Coordinates: {}", geo_info.coordinates_string()); + /// } + /// ``` + pub fn from_request(req: &Request) -> Option { + req.get_client_ip_addr() + .and_then(geo_lookup) + .map(|geo| GeoInfo { + city: geo.city().to_string(), + country: geo.country_code().to_string(), + continent: format!("{:?}", geo.continent()), + latitude: geo.latitude(), + longitude: geo.longitude(), + metro_code: geo.metro_code(), + region: geo.region().map(str::to_string), + }) + } + + /// Returns coordinates as a formatted string "latitude,longitude" + pub fn coordinates_string(&self) -> String { + format!("{},{}", self.latitude, self.longitude) + } + + /// Checks if a valid metro code is available (non-zero) + pub fn has_metro_code(&self) -> bool { + self.metro_code > 0 + } +} + /// Extracts the DMA (Designated Market Area) code from the request's geolocation data. /// /// This function: diff --git a/crates/common/src/prebid_proxy.rs b/crates/common/src/prebid_proxy.rs index 745d03c..f94079c 100644 --- a/crates/common/src/prebid_proxy.rs +++ b/crates/common/src/prebid_proxy.rs @@ -12,6 +12,7 @@ use serde_json::{json, Value}; use crate::backend::ensure_backend_from_url; use crate::constants::{HEADER_SYNTHETIC_FRESH, HEADER_SYNTHETIC_TRUSTED_SERVER}; use crate::error::TrustedServerError; +use crate::geo::GeoInfo; use crate::settings::Settings; use crate::synthetic::{generate_synthetic_id, get_or_generate_synthetic_id}; @@ -162,9 +163,19 @@ fn enhance_openrtb_request( } // Add geo information if available - // Note: get_dma_code requires a mutable reference, but we only have immutable - // For now, we'll skip DMA code in the enhancement - // TODO: Refactor get_dma_code to accept immutable reference + if let Some(geo_info) = GeoInfo::from_request(req) { + let geo_obj = json!({ + "type": 2, // 2 = IP address location + "country": geo_info.country, // Note: OpenRTB expects ISO 3166-1 alpha-3, but Fastly provides alpha-2 + "city": geo_info.city, + "region": geo_info.region, + }); + + if !request["device"].is_object() { + request["device"] = json!({}); + } + request["device"]["geo"] = geo_obj.clone(); + } // Add site information if missing if !request["site"].is_object() {