use std::env;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use uuid::Uuid;
use regex::Regex;
use api::Dsn;
use scope::{bind_client, Scope};
use protocol::{DebugMeta, Event};
use transport::Transport;
use backtrace_support::is_sys_function;
use utils::{debug_images, server_name, trim_stacktrace};
use constants::{SDK_INFO, USER_AGENT};
#[derive(Clone)]
pub struct Client {
dsn: Dsn,
options: ClientOptions,
transport: 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>,
}
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),
}
}
}
lazy_static! {
static ref CRATE_RE: Regex = Regex::new(r"^([a-zA-Z0-9_]+?)::").unwrap();
}
pub trait IntoClientConfig {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>);
}
impl IntoClientConfig for () {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
(None, None)
}
}
impl<C: IntoClientConfig> IntoClientConfig 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> IntoClientConfig 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> IntoClientConfig 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 IntoClientConfig 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 IntoClientConfig 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> IntoClientConfig for &'a Dsn {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
(Some(self.clone()), None)
}
}
impl IntoClientConfig for Dsn {
fn into_client_config(self) -> (Option<Dsn>, Option<ClientOptions>) {
(Some(self), None)
}
}
impl<C: IntoClientConfig> IntoClientConfig 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: IntoClientConfig>(cfg: C) -> Option<Client> {
let (dsn, options) = cfg.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
}
}
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());
Client {
dsn: dsn,
options: options,
transport: Arc::new(transport),
}
}
fn prepare_event(&self, event: &mut Event, scope: Option<&Scope>) {
lazy_static! {
static ref DEBUG_META: DebugMeta = DebugMeta {
images: debug_images(),
..Default::default()
};
}
if let Some(scope) = scope {
if !scope.breadcrumbs.is_empty() {
event
.breadcrumbs
.extend(scope.breadcrumbs.iter().map(|x| (*x).clone()));
}
if event.user.is_none() {
if let Some(ref user) = scope.user {
event.user = Some((**user).clone());
}
}
if !scope.extra.is_empty() {
event.extra.extend(
scope
.extra
.iter()
.map(|(k, v)| ((*k).clone(), (*v).clone())),
);
}
if !scope.tags.is_empty() {
event
.tags
.extend(scope.tags.iter().map(|(k, v)| ((*k).clone(), (*v).clone())));
}
if !scope.contexts.is_empty() {
event.contexts.extend(
scope
.contexts
.iter()
.map(|(k, v)| ((*k).clone(), (*v).clone())),
);
}
if event.fingerprint.len() == 1
&& (event.fingerprint[0] == "{{ default }}"
|| event.fingerprint[0] == "{{default}}")
{
if let Some(ref fp) = scope.fingerprint {
event.fingerprint = Cow::Owned((**fp).clone());
}
}
}
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 event.exceptions.iter_mut() {
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 stacktrace.frames.iter_mut() {
let func_name = match frame.function {
Some(ref func) => func,
None => continue,
};
if frame.package.is_none() {
frame.package = CRATE_RE
.captures(func_name)
.and_then(|caps| caps.get(1))
.map(|cr| cr.as_str().into());
}
match frame.in_app {
Some(true) => {
any_in_app = true;
continue;
}
Some(false) => {
continue;
}
None => {}
}
for m in &self.options.in_app_exclude {
if func_name.starts_with(m) {
frame.in_app = Some(false);
break;
}
}
if frame.in_app.is_some() {
continue;
}
for m in &self.options.in_app_include {
if func_name.starts_with(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 stacktrace.frames.iter_mut() {
if frame.in_app.is_none() {
frame.in_app = Some(true);
}
}
}
}
}
}
pub fn options(&self) -> &ClientOptions {
&self.options
}
pub fn dsn(&self) -> &Dsn {
&self.dsn
}
pub fn capture_event(&self, mut event: Event<'static>, scope: Option<&Scope>) -> Uuid {
self.prepare_event(&mut event, scope);
self.transport.send_event(event)
}
pub fn drain_events(&self, timeout: Option<Duration>) -> bool {
self.transport.drain(timeout)
}
}
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(Some(Duration::from_secs(2)));
}
}
}
#[cfg(feature = "with_client_implementation")]
pub fn init<C: IntoClientConfig>(cfg: C) -> ClientInitGuard {
ClientInitGuard(Client::from_config(cfg).map(|client| {
let client = Arc::new(client);
bind_client(client.clone());
client
}))
}