1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! Sentry supported panic handler.

#[cfg(doc)]
use crate::{shutdown, Shutdown};
use crate::{Event, Level, Value};
#[cfg(doc)]
use std::process::abort;
use std::{
    collections::BTreeMap,
    convert::TryFrom,
    panic::{self, PanicInfo},
};

/// Panic handler to send an [`Event`] with the current stacktrace to Sentry.
///
/// `before_send` is a callback that is able to modify the [`Event`] before it
/// is captures.
///
/// `hook` is a callback that is run after the [`Event`] is captured.
///
/// # Notes
/// This will not work properly if used with `panic = "abort"` because
/// [`Shutdown`] is never unwound. To fix this make sure you make the panic
/// handler itself call [`shutdown`].
///
/// Rust doesn't allow panics inside of a panicking thread and reacts with an
/// [`abort`]: if a custom transport or a before-send callback was registered
/// that can panic, it might lead to any [`panic!`] being an [`abort`] instead.
///
/// # Examples
/// ```should_panic
/// # use anyhow::Result;
/// # use sentry_contrib_native::{Options, set_hook};
/// fn main() -> Result<()> {
///     // pass original panic handler provided by rust to retain it's functionality
///     set_hook(None, Some(std::panic::take_hook()));
///     // it can also be removed
///     set_hook(None, None);
///     // the `Event` sent by a panic can also be modified
///     set_hook(
///         Some(Box::new(|mut event| {
///             // do something with the event and then return it
///             event
///         })),
///         None,
///     );
///
///     let _shutdown = Options::new().init()?;
///
///     panic!("application panicked")
/// }
/// ```
/// If you are using `panic = "abort"` make sure to call [`shutdown`] inside the
/// panic handler.
/// ```
/// # use sentry_contrib_native::{set_hook, shutdown};
/// set_hook(None, Some(Box::new(|_| shutdown())));
/// ```
pub fn set_hook(
    before_send: Option<Box<dyn Fn(Event) -> Event + 'static + Send + Sync>>,
    hook: Option<Box<dyn Fn(&PanicInfo) + 'static + Send + Sync>>,
) {
    panic::set_hook(Box::new(move |panic_info| {
        let mut event = Event::new_message(
            Level::Error,
            Some("rust panic".into()),
            panic_info.to_string(),
        );

        if let Some(location) = panic_info.location() {
            let mut extra = BTreeMap::new();
            extra.insert("file", Value::from(location.file()));

            if let Ok(line) = i32::try_from(location.line()) {
                extra.insert("line", line.into());
            }

            if let Ok(column) = i32::try_from(location.column()) {
                extra.insert("column", column.into());
            }

            event.insert("extra", extra);
        }

        event.add_stacktrace(0);

        if let Some(before_send) = &before_send {
            event = before_send(event);
        }

        event.capture();

        if let Some(hook) = &hook {
            hook(panic_info);
        }
    }));
}

#[cfg(test)]
#[rusty_fork::fork_test(timeout_ms = 60000)]
fn hook() {
    use std::{
        sync::atomic::{AtomicBool, Ordering},
        thread,
    };

    static BEFORE_SEND: AtomicBool = AtomicBool::new(false);
    static HOOK: AtomicBool = AtomicBool::new(false);

    set_hook(None, None);
    set_hook(
        Some(Box::new(|event| {
            BEFORE_SEND.store(true, Ordering::SeqCst);
            event
        })),
        Some(Box::new(|_| HOOK.store(true, Ordering::SeqCst))),
    );

    thread::spawn(|| panic!("this panic is a test"))
        .join()
        .unwrap_err();

    assert!(BEFORE_SEND.load(Ordering::SeqCst));
    assert!(HOOK.load(Ordering::SeqCst));
}