use chrono::Utc;
use std::sync::Arc;
use systemprompt_analytics::{BehavioralAnalysisInput, BehavioralBotDetector, SessionRepository};
use systemprompt_identifiers::SessionId;
pub(super) fn spawn_behavioral_detection_task(
session_repo: Arc<SessionRepository>,
session_id: SessionId,
fingerprint_hash: Option<String>,
user_agent: Option<String>,
request_count: i64,
) {
tokio::spawn(async move {
const BEHAVIORAL_FINGERPRINT_WINDOW_DAYS: i64 = 45;
let fingerprint_session_count = if let Some(ref fp) = fingerprint_hash {
session_repo
.count_sessions_by_fingerprint(fp, 24)
.await
.unwrap_or_else(|e| {
tracing::debug!(error = %e, "Failed to count fingerprint sessions");
1
})
} else {
1
};
let fingerprint_unique_ip_count = if let Some(ref fp) = fingerprint_hash {
session_repo
.count_unique_ips_by_fingerprint(fp, BEHAVIORAL_FINGERPRINT_WINDOW_DAYS)
.await
.unwrap_or_else(|e| {
tracing::debug!(error = %e, "Failed to count fingerprint unique IPs");
0
})
} else {
0
};
let fingerprint_engagement_event_count = if let Some(ref fp) = fingerprint_hash {
session_repo
.count_engagement_events_by_fingerprint(fp, BEHAVIORAL_FINGERPRINT_WINDOW_DAYS)
.await
.unwrap_or_else(|e| {
tracing::debug!(error = %e, "Failed to count fingerprint engagement events");
0
})
} else {
0
};
let fingerprint_session_starts = if let Some(ref fp) = fingerprint_hash {
session_repo
.get_session_starts_by_fingerprint(fp, BEHAVIORAL_FINGERPRINT_WINDOW_DAYS)
.await
.unwrap_or_else(|e| {
tracing::debug!(error = %e, "Failed to load fingerprint session starts");
Vec::new()
})
} else {
Vec::new()
};
let endpoints_accessed = session_repo
.get_endpoint_sequence(&session_id)
.await
.unwrap_or_else(|e| {
tracing::debug!(error = %e, "Failed to get endpoint sequence");
Vec::new()
});
let request_timestamps = session_repo
.get_request_timestamps(&session_id)
.await
.unwrap_or_else(|e| {
tracing::debug!(error = %e, "Failed to get request timestamps");
Vec::new()
});
let total_site_pages = session_repo
.get_total_content_pages()
.await
.unwrap_or_else(|e| {
tracing::debug!(error = %e, "Failed to get total content pages");
100
});
let has_javascript_events = session_repo
.has_analytics_events(&session_id)
.await
.unwrap_or(false);
let session_data = session_repo
.get_session_for_behavioral_analysis(&session_id)
.await
.map_err(|e| {
tracing::debug!(error = %e, "Failed to get session for behavioral analysis");
e
})
.ok()
.flatten();
let (started_at, last_activity_at, actual_request_count, landing_page, entry_url) =
session_data.map_or_else(
|| {
let now = Utc::now();
(now, now, request_count, None, None)
},
|s| {
(
s.started_at,
s.last_activity_at,
s.request_count.map_or(request_count, i64::from),
s.landing_page,
s.entry_url,
)
},
);
let session_id_for_update = session_id.clone();
let input = BehavioralAnalysisInput {
session_id,
fingerprint_hash,
user_agent,
request_count: actual_request_count,
started_at,
last_activity_at,
endpoints_accessed,
total_site_pages,
fingerprint_session_count,
fingerprint_unique_ip_count,
fingerprint_engagement_event_count,
fingerprint_session_starts,
request_timestamps,
has_javascript_events,
landing_page,
entry_url,
};
let result = BehavioralBotDetector::analyze(&input);
if result.score > 0 {
if let Err(e) = session_repo
.update_behavioral_detection(
&session_id_for_update,
result.score,
result.is_suspicious,
result.reason.as_deref(),
)
.await
{
tracing::error!(error = %e, "Failed to update behavioral detection");
}
}
});
}