Motivation for New SDKs
- We want a unified language/wording of all SDK APIs to aid support and documentation as well as making it easier for users to use Sentry in different environments.
- Design the SDK in a way where we can trivially add new features later that go past pure event reporting (transactions, APM etc.)
- Design the SDK that with the same client instance we can both work naturally in the runtime environment through dependency injection etc. as well as using an implied context dispatching to already existing clients and scopes to hook into most environments. This is important because it allows events to include data from other integrations in the process.
- The common tasks need to be easy and obvious
- For helping third party libraries the case of “non configured sentry” needs to be fast (and lazily executed)
- The common API needs make sense in most languages and must not depend on super special constructs. To make it feel more natural we should consider language specifics and explicitly support them as alternatives (disposables, stack guards etc.)
Terminology
- minimal: a minimal layer that provides a way to configure data and emit breadcrumbs and events to a configured sentry client. These layers are typically distributed separately or a library can be reconfigured to remove all actual reporting. Names might be minimal, abstraction or interface.
- hub: an object that manages the state. An implied global thread local or similar hub exists that can be used by default. Hubs can be created manually.
- context: contexts give extra data to sentry. There are the special contexts (user and similar) and the generic ones (runtime, os, device) etc. Check out https://docs.sentry.io/clientdev/interfaces/contexts/ for valid keys
- tags: tags can be arbitrary string→string pairs by which events can be searched. Contexts are converted into tags.
- extra: truly arbitrary data attached by client users
- scope: a scope holds data that should implicitly be sent with sentry events. It can hold context data, extra parameters, level overrides, fingerprints etc.
- client: a client is an object that is configured once and can be bound to the hub. The user can then auto discover the client and dispatch calls to it. Users typically do not need to work with the client directly. They either do it via the hub or static convenience functions.
- client options: are parameters that are language and runtime specific and used to configure the client. This can be release and environment but also things like which integrations to configure, how in-app works etc.
- transport: The transport is an internal construct of the client that abstracts away the event sending. Typically the transport runs in a separate thread and gets events to send via a queue. The transport is responsible for sending, retrying and handling rate limits. The transport might also persist unsent events across restarts if needed.
- integration: code that provides middlewares, bindings or hooks into certain frameworks or environments, along with code that inserts those bindings and activates them. Usage for integrations does not follow a common interface.
API Basic Guidelines
- we have a bunch of static methods / functions either on the root namespace or a helper class (named
Sentry
orSentry``Sdk
) - SDKs are supposed to export an
init()
method which binds the root client and optionally can enable integrations - An SDK supports multiple clients but does not have to support multiple hubs in cases where flow contexts are working well.
- The hub provides internal abstractions for scope and client management (push / pop etc.). It might also contain additional information that can help test the SDK (like clock sources in rust etc. where we don’t have the ability to monkey patch)
- The user is responsible for disposing the clients. There might be helpers to aid with that (for instance a guard returned from init or global helper functions).
- Integrations are patched once and never unpatched!
- The client’s capture method must never auto discover state from an out of client context which means a client is free to manage its own scope stack. However if the client is auto discovered from context local data it is not allowed to used context local data from the same out of client source to find the scope object.
Threading Behavior
- The hub should be internally synchronized and optionally have an unsynchronized mode if languages where this is possible (rust).
- Scopes can have restricted synchronization as they do not provide a writable interface once bound to a hub
- The client internally needs to be synchronized
Scope Propagation
- Follow thread of execution if possible (that’s what we do in .NET by using async locals through the execution context)
- Implicit new scope in languages where zones/domains are created (javascript)
- When threads cannot inherit data form parent threads the thread which called into
init()
becomes a special thread. Hubs created in that thread are duplicated into other threads automatically.
Common API
Functions marked with stars are optional in SDKs but SDKs should attempt to provide a path for adding them later (eg: not build APIs that contradict this contract).
When this document talks about global functions, it restricts this to languages that have that. In Java and .NET we need to put these functions into a static class. When such static class is required, ideally it should be named Sentry
. When that is not possible (e.g: namespace is already Sentry
), prefer the name Sentry``Sdk
.
Initialization
-
init(cfg)
:- takes a configuration hint (dsn etc.), configures a client and binds it to the current hub or initializes it. Should return a stand-in that can be used to drain events (a disposable)
- This might return a handle or guard for disposing. How this is implemented is entirely up to the SDK. This might even be a client if that’s something that makes sense for the SDK. In Rust it’s a
ClientInitGuard
, in JavaScript it could be a helper object with a close method that is awaitable. - You should be able to call this multiple times where calling it a second time either tears down the previous client or decrements a refcount for the previous client etc.
- Calling this multiple times should be used for testing only
- It’s undefined what happens if you call init on anything but application startup
- A user has to call init once but it’s permissible to call this with a disabled DSN of sorts. Might for instance be no parameter passed etc.
Hub API
The hub consists of a client + a stack of scopes. init()
typically creates / reinitializes the global hub which internally flows with the execution (async locals) or a hub is created per thread. If a hub can be accessed from multiple threads or not is implementation defined but should be clearly documented.
-
Hub::new(client, scope)
:- Creates a new hub with the given client and scope. The client can be reused between hubs.
- The scope should be owned by the hub (make a clone if necessary)
-
Hub::new_from_top(hub)
/ alternatively native constructor overloads:- Creates a new hub by cloning the top stack of another hub.
-
get_``current``_hub()
/Hub::``current``()
/Hub::get_``current``()
:- Global function or static function to return the current (threads) hub
-
get_main_hub()
/Hub::main()
/Hub::get_main()
:- In languages where the main thread is special this returns the main thread’s hub instead of the current thread’s hub.
- This might not exist in all languages.
-
Hub::capture_event
/Hub::capture_message
/Hub::capture_exception
- capture message / exception call into capture event
- capture event merges the event passed with the scope data and dispatches to the client
-
Hub::push_scope()
:- pushes a new scope layer that inherits the previous data
- this should return a disposable or stack guard for languages where it makes sense
-
Hub::with_scope(func)
: *- Optional helper that pushes and pops a scope for integration work.
-
Hub::pop_scope()
: *- only exists in languages without better resource management.
- Better is return value of
push_scope
to have a method that does it or be a disposable
-
Hub::configure_scope(callback)
:- Invokes the callback with a mutable reference to the scope for modifiations
-
Hub::add_breadcrumb(crumb)
:- Adds a breadcrumb to the current scope.
- The argument supported should be:
- function that creates a breadcrumb
- an already created breadcrumb object
- a list of breadcrumbs optionally
- In languages where we do not have a basic form of overloading only a raw breadcrumb object should be accepted.
-
Hub::client()
/Hub::get_client()
: *- Accessor or getter that returns the current client or
None
.
- Accessor or getter that returns the current client or
-
Hub::bind_client(new_client)
:- Binds a different client to the hub. If the hub is also the owner of the client that was created by
init
it needs to keep a reference to it still if the hub is the object responsible for disposing it.
- Binds a different client to the hub. If the hub is also the owner of the client that was created by
-
Hub::unbind_client()
: *- Optional way to unbind for languages where
bind_client
does not accept nullables.
- Optional way to unbind for languages where
-
Hub::drain_events(timeout)
:- Flushes out pending events with the given deadline.
-
Hub::add_event_processor(callback)
:- Registers a callback with the hub that returns a callback that processes a specific event. This allows the processor to hold on to closure data until the scope might have to be sent to another thread:
add_event_processor(() => (event) => …))
- The initial closure can be persisted until the scope needs to be stored or sent to a thread after which the inner closure is retained.
- Registers a callback with the hub that returns a callback that processes a specific event. This allows the processor to hold on to closure data until the scope might have to be sent to another thread:
-
Hub::run(hub, callback)
hub.run(callback)
,run_in_hub(hub, callback)
: *- Runs a callback with the hub bound as the current hub.
Client API
-
Client::from_config(config)
(alternatively normal ctor)- this takes typically:
- or a dsn
- or a tuple of dsn + options
- through this API disabled clients should not be created
- either have a separate class
- or have an
Optional<Client>
return value
- this takes typically:
-
Client::capture_event(event, scope)
- captures the the event by merging it with other data with defaults from the client + if a scope is passed to this system the data from the scope.and passes it on to the internal transport.
-
Client::drain_events(timeout)
: *- Flushes out the queue for up to timeout seconds
- If the client can guarantee delivery of events only up to the current point in time this is preferred
- This can also be just on the hub optionally
Scope (within configure``_scope
)
-
set``_u``ser(data)
:- Shallow merges user configuration (
email
,username
, …). Removing user data is SDK-defined, either with aremove_user
function or by passing nothing as data.
- Shallow merges user configuration (
-
set``_e``xtra(key, value)
:- Sets the extra key to an arbitrary value, overwriting a potential previous value. Removing a key is SDK-defined, either with a
remove_extra
function or by passing nothing as data.
- Sets the extra key to an arbitrary value, overwriting a potential previous value. Removing a key is SDK-defined, either with a
-
set_tag(key, value)
:- Sets the tag to a string value, overwriting a potential previous value. Removing a key is SDK-defined, either with a
remove_tag
function or by passing nothing as data.
- Sets the tag to a string value, overwriting a potential previous value. Removing a key is SDK-defined, either with a
-
set_``context``(key, value)
:- Sets the context key to a value, overwriting a potential previous value. Removing a key is SDK-defined, either with a
remove_``context
function or by passing nothing as data. The types are sdk specified
- Sets the context key to a value, overwriting a potential previous value. Removing a key is SDK-defined, either with a
-
set_fingerprint(fingerprint[])
:- Sets the fingerprint to group specific events together
-
clear()
:- resets a scope to default values (prevents inheriting)
-
apply_to_event(event[, max_breadcrumbs])
:- Applies the scope data to the given event object. This also applies the event processors stored in the scope internally.
- Some implementations might want to set a max breadcrumbs count here.
Minimal API Convenience Functions
-
capture_event(event)
:- takes an already assembled event and dispatches it to the client
-
capture_exception(…)
:- convenience api to report an error or exception
- This might not exist in some languages (like rust where this is type specific)
-
capture_message(message, level)
:- reports a message. The level can be optional in language with default parameters.
-
add``_b``readcrumb(``crumb``)
:- Adds a new breadcrumb to the scope. If the total number of breadcrumbs exceeds the
maxBreadcrumbs
setting, the oldest breadcrumb should be removed in turn. This works like the Hub api with regards to whatcrumb
can be.
- Adds a new breadcrumb to the scope. If the total number of breadcrumbs exceeds the
-
configure``_``scope(callback)
:- calls a callback with a scope object that can be reconfigured. This is used to attach contextual data for future events in the same scope. This is a convenience alias to
Hub::current().configure_scope(…)
.
- calls a callback with a scope object that can be reconfigured. This is used to attach contextual data for future events in the same scope. This is a convenience alias to
-
with_active_hub(callback)
/Hub::with_active(callback)
:- Invokes the callback which is passed the default hub if a client is bound or does nothing if the client is not bound.
- This one is intended for integrations and in some systems that might not be necessary to add. The advantage of having it means that a user cannot accidentally create a sentry structure in a disabled environment. Alternatively a user would need to check upfront if the client is active.
Api Extensions
-
Scope::clone
(or native clone system)- clone the scope. This is for instance currently exposed in Rust and similar behavior is expected in SDKs where this makes sense (.NET)
-
Scope::default()
(or native defaulting like default ctors)- Returns a new empty default scope (scope as if
clear
) was called on it. This makes sense to provide in systems where working with unbound clients is expected.
- Returns a new empty default scope (scope as if
-
Scope::add_breadcrumb(data[, max_breadcrumbs])
- Adds a new breadcrumb specifically to a scope. How many breadcrumbs can be contained on a scope is implementation specific.