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