Skip to main content

fluidattacks_tracks/resources/
event.rs

1//! Tracks event resource.
2
3use std::sync::Mutex;
4use std::thread::{self, JoinHandle};
5use std::time::Duration;
6
7use chrono::{DateTime, Utc};
8use reqwest::blocking::Client;
9use serde::Serialize;
10
11/// Action performed on an object.
12#[non_exhaustive]
13#[derive(Debug, Clone, Serialize)]
14#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
15pub enum Action {
16    Create,
17    Read,
18    Update,
19    Delete,
20}
21
22/// Mechanism by which the event was triggered.
23#[non_exhaustive]
24#[derive(Debug, Clone, Serialize)]
25pub enum Mechanism {
26    #[serde(rename = "API")]
27    Api,
28    #[serde(rename = "BTS")]
29    Bts,
30    #[serde(rename = "DESKTOP")]
31    Desktop,
32    #[serde(rename = "EMAIL")]
33    Email,
34    #[serde(rename = "FIXES")]
35    Fixes,
36    #[serde(rename = "FORCES")]
37    Forces,
38    #[serde(rename = "JIRA")]
39    Jira,
40    #[serde(rename = "MCP")]
41    Mcp,
42    #[serde(rename = "MELTS")]
43    Melts,
44    #[serde(rename = "MESSAGING")]
45    Messaging,
46    #[serde(rename = "MIGRATION")]
47    Migration,
48    #[serde(rename = "RETRIEVES")]
49    Retrieves,
50    #[serde(rename = "SCHEDULER")]
51    Scheduler,
52    #[serde(rename = "SMELLS")]
53    Smells,
54    #[serde(rename = "TASK")]
55    Task,
56    #[serde(rename = "WEB")]
57    Web,
58}
59
60/// A Tracks event to publish.
61#[non_exhaustive]
62#[derive(Debug, Clone, Serialize)]
63pub struct Event {
64    pub action: Action,
65    pub author: String,
66    pub date: DateTime<Utc>,
67    pub mechanism: Mechanism,
68    /// Arbitrary JSON object with additional event context.
69    ///
70    /// Must be a JSON object (i.e. `serde_json::json!({ "key": "value" })`).
71    /// Passing a non-object value (array, string, number, …) will cause the
72    /// event to be silently dropped when [`EventResource::create`] is called.
73    pub metadata: serde_json::Value,
74    pub object: String,
75    pub object_id: String,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub author_anonymous: Option<bool>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub author_ip: Option<String>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub author_role: Option<String>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub author_user_agent: Option<String>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub session_id: Option<String>,
86}
87
88impl Event {
89    /// Create a new event with required fields. Optional fields default to `None`.
90    pub fn new(
91        action: Action,
92        author: String,
93        date: DateTime<Utc>,
94        mechanism: Mechanism,
95        metadata: serde_json::Value,
96        object: String,
97        object_id: String,
98    ) -> Self {
99        Event {
100            action,
101            author,
102            date,
103            mechanism,
104            metadata,
105            object,
106            object_id,
107            author_anonymous: None,
108            author_ip: None,
109            author_role: None,
110            author_user_agent: None,
111            session_id: None,
112        }
113    }
114}
115
116/// Tracks event resource.
117pub struct EventResource {
118    base_url: String,
119    http: Client,
120    retry_attempts: u32,
121    pending: Mutex<Vec<JoinHandle<()>>>,
122}
123
124impl EventResource {
125    pub(crate) fn new(base_url: String, http: Client, retry_attempts: u32) -> Self {
126        EventResource {
127            base_url,
128            http,
129            retry_attempts,
130            pending: Mutex::new(Vec::new()),
131        }
132    }
133
134    /// Publish an event fire-and-forget in a background thread.
135    ///
136    /// Errors are silently swallowed so that tracking failures never
137    /// interrupt the caller's normal flow. Call [`EventResource::flush`]
138    /// before process exit to ensure all in-flight events are delivered.
139    ///
140    /// # Panics
141    ///
142    /// Does not panic. If `event.metadata` is not a JSON object the event is
143    /// silently dropped.
144    pub fn create(&self, event: Event) {
145        if !event.metadata.is_object() {
146            return;
147        }
148        let url = format!("{}/event", self.base_url.trim_end_matches('/'));
149        let http = self.http.clone();
150        let retry_attempts = self.retry_attempts;
151        let handle = thread::spawn(move || {
152            for attempt in 0..retry_attempts {
153                match http.post(&url).json(&event).send() {
154                    Ok(resp) if resp.status().is_success() => return,
155                    _ => {
156                        if attempt + 1 < retry_attempts {
157                            thread::sleep(Duration::from_millis(100 * 2u64.pow(attempt)));
158                        }
159                    }
160                }
161            }
162        });
163        if let Ok(mut pending) = self.pending.lock() {
164            pending.retain(|h| !h.is_finished());
165            pending.push(handle);
166        }
167    }
168
169    /// Block until all in-flight events have been sent or exhausted their retries.
170    ///
171    /// Call this before process exit to avoid dropping events that are still
172    /// being delivered in background threads.
173    pub fn flush(&self) {
174        let handles = self
175            .pending
176            .lock()
177            .map(|mut p| std::mem::take(&mut *p))
178            .unwrap_or_default();
179        for handle in handles {
180            let _ = handle.join();
181        }
182    }
183}