1use std::any::{type_name, type_name_of_val};
2use std::error::Error as StdError;
3
4use derive_builder::Builder;
5use serde::Serialize;
6use serde_json::Value;
7
8use crate::{Error, Event};
9
10const MAX_FRAMES: usize = 64;
13const MAX_ERROR_SOURCES: usize = 50;
16
17#[derive(Builder, Clone, Debug)]
35#[builder(default)]
36pub struct ErrorTrackingOptions {
37 capture_stacktrace: bool,
47 in_app_include_paths: Vec<String>,
54 in_app_exclude_paths: Vec<String>,
58}
59
60impl Default for ErrorTrackingOptions {
61 fn default() -> Self {
62 Self {
63 capture_stacktrace: true,
64 in_app_include_paths: Vec::new(),
65 in_app_exclude_paths: Vec::new(),
66 }
67 }
68}
69
70impl ErrorTrackingOptions {
71 fn capture_stacktrace(&self) -> bool {
72 self.capture_stacktrace
73 }
74
75 fn is_in_app_path(&self, filename: &str) -> bool {
76 if self
77 .in_app_exclude_paths
78 .iter()
79 .any(|path| filename.contains(path))
80 {
81 return false;
82 }
83
84 if !self.in_app_include_paths.is_empty() {
85 return self
86 .in_app_include_paths
87 .iter()
88 .any(|path| filename.contains(path));
89 }
90
91 default_in_app_path(filename)
92 }
93
94 fn is_in_app_frame(&self, filename: Option<&str>, function: Option<&str>) -> bool {
95 if self.in_app_exclude_paths.iter().any(|path| {
96 filename.is_some_and(|filename| filename.contains(path))
97 || function.is_some_and(|function| function.contains(path))
98 }) {
99 return false;
100 }
101
102 if !self.in_app_include_paths.is_empty() {
103 return self.in_app_include_paths.iter().any(|path| {
104 filename.is_some_and(|filename| filename.contains(path))
105 || function.is_some_and(|function| function.contains(path))
106 });
107 }
108
109 if filename.is_some_and(|filename| !self.is_in_app_path(filename)) {
110 return false;
111 }
112
113 if let Some(function) = function {
114 return default_in_app_function(function);
115 }
116
117 filename.is_some()
118 }
119}
120
121#[derive(Clone, Debug, Default)]
141pub struct CaptureExceptionOptions {
142 distinct_id: Option<String>,
143 properties: Vec<(String, Value)>,
144 groups: Vec<(String, String)>,
145 fingerprint: Option<String>,
146 level: Option<String>,
147}
148
149impl CaptureExceptionOptions {
150 pub fn new() -> Self {
152 Self::default()
153 }
154
155 pub fn distinct_id<S: Into<String>>(mut self, distinct_id: S) -> Self {
157 self.distinct_id = Some(distinct_id.into());
158 self
159 }
160
161 pub fn property<K: Into<String>, V: Serialize>(
163 mut self,
164 key: K,
165 value: V,
166 ) -> Result<Self, Error> {
167 let value = serde_json::to_value(value).map_err(|e| Error::Serialization(e.to_string()))?;
168 self.properties.push((key.into(), value));
169 Ok(self)
170 }
171
172 pub fn group<N: Into<String>, I: Into<String>>(mut self, group_name: N, group_id: I) -> Self {
174 self.groups.push((group_name.into(), group_id.into()));
175 self
176 }
177
178 pub fn fingerprint<S: Into<String>>(mut self, fingerprint: S) -> Self {
180 self.fingerprint = Some(fingerprint.into());
181 self
182 }
183
184 pub fn level<S: Into<String>>(mut self, level: S) -> Self {
186 self.level = Some(level.into());
187 self
188 }
189}
190
191pub(crate) fn build_exception_event<E>(
199 error: &E,
200 options: CaptureExceptionOptions,
201 et_options: &ErrorTrackingOptions,
202) -> Result<Event, Error>
203where
204 E: StdError + ?Sized,
205{
206 let CaptureExceptionOptions {
207 distinct_id,
208 properties,
209 groups,
210 fingerprint,
211 level,
212 } = options;
213
214 let mut exception = Exception::from_error(error, et_options.capture_stacktrace());
215 if let Some(fingerprint) = fingerprint {
216 exception.set_fingerprint(fingerprint);
217 }
218 if let Some(level) = level {
219 exception.set_level(level);
220 }
221
222 let mut event = match distinct_id {
223 Some(distinct_id) => Event::new("$exception".to_string(), distinct_id),
224 None => Event::new_anon("$exception"),
225 };
226 for (key, value) in properties {
227 event.insert_prop(key, value)?;
228 }
229 for (group_name, group_id) in groups {
230 event.add_group(&group_name, &group_id);
231 }
232
233 exception.write_into(&mut event, et_options)?;
236 Ok(event)
237}
238
239#[derive(Clone, Debug, PartialEq, Eq)]
248pub(crate) struct Exception {
249 items: Vec<ExceptionItem>,
250 captured_frames: Option<Vec<StackFrame>>,
254 fingerprint: Option<String>,
255 level: String,
256}
257
258impl Exception {
259 pub(crate) fn from_error<E>(error: &E, capture_stacktrace: bool) -> Self
262 where
263 E: StdError + ?Sized,
264 {
265 let mut items = vec![ExceptionItem {
266 exception_type: simple_type_name(type_name::<E>()),
267 value: error_value(error),
268 mechanism: ExceptionMechanism::default(),
269 stacktrace: None,
270 }];
271
272 let mut source = error.source();
273 while let Some(err) = source {
274 if items.len() >= MAX_ERROR_SOURCES {
275 break;
276 }
277 items.push(ExceptionItem {
278 exception_type: source_type_name(err),
279 value: error_value(err),
280 mechanism: ExceptionMechanism::default(),
281 stacktrace: None,
282 });
283 source = err.source();
284 }
285
286 link_exception_chain(&mut items);
287
288 Self {
289 items,
290 captured_frames: if capture_stacktrace {
291 Some(capture_raw_application_frames())
292 } else {
293 None
294 },
295 fingerprint: None,
296 level: "error".to_string(),
297 }
298 }
299
300 #[allow(dead_code)]
304 pub(crate) fn from_message<T: Into<String>, V: Into<String>>(
305 exception_type: T,
306 value: V,
307 capture_stacktrace: bool,
308 ) -> Self {
309 Self {
310 items: vec![ExceptionItem {
311 exception_type: exception_type.into(),
312 value: value.into(),
313 mechanism: ExceptionMechanism::default(),
314 stacktrace: None,
315 }],
316 captured_frames: if capture_stacktrace {
317 Some(capture_raw_application_frames())
318 } else {
319 None
320 },
321 fingerprint: None,
322 level: "error".to_string(),
323 }
324 }
325
326 #[allow(dead_code)]
332 pub(crate) fn from_exception_list(items: Vec<ExceptionItem>) -> Self {
333 Self {
334 items,
335 captured_frames: None,
336 fingerprint: None,
337 level: "error".to_string(),
338 }
339 }
340
341 pub(crate) fn set_fingerprint<S: Into<String>>(&mut self, fingerprint: S) {
343 self.fingerprint = Some(fingerprint.into());
344 }
345
346 pub(crate) fn set_level<S: Into<String>>(&mut self, level: S) {
348 self.level = level.into();
349 }
350
351 fn write_into(self, event: &mut Event, options: &ErrorTrackingOptions) -> Result<(), Error> {
355 let Exception {
356 mut items,
357 captured_frames,
358 fingerprint,
359 level,
360 } = self;
361 if items.is_empty() {
362 return Ok(());
363 }
364
365 if let Some(mut frames) = captured_frames {
366 for frame in frames.iter_mut() {
367 let function = (!frame.function.is_empty()).then_some(frame.function.as_str());
368 frame.in_app = options.is_in_app_frame(frame.filename.as_deref(), function);
369 }
370 trim_to_max_frames(&mut frames, MAX_FRAMES);
371 items[0].stacktrace = Some(ExceptionStacktrace::raw(frames));
372 }
373
374 event.insert_prop("$exception_level", level)?;
375 if let Some(fingerprint) = fingerprint {
376 event.insert_prop("$exception_fingerprint", fingerprint)?;
377 }
378 event.insert_prop("$exception_list", items)?;
379 Ok(())
380 }
381}
382
383#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
385pub(crate) struct ExceptionItem {
386 #[serde(rename = "type")]
387 pub exception_type: String,
388 pub value: String,
389 pub mechanism: ExceptionMechanism,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub stacktrace: Option<ExceptionStacktrace>,
392}
393
394#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
396pub(crate) struct ExceptionMechanism {
397 #[serde(rename = "type")]
398 pub mechanism_type: String,
399 pub handled: bool,
400 pub synthetic: bool,
401 #[serde(skip_serializing_if = "Option::is_none")]
404 pub exception_id: Option<usize>,
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub parent_id: Option<usize>,
408}
409
410impl Default for ExceptionMechanism {
411 fn default() -> Self {
412 Self {
413 mechanism_type: "generic".to_string(),
414 handled: true,
415 synthetic: false,
416 exception_id: None,
417 parent_id: None,
418 }
419 }
420}
421
422#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
424pub(crate) struct ExceptionStacktrace {
425 #[serde(rename = "type")]
426 pub stacktrace_type: String,
427 pub frames: Vec<StackFrame>,
428}
429
430impl ExceptionStacktrace {
431 fn raw(frames: Vec<StackFrame>) -> Self {
432 Self {
433 stacktrace_type: "raw".to_string(),
434 frames,
435 }
436 }
437}
438
439#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
441pub(crate) struct StackFrame {
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub filename: Option<String>,
444 #[serde(rename = "lineno")]
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub line_no: Option<u32>,
447 pub function: String,
448 pub lang: String,
449 pub in_app: bool,
450 pub synthetic: bool,
451 pub resolved: bool,
452 pub platform: String,
453}
454
455fn capture_frames_current_first(skip: usize) -> Vec<StackFrame> {
459 let mut frames = Vec::new();
460 let mut skipped = 0usize;
461
462 backtrace::trace(|frame| {
463 if skipped < skip {
464 skipped += 1;
465 return true;
466 }
467
468 backtrace::resolve_frame(frame, |symbol| {
473 let filename = symbol.filename().map(path_to_string);
474 let function = symbol
475 .name()
476 .map(|name| normalize_function_name(&name.to_string()));
477
478 if filename.is_none() && function.is_none() {
479 return;
480 }
481
482 frames.push(StackFrame {
483 filename,
484 line_no: symbol.lineno(),
485 function: function.unwrap_or_default(),
486 lang: "rust".to_string(),
487 in_app: false,
488 synthetic: false,
489 resolved: true,
490 platform: "rust".to_string(),
491 });
492 });
493
494 true
495 });
496
497 frames
498}
499
500fn trim_to_max_frames(frames: &mut Vec<StackFrame>, max_frames: usize) {
501 if frames.len() > max_frames {
502 frames.truncate(max_frames);
503 }
504}
505
506fn capture_raw_application_frames() -> Vec<StackFrame> {
509 let mut frames = capture_frames_current_first(0);
510 while frames
511 .first()
512 .map(|frame| is_internal_capture_frame(&frame.function))
513 .unwrap_or(false)
514 {
515 frames.remove(0);
516 }
517
518 frames
519}
520
521fn is_internal_capture_frame(function: &str) -> bool {
522 function.starts_with("backtrace::")
523 || function.contains("capture_frames_current_first")
524 || function.contains("capture_raw_application_frames")
525 || function.contains("Exception::from_error")
526 || function.contains("Exception::from_message")
527 || function.contains("build_exception_event")
528 || function.contains("Client::capture_exception")
529 || function.contains("global::capture_exception")
530}
531
532fn path_to_string(path: &std::path::Path) -> String {
533 path.to_string_lossy().into_owned()
534}
535
536fn simple_type_name(type_name: &str) -> String {
543 let trimmed = type_name.trim().trim_start_matches('&').trim();
544 let trimmed = trimmed.strip_prefix("dyn ").unwrap_or(trimmed).trim();
545 let trimmed = trimmed
546 .split_once('<')
547 .map_or(trimmed, |(outer_type, _)| outer_type)
548 .trim_end();
549 if trimmed.is_empty() || trimmed == "core::error::Error" || trimmed == "std::error::Error" {
552 return "Error".to_string();
553 }
554 trimmed.to_string()
555}
556
557fn source_type_name(error: &(dyn StdError + 'static)) -> String {
564 simple_type_name(type_name_of_val(error))
565}
566
567fn link_exception_chain(exception_list: &mut [ExceptionItem]) {
571 if exception_list.len() < 2 {
572 return;
573 }
574 for (index, item) in exception_list.iter_mut().enumerate() {
575 item.mechanism.exception_id = Some(index);
576 if index > 0 {
577 item.mechanism.parent_id = Some(index - 1);
578 item.mechanism.mechanism_type = "chained".to_string();
579 }
580 }
581}
582
583fn error_value<E>(error: &E) -> String
584where
585 E: StdError + ?Sized,
586{
587 let value = error.to_string();
588 if value.is_empty() {
589 "Error".to_string()
590 } else {
591 value
592 }
593}
594
595fn normalize_function_name(function: &str) -> String {
596 match function.rsplit_once("::") {
597 Some((prefix, suffix)) if is_rust_symbol_hash(suffix) => prefix.to_string(),
598 _ => function.to_string(),
599 }
600}
601
602fn is_rust_symbol_hash(segment: &str) -> bool {
603 segment.len() >= 9
604 && segment.starts_with('h')
605 && segment[1..].chars().all(|ch| ch.is_ascii_hexdigit())
606}
607
608fn default_in_app_path(filename: &str) -> bool {
609 let normalized = filename.replace('\\', "/");
610 if normalized.contains("/.cargo/registry/")
611 || normalized.contains("/.cargo/git/")
612 || normalized.contains("/rustc/")
613 || normalized.contains("/rustc-")
614 || normalized.contains("/library/alloc/src/")
615 || normalized.contains("/library/core/src/")
616 || normalized.contains("/library/proc_macro/src/")
617 || normalized.contains("/library/std/src/")
618 || normalized.contains("/library/test/src/")
619 || normalized.contains("/toolchains/")
620 || normalized.contains("/target/")
621 || normalized.contains("/vendor/")
622 {
623 return false;
624 }
625
626 true
627}
628
629fn default_in_app_function(function: &str) -> bool {
630 if function.is_empty() || function == "_main" {
631 return false;
632 }
633
634 !matches!(
635 function
636 .trim_start_matches('<')
637 .split("::")
638 .next()
639 .unwrap_or_default(),
640 "alloc" | "backtrace" | "core" | "posthog_rs" | "reqwest" | "std" | "tokio"
641 )
642}
643
644#[cfg(test)]
645mod tests {
646 use std::error::Error as StdError;
647 use std::fmt;
648
649 use serde_json::{json, Value};
650
651 use super::*;
652 use crate::event::InnerEvent;
653
654 #[derive(Debug)]
655 struct OuterError {
656 source: InnerError,
657 }
658
659 impl fmt::Display for OuterError {
660 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
661 write!(f, "checkout failed")
662 }
663 }
664
665 impl StdError for OuterError {
666 fn source(&self) -> Option<&(dyn StdError + 'static)> {
667 Some(&self.source)
668 }
669 }
670
671 #[derive(Debug)]
672 struct InnerError;
673
674 impl fmt::Display for InnerError {
675 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
676 write!(f, "database unavailable")
677 }
678 }
679
680 impl StdError for InnerError {}
681
682 #[derive(Debug)]
683 struct BorrowedError<'a>(&'a str);
684
685 impl fmt::Display for BorrowedError<'_> {
686 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
687 f.write_str(self.0)
688 }
689 }
690
691 impl StdError for BorrowedError<'_> {}
692
693 fn built_event_json(mut event: Event) -> Value {
694 event.prepare_for_v0();
695 serde_json::to_value(InnerEvent::new(event, "api-key".to_string())).unwrap()
696 }
697
698 fn event_json_with(exception: Exception, options: &ErrorTrackingOptions) -> Value {
699 let mut event = Event::new_anon("$exception");
700 exception.write_into(&mut event, options).unwrap();
701 built_event_json(event)
702 }
703
704 fn event_json(exception: Exception) -> Value {
705 event_json_with(exception, &ErrorTrackingOptions::default())
706 }
707
708 #[test]
709 fn from_error_builds_exception_list_with_stacktrace() {
710 let error = OuterError { source: InnerError };
711 let event = build_exception_event(
712 &error,
713 CaptureExceptionOptions::new().distinct_id("user-1"),
714 &ErrorTrackingOptions::default(),
715 )
716 .unwrap();
717 let json = built_event_json(event);
718
719 assert_eq!(json["event"], "$exception");
720 assert_eq!(json["distinct_id"], "user-1");
721 assert_eq!(json["properties"]["$exception_level"], "error");
722
723 let exception_list = json["properties"]["$exception_list"].as_array().unwrap();
724 assert!(exception_list[0]["type"]
725 .as_str()
726 .unwrap()
727 .ends_with("OuterError"));
728 assert_eq!(exception_list[0]["value"], "checkout failed");
729 assert_eq!(exception_list[0]["mechanism"]["type"], "generic");
730 assert_eq!(exception_list[0]["mechanism"]["handled"], true);
731 assert_eq!(exception_list[0]["mechanism"]["synthetic"], false);
732 assert_eq!(exception_list[0]["mechanism"]["exception_id"], 0);
733 assert_eq!(exception_list[0]["stacktrace"]["type"], "raw");
734 assert_eq!(exception_list[1]["value"], "database unavailable");
735 assert_eq!(exception_list[1]["mechanism"]["type"], "chained");
736 assert_eq!(exception_list[1]["mechanism"]["exception_id"], 1);
737 assert_eq!(exception_list[1]["mechanism"]["parent_id"], 0);
738
739 let frames = exception_list[0]["stacktrace"]["frames"]
740 .as_array()
741 .expect("expected stack frames");
742 let top_frame = frames.first().expect("expected top frame");
743 assert_eq!(top_frame["platform"], "rust");
744 assert_eq!(top_frame["lang"], "rust");
745 assert_eq!(top_frame["resolved"], true);
746 let top_function = top_frame["function"].as_str().unwrap_or_default();
747 assert!(
748 top_function.contains("from_error_builds_exception_list_with_stacktrace"),
749 "expected user frame last, got {:?}",
750 top_function
751 );
752 assert!(
753 !top_function.contains("Exception::"),
754 "expected SDK frames to be skipped, got {:?}",
755 top_function
756 );
757 }
758
759 #[test]
760 fn from_error_accepts_borrowed_error_types() {
761 let message = String::from("borrowed parse failure");
762 let error = BorrowedError(&message);
763 let json = event_json(Exception::from_error(&error, true));
764
765 assert_eq!(
766 json["properties"]["$exception_list"][0]["value"],
767 "borrowed parse failure"
768 );
769 }
770
771 #[test]
772 fn personless_capture_disables_person_profile() {
773 let json = event_json(Exception::from_message("Error", "no user context", true));
774
775 assert_eq!(json["event"], "$exception");
776 assert_eq!(json["properties"]["$process_person_profile"], false);
777 }
778
779 #[test]
780 fn custom_properties_cannot_override_reserved_exception_payload() {
781 let error = OuterError { source: InnerError };
782 let event = build_exception_event(
783 &error,
784 CaptureExceptionOptions::new()
785 .property("$exception_list", json!([{"value": "fake"}]))
786 .unwrap(),
787 &ErrorTrackingOptions::default(),
788 )
789 .unwrap();
790
791 let json = built_event_json(event);
792 assert_eq!(
793 json["properties"]["$exception_list"][0]["value"],
794 "checkout failed"
795 );
796 }
797
798 #[test]
799 fn options_can_disable_stacktrace() {
800 let options = ErrorTrackingOptionsBuilder::default()
801 .capture_stacktrace(false)
802 .build()
803 .unwrap();
804 let error = OuterError { source: InnerError };
805 let event =
806 build_exception_event(&error, CaptureExceptionOptions::new(), &options).unwrap();
807 let json = built_event_json(event);
808
809 let exception_list = json["properties"]["$exception_list"].as_array().unwrap();
810 assert_eq!(exception_list.len(), 2);
811 assert!(exception_list[0].get("stacktrace").is_none());
812 }
813
814 #[test]
815 fn in_app_path_defaults_and_overrides_are_applied() {
816 let options = ErrorTrackingOptions::default();
817 assert!(options.is_in_app_path("/app/src/main.rs"));
818 assert!(!options.is_in_app_path("/home/user/.cargo/registry/src/lib.rs"));
819 assert!(!options.is_in_app_path(
820 "/private/tmp/nix-build-rustc-1.91.1/rustc-1.91.1-src/library/core/src/ops/function.rs"
821 ));
822 assert!(options.is_in_app_frame(None, Some("checkout_service::submit")));
823 assert!(!options.is_in_app_frame(None, Some("std::rt::lang_start")));
824 assert!(!options.is_in_app_frame(None, Some("core::ops::function::FnOnce::call_once")));
825 assert!(
826 !options.is_in_app_frame(None, Some("posthog_rs::client::Client::capture_exception"))
827 );
828 assert!(!options.is_in_app_frame(None, Some("_main")));
829
830 let options = ErrorTrackingOptionsBuilder::default()
831 .in_app_include_paths(vec!["/service/".to_string(), "my_service::".to_string()])
832 .in_app_exclude_paths(vec!["/service/vendor/".to_string()])
833 .build()
834 .unwrap();
835
836 assert!(options.is_in_app_path("/service/src/main.rs"));
837 assert!(!options.is_in_app_path("/other/src/main.rs"));
838 assert!(!options.is_in_app_path("/service/vendor/lib.rs"));
839 assert!(options.is_in_app_frame(None, Some("my_service::checkout")));
840 assert!(!options.is_in_app_frame(None, Some("other_service::checkout")));
841 }
842
843 #[test]
844 fn function_names_strip_rust_symbol_hashes() {
845 assert_eq!(
846 normalize_function_name("checkout_service::submit::h9ae4817223dd0b22"),
847 "checkout_service::submit"
848 );
849 assert_eq!(
850 normalize_function_name("std::rt::lang_start::{{closure}}::ha1fd5c62e470a8cc"),
851 "std::rt::lang_start::{{closure}}"
852 );
853 assert_eq!(
854 normalize_function_name("checkout_service::submit"),
855 "checkout_service::submit"
856 );
857 }
858
859 #[test]
860 fn type_names_keep_path_and_strip_generics() {
861 assert_eq!(
863 simple_type_name("std::io::error::Error"),
864 "std::io::error::Error"
865 );
866 assert_eq!(
867 simple_type_name("mycrate::CheckoutError"),
868 "mycrate::CheckoutError"
869 );
870 assert_eq!(simple_type_name("mycrate::Error"), "mycrate::Error");
871
872 assert_eq!(simple_type_name("foo::Bar<baz::Qux>"), "foo::Bar");
874 assert_eq!(
875 simple_type_name(type_name::<Box<dyn StdError>>()),
876 "alloc::boxed::Box"
877 );
878
879 assert_eq!(simple_type_name("dyn core::error::Error"), "Error");
881 assert_eq!(simple_type_name(type_name::<&dyn StdError>()), "Error");
882 }
883
884 #[test]
885 fn frames_are_trimmed_to_max_frames_keeping_the_top() {
886 let synthetic_frame = |index: usize| StackFrame {
887 filename: None,
888 line_no: None,
889 function: format!("frame_{index}"),
890 lang: "rust".to_string(),
891 in_app: true,
892 synthetic: false,
893 resolved: true,
894 platform: "rust".to_string(),
895 };
896 let exception = Exception {
897 items: vec![ExceptionItem {
898 exception_type: "Error".to_string(),
899 value: "trimmed".to_string(),
900 mechanism: ExceptionMechanism::default(),
901 stacktrace: None,
902 }],
903 captured_frames: Some((0..MAX_FRAMES + 5).map(synthetic_frame).collect()),
904 fingerprint: None,
905 level: "error".to_string(),
906 };
907
908 let json = event_json_with(exception, &ErrorTrackingOptions::default());
909 let frames = json["properties"]["$exception_list"][0]["stacktrace"]["frames"]
910 .as_array()
911 .expect("expected stack frames");
912 assert_eq!(frames.len(), MAX_FRAMES);
913 assert_eq!(frames[0]["function"], "frame_0");
915 }
916
917 #[test]
918 fn stacktrace_keeps_top_frame_first() {
919 fn capture() -> ExceptionStacktrace {
920 let mut frames = capture_frames_current_first(0);
921 trim_to_max_frames(&mut frames, 8);
922 ExceptionStacktrace::raw(frames)
923 }
924
925 let frames = capture().frames;
926 let functions: Vec<&str> = frames
927 .iter()
928 .map(|frame| frame.function.as_str())
929 .filter(|function| !function.is_empty())
930 .collect();
931
932 let capture_index = functions
933 .iter()
934 .position(|function| function.contains("stacktrace_keeps_top_frame_first::capture"))
935 .expect("expected capture frame");
936 let test_index = functions
937 .iter()
938 .position(|function| function.ends_with("stacktrace_keeps_top_frame_first"))
939 .expect("expected test frame");
940
941 assert!(
942 capture_index < test_index,
943 "expected top frame before caller, got {:?}",
944 functions
945 );
946 }
947
948 #[test]
949 fn inlined_functions_become_separate_frames() {
950 #[inline(always)]
954 fn inline_leaf() -> Vec<StackFrame> {
955 capture_raw_application_frames()
956 }
957
958 #[inline(always)]
959 fn inline_mid() -> Vec<StackFrame> {
960 inline_leaf()
961 }
962
963 let frames = inline_mid();
964 let functions: Vec<&str> = frames.iter().map(|frame| frame.function.as_str()).collect();
965
966 let leaf_index = functions
967 .iter()
968 .position(|function| function.contains("inline_leaf"))
969 .unwrap_or_else(|| panic!("expected inline_leaf frame, got {:?}", functions));
970 let mid_index = functions
971 .iter()
972 .position(|function| function.contains("inline_mid"))
973 .unwrap_or_else(|| panic!("expected inline_mid frame, got {:?}", functions));
974 assert!(
975 leaf_index < mid_index,
976 "expected innermost inlined layer first, got {:?}",
977 functions
978 );
979 }
980
981 #[test]
982 fn build_exception_event_defaults_to_personless() {
983 let error = OuterError { source: InnerError };
984 let event = build_exception_event(
985 &error,
986 CaptureExceptionOptions::default(),
987 &ErrorTrackingOptions::default(),
988 )
989 .unwrap();
990 let json = built_event_json(event);
991
992 assert_eq!(json["event"], "$exception");
993 assert_eq!(json["properties"]["$process_person_profile"], false);
994 assert_eq!(json["properties"]["$exception_level"], "error");
995 }
996
997 #[test]
998 fn build_exception_event_applies_options() {
999 let error = OuterError { source: InnerError };
1000 let options = CaptureExceptionOptions::new()
1001 .distinct_id("user-1")
1002 .property("route", "/checkout")
1003 .unwrap()
1004 .group("company", "acme")
1005 .fingerprint("checkout-error")
1006 .level("warning");
1007 let event =
1008 build_exception_event(&error, options, &ErrorTrackingOptions::default()).unwrap();
1009 let json = built_event_json(event);
1010
1011 assert_eq!(json["distinct_id"], "user-1");
1012 assert_eq!(json["properties"]["route"], "/checkout");
1013 assert_eq!(json["properties"]["$groups"]["company"], "acme");
1014 assert_eq!(
1015 json["properties"]["$exception_fingerprint"],
1016 "checkout-error"
1017 );
1018 assert_eq!(json["properties"]["$exception_level"], "warning");
1019 }
1020}