@@ -4,6 +4,7 @@ use diesel::prelude::*;
44use diesel:: sql_types:: { BigInt , Date , Nullable , Text } ;
55use ipnetwork:: IpNetwork ;
66use serde:: { Deserialize , Serialize } ;
7+ use serde_json:: Value ;
78
89use crate :: schema:: heartbeats;
910use crate :: utils:: http:: parse_user_agent;
@@ -161,23 +162,39 @@ pub struct HeartbeatRequest {
161162#[ serde( untagged) ]
162163pub enum HeartbeatApiResponseVariant {
163164 Single ( HeartbeatApiResponse ) ,
164- Multiple ( Vec < HeartbeatResponse > ) ,
165+ Multiple ( HeartbeatBulkApiResponse ) ,
165166}
166167
167168#[ derive( Serialize , Debug ) ]
168169pub struct HeartbeatResponse {
169170 pub id : String ,
170- pub entity : String ,
171- #[ serde( rename = "type" ) ]
172- pub type_ : String ,
173- pub time : f64 ,
174171}
175172
176173#[ derive( Serialize , Debug ) ]
177174pub struct HeartbeatApiResponse {
178175 pub data : HeartbeatResponse ,
179176}
180177
178+ #[ derive( Serialize , Debug ) ]
179+ pub struct HeartbeatBulkApiResponse {
180+ pub responses : Vec < BulkResponseItem > ,
181+ }
182+
183+ #[ derive( Serialize , Debug ) ]
184+ #[ serde( untagged) ]
185+ pub enum BulkResponsePayload {
186+ Data {
187+ data : HeartbeatResponse ,
188+ } ,
189+ #[ allow( dead_code) ]
190+ Errors {
191+ errors : Value ,
192+ } ,
193+ }
194+
195+ #[ derive( Serialize , Debug ) ]
196+ pub struct BulkResponseItem ( pub BulkResponsePayload , pub u16 ) ;
197+
181198#[ derive( Queryable , Selectable , Serialize , Deserialize , Debug , Clone ) ]
182199#[ diesel( table_name = heartbeats) ]
183200#[ diesel( check_for_backend( diesel:: pg:: Pg ) ) ]
@@ -260,7 +277,11 @@ pub struct SanitizedHeartbeatRequest {
260277impl SanitizedHeartbeatRequest {
261278 pub fn from_request ( request : HeartbeatRequest ) -> Self {
262279 // Convert timestamp (seconds since epoch) to DateTime<Utc>
263- let time = DateTime :: from_timestamp ( request. time as i64 , 0 ) . unwrap_or_else ( Utc :: now) ;
280+ let time = DateTime :: from_timestamp (
281+ request. time . trunc ( ) as i64 ,
282+ ( request. time . fract ( ) * 1e9 ) as u32 ,
283+ )
284+ . unwrap_or_else ( Utc :: now) ;
264285
265286 // Handle test heartbeats
266287 let type_ = if request. entity == "test.txt" {
@@ -269,11 +290,17 @@ impl SanitizedHeartbeatRequest {
269290 request. type_
270291 } ;
271292
272- // Fallback to "coding" category
293+ // Parse the category
273294 let category = if let Some ( cat) = request. category {
274295 Some ( cat)
275296 } else {
276- Some ( "coding" . to_string ( ) )
297+ if type_ == "domain" || type_ == "url" {
298+ Some ( "browsing" . to_string ( ) )
299+ } else if type_ == "file" && request. language . is_some ( ) {
300+ Some ( "coding" . to_string ( ) )
301+ } else {
302+ None
303+ }
277304 } ;
278305
279306 // Convert dependencies and apply limits
@@ -412,22 +439,21 @@ impl NewHeartbeat {
412439 sanitized. into_new_heartbeat ( user_id, ip_address, headers)
413440 }
414441}
415-
416442impl From < Heartbeat > for HeartbeatResponse {
417443 fn from ( heartbeat : Heartbeat ) -> Self {
418- let id = heartbeat. id . to_string ( ) ;
419- // Convert DateTime<Utc> to timestamp (seconds since epoch)
420- let time = heartbeat. time . timestamp ( ) as f64 ;
421-
422444 Self {
423- id,
424- entity : heartbeat. entity ,
425- type_ : heartbeat. type_ ,
426- time,
445+ id : heartbeat. id . to_string ( ) ,
427446 }
428447 }
429448}
430449
450+ impl From < Heartbeat > for BulkResponseItem {
451+ fn from ( heartbeat : Heartbeat ) -> Self {
452+ let response = HeartbeatResponse :: from ( heartbeat) ;
453+ BulkResponseItem ( BulkResponsePayload :: Data { data : response } , 201 )
454+ }
455+ }
456+
431457impl Heartbeat {
432458 pub fn count_total_heartbeats ( conn : & mut PgConnection ) -> QueryResult < i64 > {
433459 heartbeats:: table. count ( ) . get_result ( conn)
0 commit comments