Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/common/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ pub enum TrustedServerError {
#[display("Invalid UTF-8 data: {message}")]
InvalidUtf8 { message: String },

/// Serialization error.
#[display("Serialization error: {message}")]
Serialization { message: String },

/// HTTP header value creation failed.
#[display("Invalid HTTP header value: {message}")]
InvalidHeaderValue { message: String },
Expand Down Expand Up @@ -100,6 +104,7 @@ impl IntoHttpResponse for TrustedServerError {
Self::GdprConsent { .. } => StatusCode::BAD_REQUEST,
Self::InsecureSecretKey => StatusCode::INTERNAL_SERVER_ERROR,
Self::InvalidHeaderValue { .. } => StatusCode::BAD_REQUEST,
Self::Serialization { .. } => StatusCode::INTERNAL_SERVER_ERROR,
Self::InvalidUtf8 { .. } => StatusCode::BAD_REQUEST,
Self::KvStore { .. } => StatusCode::SERVICE_UNAVAILABLE,
Self::Prebid { .. } => StatusCode::BAD_GATEWAY,
Expand Down
104 changes: 43 additions & 61 deletions crates/common/src/geo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use fastly::Request;

use crate::constants::{
HEADER_X_GEO_CITY, HEADER_X_GEO_CONTINENT, HEADER_X_GEO_COORDINATES, HEADER_X_GEO_COUNTRY,
HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE,
HEADER_X_GEO_METRO_CODE, HEADER_X_GEO_REGION,
};

/// Geographic information extracted from a request.
Expand Down Expand Up @@ -81,72 +81,54 @@ impl GeoInfo {
pub fn has_metro_code(&self) -> bool {
self.metro_code > 0
}

/// Sets the geographic information headers on the given request.
///
/// This sets the following headers:
/// - `x-geo-city`
/// - `x-geo-country`
/// - `x-geo-continent`
/// - `x-geo-coordinates`
/// - `x-geo-metro-code`
/// - `x-geo-region` (if available)
pub fn set_headers(&self, req: &mut Request) {
req.set_header(HEADER_X_GEO_CITY, &self.city);
req.set_header(HEADER_X_GEO_COUNTRY, &self.country);
req.set_header(HEADER_X_GEO_CONTINENT, &self.continent);
req.set_header(HEADER_X_GEO_COORDINATES, self.coordinates_string());
req.set_header(HEADER_X_GEO_METRO_CODE, self.metro_code.to_string());
if let Some(region) = &self.region {
req.set_header(HEADER_X_GEO_REGION, region);
}
}
}

/// Extracts the DMA (Designated Market Area) code from the request's geolocation data.
/// Returns the geographic information for the request as a JSON response.
///
/// This function:
/// 1. Checks if running in Fastly environment
/// 2. Performs geo lookup based on client IP
/// 3. Sets various geo headers on the request
/// 4. Returns the metro code (DMA) if available
/// Use this endpoint to get the client's location data (City, Country, DMA, etc.)
/// without making a third-party API call.
///
/// # Arguments
/// # Errors
///
/// * `req` - The request to extract DMA code from
///
/// # Returns
///
/// The DMA/metro code as a string if available, None otherwise
pub fn get_dma_code(req: &mut Request) -> Option<String> {
// Debug: Check if we're running in Fastly environment
log::info!("Fastly Environment Check:");
log::info!(
" FASTLY_POP: {}",
std::env::var("FASTLY_POP").unwrap_or_else(|_| "not in Fastly".to_string())
);
log::info!(
" FASTLY_REGION: {}",
std::env::var("FASTLY_REGION").unwrap_or_else(|_| "not in Fastly".to_string())
);

// Get detailed geo information using geo_lookup
if let Some(geo) = req.get_client_ip_addr().and_then(geo_lookup) {
log::info!("Geo Information Found:");
/// Returns a 500 error if JSON serialization fails (unlikely).
pub fn handle_first_party_geo(
req: &Request,
) -> Result<fastly::Response, error_stack::Report<crate::error::TrustedServerError>> {
use crate::error::TrustedServerError;
use error_stack::ResultExt;
use fastly::http::{header, StatusCode};
use fastly::Response;

// Set all available geo information in headers
let city = geo.city();
req.set_header(HEADER_X_GEO_CITY, city);
log::info!(" City: {}", city);
let geo_info = GeoInfo::from_request(req);

let country = geo.country_code();
req.set_header(HEADER_X_GEO_COUNTRY, country);
log::info!(" Country: {}", country);

req.set_header(HEADER_X_GEO_CONTINENT, format!("{:?}", geo.continent()));
log::info!(" Continent: {:?}", geo.continent());

req.set_header(
HEADER_X_GEO_COORDINATES,
format!("{},{}", geo.latitude(), geo.longitude()),
);
log::info!(" Location: ({}, {})", geo.latitude(), geo.longitude());

// Get and set the metro code (DMA)
let metro_code = geo.metro_code();
req.set_header(HEADER_X_GEO_METRO_CODE, metro_code.to_string());
log::info!("Found DMA/Metro code: {}", metro_code);
return Some(metro_code.to_string());
} else {
log::info!("No geo information available for the request");
req.set_header(HEADER_X_GEO_INFO_AVAILABLE, "false");
}

// If no metro code is found, log all request headers for debugging
log::info!("No DMA/Metro code found. All request headers:");
for (name, value) in req.get_headers() {
log::info!(" {}: {:?}", name, value);
}
// Create a JSON response
let body =
serde_json::to_string(&geo_info).change_context(TrustedServerError::Serialization {
message: "Failed to serialize geo info".to_string(),
})?;

None
Ok(Response::from_body(body)
.with_status(StatusCode::OK)
.with_header(header::CONTENT_TYPE, "application/json")
.with_header("Cache-Control", "private, no-store"))
}
11 changes: 11 additions & 0 deletions crates/common/src/html_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct HtmlWithPostProcessing {
request_host: String,
request_scheme: String,
document_state: IntegrationDocumentState,
geo_info: Option<crate::geo::GeoInfo>,
}

impl StreamProcessor for HtmlWithPostProcessing {
Expand All @@ -42,6 +43,7 @@ impl StreamProcessor for HtmlWithPostProcessing {
request_scheme: &self.request_scheme,
origin_host: &self.origin_host,
document_state: &self.document_state,
geo: self.geo_info.as_ref(),
};

// Preflight to avoid allocating a `String` unless at least one post-processor wants to run.
Expand Down Expand Up @@ -90,6 +92,7 @@ pub struct HtmlProcessorConfig {
pub request_host: String,
pub request_scheme: String,
pub integrations: IntegrationRegistry,
pub geo_info: Option<crate::geo::GeoInfo>,
}

impl HtmlProcessorConfig {
Expand All @@ -101,12 +104,14 @@ impl HtmlProcessorConfig {
origin_host: &str,
request_host: &str,
request_scheme: &str,
geo_info: Option<&crate::geo::GeoInfo>,
) -> Self {
Self {
origin_host: origin_host.to_string(),
request_host: request_host.to_string(),
request_scheme: request_scheme.to_string(),
integrations: integrations.clone(),
geo_info: geo_info.cloned(),
}
}
}
Expand All @@ -116,6 +121,7 @@ impl HtmlProcessorConfig {
pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcessor {
let post_processors = config.integrations.html_post_processors();
let document_state = IntegrationDocumentState::default();
let geo_info = config.geo_info.clone();

// Simplified URL patterns structure - stores only core data and generates variants on-demand
struct UrlPatterns {
Expand Down Expand Up @@ -194,6 +200,7 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
let integrations = integration_registry.clone();
let patterns = patterns.clone();
let document_state = document_state.clone();
let geo_info = geo_info.clone();
move |el| {
if !injected_tsjs.get() {
let mut snippet = String::new();
Expand All @@ -202,6 +209,7 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
request_scheme: &patterns.request_scheme,
origin_host: &patterns.origin_host,
document_state: &document_state,
geo: geo_info.as_ref(),
};
// First inject the unified TSJS bundle (defines tsjs.setConfig, etc.)
snippet.push_str(&tsjs::unified_script_tag());
Expand Down Expand Up @@ -466,6 +474,7 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
request_host: config.request_host,
request_scheme: config.request_scheme,
document_state,
geo_info: config.geo_info,
}
}

Expand All @@ -488,6 +497,7 @@ mod tests {
request_host: "test.example.com".to_string(),
request_scheme: "https".to_string(),
integrations: IntegrationRegistry::default(),
geo_info: None,
}
}

Expand Down Expand Up @@ -665,6 +675,7 @@ mod tests {
"origin.test-publisher.com",
"proxy.example.com",
"https",
None,
);

assert_eq!(config.origin_host, "origin.test-publisher.com");
Expand Down
1 change: 1 addition & 0 deletions crates/common/src/integrations/nextjs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ mod tests {
"origin.example.com",
"test.example.com",
"https",
None,
)
}

Expand Down
Loading