1use std::fmt::{self, Display};
2use std::time::SystemTime;
3
4use chrono::{DateTime, Utc};
5use relay_protocol::{Getter, Val};
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, Default)]
14pub enum SessionStatus {
15 #[default]
19 Ok,
20 Exited,
22 Crashed,
24 Abnormal,
26 Errored,
28 Unhandled,
30 Unknown(String),
35}
36
37impl SessionStatus {
38 pub fn is_terminal(&self) -> bool {
40 !matches!(self, SessionStatus::Ok)
41 }
42
43 pub fn is_error(&self) -> bool {
45 !matches!(self, SessionStatus::Ok | SessionStatus::Exited)
46 }
47
48 pub fn is_fatal(&self) -> bool {
50 matches!(self, SessionStatus::Crashed | SessionStatus::Abnormal)
51 }
52 fn as_str(&self) -> &str {
53 match self {
54 SessionStatus::Ok => "ok",
55 SessionStatus::Crashed => "crashed",
56 SessionStatus::Abnormal => "abnormal",
57 SessionStatus::Exited => "exited",
58 SessionStatus::Errored => "errored",
59 SessionStatus::Unhandled => "unhandled",
60 SessionStatus::Unknown(s) => s.as_str(),
61 }
62 }
63}
64
65impl Display for SessionStatus {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write!(f, "{}", self.as_str())
68 }
69}
70
71relay_common::impl_str_serde!(SessionStatus, "A session status");
72
73impl std::str::FromStr for SessionStatus {
74 type Err = ParseSessionStatusError;
75
76 fn from_str(s: &str) -> Result<Self, Self::Err> {
77 Ok(match s {
78 "ok" => SessionStatus::Ok,
79 "crashed" => SessionStatus::Crashed,
80 "abnormal" => SessionStatus::Abnormal,
81 "exited" => SessionStatus::Exited,
82 "errored" => SessionStatus::Errored,
83 "unhandled" => SessionStatus::Unhandled,
84 other => SessionStatus::Unknown(other.to_owned()),
85 })
86 }
87}
88
89#[derive(Debug)]
91pub struct ParseSessionStatusError;
92
93impl fmt::Display for ParseSessionStatusError {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 write!(f, "invalid session status")
96 }
97}
98
99impl std::error::Error for ParseSessionStatusError {}
100
101#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Default)]
102#[serde(rename_all = "snake_case")]
103pub enum AbnormalMechanism {
104 AnrForeground,
105 AnrBackground,
106 #[serde(other)]
107 #[default]
108 None,
109}
110
111#[derive(Debug)]
112pub struct ParseAbnormalMechanismError;
113
114impl fmt::Display for ParseAbnormalMechanismError {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 write!(f, "invalid abnormal mechanism")
117 }
118}
119
120relay_common::derive_fromstr_and_display!(AbnormalMechanism, ParseAbnormalMechanismError, {
121 AbnormalMechanism::AnrForeground => "anr_foreground",
122 AbnormalMechanism::AnrBackground => "anr_background",
123 AbnormalMechanism::None => "none",
124});
125
126impl AbnormalMechanism {
127 fn is_none(&self) -> bool {
128 *self == Self::None
129 }
130}
131
132#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
134pub struct SessionAttributes {
135 pub release: String,
137
138 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub environment: Option<String>,
141
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub ip_address: Option<IpAddr>,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub user_agent: Option<String>,
149}
150
151impl Getter for SessionAttributes {
152 fn get_value(&self, path: &str) -> Option<Val<'_>> {
153 Some(match path.strip_prefix("event.")? {
154 "release" => self.release.as_str().into(),
155 "environment" => self.environment.as_deref()?.into(),
156 _ => return None,
157 })
158 }
159}
160
161fn default_sequence() -> u64 {
162 SystemTime::now()
163 .duration_since(SystemTime::UNIX_EPOCH)
164 .unwrap_or_default()
165 .as_millis() as u64
166}
167
168#[allow(clippy::trivially_copy_pass_by_ref)]
169fn is_false(val: &bool) -> bool {
170 !val
171}
172
173pub enum SessionErrored {
175 Individual(Uuid),
177 Aggregated(u32),
180}
181
182pub trait SessionLike {
184 fn started(&self) -> DateTime<Utc>;
185 fn distinct_id(&self) -> Option<&String>;
186 fn total_count(&self) -> u32;
187 fn abnormal_count(&self) -> u32;
188 fn unhandled_count(&self) -> u32;
189 fn crashed_count(&self) -> u32;
190 fn all_errors(&self) -> Option<SessionErrored>;
191 fn abnormal_mechanism(&self) -> AbnormalMechanism;
192}
193
194#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
195pub struct SessionUpdate {
196 #[serde(rename = "sid", default = "Uuid::new_v4")]
198 pub session_id: Uuid,
199 #[serde(rename = "did", default)]
201 pub distinct_id: Option<String>,
202 #[serde(rename = "seq", default = "default_sequence")]
204 pub sequence: u64,
205 #[serde(default, skip_serializing_if = "is_false")]
207 pub init: bool,
208 #[serde(default = "Utc::now")]
210 pub timestamp: DateTime<Utc>,
211 pub started: DateTime<Utc>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub duration: Option<f64>,
216 #[serde(default)]
218 pub status: SessionStatus,
219 #[serde(default)]
221 pub errors: u64,
222 #[serde(rename = "attrs")]
224 pub attributes: SessionAttributes,
225 #[serde(
227 default,
228 deserialize_with = "null_to_default",
229 skip_serializing_if = "AbnormalMechanism::is_none"
230 )]
231 pub abnormal_mechanism: AbnormalMechanism,
232}
233
234impl SessionUpdate {
235 pub fn parse(payload: &[u8]) -> Result<Self, serde_json::Error> {
237 serde_json::from_slice(payload)
238 }
239
240 pub fn serialize(&self) -> Result<Vec<u8>, serde_json::Error> {
242 serde_json::to_vec(self)
243 }
244}
245
246impl SessionLike for SessionUpdate {
247 fn started(&self) -> DateTime<Utc> {
248 self.started
249 }
250
251 fn distinct_id(&self) -> Option<&String> {
252 self.distinct_id.as_ref()
253 }
254
255 fn total_count(&self) -> u32 {
256 u32::from(self.init)
257 }
258
259 fn abnormal_count(&self) -> u32 {
260 match self.status {
261 SessionStatus::Abnormal => 1,
262 _ => 0,
263 }
264 }
265
266 fn unhandled_count(&self) -> u32 {
267 match self.status {
268 SessionStatus::Unhandled => 1,
269 _ => 0,
270 }
271 }
272
273 fn crashed_count(&self) -> u32 {
274 match self.status {
275 SessionStatus::Crashed => 1,
276 _ => 0,
277 }
278 }
279
280 fn all_errors(&self) -> Option<SessionErrored> {
281 if self.errors > 0 || self.status.is_error() {
282 Some(SessionErrored::Individual(self.session_id))
283 } else {
284 None
285 }
286 }
287
288 fn abnormal_mechanism(&self) -> AbnormalMechanism {
289 self.abnormal_mechanism
290 }
291}
292
293impl Getter for SessionUpdate {
294 fn get_value(&self, path: &str) -> Option<Val<'_>> {
295 self.attributes.get_value(path)
296 }
297}
298
299#[allow(clippy::trivially_copy_pass_by_ref)]
300fn is_zero(val: &u32) -> bool {
301 *val == 0
302}
303
304#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
305pub struct SessionAggregateItem {
306 pub started: DateTime<Utc>,
308 #[serde(rename = "did", default, skip_serializing_if = "Option::is_none")]
310 pub distinct_id: Option<String>,
311 #[serde(default, skip_serializing_if = "is_zero")]
313 pub exited: u32,
314 #[serde(default, skip_serializing_if = "is_zero")]
316 pub errored: u32,
317 #[serde(default, skip_serializing_if = "is_zero")]
319 pub abnormal: u32,
320 #[serde(default, skip_serializing_if = "is_zero")]
322 pub unhandled: u32,
323 #[serde(default, skip_serializing_if = "is_zero")]
325 pub crashed: u32,
326 }
329
330impl SessionLike for SessionAggregateItem {
331 fn started(&self) -> DateTime<Utc> {
332 self.started
333 }
334
335 fn distinct_id(&self) -> Option<&String> {
336 self.distinct_id.as_ref()
337 }
338
339 fn total_count(&self) -> u32 {
340 self.exited + self.abnormal + self.errored + self.unhandled + self.crashed
341 }
342
343 fn abnormal_count(&self) -> u32 {
344 self.abnormal
345 }
346
347 fn unhandled_count(&self) -> u32 {
348 self.unhandled
349 }
350
351 fn crashed_count(&self) -> u32 {
352 self.crashed
353 }
354
355 fn all_errors(&self) -> Option<SessionErrored> {
356 let all_errored = self.abnormal + self.errored + self.unhandled + self.crashed;
359 if all_errored > 0 {
360 Some(SessionErrored::Aggregated(all_errored))
361 } else {
362 None
363 }
364 }
365 fn abnormal_mechanism(&self) -> AbnormalMechanism {
366 AbnormalMechanism::None
367 }
368}
369
370#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
371pub struct SessionAggregates {
372 #[serde(default)]
374 pub aggregates: Vec<SessionAggregateItem>,
375 #[serde(rename = "attrs")]
377 pub attributes: SessionAttributes,
378}
379
380impl SessionAggregates {
381 pub fn parse(payload: &[u8]) -> Result<Self, serde_json::Error> {
383 serde_json::from_slice(payload)
384 }
385
386 pub fn serialize(&self) -> Result<Vec<u8>, serde_json::Error> {
388 serde_json::to_vec(self)
389 }
390}
391
392impl Getter for SessionAggregates {
393 fn get_value(&self, path: &str) -> Option<Val<'_>> {
394 self.attributes.get_value(path)
395 }
396}
397
398#[cfg(test)]
399mod tests {
400
401 use std::str::FromStr;
402
403 use similar_asserts::assert_eq;
404
405 use super::*;
406
407 #[test]
408 fn test_did_you_bump_session_metrics_extraction_version() {
409 fn _assert_status(status: SessionStatus) {
410 match status {
411 SessionStatus::Ok => todo!(),
412 SessionStatus::Exited => todo!(),
413 SessionStatus::Crashed => todo!(),
414 SessionStatus::Abnormal => todo!(),
415 SessionStatus::Errored => todo!(),
416 SessionStatus::Unhandled => todo!(),
417 SessionStatus::Unknown(_) => todo!(),
418 }
421 }
422 fn _assert_aggregate_item(item: SessionAggregateItem) {
423 let SessionAggregateItem {
424 started: _,
425 distinct_id: _,
426 exited: _,
427 errored: _,
428 abnormal: _,
429 unhandled: _,
430 crashed: _,
431 } = item;
434 }
435 }
436
437 #[test]
438 fn test_sessionstatus_unknown() {
439 let unknown = SessionStatus::from_str("invalid status").unwrap();
440 if let SessionStatus::Unknown(inner) = unknown {
441 assert_eq!(inner, "invalid status".to_owned());
442 } else {
443 panic!();
444 }
445 }
446
447 #[test]
448 fn test_session_default_values() {
449 let json = r#"{
450 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
451 "timestamp": "2020-02-07T15:17:00Z",
452 "started": "2020-02-07T14:16:00Z",
453 "attrs": {
454 "release": "sentry-test@1.0.0"
455 }
456}"#;
457
458 let output = r#"{
459 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
460 "did": null,
461 "seq": 4711,
462 "timestamp": "2020-02-07T15:17:00Z",
463 "started": "2020-02-07T14:16:00Z",
464 "status": "ok",
465 "errors": 0,
466 "attrs": {
467 "release": "sentry-test@1.0.0"
468 }
469}"#;
470
471 let update = SessionUpdate {
472 session_id: "8333339f-5675-4f89-a9a0-1c935255ab58".parse().unwrap(),
473 distinct_id: None,
474 sequence: 4711, timestamp: "2020-02-07T15:17:00Z".parse().unwrap(),
476 started: "2020-02-07T14:16:00Z".parse().unwrap(),
477 duration: None,
478 init: false,
479 status: SessionStatus::Ok,
480 abnormal_mechanism: AbnormalMechanism::None,
481 errors: 0,
482 attributes: SessionAttributes {
483 release: "sentry-test@1.0.0".to_owned(),
484 environment: None,
485 ip_address: None,
486 user_agent: None,
487 },
488 };
489
490 let mut parsed = SessionUpdate::parse(json.as_bytes()).unwrap();
491
492 assert!((default_sequence() - parsed.sequence) <= 1);
494 parsed.sequence = 4711;
495
496 assert_eq!(update, parsed);
497 assert_eq!(output, serde_json::to_string_pretty(&update).unwrap());
498 }
499
500 #[test]
501 fn test_session_default_timestamp_and_sid() {
502 let json = r#"{
503 "started": "2020-02-07T14:16:00Z",
504 "attrs": {
505 "release": "sentry-test@1.0.0"
506 }
507}"#;
508
509 let parsed = SessionUpdate::parse(json.as_bytes()).unwrap();
510 assert!(!parsed.session_id.is_nil());
511 }
512
513 #[test]
514 fn test_session_roundtrip() {
515 let json = r#"{
516 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
517 "did": "foobarbaz",
518 "seq": 42,
519 "init": true,
520 "timestamp": "2020-02-07T15:17:00Z",
521 "started": "2020-02-07T14:16:00Z",
522 "duration": 1947.49,
523 "status": "exited",
524 "errors": 0,
525 "attrs": {
526 "release": "sentry-test@1.0.0",
527 "environment": "production",
528 "ip_address": "::1",
529 "user_agent": "Firefox/72.0"
530 }
531}"#;
532
533 let update = SessionUpdate {
534 session_id: "8333339f-5675-4f89-a9a0-1c935255ab58".parse().unwrap(),
535 distinct_id: Some("foobarbaz".into()),
536 sequence: 42,
537 timestamp: "2020-02-07T15:17:00Z".parse().unwrap(),
538 started: "2020-02-07T14:16:00Z".parse().unwrap(),
539 duration: Some(1947.49),
540 status: SessionStatus::Exited,
541 abnormal_mechanism: AbnormalMechanism::None,
542 errors: 0,
543 init: true,
544 attributes: SessionAttributes {
545 release: "sentry-test@1.0.0".to_owned(),
546 environment: Some("production".to_owned()),
547 ip_address: Some(IpAddr::parse("::1").unwrap()),
548 user_agent: Some("Firefox/72.0".to_owned()),
549 },
550 };
551
552 assert_eq!(update, SessionUpdate::parse(json.as_bytes()).unwrap());
553 assert_eq!(json, serde_json::to_string_pretty(&update).unwrap());
554 }
555
556 #[test]
557 fn test_session_ip_addr_auto() {
558 let json = r#"{
559 "started": "2020-02-07T14:16:00Z",
560 "attrs": {
561 "release": "sentry-test@1.0.0",
562 "ip_address": "{{auto}}"
563 }
564}"#;
565
566 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
567 assert_eq!(update.attributes.ip_address, Some(IpAddr::auto()));
568 }
569 #[test]
570 fn test_session_abnormal_mechanism() {
571 let json = r#"{
572 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
573 "started": "2020-02-07T14:16:00Z",
574 "status": "abnormal",
575 "abnormal_mechanism": "anr_background",
576 "attrs": {
577 "release": "sentry-test@1.0.0",
578 "environment": "production"
579 }
580 }"#;
581
582 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
583 assert_eq!(update.abnormal_mechanism, AbnormalMechanism::AnrBackground);
584 }
585
586 #[test]
587 fn test_session_invalid_abnormal_mechanism() {
588 let json = r#"{
589 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
590 "started": "2020-02-07T14:16:00Z",
591 "status": "abnormal",
592 "abnormal_mechanism": "invalid_mechanism",
593 "attrs": {
594 "release": "sentry-test@1.0.0",
595 "environment": "production"
596 }
597}"#;
598
599 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
600 assert_eq!(update.abnormal_mechanism, AbnormalMechanism::None);
601 }
602
603 #[test]
604 fn test_session_null_abnormal_mechanism() {
605 let json = r#"{
606 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
607 "started": "2020-02-07T14:16:00Z",
608 "status": "abnormal",
609 "abnormal_mechanism": null,
610 "attrs": {
611 "release": "sentry-test@1.0.0",
612 "environment": "production"
613 }
614}"#;
615
616 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
617 assert_eq!(update.abnormal_mechanism, AbnormalMechanism::None);
618 }
619
620 #[test]
621 fn test_session_update_get_value() {
622 let json = r#"{
623 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
624 "started": "2020-02-07T14:16:00Z",
625 "attrs": {
626 "release": "sentry-test@1.0.0",
627 "environment": "production"
628 }
629}"#;
630
631 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
632 assert_eq!(
633 update.get_value("event.release"),
634 Some(Val::String("sentry-test@1.0.0"))
635 );
636 assert_eq!(
637 update.get_value("event.environment"),
638 Some(Val::String("production"))
639 );
640 assert_eq!(update.get_value("event.transaction"), None);
641 assert_eq!(
642 update.get_value("log.attributes.sentry.release.value"),
643 None
644 );
645 assert_eq!(update.get_value("release"), None);
646 }
647
648 #[test]
649 fn test_session_update_get_value_without_environment() {
650 let json = r#"{
651 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
652 "started": "2020-02-07T14:16:00Z",
653 "attrs": {
654 "release": "sentry-test@1.0.0"
655 }
656}"#;
657
658 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
659 assert_eq!(
660 update.get_value("event.release"),
661 Some(Val::String("sentry-test@1.0.0"))
662 );
663 assert_eq!(update.get_value("event.environment"), None);
664 }
665}