use crate::ab_testing::shadow_mode::{SearchResult, ShadowModeResults};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InteractionEventType {
Click,
Dwell,
Selection,
Abandon,
Reformulation,
}
impl std::fmt::Display for InteractionEventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InteractionEventType::Click => write!(f, "click"),
InteractionEventType::Dwell => write!(f, "dwell"),
InteractionEventType::Selection => write!(f, "selection"),
InteractionEventType::Abandon => write!(f, "abandon"),
InteractionEventType::Reformulation => write!(f, "reformulation"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InteractionEvent {
pub id: Uuid,
pub experiment_id: Uuid,
pub query: String,
pub event_type: InteractionEventType,
pub result_position: Option<i32>,
pub dwell_time_ms: Option<i32>,
pub timestamp: DateTime<Utc>,
pub user_id: Option<String>,
pub metadata: Option<serde_json::Value>,
}
impl InteractionEvent {
pub fn new(
experiment_id: Uuid,
query: String,
event_type: InteractionEventType,
user_id: Option<String>,
) -> Self {
Self {
id: Uuid::new_v4(),
experiment_id,
query,
event_type,
result_position: None,
dwell_time_ms: None,
timestamp: Utc::now(),
user_id,
metadata: None,
}
}
pub fn click(
experiment_id: Uuid,
query: String,
position: i32,
user_id: Option<String>,
) -> Self {
let mut event = Self::new(experiment_id, query, InteractionEventType::Click, user_id);
event.result_position = Some(position);
event
}
pub fn dwell(
experiment_id: Uuid,
query: String,
position: i32,
dwell_time_ms: i32,
user_id: Option<String>,
) -> Self {
let mut event = Self::new(experiment_id, query, InteractionEventType::Dwell, user_id);
event.result_position = Some(position);
event.dwell_time_ms = Some(dwell_time_ms);
event
}
pub fn selection(
experiment_id: Uuid,
query: String,
position: i32,
user_id: Option<String>,
) -> Self {
let mut event = Self::new(
experiment_id,
query,
InteractionEventType::Selection,
user_id,
);
event.result_position = Some(position);
event
}
pub fn abandon(experiment_id: Uuid, query: String, user_id: Option<String>) -> Self {
Self::new(experiment_id, query, InteractionEventType::Abandon, user_id)
}
pub fn reformulation(experiment_id: Uuid, old_query: String, user_id: Option<String>) -> Self {
Self::new(
experiment_id,
old_query,
InteractionEventType::Reformulation,
user_id,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShadowResultLog {
pub id: Uuid,
pub experiment_id: Uuid,
pub query: String,
pub old_results: Vec<SearchResult>,
pub new_results: Option<Vec<SearchResult>>,
pub old_latency_ms: i32,
pub new_latency_ms: Option<i32>,
pub new_error: Option<String>,
pub timestamp: DateTime<Utc>,
pub user_id: Option<String>,
}
impl From<(Uuid, &ShadowModeResults)> for ShadowResultLog {
fn from((experiment_id, results): (Uuid, &ShadowModeResults)) -> Self {
Self {
id: Uuid::new_v4(),
experiment_id,
query: results.query.clone(),
old_results: results.old_results.clone(),
new_results: results.new_results.clone(),
old_latency_ms: results.old_latency_ms as i32,
new_latency_ms: results.new_latency_ms.map(|v| v as i32),
new_error: results.new_error.clone(),
timestamp: results.timestamp,
user_id: results.user_id.clone(),
}
}
}
pub struct ABTestLogger {
_phantom: std::marker::PhantomData<()>,
}
impl Default for ABTestLogger {
fn default() -> Self {
Self::new()
}
}
impl ABTestLogger {
pub fn new() -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
pub fn with_config(_batch_size: usize, _flush_interval_secs: u64) -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
pub async fn log_shadow_results(
&self,
_experiment_id: Uuid,
_results: &ShadowModeResults,
) -> anyhow::Result<()> {
anyhow::bail!("A/B testing not implemented for SQLite backend")
}
pub async fn log_interaction(&self, _event: InteractionEvent) -> anyhow::Result<()> {
anyhow::bail!("A/B testing not implemented for SQLite backend")
}
pub async fn flush_all(&self) -> anyhow::Result<()> {
anyhow::bail!("A/B testing not implemented for SQLite backend")
}
pub fn start_background_flusher(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interaction_event_creation() {
let experiment_id = Uuid::new_v4();
let click = InteractionEvent::click(
experiment_id,
"test query".to_string(),
3,
Some("user123".to_string()),
);
assert_eq!(click.event_type, InteractionEventType::Click);
assert_eq!(click.result_position, Some(3));
assert_eq!(click.query, "test query");
let dwell = InteractionEvent::dwell(experiment_id, "test query".to_string(), 1, 5000, None);
assert_eq!(dwell.event_type, InteractionEventType::Dwell);
assert_eq!(dwell.result_position, Some(1));
assert_eq!(dwell.dwell_time_ms, Some(5000));
let abandon = InteractionEvent::abandon(experiment_id, "test query".to_string(), None);
assert_eq!(abandon.event_type, InteractionEventType::Abandon);
assert_eq!(abandon.result_position, None);
}
#[test]
fn test_shadow_result_log_conversion() {
let experiment_id = Uuid::new_v4();
let results = ShadowModeResults {
query: "test".to_string(),
user_id: Some("user1".to_string()),
old_results: vec![],
new_results: Some(vec![]),
old_latency_ms: 100,
new_latency_ms: Some(120),
new_error: None,
timestamp: Utc::now(),
};
let log = ShadowResultLog::from((experiment_id, &results));
assert_eq!(log.experiment_id, experiment_id);
assert_eq!(log.query, "test");
assert_eq!(log.old_latency_ms, 100);
assert_eq!(log.new_latency_ms, Some(120));
}
}