1use std::fmt::{self, Display};
2use std::time::SystemTime;
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8use crate::protocol::utils::null_to_default;
9use crate::protocol::IpAddr;
10
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub enum SessionStatus {
14 Ok,
18 Exited,
20 Crashed,
22 Abnormal,
24 Errored,
26 Unknown(String),
28}
29
30impl SessionStatus {
31 pub fn is_terminal(&self) -> bool {
33 !matches!(self, SessionStatus::Ok)
34 }
35
36 pub fn is_error(&self) -> bool {
38 !matches!(self, SessionStatus::Ok | SessionStatus::Exited)
39 }
40
41 pub fn is_fatal(&self) -> bool {
43 matches!(self, SessionStatus::Crashed | SessionStatus::Abnormal)
44 }
45 fn as_str(&self) -> &str {
46 match self {
47 SessionStatus::Ok => "ok",
48 SessionStatus::Crashed => "crashed",
49 SessionStatus::Abnormal => "abnormal",
50 SessionStatus::Exited => "exited",
51 SessionStatus::Errored => "errored",
52 SessionStatus::Unknown(s) => s.as_str(),
53 }
54 }
55}
56
57impl Display for SessionStatus {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 write!(f, "{}", self.as_str())
60 }
61}
62
63relay_common::impl_str_serde!(SessionStatus, "A session status");
64
65impl std::str::FromStr for SessionStatus {
66 type Err = ParseSessionStatusError;
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 Ok(match s {
70 "ok" => SessionStatus::Ok,
71 "crashed" => SessionStatus::Crashed,
72 "abnormal" => SessionStatus::Abnormal,
73 "exited" => SessionStatus::Exited,
74 "errored" => SessionStatus::Errored,
75 other => SessionStatus::Unknown(other.to_owned()),
76 })
77 }
78}
79
80impl Default for SessionStatus {
81 fn default() -> Self {
82 Self::Ok
83 }
84}
85
86#[derive(Debug)]
88pub struct ParseSessionStatusError;
89
90impl fmt::Display for ParseSessionStatusError {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(f, "invalid session status")
93 }
94}
95
96impl std::error::Error for ParseSessionStatusError {}
97
98#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
99#[serde(rename_all = "snake_case")]
100pub enum AbnormalMechanism {
101 AnrForeground,
102 AnrBackground,
103 #[serde(other)]
104 #[default]
105 None,
106}
107
108#[derive(Debug)]
109pub struct ParseAbnormalMechanismError;
110
111impl fmt::Display for ParseAbnormalMechanismError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(f, "invalid abnormal mechanism")
114 }
115}
116
117relay_common::derive_fromstr_and_display!(AbnormalMechanism, ParseAbnormalMechanismError, {
118 AbnormalMechanism::AnrForeground => "anr_foreground",
119 AbnormalMechanism::AnrBackground => "anr_background",
120 AbnormalMechanism::None => "none",
121});
122
123impl AbnormalMechanism {
124 fn is_none(&self) -> bool {
125 *self == Self::None
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
131pub struct SessionAttributes {
132 pub release: String,
134
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub environment: Option<String>,
138
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub ip_address: Option<IpAddr>,
142
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub user_agent: Option<String>,
146}
147
148fn default_sequence() -> u64 {
149 SystemTime::now()
150 .duration_since(SystemTime::UNIX_EPOCH)
151 .unwrap_or_default()
152 .as_millis() as u64
153}
154
155#[allow(clippy::trivially_copy_pass_by_ref)]
156fn is_false(val: &bool) -> bool {
157 !val
158}
159
160pub enum SessionErrored {
162 Individual(Uuid),
164 Aggregated(u32),
167}
168
169pub trait SessionLike {
171 fn started(&self) -> DateTime<Utc>;
172 fn distinct_id(&self) -> Option<&String>;
173 fn total_count(&self) -> u32;
174 fn abnormal_count(&self) -> u32;
175 fn crashed_count(&self) -> u32;
176 fn all_errors(&self) -> Option<SessionErrored>;
177 fn abnormal_mechanism(&self) -> AbnormalMechanism;
178}
179
180#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
181pub struct SessionUpdate {
182 #[serde(rename = "sid", default = "Uuid::new_v4")]
184 pub session_id: Uuid,
185 #[serde(rename = "did", default)]
187 pub distinct_id: Option<String>,
188 #[serde(rename = "seq", default = "default_sequence")]
190 pub sequence: u64,
191 #[serde(default, skip_serializing_if = "is_false")]
193 pub init: bool,
194 #[serde(default = "Utc::now")]
196 pub timestamp: DateTime<Utc>,
197 pub started: DateTime<Utc>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub duration: Option<f64>,
202 #[serde(default)]
204 pub status: SessionStatus,
205 #[serde(default)]
207 pub errors: u64,
208 #[serde(rename = "attrs")]
210 pub attributes: SessionAttributes,
211 #[serde(
213 default,
214 deserialize_with = "null_to_default",
215 skip_serializing_if = "AbnormalMechanism::is_none"
216 )]
217 pub abnormal_mechanism: AbnormalMechanism,
218}
219
220impl SessionUpdate {
221 pub fn parse(payload: &[u8]) -> Result<Self, serde_json::Error> {
223 serde_json::from_slice(payload)
224 }
225
226 pub fn serialize(&self) -> Result<Vec<u8>, serde_json::Error> {
228 serde_json::to_vec(self)
229 }
230}
231
232impl SessionLike for SessionUpdate {
233 fn started(&self) -> DateTime<Utc> {
234 self.started
235 }
236
237 fn distinct_id(&self) -> Option<&String> {
238 self.distinct_id.as_ref()
239 }
240
241 fn total_count(&self) -> u32 {
242 u32::from(self.init)
243 }
244
245 fn abnormal_count(&self) -> u32 {
246 match self.status {
247 SessionStatus::Abnormal => 1,
248 _ => 0,
249 }
250 }
251
252 fn crashed_count(&self) -> u32 {
253 match self.status {
254 SessionStatus::Crashed => 1,
255 _ => 0,
256 }
257 }
258
259 fn all_errors(&self) -> Option<SessionErrored> {
260 if self.errors > 0 || self.status.is_error() {
261 Some(SessionErrored::Individual(self.session_id))
262 } else {
263 None
264 }
265 }
266
267 fn abnormal_mechanism(&self) -> AbnormalMechanism {
268 self.abnormal_mechanism
269 }
270}
271
272#[allow(clippy::trivially_copy_pass_by_ref)]
273fn is_zero(val: &u32) -> bool {
274 *val == 0
275}
276
277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
278pub struct SessionAggregateItem {
279 pub started: DateTime<Utc>,
281 #[serde(rename = "did", default, skip_serializing_if = "Option::is_none")]
283 pub distinct_id: Option<String>,
284 #[serde(default, skip_serializing_if = "is_zero")]
286 pub exited: u32,
287 #[serde(default, skip_serializing_if = "is_zero")]
289 pub errored: u32,
290 #[serde(default, skip_serializing_if = "is_zero")]
292 pub abnormal: u32,
293 #[serde(default, skip_serializing_if = "is_zero")]
295 pub crashed: u32,
296}
297
298impl SessionLike for SessionAggregateItem {
299 fn started(&self) -> DateTime<Utc> {
300 self.started
301 }
302
303 fn distinct_id(&self) -> Option<&String> {
304 self.distinct_id.as_ref()
305 }
306
307 fn total_count(&self) -> u32 {
308 self.exited + self.errored + self.abnormal + self.crashed
309 }
310
311 fn abnormal_count(&self) -> u32 {
312 self.abnormal
313 }
314
315 fn crashed_count(&self) -> u32 {
316 self.crashed
317 }
318
319 fn all_errors(&self) -> Option<SessionErrored> {
320 let all_errored = self.abnormal + self.crashed + self.errored;
323 if all_errored > 0 {
324 Some(SessionErrored::Aggregated(all_errored))
325 } else {
326 None
327 }
328 }
329 fn abnormal_mechanism(&self) -> AbnormalMechanism {
330 AbnormalMechanism::None
331 }
332}
333
334#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
335pub struct SessionAggregates {
336 #[serde(default)]
338 pub aggregates: Vec<SessionAggregateItem>,
339 #[serde(rename = "attrs")]
341 pub attributes: SessionAttributes,
342}
343
344impl SessionAggregates {
345 pub fn parse(payload: &[u8]) -> Result<Self, serde_json::Error> {
347 serde_json::from_slice(payload)
348 }
349
350 pub fn serialize(&self) -> Result<Vec<u8>, serde_json::Error> {
352 serde_json::to_vec(self)
353 }
354}
355
356#[cfg(test)]
357mod tests {
358
359 use std::str::FromStr;
360
361 use similar_asserts::assert_eq;
362
363 use super::*;
364
365 #[test]
366 fn test_sessionstatus_unknown() {
367 let unknown = SessionStatus::from_str("invalid status").unwrap();
368 if let SessionStatus::Unknown(inner) = unknown {
369 assert_eq!(inner, "invalid status".to_owned());
370 } else {
371 panic!();
372 }
373 }
374
375 #[test]
376 fn test_session_default_values() {
377 let json = r#"{
378 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
379 "timestamp": "2020-02-07T15:17:00Z",
380 "started": "2020-02-07T14:16:00Z",
381 "attrs": {
382 "release": "sentry-test@1.0.0"
383 }
384}"#;
385
386 let output = r#"{
387 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
388 "did": null,
389 "seq": 4711,
390 "timestamp": "2020-02-07T15:17:00Z",
391 "started": "2020-02-07T14:16:00Z",
392 "status": "ok",
393 "errors": 0,
394 "attrs": {
395 "release": "sentry-test@1.0.0"
396 }
397}"#;
398
399 let update = SessionUpdate {
400 session_id: "8333339f-5675-4f89-a9a0-1c935255ab58".parse().unwrap(),
401 distinct_id: None,
402 sequence: 4711, timestamp: "2020-02-07T15:17:00Z".parse().unwrap(),
404 started: "2020-02-07T14:16:00Z".parse().unwrap(),
405 duration: None,
406 init: false,
407 status: SessionStatus::Ok,
408 abnormal_mechanism: AbnormalMechanism::None,
409 errors: 0,
410 attributes: SessionAttributes {
411 release: "sentry-test@1.0.0".to_owned(),
412 environment: None,
413 ip_address: None,
414 user_agent: None,
415 },
416 };
417
418 let mut parsed = SessionUpdate::parse(json.as_bytes()).unwrap();
419
420 assert!((default_sequence() - parsed.sequence) <= 1);
422 parsed.sequence = 4711;
423
424 assert_eq!(update, parsed);
425 assert_eq!(output, serde_json::to_string_pretty(&update).unwrap());
426 }
427
428 #[test]
429 fn test_session_default_timestamp_and_sid() {
430 let json = r#"{
431 "started": "2020-02-07T14:16:00Z",
432 "attrs": {
433 "release": "sentry-test@1.0.0"
434 }
435}"#;
436
437 let parsed = SessionUpdate::parse(json.as_bytes()).unwrap();
438 assert!(!parsed.session_id.is_nil());
439 }
440
441 #[test]
442 fn test_session_roundtrip() {
443 let json = r#"{
444 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
445 "did": "foobarbaz",
446 "seq": 42,
447 "init": true,
448 "timestamp": "2020-02-07T15:17:00Z",
449 "started": "2020-02-07T14:16:00Z",
450 "duration": 1947.49,
451 "status": "exited",
452 "errors": 0,
453 "attrs": {
454 "release": "sentry-test@1.0.0",
455 "environment": "production",
456 "ip_address": "::1",
457 "user_agent": "Firefox/72.0"
458 }
459}"#;
460
461 let update = SessionUpdate {
462 session_id: "8333339f-5675-4f89-a9a0-1c935255ab58".parse().unwrap(),
463 distinct_id: Some("foobarbaz".into()),
464 sequence: 42,
465 timestamp: "2020-02-07T15:17:00Z".parse().unwrap(),
466 started: "2020-02-07T14:16:00Z".parse().unwrap(),
467 duration: Some(1947.49),
468 status: SessionStatus::Exited,
469 abnormal_mechanism: AbnormalMechanism::None,
470 errors: 0,
471 init: true,
472 attributes: SessionAttributes {
473 release: "sentry-test@1.0.0".to_owned(),
474 environment: Some("production".to_owned()),
475 ip_address: Some(IpAddr::parse("::1").unwrap()),
476 user_agent: Some("Firefox/72.0".to_owned()),
477 },
478 };
479
480 assert_eq!(update, SessionUpdate::parse(json.as_bytes()).unwrap());
481 assert_eq!(json, serde_json::to_string_pretty(&update).unwrap());
482 }
483
484 #[test]
485 fn test_session_ip_addr_auto() {
486 let json = r#"{
487 "started": "2020-02-07T14:16:00Z",
488 "attrs": {
489 "release": "sentry-test@1.0.0",
490 "ip_address": "{{auto}}"
491 }
492}"#;
493
494 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
495 assert_eq!(update.attributes.ip_address, Some(IpAddr::auto()));
496 }
497 #[test]
498 fn test_session_abnormal_mechanism() {
499 let json = r#"{
500 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
501 "started": "2020-02-07T14:16:00Z",
502 "status": "abnormal",
503 "abnormal_mechanism": "anr_background",
504 "attrs": {
505 "release": "sentry-test@1.0.0",
506 "environment": "production"
507 }
508 }"#;
509
510 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
511 assert_eq!(update.abnormal_mechanism, AbnormalMechanism::AnrBackground);
512 }
513
514 #[test]
515 fn test_session_invalid_abnormal_mechanism() {
516 let json = r#"{
517 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
518 "started": "2020-02-07T14:16:00Z",
519 "status": "abnormal",
520 "abnormal_mechanism": "invalid_mechanism",
521 "attrs": {
522 "release": "sentry-test@1.0.0",
523 "environment": "production"
524 }
525}"#;
526
527 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
528 assert_eq!(update.abnormal_mechanism, AbnormalMechanism::None);
529 }
530
531 #[test]
532 fn test_session_null_abnormal_mechanism() {
533 let json = r#"{
534 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
535 "started": "2020-02-07T14:16:00Z",
536 "status": "abnormal",
537 "abnormal_mechanism": null,
538 "attrs": {
539 "release": "sentry-test@1.0.0",
540 "environment": "production"
541 }
542}"#;
543
544 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
545 assert_eq!(update.abnormal_mechanism, AbnormalMechanism::None);
546 }
547}