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, 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
151fn default_sequence() -> u64 {
152 SystemTime::now()
153 .duration_since(SystemTime::UNIX_EPOCH)
154 .unwrap_or_default()
155 .as_millis() as u64
156}
157
158#[allow(clippy::trivially_copy_pass_by_ref)]
159fn is_false(val: &bool) -> bool {
160 !val
161}
162
163pub enum SessionErrored {
165 Individual(Uuid),
167 Aggregated(u32),
170}
171
172pub trait SessionLike {
174 fn started(&self) -> DateTime<Utc>;
175 fn distinct_id(&self) -> Option<&String>;
176 fn total_count(&self) -> u32;
177 fn abnormal_count(&self) -> u32;
178 fn unhandled_count(&self) -> u32;
179 fn crashed_count(&self) -> u32;
180 fn all_errors(&self) -> Option<SessionErrored>;
181 fn abnormal_mechanism(&self) -> AbnormalMechanism;
182}
183
184#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
185pub struct SessionUpdate {
186 #[serde(rename = "sid", default = "Uuid::new_v4")]
188 pub session_id: Uuid,
189 #[serde(rename = "did", default)]
191 pub distinct_id: Option<String>,
192 #[serde(rename = "seq", default = "default_sequence")]
194 pub sequence: u64,
195 #[serde(default, skip_serializing_if = "is_false")]
197 pub init: bool,
198 #[serde(default = "Utc::now")]
200 pub timestamp: DateTime<Utc>,
201 pub started: DateTime<Utc>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub duration: Option<f64>,
206 #[serde(default)]
208 pub status: SessionStatus,
209 #[serde(default)]
211 pub errors: u64,
212 #[serde(rename = "attrs")]
214 pub attributes: SessionAttributes,
215 #[serde(
217 default,
218 deserialize_with = "null_to_default",
219 skip_serializing_if = "AbnormalMechanism::is_none"
220 )]
221 pub abnormal_mechanism: AbnormalMechanism,
222}
223
224impl SessionUpdate {
225 pub fn parse(payload: &[u8]) -> Result<Self, serde_json::Error> {
227 serde_json::from_slice(payload)
228 }
229
230 pub fn serialize(&self) -> Result<Vec<u8>, serde_json::Error> {
232 serde_json::to_vec(self)
233 }
234}
235
236impl SessionLike for SessionUpdate {
237 fn started(&self) -> DateTime<Utc> {
238 self.started
239 }
240
241 fn distinct_id(&self) -> Option<&String> {
242 self.distinct_id.as_ref()
243 }
244
245 fn total_count(&self) -> u32 {
246 u32::from(self.init)
247 }
248
249 fn abnormal_count(&self) -> u32 {
250 match self.status {
251 SessionStatus::Abnormal => 1,
252 _ => 0,
253 }
254 }
255
256 fn unhandled_count(&self) -> u32 {
257 match self.status {
258 SessionStatus::Unhandled => 1,
259 _ => 0,
260 }
261 }
262
263 fn crashed_count(&self) -> u32 {
264 match self.status {
265 SessionStatus::Crashed => 1,
266 _ => 0,
267 }
268 }
269
270 fn all_errors(&self) -> Option<SessionErrored> {
271 if self.errors > 0 || self.status.is_error() {
272 Some(SessionErrored::Individual(self.session_id))
273 } else {
274 None
275 }
276 }
277
278 fn abnormal_mechanism(&self) -> AbnormalMechanism {
279 self.abnormal_mechanism
280 }
281}
282
283impl Getter for SessionUpdate {
286 fn get_value(&self, _path: &str) -> Option<relay_protocol::Val<'_>> {
287 None
288 }
289}
290
291#[allow(clippy::trivially_copy_pass_by_ref)]
292fn is_zero(val: &u32) -> bool {
293 *val == 0
294}
295
296#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
297pub struct SessionAggregateItem {
298 pub started: DateTime<Utc>,
300 #[serde(rename = "did", default, skip_serializing_if = "Option::is_none")]
302 pub distinct_id: Option<String>,
303 #[serde(default, skip_serializing_if = "is_zero")]
305 pub exited: u32,
306 #[serde(default, skip_serializing_if = "is_zero")]
308 pub errored: u32,
309 #[serde(default, skip_serializing_if = "is_zero")]
311 pub abnormal: u32,
312 #[serde(default, skip_serializing_if = "is_zero")]
314 pub unhandled: u32,
315 #[serde(default, skip_serializing_if = "is_zero")]
317 pub crashed: u32,
318 }
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_did_you_bump_session_metrics_extraction_version() {
403 fn _assert_status(status: SessionStatus) {
404 match status {
405 SessionStatus::Ok => todo!(),
406 SessionStatus::Exited => todo!(),
407 SessionStatus::Crashed => todo!(),
408 SessionStatus::Abnormal => todo!(),
409 SessionStatus::Errored => todo!(),
410 SessionStatus::Unhandled => todo!(),
411 SessionStatus::Unknown(_) => todo!(),
412 }
415 }
416 fn _assert_aggregate_item(item: SessionAggregateItem) {
417 let SessionAggregateItem {
418 started: _,
419 distinct_id: _,
420 exited: _,
421 errored: _,
422 abnormal: _,
423 unhandled: _,
424 crashed: _,
425 } = item;
428 }
429 }
430
431 #[test]
432 fn test_sessionstatus_unknown() {
433 let unknown = SessionStatus::from_str("invalid status").unwrap();
434 if let SessionStatus::Unknown(inner) = unknown {
435 assert_eq!(inner, "invalid status".to_owned());
436 } else {
437 panic!();
438 }
439 }
440
441 #[test]
442 fn test_session_default_values() {
443 let json = r#"{
444 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
445 "timestamp": "2020-02-07T15:17:00Z",
446 "started": "2020-02-07T14:16:00Z",
447 "attrs": {
448 "release": "sentry-test@1.0.0"
449 }
450}"#;
451
452 let output = r#"{
453 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
454 "did": null,
455 "seq": 4711,
456 "timestamp": "2020-02-07T15:17:00Z",
457 "started": "2020-02-07T14:16:00Z",
458 "status": "ok",
459 "errors": 0,
460 "attrs": {
461 "release": "sentry-test@1.0.0"
462 }
463}"#;
464
465 let update = SessionUpdate {
466 session_id: "8333339f-5675-4f89-a9a0-1c935255ab58".parse().unwrap(),
467 distinct_id: None,
468 sequence: 4711, timestamp: "2020-02-07T15:17:00Z".parse().unwrap(),
470 started: "2020-02-07T14:16:00Z".parse().unwrap(),
471 duration: None,
472 init: false,
473 status: SessionStatus::Ok,
474 abnormal_mechanism: AbnormalMechanism::None,
475 errors: 0,
476 attributes: SessionAttributes {
477 release: "sentry-test@1.0.0".to_owned(),
478 environment: None,
479 ip_address: None,
480 user_agent: None,
481 },
482 };
483
484 let mut parsed = SessionUpdate::parse(json.as_bytes()).unwrap();
485
486 assert!((default_sequence() - parsed.sequence) <= 1);
488 parsed.sequence = 4711;
489
490 assert_eq!(update, parsed);
491 assert_eq!(output, serde_json::to_string_pretty(&update).unwrap());
492 }
493
494 #[test]
495 fn test_session_default_timestamp_and_sid() {
496 let json = r#"{
497 "started": "2020-02-07T14:16:00Z",
498 "attrs": {
499 "release": "sentry-test@1.0.0"
500 }
501}"#;
502
503 let parsed = SessionUpdate::parse(json.as_bytes()).unwrap();
504 assert!(!parsed.session_id.is_nil());
505 }
506
507 #[test]
508 fn test_session_roundtrip() {
509 let json = r#"{
510 "sid": "8333339f-5675-4f89-a9a0-1c935255ab58",
511 "did": "foobarbaz",
512 "seq": 42,
513 "init": true,
514 "timestamp": "2020-02-07T15:17:00Z",
515 "started": "2020-02-07T14:16:00Z",
516 "duration": 1947.49,
517 "status": "exited",
518 "errors": 0,
519 "attrs": {
520 "release": "sentry-test@1.0.0",
521 "environment": "production",
522 "ip_address": "::1",
523 "user_agent": "Firefox/72.0"
524 }
525}"#;
526
527 let update = SessionUpdate {
528 session_id: "8333339f-5675-4f89-a9a0-1c935255ab58".parse().unwrap(),
529 distinct_id: Some("foobarbaz".into()),
530 sequence: 42,
531 timestamp: "2020-02-07T15:17:00Z".parse().unwrap(),
532 started: "2020-02-07T14:16:00Z".parse().unwrap(),
533 duration: Some(1947.49),
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}