use std::any::TypeId;
use std::borrow::Cow;
#[cfg(feature = "logs")]
use std::collections::BTreeMap;
use std::fmt;
use std::panic::RefUnwindSafe;
use std::sync::{Arc, RwLock};
use std::time::Duration;
#[cfg(feature = "release-health")]
use crate::protocol::SessionUpdate;
use rand::random;
use sentry_types::random_uuid;
use crate::constants::SDK_INFO;
#[cfg(feature = "logs")]
use crate::logs::LogsBatcher;
use crate::protocol::{ClientSdkInfo, Event};
#[cfg(feature = "release-health")]
use crate::session::SessionFlusher;
use crate::types::{Dsn, Uuid};
#[cfg(feature = "release-health")]
use crate::SessionMode;
use crate::{ClientOptions, Envelope, Hub, Integration, Scope, Transport};
#[cfg(feature = "logs")]
use sentry_types::protocol::v7::Context;
#[cfg(feature = "logs")]
use sentry_types::protocol::v7::{Log, LogAttribute};
impl<T: Into<ClientOptions>> From<T> for Client {
fn from(o: T) -> Client {
Client::with_options(o.into())
}
}
pub(crate) type TransportArc = Arc<RwLock<Option<Arc<dyn Transport>>>>;
pub struct Client {
options: ClientOptions,
transport: TransportArc,
#[cfg(feature = "release-health")]
session_flusher: RwLock<Option<SessionFlusher>>,
#[cfg(feature = "logs")]
logs_batcher: RwLock<Option<LogsBatcher>>,
#[cfg(feature = "logs")]
default_log_attributes: Option<BTreeMap<String, LogAttribute>>,
integrations: Vec<(TypeId, Arc<dyn Integration>)>,
pub(crate) sdk_info: ClientSdkInfo,
}
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()
}
}
impl Clone for Client {
fn clone(&self) -> Client {
let transport = Arc::new(RwLock::new(self.transport.read().unwrap().clone()));
#[cfg(feature = "release-health")]
let session_flusher = RwLock::new(Some(SessionFlusher::new(
transport.clone(),
self.options.session_mode,
)));
#[cfg(feature = "logs")]
let logs_batcher = RwLock::new(if self.options.enable_logs {
Some(LogsBatcher::new(transport.clone()))
} else {
None
});
Client {
options: self.options.clone(),
transport,
#[cfg(feature = "release-health")]
session_flusher,
#[cfg(feature = "logs")]
logs_batcher,
#[cfg(feature = "logs")]
default_log_attributes: self.default_log_attributes.clone(),
integrations: self.integrations.clone(),
sdk_info: self.sdk_info.clone(),
}
}
}
impl Client {
pub fn from_config<O: Into<ClientOptions>>(opts: O) -> Client {
Client::with_options(opts.into())
}
pub fn with_options(mut options: ClientOptions) -> Client {
Hub::with(|_| {});
let create_transport = || {
options.dsn.as_ref()?;
let factory = options.transport.as_ref()?;
Some(factory.create_transport(&options))
};
let transport = Arc::new(RwLock::new(create_transport()));
let mut sdk_info = SDK_INFO.clone();
let integrations: Vec<_> = options
.integrations
.iter()
.map(|integration| (integration.as_ref().type_id(), integration.clone()))
.collect();
for (_, integration) in integrations.iter() {
integration.setup(&mut options);
sdk_info.integrations.push(integration.name().to_string());
}
#[cfg(feature = "release-health")]
let session_flusher = RwLock::new(Some(SessionFlusher::new(
transport.clone(),
options.session_mode,
)));
#[cfg(feature = "logs")]
let logs_batcher = RwLock::new(if options.enable_logs {
Some(LogsBatcher::new(transport.clone()))
} else {
None
});
#[allow(unused_mut)]
let mut client = Client {
options,
transport,
#[cfg(feature = "release-health")]
session_flusher,
#[cfg(feature = "logs")]
logs_batcher,
#[cfg(feature = "logs")]
default_log_attributes: None,
integrations,
sdk_info,
};
#[cfg(feature = "logs")]
client.cache_default_log_attributes();
client
}
#[cfg(feature = "logs")]
fn cache_default_log_attributes(&mut self) {
let mut attributes = BTreeMap::new();
if let Some(environment) = self.options.environment.as_ref() {
attributes.insert("sentry.environment".to_owned(), environment.clone().into());
}
if let Some(release) = self.options.release.as_ref() {
attributes.insert("sentry.release".to_owned(), release.clone().into());
}
attributes.insert(
"sentry.sdk.name".to_owned(),
self.sdk_info.name.to_owned().into(),
);
attributes.insert(
"sentry.sdk.version".to_owned(),
self.sdk_info.version.to_owned().into(),
);
let mut fake_event = Event::default();
for (_, integration) in self.integrations.iter() {
if let Some(res) = integration.process_event(fake_event.clone(), &self.options) {
fake_event = res;
}
}
if let Some(Context::Os(os)) = fake_event.contexts.get("os") {
if let Some(name) = os.name.as_ref() {
attributes.insert("os.name".to_owned(), name.to_owned().into());
}
if let Some(version) = os.version.as_ref() {
attributes.insert("os.version".to_owned(), version.to_owned().into());
}
}
if let Some(server) = &self.options.server_name {
attributes.insert("server.address".to_owned(), server.clone().into());
}
self.default_log_attributes = Some(attributes);
}
pub(crate) fn get_integration<I>(&self) -> Option<&I>
where
I: Integration,
{
let id = TypeId::of::<I>();
let integration = &self.integrations.iter().find(|(iid, _)| *iid == id)?.1;
integration.as_ref().as_any().downcast_ref()
}
pub fn prepare_event(
&self,
mut event: Event<'static>,
scope: Option<&Scope>,
) -> Option<Event<'static>> {
if event.event_id.is_nil() {
event.event_id = random_uuid();
}
if event.sdk.is_none() {
event.sdk = Some(Cow::Owned(self.sdk_info.clone()));
}
if let Some(scope) = scope {
event = scope.apply_to_event(event)?;
}
for (_, integration) in self.integrations.iter() {
let id = event.event_id;
event = match integration.process_event(event, &self.options) {
Some(event) => event,
None => {
sentry_debug!("integration dropped event {:?}", id);
return None;
}
}
}
if event.release.is_none() {
event.release.clone_from(&self.options.release);
}
if event.environment.is_none() {
event.environment.clone_from(&self.options.environment);
}
if event.server_name.is_none() {
event.server_name.clone_from(&self.options.server_name);
}
if &event.platform == "other" {
event.platform = "native".into();
}
if let Some(ref func) = self.options.before_send {
sentry_debug!("invoking before_send callback");
let id = event.event_id;
if let Some(processed_event) = func(event) {
event = processed_event;
} else {
sentry_debug!("before_send dropped event {:?}", id);
return None;
}
}
if let Some(scope) = scope {
scope.update_session_from_event(&event);
}
if !self.sample_should_send(self.options.sample_rate) {
None
} else {
Some(event)
}
}
pub fn options(&self) -> &ClientOptions {
&self.options
}
pub fn dsn(&self) -> Option<&Dsn> {
self.options.dsn.as_ref()
}
pub fn is_enabled(&self) -> bool {
self.options.dsn.is_some() && self.transport.read().unwrap().is_some()
}
pub fn capture_event(&self, event: Event<'static>, scope: Option<&Scope>) -> Uuid {
if let Some(ref transport) = *self.transport.read().unwrap() {
if let Some(event) = self.prepare_event(event, scope) {
let event_id = event.event_id;
let mut envelope: Envelope = event.into();
#[cfg(feature = "release-health")]
if self.options.session_mode == SessionMode::Application {
let session_item = scope.and_then(|scope| {
scope
.session
.lock()
.unwrap()
.as_mut()
.and_then(|session| session.create_envelope_item())
});
if let Some(session_item) = session_item {
envelope.add_item(session_item);
}
}
if let Some(scope) = scope {
for attachment in scope.attachments.iter().cloned() {
envelope.add_item(attachment);
}
}
transport.send_envelope(envelope);
return event_id;
}
}
Default::default()
}
pub fn send_envelope(&self, envelope: Envelope) {
if let Some(ref transport) = *self.transport.read().unwrap() {
transport.send_envelope(envelope);
}
}
#[cfg(feature = "release-health")]
pub(crate) fn enqueue_session(&self, session_update: SessionUpdate<'static>) {
if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
flusher.enqueue(session_update);
}
}
pub fn flush(&self, timeout: Option<Duration>) -> bool {
#[cfg(feature = "release-health")]
if let Some(ref flusher) = *self.session_flusher.read().unwrap() {
flusher.flush();
}
#[cfg(feature = "logs")]
if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
batcher.flush();
}
if let Some(ref transport) = *self.transport.read().unwrap() {
transport.flush(timeout.unwrap_or(self.options.shutdown_timeout))
} else {
true
}
}
pub fn close(&self, timeout: Option<Duration>) -> bool {
#[cfg(feature = "release-health")]
drop(self.session_flusher.write().unwrap().take());
#[cfg(feature = "logs")]
drop(self.logs_batcher.write().unwrap().take());
let transport_opt = self.transport.write().unwrap().take();
if let Some(transport) = transport_opt {
sentry_debug!("client close; request transport to shut down");
transport.shutdown(timeout.unwrap_or(self.options.shutdown_timeout))
} else {
sentry_debug!("client close; no transport to shut down");
true
}
}
pub fn sample_should_send(&self, rate: f32) -> bool {
if rate >= 1.0 {
true
} else if rate <= 0.0 {
false
} else {
random::<f32>() < rate
}
}
#[cfg(feature = "logs")]
pub fn capture_log(&self, log: Log, scope: &Scope) {
if !self.options.enable_logs {
sentry_debug!("[Client] called capture_log, but options.enable_logs is set to false");
return;
}
if let Some(log) = self.prepare_log(log, scope) {
if let Some(ref batcher) = *self.logs_batcher.read().unwrap() {
batcher.enqueue(log);
}
}
}
#[cfg(feature = "logs")]
fn prepare_log(&self, mut log: Log, scope: &Scope) -> Option<Log> {
scope.apply_to_log(&mut log);
if let Some(default_attributes) = self.default_log_attributes.as_ref() {
for (key, val) in default_attributes.iter() {
log.attributes.entry(key.to_owned()).or_insert(val.clone());
}
}
if let Some(ref func) = self.options.before_send_log {
log = func(log)?;
}
Some(log)
}
}
impl RefUnwindSafe for Client {}