sentry-contrib-native 0.3.1

Unofficial bindings to the Sentry Native SDK for Rust.
Documentation
//! Sentry options implementation.

use crate::{
    before_send, logger, transport, BeforeSend, BeforeSendData, CPath, CToR, Error, Logger,
    LoggerData, RToC, Transport, TransportState, BEFORE_SEND, LOGGER,
};
#[cfg(doc)]
use crate::{end_session, set_user_consent, shutdown, start_session, Consent, Event};
#[cfg(feature = "test")]
use std::env;
#[cfg(doc)]
use std::process::abort;
use std::{
    fmt::{Debug, Formatter, Result as FmtResult},
    mem,
    path::PathBuf,
};

/// The Sentry client options.
///
/// # Examples
/// ```
/// # use sentry_contrib_native::Options;
/// # fn main() -> anyhow::Result<()> {
/// let _shutdown = Options::new().init()?;
/// # Ok(()) }
/// ```
pub struct Options {
    /// Raw Sentry options.
    raw: Option<Ownership>,
    /// Storing a fake DSN to make documentation tests and examples work.
    #[cfg(feature = "test")]
    dsn: Option<String>,
    /// Storing [`Options::set_before_send`] data to save it globally on
    /// [`Options::init`] and properly deallocate it on [`shutdown`].
    before_send: Option<BeforeSendData>,
    /// Storing [`Options::set_logger`] data to save it globally on
    /// [`Options::init`] and properly deallocate it on [`shutdown`].
    logger: Option<LoggerData>,
}

/// Represents the ownership status of [`Options`].
#[derive(Clone, Copy, Debug)]
pub enum Ownership {
    /// [`Options`] is owned.
    Owned(*mut sys::Options),
    /// [`Options`] is borrowed.
    Borrowed(*const sys::Options),
}

unsafe impl Send for Options {}
unsafe impl Sync for Options {}

impl Default for Options {
    fn default() -> Self {
        Self::new()
    }
}

impl Debug for Options {
    fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
        let mut debug = fmt.debug_struct("Options");
        debug.field("raw", &self.raw);
        #[cfg(feature = "test")]
        debug.field("dsn", &self.dsn);
        debug.field(
            "before_send",
            if self.before_send.is_some() {
                &"Some"
            } else {
                &"None"
            },
        );
        debug
            .field(
                "logger",
                if self.logger.is_some() {
                    &"Some"
                } else {
                    &"None"
                },
            )
            .finish()
    }
}

impl Drop for Options {
    fn drop(&mut self) {
        if let Some(Ownership::Owned(options)) = self.raw.take() {
            unsafe { sys::options_free(options) }
        }
    }
}

impl PartialEq for Options {
    fn eq(&self, _: &Self) -> bool {
        false
    }
}

impl Eq for Options {}

impl Options {
    /// Creates new Sentry client options.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// ```
    #[must_use = "`Options` doesn't do anything without `Options::init`"]
    pub fn new() -> Self {
        Self::from_sys(Ownership::Owned(unsafe { sys::options_new() }))
    }

    /// Creates new [`Options`] from a [`sys::Options`] wrapped in
    /// [`Ownership`].
    pub(crate) fn from_sys(options: Ownership) -> Self {
        #[cfg_attr(not(feature = "test"), allow(unused_mut))]
        let mut options = Self {
            raw: Some(options),
            #[cfg(feature = "test")]
            dsn: None,
            before_send: None,
            logger: None,
        };

        #[cfg(feature = "test")]
        {
            if let Some(Ownership::Owned(_)) = options.raw {
                // will be set up properly for us inside those functions
                options.set_database_path(".sentry-native");
                options.set_handler_path("");
                options.set_dsn(
                    &env::var("SENTRY_DSN")
                        .expect("tests require a valid `SENTRY_DSN` environment variable"),
                )
            }
        }

        options
    }

    /// Yields a pointer to [`sys::Options`], ownership is retained.
    fn as_ref(&self) -> *const sys::Options {
        match self.raw.expect("use after free") {
            Ownership::Owned(options) => options,
            Ownership::Borrowed(options) => options,
        }
    }

    /// Yields a mutable pointer to [`sys::Options`], ownership is retained.
    fn as_mut(&mut self) -> *mut sys::Options {
        if let Ownership::Owned(options) = self.raw.expect("use after free") {
            options
        } else {
            unreachable!("can't mutably borrow `Options`")
        }
    }

    /// Sets a custom transport. This only affects events sent through
    /// [`Event::capture`], not the crash handler.
    ///
    /// The `startup` parameter is a function that serves as a one-time
    /// initialization event for your [`Transport`], it takes a
    /// [`&Options`](Options) and has to return an [`Result<Transport,
    /// ()>`](Transport), an [`Err`] will cause [`Options::init`] to fail.
    ///
    /// # Notes
    /// Unwinding panics of functions in `startup` will be cought and
    /// [`abort`] will be called if any occured.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::{Options, RawEnvelope};
    /// let mut options = Options::new();
    /// options.set_transport(|_| {
    ///     Ok(|envelope: RawEnvelope| println!("Event to be sent: {:?}", envelope.event()))
    /// });
    /// ```
    /// See [`Transport`] for a more detailed documentation.
    pub fn set_transport<
        S: (FnOnce(&Self) -> Result<T, ()>) + 'static + Send + Sync,
        T: Into<Box<T>> + Transport,
    >(
        &mut self,
        startup: S,
    ) {
        let startup = TransportState::Startup(Box::new(|options: &Self| {
            startup(options).map(|startup| startup.into() as _)
        }));
        let startup = Box::into_raw(Box::new(Some(startup)));

        unsafe {
            let transport = sys::transport_new(Some(transport::send));
            sys::transport_set_state(transport, startup.cast());
            sys::transport_set_startup_func(transport, Some(transport::startup));
            sys::transport_set_shutdown_func(transport, Some(transport::shutdown));
            sys::options_set_transport(self.as_mut(), transport)
        }
    }

    /// Sets a callback that is triggered before sending an event through
    /// [`Event::capture`].
    ///
    /// # Notes
    /// Unwinding panics of functions in `before_send` will be cought and
    /// [`abort`] will be called if any occured.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_before_send(|mut value| {
    ///     // do something with the value and then return it
    ///     value
    /// });
    /// ```
    pub fn set_before_send<B: Into<Box<B>> + BeforeSend>(&mut self, before_send: B) {
        let fun = Box::into_raw(Box::<Box<dyn BeforeSend>>::new(before_send.into()));
        self.before_send = Some(unsafe { Box::from_raw(fun) });

        unsafe {
            sys::options_set_before_send(self.as_mut(), Some(before_send::before_send), fun.cast())
        }
    }

    /// Sets the DSN.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_dsn("yourdsn.com");
    /// ```
    pub fn set_dsn<S: Into<String>>(&mut self, dsn: S) {
        #[cfg(feature = "test")]
        let dsn = {
            self.dsn = Some(dsn.into());
            env::var("SENTRY_DSN")
                .expect("tests require a valid `SENTRY_DSN` environment variable")
                .into_cstring()
        };
        #[cfg(not(feature = "test"))]
        let dsn = dsn.into().into_cstring();
        unsafe { sys::options_set_dsn(self.as_mut(), dsn.as_ptr()) }
    }

    /// Gets the DSN.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_dsn("yourdsn.com");
    ///
    /// assert_eq!(Some("yourdsn.com"), options.dsn());
    /// ```
    #[must_use]
    pub fn dsn(&self) -> Option<&str> {
        #[cfg(feature = "test")]
        if let Some(Ownership::Owned(_)) = self.raw {
            return self.dsn.as_deref();
        }

        unsafe { sys::options_get_dsn(self.as_ref()).as_str() }
    }

    /// Sets the sample rate, which should be a [`f64`] between `0.0` and `1.0`.
    /// Sentry will randomly discard any event that is captured using [`Event`]
    /// when a sample rate < 1.0 is set.
    ///
    /// # Errors
    /// Fails with [`Error::SampleRateRange`] if `sample_rate` is smaller than
    /// `0.0` or bigger than `1.0`.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_sample_rate(0.5);
    /// ```
    pub fn set_sample_rate(&mut self, sample_rate: f64) -> Result<(), Error> {
        if (0. ..=1.).contains(&sample_rate) {
            unsafe { sys::options_set_sample_rate(self.as_mut(), sample_rate) };

            Ok(())
        } else {
            Err(Error::SampleRateRange)
        }
    }

    /// Gets the sample rate.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// # fn main() -> anyhow::Result<()> {
    /// let mut options = Options::new();
    /// options.set_sample_rate(0.5)?;
    ///
    /// assert_eq!(0.5, options.sample_rate());
    /// # Ok(()) }
    /// ```
    #[must_use]
    pub fn sample_rate(&self) -> f64 {
        unsafe { sys::options_get_sample_rate(self.as_ref()) }
    }

    /// Sets the release.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_release("1.0");
    /// ```
    pub fn set_release<S: Into<String>>(&mut self, release: S) {
        let release = release.into().into_cstring();
        unsafe { sys::options_set_release(self.as_mut(), release.as_ptr()) }
    }

    /// Gets the release.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_release("1.0");
    ///
    /// assert_eq!(Some("1.0"), options.release());
    /// ```
    #[must_use]
    pub fn release(&self) -> Option<&str> {
        unsafe { sys::options_get_release(self.as_ref()).as_str() }
    }

    /// Sets the environment.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_environment("production");
    /// ```
    pub fn set_environment<S: Into<String>>(&mut self, environment: S) {
        let environment = environment.into().into_cstring();
        unsafe { sys::options_set_environment(self.as_mut(), environment.as_ptr()) }
    }

    /// Gets the environment.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_environment("production");
    ///
    /// assert_eq!(Some("production"), options.environment());
    /// ```
    #[must_use]
    pub fn environment(&self) -> Option<&str> {
        unsafe { sys::options_get_environment(self.as_ref()).as_str() }
    }

    /// Sets the distribution.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_distribution("release-pgo");
    /// ```
    pub fn set_distribution<S: Into<String>>(&mut self, distribution: S) {
        let distribution = distribution.into().into_cstring();
        unsafe { sys::options_set_dist(self.as_mut(), distribution.as_ptr()) }
    }

    /// Gets the distribution.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_distribution("release-pgo");
    ///
    /// assert_eq!(Some("release-pgo"), options.distribution());
    /// ```
    #[must_use]
    pub fn distribution(&self) -> Option<&str> {
        unsafe { sys::options_get_dist(self.as_ref()).as_str() }
    }

    /// Configures the http proxy.
    ///
    /// The given proxy has to include the full scheme, eg. `http://some.proxy/`.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_http_proxy("http://some.proxy/");
    /// ```
    pub fn set_http_proxy<S: Into<String>>(&mut self, proxy: S) {
        let proxy = proxy.into().into_cstring();
        unsafe { sys::options_set_http_proxy(self.as_mut(), proxy.as_ptr()) }
    }

    /// Returns the configured http proxy.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_http_proxy("http://some.proxy/");
    ///
    /// assert_eq!(Some("http://some.proxy/"), options.http_proxy());
    /// ```
    #[must_use]
    pub fn http_proxy(&self) -> Option<&str> {
        unsafe { sys::options_get_http_proxy(self.as_ref()).as_str() }
    }

    /// Configures the path to a file containing SSL certificates for
    /// verification.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_ca_certs("certs.pem");
    /// ```
    pub fn set_ca_certs<S: Into<String>>(&mut self, path: S) {
        let path = path.into().into_cstring();
        unsafe { sys::options_set_ca_certs(self.as_mut(), path.as_ptr()) }
    }

    /// Returns the configured path for CA certificates.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_ca_certs("certs.pem");
    ///
    /// assert_eq!(Some("certs.pem"), options.ca_certs());
    /// ```
    #[must_use]
    pub fn ca_certs(&self) -> Option<&str> {
        unsafe { sys::options_get_ca_certs(self.as_ref()).as_str() }
    }

    /// Configures the name of the default transport thread. Has no effect when
    /// using a custom transport.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_transport_thread_name("sentry transport");
    /// ```
    #[cfg(feature = "transport-default")]
    #[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-default")))]
    pub fn set_transport_thread_name<S: Into<String>>(&mut self, name: S) {
        let name = name.into().into_cstring();
        unsafe { sys::options_set_transport_thread_name(self.as_mut(), name.as_ptr()) }
    }

    /// Returns the configured default transport thread name.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_transport_thread_name("sentry transport");
    ///
    /// assert_eq!(Some("sentry transport"), options.transport_thread_name());
    /// ```
    #[cfg(feature = "transport-default")]
    #[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-default")))]
    #[must_use]
    pub fn transport_thread_name(&self) -> Option<&str> {
        unsafe { sys::options_get_transport_thread_name(self.as_ref()).as_str() }
    }

    /// Enables or disables debug printing mode.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_debug(true);
    /// ```
    pub fn set_debug(&mut self, debug: bool) {
        let debug = debug.into();
        unsafe { sys::options_set_debug(self.as_mut(), debug) }
    }

    /// Returns the current value of the debug flag.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_debug(true);
    ///
    /// assert!(options.debug());
    /// ```
    #[allow(clippy::missing_panics_doc)]
    #[must_use]
    pub fn debug(&self) -> bool {
        match unsafe { sys::options_get_debug(self.as_ref()) } {
            0 => false,
            1 => true,
            error => unreachable!("{} couldn't be converted to a bool", error),
        }
    }

    /// Sets the number of breadcrumbs being tracked and attached to events.
    /// Defaults to 100.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_max_breadcrumbs(10);
    /// ```
    pub fn set_max_breadcrumbs(&mut self, max_breadcrumbs: usize) {
        unsafe { sys::options_set_max_breadcrumbs(self.as_mut(), max_breadcrumbs) }
    }

    /// Gets the number of breadcrumbs being tracked and attached to events.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_max_breadcrumbs(10);
    ///
    /// assert_eq!(options.max_breadcrumbs(), 10);
    /// ```
    #[must_use]
    pub fn max_breadcrumbs(&self) -> usize {
        unsafe { sys::options_get_max_breadcrumbs(self.as_ref()) }
    }

    /// Sets a callback that is used for logging purposes when
    /// [`Options::debug`] is `true`.
    ///
    /// # Notes
    /// Unwinding panics in `logger` will be cought and [`abort`]
    /// will be called if any occured.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::{Level, Options};
    /// # use std::iter::FromIterator;
    /// let mut options = Options::new();
    /// options.set_debug(true);
    /// options.set_logger(|level, message| {
    ///     println!("[{}]: {}", level, message);
    /// });
    /// ```
    pub fn set_logger<L: Into<Box<L>> + Logger>(&mut self, logger: L) {
        let fun = Box::into_raw(Box::<Box<dyn Logger>>::new(logger.into()));
        self.logger = Some(unsafe { Box::from_raw(fun) });

        unsafe { sys::options_set_logger(self.as_mut(), Some(logger::logger), fun.cast()) }
    }

    /// Enables or disables automatic session tracking.
    ///
    /// Automatic session tracking is enabled by default and is equivalent to
    /// calling [`start_session`] after startup.
    /// There can only be one running session, and the current session will
    /// always be closed implicitly by [`shutdown`], when starting a new session
    /// with [`start_session`], or manually by calling [`end_session`].
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::{Options, start_session};
    /// # fn main() -> anyhow::Result<()> {
    /// let mut options = Options::new();
    /// options.set_auto_session_tracking(false);
    /// let _shutdown = options.init()?;
    ///
    /// // code to run before starting the session
    ///
    /// start_session();
    /// # Ok(()) }
    /// ```
    pub fn set_auto_session_tracking(&mut self, val: bool) {
        let val = val.into();
        unsafe { sys::options_set_auto_session_tracking(self.as_mut(), val) }
    }

    /// Returns `true` if automatic session tracking is enabled.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_auto_session_tracking(false);
    ///
    /// assert!(!options.auto_session_tracking());
    /// ```
    #[allow(clippy::missing_panics_doc)]
    #[must_use]
    pub fn auto_session_tracking(&self) -> bool {
        match unsafe { sys::options_get_auto_session_tracking(self.as_ref()) } {
            0 => false,
            1 => true,
            error => unreachable!("{} couldn't be converted to a bool", error),
        }
    }

    /// Enables or disabled user consent requirements for uploads.
    ///
    /// This disables uploads until the user has given the consent to the SDK.
    /// Consent itself is given with [`set_user_consent`] and [`Consent::Given`]
    /// or revoked with [`Consent::Revoked`].
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_require_user_consent(true);
    /// ```
    pub fn set_require_user_consent(&mut self, val: bool) {
        let val = val.into();
        unsafe { sys::options_set_require_user_consent(self.as_mut(), val) }
    }

    /// Returns `true` if user consent is required.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_require_user_consent(true);
    ///
    /// assert!(options.require_user_consent());
    /// ```
    #[allow(clippy::missing_panics_doc)]
    #[must_use]
    pub fn require_user_consent(&self) -> bool {
        match unsafe { sys::options_get_require_user_consent(self.as_ref()) } {
            0 => false,
            1 => true,
            error => unreachable!("{} couldn't be converted to a bool", error),
        }
    }

    /// Enables or disables on-device symbolication of stack traces.
    ///
    /// This feature can have a performance impact, and is enabled by default on
    /// Android. It is usually only needed when it is not possible to provide
    /// debug information files for system libraries which are needed for
    /// serverside symbolication.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_symbolize_stacktraces(true);
    /// ```
    pub fn set_symbolize_stacktraces(&mut self, val: bool) {
        let val = val.into();
        unsafe { sys::options_set_symbolize_stacktraces(self.as_mut(), val) }
    }

    /// Returns `true` if on-device symbolication of stack traces is enabled.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_symbolize_stacktraces(true);
    ///
    /// assert!(options.symbolize_stacktraces());
    /// ```
    #[allow(clippy::missing_panics_doc)]
    #[must_use]
    pub fn symbolize_stacktraces(&self) -> bool {
        match unsafe { sys::options_get_symbolize_stacktraces(self.as_ref()) } {
            0 => false,
            1 => true,
            error => unreachable!("{} couldn't be converted to a bool", error),
        }
    }

    /// Adds a new attachment to be sent along.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.add_attachment("server.log");
    /// ```
    pub fn add_attachment<P: Into<PathBuf>>(&mut self, path: P) {
        let path = path.into().into_os_vec();

        #[cfg(windows)]
        unsafe {
            sys::options_add_attachmentw(self.as_mut(), path.as_ptr())
        };
        #[cfg(not(windows))]
        unsafe {
            sys::options_add_attachment(self.as_mut(), path.as_ptr())
        }
    }

    /// Sets the path to the crashpad handler if the crashpad backend is used.
    ///
    /// The path defaults to the `crashpad_handler`/`crashpad_handler.exe`
    /// executable, depending on platform, which is expected to be present in
    /// the same directory as the app executable.
    ///
    /// It is recommended that library users set an explicit handler path,
    /// depending on the directory/executable structure of their app.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_handler_path("crashpad_handler");
    /// ```
    #[cfg_attr(all(feature = "test", crashpad), allow(clippy::needless_pass_by_value))]
    pub fn set_handler_path<P: Into<PathBuf>>(
        &mut self,
        #[cfg_attr(all(feature = "test", crashpad), allow(unused_variables))] path: P,
    ) {
        #[cfg(all(feature = "test", crashpad))]
        let path = PathBuf::from(env!("CRASHPAD_HANDLER")).into_os_vec();
        #[cfg(not(all(feature = "test", crashpad)))]
        let path = path.into().into_os_vec();

        #[cfg(windows)]
        unsafe {
            sys::options_set_handler_pathw(self.as_mut(), path.as_ptr())
        };
        #[cfg(not(windows))]
        unsafe {
            sys::options_set_handler_path(self.as_mut(), path.as_ptr())
        }
    }

    /// Sets the path to the Sentry database directory.
    ///
    /// Sentry will use this path to persist user consent, sessions, and other
    /// artifacts in case of a crash. This will also be used by the crashpad
    /// backend if it is configured.
    ///
    /// The path defaults to `.sentry-native` in the current working directory,
    /// will be created if it does not exist, and will be resolved to an
    /// absolute path inside of [`Options::init`].
    ///
    /// It is recommended that library users set an explicit absolute path,
    /// depending on their apps runtime directory.
    ///
    /// The directory should not be shared with other application
    /// data/configuration, as Sentry will enumerate and possibly delete files
    /// in that directory.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_database_path(".sentry-native");
    /// ```
    pub fn set_database_path<P: Into<PathBuf>>(&mut self, path: P) {
        #[cfg(feature = "test")]
        let path = PathBuf::from(env!("OUT_DIR"))
            .join(path.into())
            .into_os_vec();
        #[cfg(not(feature = "test"))]
        let path = path.into().into_os_vec();

        #[cfg(windows)]
        unsafe {
            sys::options_set_database_pathw(self.as_mut(), path.as_ptr())
        };
        #[cfg(not(windows))]
        unsafe {
            sys::options_set_database_path(self.as_mut(), path.as_ptr())
        };
    }

    /// Enables forwarding to the system crash reporter. Disabled by default.
    ///
    /// This setting only has an effect when using Crashpad on macOS. If
    /// enabled, Crashpad forwards crashes to the macOS system crash reporter.
    /// Depending on the crash, this may impact the crash time. Even if enabled,
    /// Crashpad may choose not to forward certain crashes.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// let mut options = Options::new();
    /// options.set_system_crash_reporter(true);
    /// ```
    pub fn set_system_crash_reporter(&mut self, enabled: bool) {
        let enabled = enabled.into();
        unsafe { sys::options_set_system_crash_reporter_enabled(self.as_mut(), enabled) }
    }

    /// Initializes the Sentry SDK with the specified options. Make sure to
    /// capture the resulting [`Shutdown`], this makes sure to automatically
    /// call [`shutdown`] when it drops.
    ///
    /// # Errors
    /// Fails with [`Error::Init`] if Sentry couldn't initialize - should only
    /// occur in these situations:
    /// - Fails to create database directory.
    /// - Fails to lock database directory.
    ///
    /// # Examples
    /// ```
    /// # use sentry_contrib_native::Options;
    /// # fn main() -> anyhow::Result<()> {
    /// let _shutdown = Options::new().init()?;
    /// # Ok(()) }
    /// ```
    #[allow(clippy::missing_panics_doc)]
    pub fn init(mut self) -> Result<Shutdown, Error> {
        // disolve `Options`, `sys::init` is going to take ownership now
        let options = if let Ownership::Owned(options) = self.raw.take().expect("use after free") {
            options
        } else {
            unreachable!("can't mutably borrow `Options`")
        };

        // only lock if we need it
        let mut before_send = self.before_send.take().map(|before_send| {
            let mut lock = BEFORE_SEND.lock().expect("lock poisoned");
            *lock = Some(before_send);
            lock
        });

        let mut logger = self.logger.take().map(|logger| {
            let mut lock = LOGGER.lock().expect("lock poisoned");
            *lock = Some(logger);
            lock
        });

        match unsafe { sys::init(options) } {
            0 => Ok(Shutdown),
            1 => {
                // deallocate globals on failure, which are otherwise unused
                before_send.take().take();
                logger.take().take();

                Err(Error::Init)
            }
            _ => unreachable!("invalid return value"),
        }
    }
}

/// Automatically shuts down the Sentry client on drop.
///
/// # Examples
/// ```
/// # use anyhow::Result;
/// # use sentry_contrib_native::{Options, Shutdown};
/// fn main() -> Result<()> {
///     let options = Options::new();
///     let _shutdown: Shutdown = options.init()?;
///
///     // ...
///     // your application code
///     // ...
///
///     Ok(())
///     // Sentry client will automatically shutdown because `Shutdown` is leaving context
/// }
/// ```
#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub struct Shutdown;

impl Drop for Shutdown {
    fn drop(&mut self) {
        crate::shutdown();
    }
}

impl Shutdown {
    /// Disable automatic shutdown. Call [`shutdown`] manually to force
    /// transports to flush out before the program exits.
    ///
    /// # Examples
    /// ```
    /// # use anyhow::Result;
    /// # use sentry_contrib_native::{Options, shutdown};
    /// fn main() -> Result<()> {
    ///     sentry_init()?;
    ///
    ///     // ...
    ///     // your application code
    ///     // ...
    ///
    ///     // call shutdown manually to make sure transports flush out
    ///     shutdown();
    ///     Ok(())
    /// }
    ///
    /// fn sentry_init() -> Result<()> {
    ///     let options = Options::new();
    ///     // call forget because we are leaving the context and we don't want to shut down the Sentry client yet
    ///     options.init()?.forget();
    ///     Ok(())
    /// }
    /// ```
    pub fn forget(self) {
        mem::forget(self)
    }

    /// Manually shutdown.
    ///
    /// # Examples
    /// ```
    /// # use anyhow::Result;
    /// # use sentry_contrib_native::Options;
    /// fn main() -> Result<()> {
    ///     let options = Options::new();
    ///     let shutdown = options.init()?;
    ///
    ///     // ...
    ///     // your application code
    ///     // ...
    ///
    ///     // call shutdown manually to make sure transports flush out
    ///     shutdown.shutdown();
    ///     Ok(())
    /// }
    /// ```
    pub fn shutdown(self) {
        drop(self)
    }
}

#[test]
fn options() -> anyhow::Result<()> {
    use crate::{Level, Message, RawEnvelope, Value};

    struct CustomTransport;

    impl CustomTransport {
        #[allow(warnings)]
        const fn new(_: &Options) -> Result<Self, ()> {
            Ok(Self)
        }
    }

    impl Transport for CustomTransport {
        fn send(&self, _: RawEnvelope) {}
    }

    struct Filter;

    impl BeforeSend for Filter {
        fn before_send(&self, value: Value) -> Value {
            value
        }
    }

    struct Log;

    impl Logger for Log {
        fn log(&self, _level: Level, _message: Message) {}
    }

    let mut options = Options::new();

    options.set_transport(|_| Ok(|_| {}));
    options.set_transport(CustomTransport::new);

    options.set_before_send(|value| value);
    options.set_before_send(Filter);

    options.set_dsn("yourdsn.com");
    assert_eq!(Some("yourdsn.com"), options.dsn());

    let sample_rate = 0.5;
    options.set_sample_rate(sample_rate)?;
    #[allow(clippy::float_cmp)]
    {
        assert_eq!(sample_rate, options.sample_rate());
    }

    options.set_release("1.0");
    assert_eq!(Some("1.0"), options.release());

    options.set_environment("production");
    assert_eq!(Some("production"), options.environment());

    options.set_distribution("release-pgo");
    assert_eq!(Some("release-pgo"), options.distribution());

    options.set_http_proxy("http://some.proxy/");
    assert_eq!(Some("http://some.proxy/"), options.http_proxy());

    options.set_ca_certs("certs.pem");
    assert_eq!(Some("certs.pem"), options.ca_certs());

    #[cfg(feature = "transport-default")]
    {
        options.set_transport_thread_name("sentry transport");
        assert_eq!(Some("sentry transport"), options.transport_thread_name());
    }

    options.set_debug(true);
    assert!(options.debug());

    assert_eq!(options.max_breadcrumbs(), 100);

    options.set_max_breadcrumbs(10);
    assert_eq!(options.max_breadcrumbs(), 10);

    options.set_logger(|_, _| ());
    options.set_logger(Log);

    options.set_auto_session_tracking(false);
    assert!(!options.auto_session_tracking());

    options.set_require_user_consent(true);
    assert!(options.require_user_consent());

    options.set_symbolize_stacktraces(true);
    assert!(options.symbolize_stacktraces());

    options.add_attachment("server.log");

    options.set_handler_path("crashpad_handler");

    options.set_database_path(".sentry-native");

    options.set_system_crash_reporter(true);

    Ok(())
}

#[cfg(test)]
#[rusty_fork::fork_test(timeout_ms = 60000)]
fn threaded_stress() -> anyhow::Result<()> {
    use crate::test;
    use std::{
        convert::TryFrom,
        sync::{Arc, RwLock},
        thread,
    };

    #[allow(clippy::type_complexity)]
    fn spawns(tests: Vec<fn(Arc<RwLock<Options>>, usize)>) -> Options {
        /// Github Actions MacOS CI machines can't handle that many threads.
        #[cfg(target_os = "macos")]
        static THREADS: usize = 50;
        #[cfg(not(target_os = "macos"))]
        static THREADS: usize = 100;

        let options = Arc::new(RwLock::new(Options::new()));

        let mut spawns = Vec::with_capacity(tests.len());
        for test in tests {
            let options = Arc::clone(&options);

            let handle = thread::spawn(move || {
                let mut handles = Vec::with_capacity(100);

                for index in 0..THREADS {
                    let options = Arc::clone(&options);

                    handles.push(thread::spawn(move || test(options, index)))
                }

                handles
            });
            spawns.push(handle)
        }

        for spawn in spawns {
            for handle in spawn.join().unwrap() {
                handle.join().unwrap()
            }
        }

        Arc::try_unwrap(options).unwrap().into_inner().unwrap()
    }

    test::set_hook();

    let options = spawns(vec![
        |options, index| {
            options
                .write()
                .unwrap()
                .set_transport(move |_| Ok(move |_| println!("{}", index)))
        },
        |options, _| options.write().unwrap().set_before_send(|value| value),
        |options, index| options.write().unwrap().set_dsn(index.to_string()),
        |options, _| println!("{:?}", options.read().unwrap().dsn()),
        |options, index| {
            let sample_rate = f64::from(u32::try_from(index).unwrap()) / 100.;
            options
                .write()
                .unwrap()
                .set_sample_rate(sample_rate)
                .unwrap()
        },
        |options, _| println!("{:?}", options.read().unwrap().sample_rate()),
        |options, index| options.write().unwrap().set_release(index.to_string()),
        |options, _| println!("{:?}", options.read().unwrap().release()),
        |options, index| options.write().unwrap().set_environment(index.to_string()),
        |options, _| println!("{:?}", options.read().unwrap().environment()),
        |options, index| options.write().unwrap().set_distribution(index.to_string()),
        |options, _| println!("{:?}", options.read().unwrap().distribution()),
        |options, index| options.write().unwrap().set_http_proxy(index.to_string()),
        |options, _| println!("{:?}", options.read().unwrap().http_proxy()),
        |options, index| options.write().unwrap().set_ca_certs(index.to_string()),
        |options, _| println!("{:?}", options.read().unwrap().ca_certs()),
        #[cfg(feature = "transport-default")]
        |options, index| {
            options
                .write()
                .unwrap()
                .set_transport_thread_name(index.to_string())
        },
        #[cfg(feature = "transport-default")]
        |options, _| println!("{:?}", options.read().unwrap().transport_thread_name()),
        |options, index| {
            options.write().unwrap().set_debug(match index % 2 {
                0 => false,
                1 => true,
                _ => unreachable!(),
            })
        },
        |options, _| println!("{:?}", options.read().unwrap().debug()),
        |options, index| {
            options
                .write()
                .unwrap()
                .set_logger(move |_, _| println!("{}", index))
        },
        |options, index| {
            options
                .write()
                .unwrap()
                .set_auto_session_tracking(match index % 2 {
                    0 => false,
                    1 => true,
                    _ => unreachable!(),
                })
        },
        |options, _| println!("{:?}", options.read().unwrap().auto_session_tracking()),
        |options, index| {
            options
                .write()
                .unwrap()
                .set_require_user_consent(match index % 2 {
                    0 => false,
                    1 => true,
                    _ => unreachable!(),
                })
        },
        |options, _| println!("{:?}", options.write().unwrap().require_user_consent()),
        |options, index| {
            options
                .write()
                .unwrap()
                .set_symbolize_stacktraces(match index % 2 {
                    0 => false,
                    1 => true,
                    _ => unreachable!(),
                })
        },
        |options, _| println!("{:?}", options.read().unwrap().symbolize_stacktraces()),
        |options, index| options.write().unwrap().add_attachment(index.to_string()),
        |options, index| options.write().unwrap().set_handler_path(index.to_string()),
        |options, index| {
            options
                .write()
                .unwrap()
                .set_database_path(index.to_string())
        },
        |options, index| {
            options
                .write()
                .unwrap()
                .set_system_crash_reporter(match index % 2 {
                    0 => false,
                    1 => true,
                    _ => unreachable!(),
                })
        },
    ]);

    options.init()?.shutdown();

    test::verify_panics();

    Ok(())
}

#[cfg(test)]
#[rusty_fork::fork_test(timeout_ms = 60000)]
fn sync() -> anyhow::Result<()> {
    use crate::test;
    use anyhow::{anyhow, Result};
    use std::{sync::Arc, thread};

    test::set_hook();

    let mut options = Options::new();
    options.set_debug(true);
    let options = Arc::new(options);
    let mut handles = Vec::new();

    for _ in 0..100 {
        let options = Arc::clone(&options);
        let handle = thread::spawn(move || {
            println!("{}", options.debug());
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    #[allow(clippy::map_err_ignore)]
    thread::spawn(move || -> Result<Shutdown> {
        Arc::try_unwrap(options)
            .map_err(|_| anyhow!("failed to unwrap arc"))?
            .init()
            .map_err(Into::into)
    })
    .join()
    .unwrap()?
    .shutdown();

    test::verify_panics();

    Ok(())
}