#[cfg(doc)]
use crate::Event;
use crate::{ffi, Options, Ownership, Value};
use std::{
mem::ManuallyDrop,
os::raw::{c_char, c_int, c_void},
process, slice, thread,
time::Duration,
};
#[cfg(doc)]
use std::{process::abort, sync::Mutex};
pub use sys::SDK_USER_AGENT;
#[cfg(feature = "transport-custom")]
use ::{
http::{HeaderMap, HeaderValue, Request as HttpRequest},
std::{
convert::{Infallible, TryFrom, TryInto},
str::FromStr,
},
thiserror::Error,
url::{ParseError, Url},
};
#[cfg(feature = "transport-custom")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-custom")))]
#[derive(Debug, Error, PartialEq)]
pub enum Error {
#[error("failed to parse DSN URL")]
UrlParse(#[from] ParseError),
#[error("DSN doesn't have a http(s) scheme")]
Scheme,
#[error("DSN has no username")]
Username,
#[error("DSN has no project ID")]
ProjectId,
#[error("DSN has no host")]
Host,
}
#[cfg(feature = "transport-custom")]
impl From<Infallible> for Error {
fn from(from: Infallible) -> Self {
match from {}
}
}
#[cfg(feature = "transport-custom")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-custom")))]
pub type Request = HttpRequest<Envelope>;
pub const ENVELOPE_MIME: &str = "application/x-sentry-envelope";
pub const API_VERSION: i8 = 7;
#[derive(Copy, Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub enum Shutdown {
Success,
TimedOut,
}
impl Shutdown {
const fn into_raw(self) -> c_int {
match self {
Self::Success => 0,
Self::TimedOut => 1,
}
}
}
pub trait Transport: 'static + Send + Sync {
fn send(&self, envelope: RawEnvelope);
#[must_use]
#[allow(clippy::boxed_local)]
fn shutdown(self: Box<Self>, timeout: Duration) -> Shutdown {
thread::sleep(timeout);
Shutdown::TimedOut
}
}
impl<T: Fn(RawEnvelope) + 'static + Send + Sync> Transport for T {
fn send(&self, envelope: RawEnvelope) {
self(envelope)
}
}
type Startup =
Box<dyn (FnOnce(&Options) -> Result<Box<dyn Transport>, ()>) + 'static + Send + Sync>;
pub enum State {
Startup(Startup),
Send(Box<dyn Transport>),
}
pub extern "C" fn startup(options: *const sys::Options, state: *mut c_void) -> c_int {
let options = Options::from_sys(Ownership::Borrowed(options));
let state = unsafe { Box::from_raw(state.cast::<Option<State>>()) };
let mut state = ManuallyDrop::new(state);
if let Some(State::Startup(startup)) = state.take() {
if let Ok(transport) = ffi::catch(|| startup(&options)) {
state.replace(State::Send(transport));
0
} else {
1
}
} else {
process::abort();
}
}
pub extern "C" fn send(envelope: *mut sys::Envelope, state: *mut c_void) {
let envelope = RawEnvelope(envelope);
let state = unsafe { Box::from_raw(state.cast::<Option<State>>()) };
let state = ManuallyDrop::new(state);
if let Some(State::Send(transport)) = state.as_ref() {
ffi::catch(|| transport.send(envelope));
} else {
process::abort();
}
}
pub extern "C" fn shutdown(timeout: u64, state: *mut c_void) -> c_int {
let timeout = Duration::from_millis(timeout);
let mut state = unsafe { Box::from_raw(state.cast::<Option<State>>()) };
if let Some(State::Send(transport)) = state.take() {
ffi::catch(|| transport.shutdown(timeout)).into_raw()
} else {
process::abort();
}
}
#[derive(Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub struct RawEnvelope(*mut sys::Envelope);
unsafe impl Send for RawEnvelope {}
unsafe impl Sync for RawEnvelope {}
impl Drop for RawEnvelope {
fn drop(&mut self) {
unsafe { sys::envelope_free(self.0) }
}
}
impl RawEnvelope {
#[must_use = "`RawEnvelope::serialize` only converts it to an `Envelope`, this doesn't do anything until it is sent"]
pub fn serialize(&self) -> Envelope {
let mut envelope_size = 0;
let serialized_envelope = unsafe { sys::envelope_serialize(self.0, &mut envelope_size) };
Envelope {
data: serialized_envelope,
len: envelope_size,
}
}
#[must_use]
pub fn event(&self) -> Value {
Value::from_raw_borrowed(unsafe { sys::envelope_get_event(self.0) })
}
#[cfg(feature = "transport-custom")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-custom")))]
#[must_use = "`Request` doesn't do anything until it is sent"]
pub fn to_request(&self, dsn: Dsn) -> Request {
self.serialize().into_request(dsn)
}
}
#[derive(Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub struct Envelope {
data: *const c_char,
len: usize,
}
unsafe impl Send for Envelope {}
unsafe impl Sync for Envelope {}
impl Drop for Envelope {
fn drop(&mut self) {
unsafe { sys::free(self.data as _) }
}
}
impl AsRef<[u8]> for Envelope {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl Envelope {
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
unsafe { slice::from_raw_parts(self.data.cast(), self.len) }
}
#[cfg(feature = "transport-custom")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-custom")))]
#[must_use = "`Request` doesn't do anything until it is sent"]
pub fn into_request(self, dsn: Dsn) -> Request {
let mut request = HttpRequest::builder();
*request.headers_mut().expect("failed to build headers") = dsn.to_headers();
request
.method("POST")
.uri(dsn.url)
.header("content-length", self.as_bytes().len())
.body(self)
.expect("failed to build request")
}
}
#[cfg(feature = "transport-custom")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-custom")))]
#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub struct Dsn {
auth: String,
url: String,
}
#[cfg(feature = "transport-custom")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-custom")))]
impl Dsn {
pub fn new(dsn: &str) -> Result<Self, crate::Error> {
let dsn_url = Url::parse(dsn).map_err(Error::from)?;
if !dsn_url.scheme().starts_with("http") {
return Err(Error::Scheme.into());
}
if dsn_url.username().is_empty() {
return Err(Error::Username.into());
}
if dsn_url.path().is_empty() || dsn_url.path() == "/" {
return Err(Error::ProjectId.into());
}
match dsn_url.host_str() {
None => Err(Error::Host.into()),
Some(host) => {
let mut auth = format!(
"Sentry sentry_key={}, sentry_version={}, sentry_client={}",
dsn_url.username(),
API_VERSION,
SDK_USER_AGENT
);
if let Some(password) = dsn_url.password() {
auth.push_str(", sentry_secret=");
auth.push_str(password);
}
let host = if let Some(port) = dsn_url.port() {
format!("{}:{}", host, port)
} else {
host.to_owned()
};
let url = format!(
"{}://{}/api/{}/envelope/",
dsn_url.scheme(),
host,
&dsn_url.path()[1..]
);
Ok(Self { auth, url })
}
}
}
#[must_use]
pub fn auth(&self) -> &str {
&self.auth
}
#[must_use]
pub fn url(&self) -> &str {
&self.url
}
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn into_parts(self) -> Parts {
Parts {
auth: self.auth,
url: self.url,
}
}
#[cfg(feature = "transport-custom")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-custom")))]
#[must_use]
pub fn to_headers(&self) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert("user-agent", HeaderValue::from_static(SDK_USER_AGENT));
headers.insert("content-type", HeaderValue::from_static(ENVELOPE_MIME));
headers.insert("accept", HeaderValue::from_static("*/*"));
headers.insert(
"x-sentry-auth",
(&self.auth)
.try_into()
.expect("failed to insert `x-sentry-auth`"),
);
headers
}
}
#[cfg(feature = "transport-custom")]
impl FromStr for Dsn {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[cfg(feature = "transport-custom")]
impl TryFrom<&str> for Dsn {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[cfg(feature = "transport-custom")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "transport-custom")))]
#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)]
pub struct Parts {
pub auth: String,
pub url: String,
}
#[cfg(all(test, feature = "transport-custom"))]
#[rusty_fork::fork_test(timeout_ms = 60000)]
fn transport() -> anyhow::Result<()> {
use crate::Event;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
struct CustomTransport {
dsn: Dsn,
}
impl CustomTransport {
fn new(dsn: Dsn, options: &Options) -> Self {
assert_eq!(false, STARTUP.swap(true, Ordering::SeqCst));
assert_eq!(dsn, Dsn::new(options.dsn().unwrap()).unwrap());
Self { dsn }
}
}
impl Transport for CustomTransport {
fn send(&self, envelope: RawEnvelope) {
SEND.fetch_add(1, Ordering::SeqCst);
let _event = envelope.event();
let request_1 = envelope.to_request(self.dsn.clone());
let envelope = envelope.serialize();
let request_2 = envelope.into_request(self.dsn.clone());
assert_eq!(request_1.uri(), request_2.uri());
assert_eq!(request_1.headers(), request_2.headers());
assert_eq!(request_1.body().as_bytes(), request_2.body().as_bytes());
}
fn shutdown(self: Box<Self>, _: Duration) -> Shutdown {
assert_eq!(false, SHUTDOWN.swap(true, Ordering::SeqCst));
Shutdown::Success
}
}
static STARTUP: AtomicBool = AtomicBool::new(false);
static SEND: AtomicUsize = AtomicUsize::new(0);
static SHUTDOWN: AtomicBool = AtomicBool::new(false);
let mut options = Options::new();
let dsn = Dsn::new(options.dsn().unwrap())?;
let _event = dsn.to_headers();
options.set_transport(|options| Ok(CustomTransport::new(dsn, options)));
let shutdown = options.init()?;
Event::new().capture();
Event::new().capture();
Event::new().capture();
shutdown.shutdown();
assert!(STARTUP.load(Ordering::SeqCst));
assert_eq!(3, SEND.load(Ordering::SeqCst));
assert!(SHUTDOWN.load(Ordering::SeqCst));
Ok(())
}
#[cfg(all(test, feature = "transport-custom"))]
#[rusty_fork::fork_test(timeout_ms = 60000)]
fn dsn() {
use crate::Event;
#[allow(clippy::needless_pass_by_value)]
fn send(envelope: RawEnvelope) {
{
let dsn = Dsn::new(
"https://a0b1c2d3e4f5678910abcdeffedcba12@o209016.ingest.sentry.io/0123456",
)
.unwrap();
let request = envelope.to_request(dsn);
assert_eq!(
request.uri(),
"https://o209016.ingest.sentry.io/api/0123456/envelope/"
);
let headers = request.headers();
assert_eq!(headers.get("x-sentry-auth").unwrap(), &format!("Sentry sentry_key=a0b1c2d3e4f5678910abcdeffedcba12, sentry_version={}, sentry_client={}", API_VERSION, SDK_USER_AGENT));
}
{
let dsn = Dsn::new("http://a0b1c2d3e4f5678910abcdeffedcba12@192.168.1.1:9000/0123456")
.unwrap();
let request = envelope.to_request(dsn);
assert_eq!(
request.uri(),
"http://192.168.1.1:9000/api/0123456/envelope/"
);
let headers = request.headers();
assert_eq!(headers.get("x-sentry-auth").unwrap(), &format!("Sentry sentry_key=a0b1c2d3e4f5678910abcdeffedcba12, sentry_version={}, sentry_client={}", API_VERSION, SDK_USER_AGENT));
}
}
let mut options = Options::new();
options.set_transport(|_| Ok(send));
let _shutdown = options.init();
Event::new().capture();
}