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