use std::borrow::Cow;
use std::env;
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use regex::Regex;
use uuid::Uuid;
use api::protocol::{DebugMeta, Event};
use api::Dsn;
use backtrace_support::{function_starts_with, is_sys_function};
use constants::{SDK_INFO, USER_AGENT};
use hub::Hub;
use scope::Scope;
use transport::Transport;
use utils::{debug_images, server_name, trim_stacktrace};
#[derive(Clone)]
pub struct Client {
options: ClientOptions,
transport: Option<Arc<Transport>>,
}
impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Client")
.field("dsn", &self.dsn())
.field("options", &self.options)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct ClientOptions {
pub in_app_include: Vec<&'static str>,
pub in_app_exclude: Vec<&'static str>,
pub extra_border_frames: Vec<&'static str>,
pub max_breadcrumbs: usize,
pub trim_backtraces: bool,
pub release: Option<Cow<'static, str>>,
pub environment: Option<Cow<'static, str>>,
pub server_name: Option<Cow<'static, str>>,
pub user_agent: Cow<'static, str>,
pub http_proxy: Option<Cow<'static, str>>,
pub https_proxy: Option<Cow<'static, str>>,
pub shutdown_timeout: Option<Duration>,
}
impl Default for ClientOptions {
fn default() -> ClientOptions {
ClientOptions {
in_app_include: vec![],
in_app_exclude: vec![],
extra_border_frames: vec![],
max_breadcrumbs: 100,
trim_backtraces: true,
release: None,
environment: Some(if cfg!(debug_assertions) {
"debug".into()
} else {
"release".into()
}),
server_name: server_name().map(Cow::Owned),
user_agent: Cow::Borrowed(&USER_AGENT),
http_proxy: env::var("http_proxy").ok().map(Cow::Owned),
https_proxy: env::var("https_proxy")
.ok()
.map(Cow::Owned)
.or_else(|| env::var("HTTPS_PROXY").ok().map(Cow::Owned))
.or_else(|| env::var("http_proxy").ok().map(Cow::Owned)),
shutdown_timeout: Some(Duration::from_secs(2)),
}
}
}
lazy_static! {
static ref CRATE_RE: Regex = Regex::new(r"^(?:_<)?([a-zA-Z0-9_]+?)(?:\.\.|::)").unwrap();
}
fn parse_crate_name(func_name: &str) -> Option<String> {
CRATE_RE
.captures(func_name)
.and_then(|caps| caps.get(1))
.map(|cr| cr.as_str().into())
}
pub trait IntoClient: Sized {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>);
fn into_client(self) -> Option<Client> {
let (dsn, options) = self.into_client_config();
let dsn = dsn.or_else(|| {
env::var("SENTRY_DSN")
.ok()
.and_then(|dsn| dsn.parse::<Dsn>().ok())
});
if let Some(dsn) = dsn {
Some(if let Some(options) = options {
Client::with_dsn_and_options(dsn, options)
} else {
Client::with_dsn(dsn)
})
} else {
None
}
}
}
impl IntoClient for Client {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
(self.dsn().map(|x| x.clone()), Some(self.options().clone()))
}
fn into_client(self) -> Option<Client> {
Some(self)
}
}
impl IntoClient for () {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
(None, None)
}
}
impl<C: IntoClient> IntoClient for Option<C> {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
self.map(|x| x.into_client_config()).unwrap_or((None, None))
}
}
impl<'a> IntoClient for &'a str {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
if self.is_empty() {
(None, None)
} else {
(Some(self.parse().unwrap()), None)
}
}
}
impl<'a> IntoClient for &'a OsStr {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
if self.is_empty() {
(None, None)
} else {
(Some(self.to_string_lossy().parse().unwrap()), None)
}
}
}
impl IntoClient for OsString {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
if self.is_empty() {
(None, None)
} else {
(Some(self.to_string_lossy().parse().unwrap()), None)
}
}
}
impl IntoClient for String {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
if self.is_empty() {
(None, None)
} else {
(Some(self.parse().unwrap()), None)
}
}
}
impl<'a> IntoClient for &'a Dsn {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
(Some(self.clone()), None)
}
}
impl IntoClient for Dsn {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
(Some(self), None)
}
}
impl<C: IntoClient> IntoClient for (C, ClientOptions) {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
let (dsn, _) = self.0.into_client_config();
(dsn, Some(self.1))
}
}
impl Client {
pub fn from_config<C: IntoClient>(cfg: C) -> Option<Client> {
cfg.into_client()
}
pub fn with_dsn(dsn: Dsn) -> Client {
Client::with_dsn_and_options(dsn, Default::default())
}
pub fn with_dsn_and_options(dsn: Dsn, options: ClientOptions) -> Client {
let transport = Transport::new(
dsn,
options.user_agent.to_string(),
options.http_proxy.as_ref().map(|x| &x[..]),
options.https_proxy.as_ref().map(|x| &x[..]),
);
Client {
options,
transport: Some(Arc::new(transport)),
}
}
pub fn disabled() -> Client {
Client::disabled_with_options(Default::default())
}
#[cfg(any(test, feature = "with_test_support"))]
pub(crate) fn testable(dsn: Dsn, options: ClientOptions) -> Client {
Client {
options,
transport: Some(Arc::new(Transport::testable(dsn))),
}
}
pub fn disabled_with_options(options: ClientOptions) -> Client {
Client {
options,
transport: None,
}
}
#[cfg_attr(feature = "cargo-clippy", allow(cyclomatic_complexity))]
fn prepare_event(&self, event: &mut Event, scope: Option<&Scope>) -> Uuid {
lazy_static! {
static ref DEBUG_META: DebugMeta = DebugMeta {
images: debug_images(),
..Default::default()
};
}
if event.id.is_none() {
event.id = Some(Uuid::new_v4());
}
if let Some(scope) = scope {
scope.apply_to_event(event);
}
if event.release.is_none() {
event.release = self.options.release.clone();
}
if event.environment.is_none() {
event.environment = self.options.environment.clone();
}
if event.server_name.is_none() {
event.server_name = self.options.server_name.clone();
}
if event.sdk_info.is_none() {
event.sdk_info = Some(Cow::Borrowed(&SDK_INFO));
}
if &event.platform == "other" {
event.platform = "native".into();
}
if event.debug_meta.is_empty() {
event.debug_meta = Cow::Borrowed(&DEBUG_META);
}
for exc in &mut event.exceptions {
if let Some(ref mut stacktrace) = exc.stacktrace {
if self.options.trim_backtraces {
trim_stacktrace(stacktrace, |frame, _| {
if let Some(ref func) = frame.function {
self.options.extra_border_frames.contains(&func.as_str())
} else {
false
}
})
}
let mut any_in_app = false;
for frame in &mut stacktrace.frames {
let func_name = match frame.function {
Some(ref func) => func,
None => continue,
};
if frame.package.is_none() {
frame.package = parse_crate_name(func_name);
}
match frame.in_app {
Some(true) => {
any_in_app = true;
continue;
}
Some(false) => {
continue;
}
None => {}
}
for m in &self.options.in_app_exclude {
if function_starts_with(func_name, m) {
frame.in_app = Some(false);
break;
}
}
if frame.in_app.is_some() {
continue;
}
for m in &self.options.in_app_include {
if function_starts_with(func_name, m) {
frame.in_app = Some(true);
any_in_app = true;
break;
}
}
if frame.in_app.is_some() {
continue;
}
if is_sys_function(func_name) {
frame.in_app = Some(false);
}
}
if !any_in_app {
for frame in &mut stacktrace.frames {
if frame.in_app.is_none() {
frame.in_app = Some(true);
}
}
}
}
}
event.id.unwrap()
}
pub fn options(&self) -> &ClientOptions {
&self.options
}
pub fn dsn(&self) -> Option<&Dsn> {
self.transport.as_ref().map(|x| x.dsn())
}
pub fn capture_event(&self, mut event: Event<'static>, scope: Option<&Scope>) -> Uuid {
if let Some(ref transport) = self.transport {
let event_id = self.prepare_event(&mut event, scope);
transport.send_event(event);
event_id
} else {
Default::default()
}
}
pub fn drain_events(&self, timeout: Option<Duration>) -> bool {
if let Some(ref transport) = self.transport {
transport.drain(timeout)
} else {
true
}
}
#[cfg(any(test, feature = "with_test_support"))]
pub(crate) fn transport(&self) -> &Transport {
self.transport
.as_ref()
.expect("Client has no associated transport")
}
}
pub struct ClientInitGuard(Option<Arc<Client>>);
impl ClientInitGuard {
pub fn is_enabled(&self) -> bool {
self.0.is_some()
}
pub fn client(&self) -> Option<Arc<Client>> {
self.0.clone()
}
}
impl Drop for ClientInitGuard {
fn drop(&mut self) {
if let Some(ref client) = self.0 {
client.drain_events(client.options.shutdown_timeout);
}
}
}
#[cfg(feature = "with_client_implementation")]
pub fn init<C: IntoClient>(cfg: C) -> ClientInitGuard {
ClientInitGuard(Client::from_config(cfg).map(|client| {
let client = Arc::new(client);
Hub::with(|hub| hub.bind_client(Some(client.clone())));
client
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_crate_name() {
assert_eq!(
parse_crate_name("futures::task_impl::std::set"),
Some("futures".into())
);
}
#[test]
fn test_parse_crate_name_impl() {
assert_eq!(
parse_crate_name("_<futures..task_impl..Spawn<T>>::enter::_{{closure}}"),
Some("futures".into())
);
}
#[test]
fn test_parse_crate_name_unknown() {
assert_eq!(
parse_crate_name("_<F as alloc..boxed..FnBox<A>>::call_box"),
None
);
}
}