Skip to main content

systemprompt_analytics/repository/fingerprint/
mutations.rs

1//! Fingerprint-reputation state changes for `FingerprintRepository`.
2//!
3//! Upserts a fingerprint on each session, counts its requests and raises
4//! abuse flags. All writes go to the write pool.
5//!
6//! Copyright (c) systemprompt.io — Business Source License 1.1.
7//! See <https://systemprompt.io> for licensing details.
8
9use crate::Result;
10use systemprompt_identifiers::UserId;
11
12use super::FingerprintRepository;
13use crate::models::{FingerprintReputation, FlagReason};
14
15impl FingerprintRepository {
16    pub async fn upsert_fingerprint(
17        &self,
18        fingerprint_hash: &str,
19        ip_address: Option<&str>,
20        user_agent: Option<&str>,
21        user_id: Option<&UserId>,
22    ) -> Result<FingerprintReputation> {
23        let user_ids = user_id.map_or_else(Vec::new, |u| vec![u.as_str().to_owned()]);
24
25        let row = sqlx::query_as!(
26            FingerprintReputation,
27            r#"
28            INSERT INTO fingerprint_reputation (
29                fingerprint_hash, last_ip_address, last_user_agent,
30                associated_user_ids, total_session_count
31            )
32            VALUES ($1, $2, $3, $4, 1)
33            ON CONFLICT (fingerprint_hash) DO UPDATE SET
34                last_seen_at = CURRENT_TIMESTAMP,
35                last_ip_address = COALESCE($2, fingerprint_reputation.last_ip_address),
36                last_user_agent = COALESCE($3, fingerprint_reputation.last_user_agent),
37                total_session_count = fingerprint_reputation.total_session_count + 1,
38                associated_user_ids = CASE
39                    WHEN array_length($4, 1) > 0 AND NOT ($4[1] = ANY(fingerprint_reputation.associated_user_ids))
40                    THEN array_cat(fingerprint_reputation.associated_user_ids, $4)
41                    ELSE fingerprint_reputation.associated_user_ids
42                END,
43                updated_at = CURRENT_TIMESTAMP
44            RETURNING
45                fingerprint_hash,
46                first_seen_at,
47                last_seen_at,
48                total_session_count,
49                active_session_count,
50                total_request_count,
51                requests_last_hour,
52                peak_requests_per_minute,
53                sustained_high_velocity_minutes,
54                is_flagged,
55                flag_reason,
56                flagged_at,
57                reputation_score,
58                abuse_incidents,
59                last_abuse_at,
60                last_ip_address,
61                last_user_agent,
62                associated_user_ids as "associated_user_ids: Vec<UserId>",
63                updated_at
64            "#,
65            fingerprint_hash,
66            ip_address,
67            user_agent,
68            &user_ids[..],
69        )
70        .fetch_one(&*self.write_pool)
71        .await?;
72
73        Ok(row)
74    }
75
76    pub async fn flag_fingerprint(
77        &self,
78        fingerprint_hash: &str,
79        reason: FlagReason,
80        new_score: i32,
81    ) -> Result<()> {
82        sqlx::query!(
83            r#"
84            UPDATE fingerprint_reputation
85            SET is_flagged = TRUE,
86                flag_reason = $2,
87                flagged_at = CURRENT_TIMESTAMP,
88                reputation_score = $3,
89                abuse_incidents = abuse_incidents + 1,
90                last_abuse_at = CURRENT_TIMESTAMP,
91                updated_at = CURRENT_TIMESTAMP
92            WHERE fingerprint_hash = $1
93            "#,
94            fingerprint_hash,
95            reason.as_str(),
96            new_score,
97        )
98        .execute(&*self.write_pool)
99        .await?;
100
101        Ok(())
102    }
103
104    pub async fn increment_request_count(&self, fingerprint_hash: &str) -> Result<()> {
105        sqlx::query!(
106            r#"
107            UPDATE fingerprint_reputation
108            SET total_request_count = total_request_count + 1,
109                updated_at = CURRENT_TIMESTAMP
110            WHERE fingerprint_hash = $1
111            "#,
112            fingerprint_hash,
113        )
114        .execute(&*self.write_pool)
115        .await?;
116
117        Ok(())
118    }
119}