mockforge_http/middleware/
ab_testing.rs

1//! A/B testing middleware for HTTP requests
2//!
3//! This middleware intercepts requests, selects appropriate variants based on
4//! A/B test configuration, and applies the variant response.
5
6use crate::handlers::ab_testing::ABTestingState;
7use axum::{body::Body, extract::Request, http::StatusCode, middleware::Next, response::Response};
8use mockforge_core::ab_testing::{apply_variant_to_response, select_variant};
9use std::time::Instant;
10use tracing::{debug, trace};
11
12/// A/B testing middleware
13///
14/// This middleware:
15/// 1. Checks if there's an A/B test configured for the request endpoint
16/// 2. Selects a variant based on the test configuration
17/// 3. Applies the variant response (status code, headers, body)
18/// 4. Records analytics for the selected variant
19pub async fn ab_testing_middleware(req: Request, next: Next) -> Response<Body> {
20    let start_time = Instant::now();
21
22    // Extract method and path before borrowing req
23    let method = req.method().to_string();
24    let path = req.uri().path().to_string();
25    let uri = req.uri().to_string();
26
27    // Extract headers before borrowing req
28    let headers = req.headers().clone();
29
30    // Get A/B testing state from extensions (clone to avoid borrow issues)
31    let state_opt = req.extensions().get::<ABTestingState>().cloned();
32
33    if let Some(state) = state_opt {
34        // Get A/B test configuration for this endpoint
35        if let Some(test_config) = state.variant_manager.get_test(&method, &path).await {
36            trace!("A/B test found for {} {}", method, path);
37
38            // Select a variant using extracted headers and URI
39            match select_variant(&test_config, &headers, &uri, &state.variant_manager).await {
40                Ok(Some(variant)) => {
41                    debug!("Selected variant '{}' for {} {}", variant.variant_id, method, path);
42
43                    // Continue with request processing
44                    let response = next.run(req).await;
45
46                    // Apply variant to response
47                    let response = apply_variant_to_response(&variant, response);
48
49                    // Record analytics
50                    let response_time_ms = start_time.elapsed().as_millis() as f64;
51                    let status_code = response.status().as_u16();
52                    state
53                        .variant_manager
54                        .record_request(
55                            &method,
56                            &path,
57                            &variant.variant_id,
58                            status_code,
59                            response_time_ms,
60                        )
61                        .await;
62
63                    // Add latency if configured
64                    if let Some(latency_ms) = variant.latency_ms {
65                        tokio::time::sleep(tokio::time::Duration::from_millis(latency_ms)).await;
66                    }
67
68                    return response;
69                }
70                Ok(None) => {
71                    trace!("No variant selected for {} {}", method, path);
72                }
73                Err(e) => {
74                    debug!("Error selecting variant for {} {}: {}", method, path, e);
75                }
76            }
77        }
78    }
79
80    // No A/B test configured or variant selection failed - proceed normally
81    next.run(req).await
82}