|
| 1 | +//! Ad serving and advertiser integration functionality. |
| 2 | +//! |
| 3 | +//! This module handles ad requests, including GDPR consent checking, |
| 4 | +//! synthetic ID generation, visitor tracking, and communication with |
| 5 | +//! external ad partners. |
| 6 | +
|
| 7 | +use std::env; |
| 8 | + |
| 9 | +use error_stack::Report; |
| 10 | +use fastly::http::{header, StatusCode}; |
| 11 | +use fastly::{KVStore, Request, Response}; |
| 12 | + |
| 13 | +use crate::constants::{ |
| 14 | + HEADER_SYNTHETIC_TRUSTED_SERVER, HEADER_X_COMPRESS_HINT, HEADER_X_CONSENT_ADVERTISING, |
| 15 | + HEADER_X_FORWARDED_FOR, |
| 16 | +}; |
| 17 | +use crate::error::TrustedServerError; |
| 18 | +use crate::gdpr::{get_consent_from_request, GdprConsent}; |
| 19 | +use crate::geo::get_dma_code; |
| 20 | +use crate::models::AdResponse; |
| 21 | +use crate::settings::Settings; |
| 22 | +use crate::synthetic::generate_synthetic_id; |
| 23 | + |
| 24 | +/// Handles ad creative requests. |
| 25 | +/// |
| 26 | +/// Processes ad requests with synthetic ID and consent checking. |
| 27 | +/// |
| 28 | +/// # Errors |
| 29 | +/// |
| 30 | +/// Returns a [`TrustedServerError`] if: |
| 31 | +/// - Synthetic ID generation fails |
| 32 | +/// - Backend communication fails |
| 33 | +/// - Response creation fails |
| 34 | +pub fn handle_ad_request( |
| 35 | + settings: &Settings, |
| 36 | + mut req: Request, |
| 37 | +) -> Result<Response, Report<TrustedServerError>> { |
| 38 | + // Check GDPR consent to determine if we should serve personalized or non-personalized ads |
| 39 | + let _consent = match get_consent_from_request(&req) { |
| 40 | + Some(c) => c, |
| 41 | + None => { |
| 42 | + log::debug!("No GDPR consent found in ad request, using default"); |
| 43 | + GdprConsent::default() |
| 44 | + } |
| 45 | + }; |
| 46 | + let advertising_consent = req |
| 47 | + .get_header(HEADER_X_CONSENT_ADVERTISING) |
| 48 | + .and_then(|h| h.to_str().ok()) |
| 49 | + .map(|v| v == "true") |
| 50 | + .unwrap_or(false); |
| 51 | + |
| 52 | + // Add DMA code extraction |
| 53 | + let dma_code = get_dma_code(&mut req); |
| 54 | + |
| 55 | + log::info!("Client location - DMA Code: {:?}", dma_code); |
| 56 | + |
| 57 | + // Log headers for debugging |
| 58 | + let client_ip = req |
| 59 | + .get_client_ip_addr() |
| 60 | + .map(|ip| ip.to_string()) |
| 61 | + .unwrap_or_else(|| "Unknown".to_string()); |
| 62 | + let x_forwarded_for = req |
| 63 | + .get_header(HEADER_X_FORWARDED_FOR) |
| 64 | + .map(|h| h.to_str().unwrap_or("Unknown")); |
| 65 | + |
| 66 | + log::info!("Client IP: {}", client_ip); |
| 67 | + log::info!("X-Forwarded-For: {}", x_forwarded_for.unwrap_or("None")); |
| 68 | + log::info!("Advertising consent: {}", advertising_consent); |
| 69 | + |
| 70 | + // Generate synthetic ID only if we have consent |
| 71 | + let synthetic_id = if advertising_consent { |
| 72 | + generate_synthetic_id(settings, &req)? |
| 73 | + } else { |
| 74 | + // Use a generic ID for non-personalized ads |
| 75 | + "non-personalized".to_string() |
| 76 | + }; |
| 77 | + |
| 78 | + // Only track visits if we have consent |
| 79 | + if advertising_consent { |
| 80 | + // Increment visit counter in KV store |
| 81 | + log::info!("Opening KV store: {}", settings.synthetic.counter_store); |
| 82 | + if let Ok(Some(store)) = KVStore::open(settings.synthetic.counter_store.as_str()) { |
| 83 | + log::info!("Fetching current count for synthetic ID: {}", synthetic_id); |
| 84 | + let current_count: i32 = store |
| 85 | + .lookup(&synthetic_id) |
| 86 | + .map(|mut val| match String::from_utf8(val.take_body_bytes()) { |
| 87 | + Ok(s) => { |
| 88 | + log::info!("Value from KV store: {}", s); |
| 89 | + Some(s) |
| 90 | + } |
| 91 | + Err(e) => { |
| 92 | + log::error!("Error converting bytes to string: {}", e); |
| 93 | + None |
| 94 | + } |
| 95 | + }) |
| 96 | + .map(|opt_s| { |
| 97 | + log::info!("Parsing string value: {:?}", opt_s); |
| 98 | + opt_s.and_then(|s| s.parse().ok()) |
| 99 | + }) |
| 100 | + .unwrap_or_else(|_| { |
| 101 | + log::info!("No existing count found, starting at 0"); |
| 102 | + None |
| 103 | + }) |
| 104 | + .unwrap_or(0); |
| 105 | + |
| 106 | + let new_count = current_count + 1; |
| 107 | + log::info!("Incrementing count from {} to {}", current_count, new_count); |
| 108 | + |
| 109 | + if let Err(e) = store.insert(&synthetic_id, new_count.to_string().as_bytes()) { |
| 110 | + log::error!("Error updating KV store: {:?}", e); |
| 111 | + } |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + // Modify the ad server URL construction to include DMA code if available |
| 116 | + let ad_server_url = if advertising_consent { |
| 117 | + let mut url = settings |
| 118 | + .ad_server |
| 119 | + .sync_url |
| 120 | + .replace("{{synthetic_id}}", &synthetic_id); |
| 121 | + if let Some(dma) = dma_code { |
| 122 | + url = format!("{}&dma={}", url, dma); |
| 123 | + } |
| 124 | + url |
| 125 | + } else { |
| 126 | + // Use a different URL or parameter for non-personalized ads |
| 127 | + settings |
| 128 | + .ad_server |
| 129 | + .sync_url |
| 130 | + .replace("{{synthetic_id}}", "non-personalized") |
| 131 | + }; |
| 132 | + |
| 133 | + log::info!("Sending request to backend: {}", ad_server_url); |
| 134 | + |
| 135 | + // Add header logging here |
| 136 | + let mut ad_req = Request::get(ad_server_url); |
| 137 | + |
| 138 | + // Add consent information to the ad request |
| 139 | + ad_req.set_header( |
| 140 | + HEADER_X_CONSENT_ADVERTISING, |
| 141 | + if advertising_consent { "true" } else { "false" }, |
| 142 | + ); |
| 143 | + |
| 144 | + log::info!("Request headers to Equativ:"); |
| 145 | + for (name, value) in ad_req.get_headers() { |
| 146 | + log::info!(" {}: {:?}", name, value); |
| 147 | + } |
| 148 | + |
| 149 | + match ad_req.send(settings.ad_server.ad_partner_url.as_str()) { |
| 150 | + Ok(mut res) => { |
| 151 | + log::info!( |
| 152 | + "Received response from backend with status: {}", |
| 153 | + res.get_status() |
| 154 | + ); |
| 155 | + |
| 156 | + // Extract Fastly PoP from the Compute environment |
| 157 | + let fastly_pop = env::var("FASTLY_POP").unwrap_or_else(|_| "unknown".to_string()); |
| 158 | + let fastly_cache_generation = |
| 159 | + env::var("FASTLY_CACHE_GENERATION").unwrap_or_else(|_| "unknown".to_string()); |
| 160 | + let fastly_customer_id = |
| 161 | + env::var("FASTLY_CUSTOMER_ID").unwrap_or_else(|_| "unknown".to_string()); |
| 162 | + let fastly_hostname = |
| 163 | + env::var("FASTLY_HOSTNAME").unwrap_or_else(|_| "unknown".to_string()); |
| 164 | + let fastly_region = env::var("FASTLY_REGION").unwrap_or_else(|_| "unknown".to_string()); |
| 165 | + let fastly_service_id = |
| 166 | + env::var("FASTLY_SERVICE_ID").unwrap_or_else(|_| "unknown".to_string()); |
| 167 | + // let fastly_service_version = env::var("FASTLY_SERVICE_VERSION").unwrap_or_else(|_| "unknown".to_string()); |
| 168 | + let fastly_trace_id = |
| 169 | + env::var("FASTLY_TRACE_ID").unwrap_or_else(|_| "unknown".to_string()); |
| 170 | + |
| 171 | + log::info!("Fastly Jason PoP: {}", fastly_pop); |
| 172 | + log::info!("Fastly Compute Variables:"); |
| 173 | + log::info!(" - FASTLY_CACHE_GENERATION: {}", fastly_cache_generation); |
| 174 | + log::info!(" - FASTLY_CUSTOMER_ID: {}", fastly_customer_id); |
| 175 | + log::info!(" - FASTLY_HOSTNAME: {}", fastly_hostname); |
| 176 | + log::info!(" - FASTLY_POP: {}", fastly_pop); |
| 177 | + log::info!(" - FASTLY_REGION: {}", fastly_region); |
| 178 | + log::info!(" - FASTLY_SERVICE_ID: {}", fastly_service_id); |
| 179 | + //log::info!(" - FASTLY_SERVICE_VERSION: {}", fastly_service_version); |
| 180 | + log::info!(" - FASTLY_TRACE_ID: {}", fastly_trace_id); |
| 181 | + |
| 182 | + // Log all response headers |
| 183 | + log::info!("Response headers from Equativ:"); |
| 184 | + for (name, value) in res.get_headers() { |
| 185 | + log::info!(" {}: {:?}", name, value); |
| 186 | + } |
| 187 | + |
| 188 | + if res.get_status().is_success() { |
| 189 | + let body = res.take_body_str(); |
| 190 | + log::info!("Backend response body: {}", body); |
| 191 | + |
| 192 | + // Parse the JSON response and extract opid |
| 193 | + if let Ok(ad_response) = serde_json::from_str::<AdResponse>(&body) { |
| 194 | + // Look for the callback with type "impression" |
| 195 | + if let Some(callback) = ad_response |
| 196 | + .callbacks |
| 197 | + .iter() |
| 198 | + .find(|c| c.callback_type == "impression") |
| 199 | + { |
| 200 | + // Extract opid from the URL |
| 201 | + if let Some(opid) = callback |
| 202 | + .url |
| 203 | + .split('&') |
| 204 | + .find(|¶m| param.starts_with("opid=")) |
| 205 | + .and_then(|param| param.split('=').nth(1)) |
| 206 | + { |
| 207 | + log::info!("Found opid: {}", opid); |
| 208 | + |
| 209 | + // Store in opid KV store |
| 210 | + log::info!( |
| 211 | + "Attempting to open KV store: {}", |
| 212 | + settings.synthetic.opid_store |
| 213 | + ); |
| 214 | + match KVStore::open(settings.synthetic.opid_store.as_str()) { |
| 215 | + Ok(Some(store)) => { |
| 216 | + log::info!("Successfully opened KV store"); |
| 217 | + match store.insert(&synthetic_id, opid.as_bytes()) { |
| 218 | + Ok(_) => log::info!( |
| 219 | + "Successfully stored opid {} for synthetic ID: {}", |
| 220 | + opid, |
| 221 | + synthetic_id |
| 222 | + ), |
| 223 | + Err(e) => { |
| 224 | + log::error!("Error storing opid in KV store: {:?}", e) |
| 225 | + } |
| 226 | + } |
| 227 | + } |
| 228 | + Ok(None) => { |
| 229 | + log::warn!( |
| 230 | + "KV store returned None: {}", |
| 231 | + settings.synthetic.opid_store |
| 232 | + ); |
| 233 | + } |
| 234 | + Err(e) => { |
| 235 | + log::error!( |
| 236 | + "Error opening KV store {}: {:?}", |
| 237 | + settings.synthetic.opid_store, |
| 238 | + e |
| 239 | + ); |
| 240 | + } |
| 241 | + } |
| 242 | + } else { |
| 243 | + log::warn!("Could not extract opid from impression callback URL"); |
| 244 | + } |
| 245 | + } else { |
| 246 | + log::warn!("No impression callback found in ad response"); |
| 247 | + } |
| 248 | + } else { |
| 249 | + log::warn!("Could not parse JSON response to extract opid"); |
| 250 | + } |
| 251 | + |
| 252 | + let synthetic_header = req |
| 253 | + .get_header(HEADER_SYNTHETIC_TRUSTED_SERVER) |
| 254 | + .map(|h| h.to_str().unwrap_or("")); |
| 255 | + log::info!( |
| 256 | + "Returning response with Synthetic header: {:?}", |
| 257 | + synthetic_header |
| 258 | + ); |
| 259 | + log::info!("Advertising consent: {}", advertising_consent); |
| 260 | + |
| 261 | + // Return the response to the client |
| 262 | + Ok(Response::from_body(body) |
| 263 | + .with_status(res.get_status()) |
| 264 | + .with_header(header::CONTENT_TYPE, "application/json") |
| 265 | + .with_header("X-Synthetic-ID", &synthetic_id) |
| 266 | + .with_header( |
| 267 | + "X-Consent-Advertising", |
| 268 | + if advertising_consent { "true" } else { "false" }, |
| 269 | + ) |
| 270 | + .with_header("X-Fastly-PoP", &fastly_pop) |
| 271 | + .with_header(HEADER_X_COMPRESS_HINT, "on")) |
| 272 | + } else { |
| 273 | + Ok(Response::from_status(res.get_status()) |
| 274 | + .with_header(header::CONTENT_TYPE, "application/json") |
| 275 | + .with_header(HEADER_X_COMPRESS_HINT, "on") |
| 276 | + .with_body("{}")) |
| 277 | + } |
| 278 | + } |
| 279 | + Err(e) => { |
| 280 | + log::error!("Error making backend request: {:?}", e); |
| 281 | + Ok(Response::from_status(StatusCode::NO_CONTENT) |
| 282 | + .with_header(header::CONTENT_TYPE, "application/json") |
| 283 | + .with_header(HEADER_X_COMPRESS_HINT, "on") |
| 284 | + .with_body("{}")) |
| 285 | + } |
| 286 | + } |
| 287 | +} |
0 commit comments