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)]
215 pub status: SessionStatus,
216 #[serde(default)]
218 pub errors: u64,
219 #[serde(rename = "attrs")]
221 pub attributes: SessionAttributes,
222 #[serde(
224 default,
225 deserialize_with = "null_to_default",
226 skip_serializing_if = "AbnormalMechanism::is_none"
227 )]
228 pub abnormal_mechanism: AbnormalMechanism,
229}
230
231impl SessionUpdate {
232 pub fn parse(payload: &[u8]) -> Result<Self, serde_json::Error> {
234 serde_json::from_slice(payload)
235 }
236
237 pub fn serialize(&self) -> Result<Vec<u8>, serde_json::Error> {
239 serde_json::to_vec(self)
240 }
241}
242
243impl SessionLike for SessionUpdate {
244 fn started(&self) -> DateTime<Utc> {
245 self.started
246 }
247
248 fn distinct_id(&self) -> Option<&String> {
249 self.distinct_id.as_ref()
250 }
251
252 fn total_count(&self) -> u32 {
253 u32::from(self.init)
254 }
255
256 fn abnormal_count(&self) -> u32 {
257 match self.status {
258 SessionStatus::Abnormal => 1,
259 _ => 0,
260 }
261 }
262
263 fn unhandled_count(&self) -> u32 {
264 match self.status {
265 SessionStatus::Unhandled => 1,
266 _ => 0,
267 }
268 }
269
270 fn crashed_count(&self) -> u32 {
271 match self.status {
272 SessionStatus::Crashed => 1,
273 _ => 0,
274 }
275 }
276
277 fn all_errors(&self) -> Option<SessionErrored> {
278 if self.errors > 0 || self.status.is_error() {
279 Some(SessionErrored::Individual(self.session_id))
280 } else {
281 None
282 }
283 }
284
285 fn abnormal_mechanism(&self) -> AbnormalMechanism {
286 self.abnormal_mechanism
287 }
288}
289
290impl Getter for SessionUpdate {
291 fn get_value(&self, path: &str) -> Option<Val<'_>> {
292 self.attributes.get_value(path)
293 }
294}
295
296#[allow(clippy::trivially_copy_pass_by_ref)]
297fn is_zero(val: &u32) -> bool {
298 *val == 0
299}
300
301#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
302pub struct SessionAggregateItem {
303 pub started: DateTime<Utc>,
305 #[serde(rename = "did", default, skip_serializing_if = "Option::is_none")]
307 pub distinct_id: Option<String>,
308 #[serde(default, skip_serializing_if = "is_zero")]
310 pub exited: u32,
311 #[serde(default, skip_serializing_if = "is_zero")]
313 pub errored: u32,
314 #[serde(default, skip_serializing_if = "is_zero")]
316 pub abnormal: u32,
317 #[serde(default, skip_serializing_if = "is_zero")]
319 pub unhandled: u32,
320 #[serde(default, skip_serializing_if = "is_zero")]
322 pub crashed: u32,
323 }
326
327impl SessionLike for SessionAggregateItem {
328 fn started(&self) -> DateTime<Utc> {
329 self.started
330 }
331
332 fn distinct_id(&self) -> Option<&String> {
333 self.distinct_id.as_ref()
334 }
335
336 fn total_count(&self) -> u32 {
337 self.exited + self.abnormal + self.errored + self.unhandled + self.crashed
338 }
339
340 fn abnormal_count(&self) -> u32 {
341 self.abnormal
342 }
343
344 fn unhandled_count(&self) -> u32 {
345 self.unhandled
346 }
347
348 fn crashed_count(&self) -> u32 {
349 self.crashed
350 }
351
352 fn all_errors(&self) -> Option<SessionErrored> {
353 let all_errored = self.abnormal + self.errored + self.unhandled + self.crashed;
356 if all_errored > 0 {
357 Some(SessionErrored::Aggregated(all_errored))
358 } else {
359 None
360 }
361 }
362 fn abnormal_mechanism(&self) -> AbnormalMechanism {
363 AbnormalMechanism::None
364 }
365}
366
367#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
368pub struct SessionAggregates {
369 #[serde(default)]
371 pub aggregates: Vec<SessionAggregateItem>,
372 #[serde(rename = "attrs")]
374 pub attributes: SessionAttributes,
375}
376
377impl SessionAggregates {
378 pub fn parse(payload: &[u8]) -> Result<Self, serde_json::Error> {
380 serde_json::from_slice(payload)
381 }
382
383 pub fn serialize(&self) -> Result<Vec<u8>, serde_json::Error> {
385 serde_json::to_vec(self)
386 }
387}
388
389impl Getter for SessionAggregates {
390 fn get_value(&self, path: &str) -> Option<Val<'_>> {
391 self.attributes.get_value(path)
392 }
393}
394
395#[cfg(test)]
396mod tests {
397
398 use std::str::FromStr;
399
400 use similar_asserts::assert_eq;
401
402 use super::*;
403
404 #[test]
405 fn test_did_you_bump_session_metrics_extraction_version() {
406 fn _assert_status(status: SessionStatus) {
407 match status {
408 SessionStatus::Ok => todo!(),
409 SessionStatus::Exited => todo!(),
410 SessionStatus::Crashed => todo!(),
411 SessionStatus::Abnormal => todo!(),
412 SessionStatus::Errored => todo!(),
413 SessionStatus::Unhandled => todo!(),
414 SessionStatus::Unknown(_) => todo!(),
415 }
418 }
419 fn _assert_aggregate_item(item: SessionAggregateItem) {
420 let SessionAggregateItem {
421 started: _,
422 distinct_id: _,
423 exited: _,
424 errored: _,
425 abnormal: _,
426 unhandled: _,
427 crashed: _,
428 } = item;
431 }
432 }
433
434 #[test]
435 fn test_sessionstatus_unknown() {
436 let unknown = SessionStatus::from_str("invalid status").unwrap();
437 if let SessionStatus::Unknown(inner) = unknown {
438 assert_eq!(inner, "invalid status".to_owned());
439 } else {
440 panic!();
441 }
442 }
443
444 #[test]
445 fn test_session_default_values() {
446 let json = r#"{
447 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
448 "timestamp": "2020-02-07T15:17:00Z",
449 "started": "2020-02-07T14:16:00Z",
450 "attrs": {
451 "release": "sentry-test@1.0.0"
452 }
453}"#;
454
455 let output = r#"{
456 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
457 "did": null,
458 "seq": 4711,
459 "timestamp": "2020-02-07T15:17:00Z",
460 "started": "2020-02-07T14:16:00Z",
461 "status": "ok",
462 "errors": 0,
463 "attrs": {
464 "release": "sentry-test@1.0.0"
465 }
466}"#;
467
468 let update = SessionUpdate {
469 session_id: "8333339f-5675-4f89-a9a0-1c935255ab58".parse().unwrap(),
470 distinct_id: None,
471 sequence: 4711, timestamp: "2020-02-07T15:17:00Z".parse().unwrap(),
473 started: "2020-02-07T14:16:00Z".parse().unwrap(),
474 init: false,
475 status: SessionStatus::Ok,
476 abnormal_mechanism: AbnormalMechanism::None,
477 errors: 0,
478 attributes: SessionAttributes {
479 release: "sentry-test@1.0.0".to_owned(),
480 environment: None,
481 ip_address: None,
482 user_agent: None,
483 },
484 };
485
486 let mut parsed = SessionUpdate::parse(json.as_bytes()).unwrap();
487
488 assert!((default_sequence() - parsed.sequence) <= 1);
490 parsed.sequence = 4711;
491
492 assert_eq!(update, parsed);
493 assert_eq!(output, serde_json::to_string_pretty(&update).unwrap());
494 }
495
496 #[test]
497 fn test_session_default_timestamp_and_sid() {
498 let json = r#"{
499 "started": "2020-02-07T14:16:00Z",
500 "attrs": {
501 "release": "sentry-test@1.0.0"
502 }
503}"#;
504
505 let parsed = SessionUpdate::parse(json.as_bytes()).unwrap();
506 assert!(!parsed.session_id.is_nil());
507 }
508
509 #[test]
510 fn test_session_roundtrip() {
511 let json = r#"{
512 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
513 "did": "foobarbaz",
514 "seq": 42,
515 "init": true,
516 "timestamp": "2020-02-07T15:17:00Z",
517 "started": "2020-02-07T14:16:00Z",
518 "status": "exited",
519 "errors": 0,
520 "attrs": {
521 "release": "sentry-test@1.0.0",
522 "environment": "production",
523 "ip_address": "::1",
524 "user_agent": "Firefox/72.0"
525 }
526}"#;
527
528 let update = SessionUpdate {
529 session_id: "8333339f-5675-4f89-a9a0-1c935255ab58".parse().unwrap(),
530 distinct_id: Some("foobarbaz".into()),
531 sequence: 42,
532 timestamp: "2020-02-07T15:17:00Z".parse().unwrap(),
533 started: "2020-02-07T14:16:00Z".parse().unwrap(),
534 status: SessionStatus::Exited,
535 abnormal_mechanism: AbnormalMechanism::None,
536 errors: 0,
537 init: true,
538 attributes: SessionAttributes {
539 release: "sentry-test@1.0.0".to_owned(),
540 environment: Some("production".to_owned()),
541 ip_address: Some(IpAddr::parse("::1").unwrap()),
542 user_agent: Some("Firefox/72.0".to_owned()),
543 },
544 };
545
546 assert_eq!(update, SessionUpdate::parse(json.as_bytes()).unwrap());
547 assert_eq!(json, serde_json::to_string_pretty(&update).unwrap());
548 }
549
550 #[test]
551 fn test_session_ip_addr_auto() {
552 let json = r#"{
553 "started": "2020-02-07T14:16:00Z",
554 "attrs": {
555 "release": "sentry-test@1.0.0",
556 "ip_address": "{{auto}}"
557 }
558}"#;
559
560 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
561 assert_eq!(update.attributes.ip_address, Some(IpAddr::auto()));
562 }
563 #[test]
564 fn test_session_abnormal_mechanism() {
565 let json = r#"{
566 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
567 "started": "2020-02-07T14:16:00Z",
568 "status": "abnormal",
569 "abnormal_mechanism": "anr_background",
570 "attrs": {
571 "release": "sentry-test@1.0.0",
572 "environment": "production"
573 }
574 }"#;
575
576 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
577 assert_eq!(update.abnormal_mechanism, AbnormalMechanism::AnrBackground);
578 }
579
580 #[test]
581 fn test_session_invalid_abnormal_mechanism() {
582 let json = r#"{
583 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
584 "started": "2020-02-07T14:16:00Z",
585 "status": "abnormal",
586 "abnormal_mechanism": "invalid_mechanism",
587 "attrs": {
588 "release": "sentry-test@1.0.0",
589 "environment": "production"
590 }
591}"#;
592
593 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
594 assert_eq!(update.abnormal_mechanism, AbnormalMechanism::None);
595 }
596
597 #[test]
598 fn test_session_null_abnormal_mechanism() {
599 let json = r#"{
600 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
601 "started": "2020-02-07T14:16:00Z",
602 "status": "abnormal",
603 "abnormal_mechanism": null,
604 "attrs": {
605 "release": "sentry-test@1.0.0",
606 "environment": "production"
607 }
608}"#;
609
610 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
611 assert_eq!(update.abnormal_mechanism, AbnormalMechanism::None);
612 }
613
614 #[test]
615 fn test_session_update_get_value() {
616 let json = r#"{
617 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
618 "started": "2020-02-07T14:16:00Z",
619 "attrs": {
620 "release": "sentry-test@1.0.0",
621 "environment": "production"
622 }
623}"#;
624
625 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
626 assert_eq!(
627 update.get_value("event.release"),
628 Some(Val::String("sentry-test@1.0.0"))
629 );
630 assert_eq!(
631 update.get_value("event.environment"),
632 Some(Val::String("production"))
633 );
634 assert_eq!(update.get_value("event.transaction"), None);
635 assert_eq!(
636 update.get_value("log.attributes.sentry.release.value"),
637 None
638 );
639 assert_eq!(update.get_value("release"), None);
640 }
641
642 #[test]
643 fn test_session_update_get_value_without_environment() {
644 let json = r#"{
645 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
646 "started": "2020-02-07T14:16:00Z",
647 "attrs": {
648 "release": "sentry-test@1.0.0"
649 }
650}"#;
651
652 let update = SessionUpdate::parse(json.as_bytes()).unwrap();
653 assert_eq!(
654 update.get_value("event.release"),
655 Some(Val::String("sentry-test@1.0.0"))
656 );
657 assert_eq!(update.get_value("event.environment"), None);
658 }
659}