Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

## Unreleased

### New Features

- Added [`TransportFactory::create_transport_with_options`](https://docs.rs/sentry-core/latest/sentry_core/trait.TransportFactory.html#method.create_transport_with_options), which constructs transports from [`TransportOptions`](https://docs.rs/sentry-core/latest/sentry_core/struct.TransportOptions.html) instead of full [`ClientOptions`](https://docs.rs/sentry-core/latest/sentry_core/struct.ClientOptions.html) ([#1142](https://github.com/getsentry/sentry-rust/pull/1142)).
- Added transport-specific options types and `with_options` constructors for built-in HTTP transports, including `ReqwestHttpTransportOptions`, `CurlHttpTransportOptions`, `UreqHttpTransportOptions`, and `EmbeddedSVCHttpTransportOptions` ([#1142](https://github.com/getsentry/sentry-rust/pull/1142)).
- Added transport worker options types and deprecated the legacy constructors for built-in background transport workers, including `StdTransportThreadOptions` and `TokioTransportThreadOptions` ([#1142](https://github.com/getsentry/sentry-rust/pull/1142)).

### Fixes

- Fixed `ureq` transport handling for HTTP error statuses so `429` rate limits and `413` payload-too-large responses are processed correctly ([#1177](https://github.com/getsentry/sentry-rust/pull/1177)).

### Behavior Changes

- Custom transport factories that implement [`TransportFactory::create_transport`](https://docs.rs/sentry-core/latest/sentry_core/trait.TransportFactory.html#method.create_transport) now receive [`ClientOptions`](https://docs.rs/sentry-core/latest/sentry_core/struct.ClientOptions.html) reconstructed from [`TransportOptions`](https://docs.rs/sentry-core/latest/sentry_core/struct.TransportOptions.html). The reconstructed options include only transport-relevant fields, such as DSN, user agent, proxy settings, and TLS certificate validation settings. This may affect code that reads non-transport fields in `create_transport`, but the API remains source-compatible and this change is included in a minor/patch release ([#1142](https://github.com/getsentry/sentry-rust/pull/1142)).
Comment thread
lcian marked this conversation as resolved.

## 0.48.2

### New Features
Expand Down
19 changes: 16 additions & 3 deletions sentry-core/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::time::Duration;
use crate::metrics::IntoProtocolMetric;
#[cfg(feature = "release-health")]
use crate::protocol::SessionUpdate;
use crate::transport::TransportOptions;
use rand::random;
use sentry_types::random_uuid;

Expand Down Expand Up @@ -594,13 +595,25 @@ fn build_envelope_sender(client_options: &ClientOptions) -> EnvelopeSender {
let ClientOptions {
dsn,
transport: transport_factory,
user_agent,
http_proxy,
https_proxy,
accept_invalid_certs,
..
} = client_options;

match (dsn.as_ref(), transport_factory.as_ref()) {
(Some(_), Some(transport_factory)) => {
EnvelopeSender::new(|| transport_factory.create_transport(client_options))
}
(Some(dsn), Some(transport_factory)) => EnvelopeSender::new(|| {
let options = TransportOptions {
dsn: dsn.clone(),
user_agent: user_agent.clone(),
http_proxy: http_proxy.clone(),
https_proxy: https_proxy.clone(),
accept_invalid_certs: *accept_invalid_certs,
};

transport_factory.create_transport_with_options(options)
}),
_ => Default::default(),
}
}
2 changes: 1 addition & 1 deletion sentry-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub use crate::integration::Integration;
pub use crate::intodsn::IntoDsn;
pub use crate::performance::*;
pub use crate::scope::{Scope, ScopeGuard};
pub use crate::transport::{Transport, TransportFactory};
pub use crate::transport::{Transport, TransportFactory, TransportOptions};
#[cfg(feature = "logs")]
mod logger; // structured logging macros exported with `#[macro_export]`

Expand Down
72 changes: 61 additions & 11 deletions sentry-core/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ use std::time::Duration;

use crate::{ClientOptions, Envelope};

mod options;

pub use self::options::TransportOptions;

/// The trait for transports.
///
/// A transport is responsible for sending events to Sentry. Custom implementations
Expand Down Expand Up @@ -32,21 +36,62 @@ pub trait Transport: Send + Sync + 'static {
/// A factory creating transport instances.
///
/// Because options are potentially reused between different clients the
/// options do not actually contain a transport but a factory object that
/// [`ClientOptions`] do not actually contain a transport but a factory object that
/// can create transports instead.
///
/// The factory has a single method that creates a new arced transport.
/// Because transports can be wrapped in `Arc`s and those are clonable
/// any `Arc<Transport>` is also a valid transport factory. This for
/// instance lets you put a `Arc<TestTransport>` directly into the options.
/// This factory has two methods. Although both methods have default implementations, the default
/// implementations call each other, so to avoid an infinitely recursive loop, types implementing
/// this trait **must implement at least one of these methods**. We recommend that implementors
/// implement only [`TransportFactory::create_transport_with_options`] because the other method,
/// [`TransportFactory::create_transport`] only exists for backwards compatibility.
///
/// This is automatically implemented for all closures optionally taking
/// options and returning a boxed factory.
/// Both factory methods create a new transport wrapped in an [`Arc`]. Because transports can be
/// wrapped in `Arc`s and those are clonable, `Arc<Transport>` is also a valid transport factory.
/// This for instance lets you put a `Arc<TestTransport>` directly into the options.
pub trait TransportFactory: Send + Sync {
/// Given some options creates a transport.
fn create_transport(&self, options: &ClientOptions) -> Arc<dyn Transport>;
/// Create a transport with the given `options`.
///
/// Although a default implementation is provided for this trait method, we recommend that all
/// custom transport factories implement this method, as it is the way the SDK constructs the
/// transport. The default implementaton calls [`TransportFactory::create_transport`] as a
/// fallback.
fn create_transport_with_options(&self, options: TransportOptions) -> Arc<dyn Transport> {
#[expect(deprecated, reason = "need to call deprecated method for back-compat")]
self.create_transport(&options.into_client_options())
}

/// The legacy method for creating a transport.
///
/// This method exists for backwards compatiblity with custom transport factories, which were
/// created before [`TransportFactory::create_transport_with_options`] was added, and thus only
Comment thread
szokeasaurusrex marked this conversation as resolved.
/// implement this method.
///
/// New custom transport factories **should not** implement this method, as it is not called
/// from the SDK. A sensible default implementation, which forwards to
/// [`TransportFactory::create_transport_with_options`] is provided.
#[deprecated = "use and implement `create_transport_with_options` instead"]
fn create_transport(&self, options: &ClientOptions) -> Arc<dyn Transport> {
TransportOptions::try_from_client_options(options).map_or_else(
|| {
let no_op: Arc<dyn Transport> = Arc::new(NoOpTransport);
no_op
},
|options| self.create_transport_with_options(options),
)
}
}

/// A no-op transport.
///
/// This is returned by [`TransportFactory::create_transport`] when called without a `dsn` in the
/// [`ClientOptions`], rendering the transport disabled.
struct NoOpTransport;

/// This implementor is **deprecated**, as the closure is used as the deprecated
/// [`TransportFactory::create_transport`] method.
///
/// Use or create a [`TransportFactory`] which provides
/// [`TransportFactory::create_transport_with_options`], instead.
impl<F> TransportFactory for F
where
F: Fn(&ClientOptions) -> Arc<dyn Transport> + Clone + Send + Sync + 'static,
Expand All @@ -67,8 +112,13 @@ impl<T: Transport> Transport for Arc<T> {
}

impl<T: Transport> TransportFactory for Arc<T> {
fn create_transport(&self, options: &ClientOptions) -> Arc<dyn Transport> {
let _options = options;
fn create_transport_with_options(&self, _: TransportOptions) -> Arc<dyn Transport> {
self.clone()
}
}

impl Transport for NoOpTransport {
fn send_envelope(&self, envelope: Envelope) {
let _ = envelope;
}
}
80 changes: 80 additions & 0 deletions sentry-core/src/transport/options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//! Includes the [`TransportOptions`] struct.

use std::borrow::Cow;

use sentry_types::Dsn;

use crate::ClientOptions;

/// Options for a transport.
#[derive(Debug)]
#[must_use]
#[non_exhaustive]
pub struct TransportOptions {
Comment thread
lcian marked this conversation as resolved.
Comment thread
lcian marked this conversation as resolved.
/// The transport's Sentry DSN.
pub dsn: Dsn,
/// The user agent sent with transport requests.
pub user_agent: Cow<'static, str>,
/// An optional HTTP proxy to use.
pub http_proxy: Option<Cow<'static, str>>,
/// An optional HTTPS proxy to use.
pub https_proxy: Option<Cow<'static, str>>,
/// Whether TLS certificate validation should be disabled.
pub accept_invalid_certs: bool,
}

impl TransportOptions {
/// Try to convert a [`&ClientOptions`](ClientOptions) to a [`TransportOptions`] by extracting
/// the relevant fields from the `ClientOptions`.
///
/// This method is provided so that code which expects [`TransportOptions`] can be
/// backwards-compatible with older code, which provides `ClientOptions`.
///
/// Returns [`None`] if `options.dsn` is `None`, `Some(_)` otherwise.
pub fn try_from_client_options(options: &ClientOptions) -> Option<Self> {
let ClientOptions {
dsn,
http_proxy,
https_proxy,
accept_invalid_certs,
user_agent,
..
} = options;

dsn.as_ref().cloned().map(|dsn| Self {
dsn,
user_agent: user_agent.clone(),
http_proxy: http_proxy.clone(),
https_proxy: https_proxy.clone(),
accept_invalid_certs: *accept_invalid_certs,
})
}

/// Converts these [`TransportOptions`] into [`ClientOptions`].
///
/// This method is provided for backwards-compatibility with custom transports which cannot
/// be contructed from [`TransportOptions`] because they expect [`ClientOptions`].
///
/// Any fields on [`ClientOptions`] which are not present in [`TransportOptions`] will be
/// set to their default values.
pub(crate) fn into_client_options(self) -> ClientOptions {
let Self {
dsn,
user_agent,
http_proxy,
https_proxy,
accept_invalid_certs,
} = self;

let dsn = Some(dsn);

ClientOptions {
dsn,
user_agent,
http_proxy,
https_proxy,
accept_invalid_certs,
..Default::default()
}
}
Comment on lines +69 to +79

This comment was marked as spam.

}
105 changes: 92 additions & 13 deletions sentry/src/transports/curl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ use std::io::{Cursor, Read};
use std::time::Duration;

use curl::easy::Easy as CurlClient;
use sentry_core::TransportOptions;

use super::{thread::TransportThread, HTTP_PAYLOAD_TOO_LARGE, HTTP_PAYLOAD_TOO_LARGE_MESSAGE};
use super::{
thread::{TransportThread, TransportThreadOptions},
RateLimiter, HTTP_PAYLOAD_TOO_LARGE, HTTP_PAYLOAD_TOO_LARGE_MESSAGE,
};

use crate::{sentry_debug, types::Scheme, ClientOptions, Envelope, Transport};

Expand All @@ -15,30 +19,78 @@ pub struct CurlHttpTransport {
thread: TransportThread,
}

/// Options for constructing a [`CurlHttpTransport`].
///
/// Currently, this is primarily a wrapper around a [`TransportOptions`], and must be created with
/// the `From<TransportOptions>` implementation. Optionally, a [`curl::easy::Easy`] client for the
/// transport may be provided with [`Self::with_client`].
#[derive(Debug)]
#[must_use]
pub struct CurlHttpTransportOptions {
general_options: TransportOptions,
client: Option<CurlClient>,
}

impl CurlHttpTransport {
/// Creates a new Transport.
/// Backwards-compatible method for creating a [`CurlHttpTransport`].
///
/// Please use [`CurlHttpTransportOptions::build`] instead.
///
/// ### Panics
///
/// Panics if called with `options` that lack a DSN.
Comment on lines +39 to +41

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This panic is not new. It was previously undocumented.

#[inline]
#[deprecated = "use `CurlHttpTransportOptions::build` instead"]
pub fn new(options: &ClientOptions) -> Self {
Self::new_internal(options, None)
let general_options = TransportOptions::try_from_client_options(options)
.expect("this method should only be called when options has a DSN");

CurlHttpTransportOptions::from(general_options).build()
}

/// Creates a new Transport that uses the specified [`CurlClient`].
/// Backwards-compatible method for creating a [`CurlHttpTransport`] that uses the specified
/// [`CurlClient`].
///
/// Please use [`CurlHttpTransportOptions::build`] instead.
///
/// ### Panics
///
/// Panics if called with `options` that lack a DSN.
#[inline]
#[deprecated = "use `CurlHttpTransportOptions::build` instead"]
pub fn with_client(options: &ClientOptions, client: CurlClient) -> Self {
Self::new_internal(options, Some(client))
let general_options = TransportOptions::try_from_client_options(options)
.expect("this method should only be called when options has a DSN");

CurlHttpTransportOptions::from(general_options)
.with_client(client)
.build()
}

fn new_internal(options: &ClientOptions, client: Option<CurlClient>) -> Self {
/// Creates a new [`CurlHttpTransport`] with the given `options`.
#[inline]
pub(super) fn with_options(options: CurlHttpTransportOptions) -> Self {
let CurlHttpTransportOptions {
general_options:
TransportOptions {
dsn,
user_agent,
http_proxy,
https_proxy,
accept_invalid_certs,
..
},
client,
} = options;

let client = client.unwrap_or_else(CurlClient::new);
let http_proxy = options.http_proxy.as_ref().map(ToString::to_string);
let https_proxy = options.https_proxy.as_ref().map(ToString::to_string);
let dsn = options.dsn.as_ref().unwrap();
let user_agent = options.user_agent.clone();
let auth = dsn.to_auth(Some(&user_agent)).to_string();
let url = dsn.envelope_api_url().to_string();
let scheme = dsn.scheme();
let accept_invalid_certs = options.accept_invalid_certs;

let mut handle = client;
let thread = TransportThread::new(move |envelope, rl| {

let send_fn = move |envelope: Envelope, rl: &mut RateLimiter| {
handle.reset();
handle.url(&url).unwrap();
handle.custom_request("POST").unwrap();
Expand Down Expand Up @@ -130,7 +182,9 @@ impl CurlHttpTransport {
sentry_debug!("Failed to send envelope: {}", err);
}
}
});
};

let thread = TransportThreadOptions::new(send_fn).spawn_thread();
Self { thread }
}
}
Expand All @@ -147,3 +201,28 @@ impl Transport for CurlHttpTransport {
self.flush(timeout)
}
}

impl From<TransportOptions> for CurlHttpTransportOptions {
#[inline]
fn from(value: TransportOptions) -> Self {
Self {
general_options: value,
client: None,
}
}
}

impl CurlHttpTransportOptions {
/// Specify the [`CurlClient`] for the [`CurlHttpTransport`].
#[inline]
pub fn with_client(self, client: CurlClient) -> Self {
let client = Some(client);
Self { client, ..self }
}

/// Create a [`CurlHttpTransport`] using these options.
#[inline]
pub fn build(self) -> CurlHttpTransport {
CurlHttpTransport::with_options(self)
}
}
Loading
Loading