Unified API
Note
Everything written here sounds real nice. However, we live in a practical world, and expect each platform to adhere to what makese sense for the developers there. What is written below is not to be taken as strict rules. It is a guide and to be thought of as a tool to help developers build SDKs which work well which eachother.
The top priority when building SDKs is a high quality API and developer experience, don’t let unified API be an excuse to build things which don't make sense for a given language.
- Types Example: Typed languages might have very different developer experience than dynamic.
- Platform Example: Does Rust have exceptions? Nope, don't name APIs for rust like
captureException
.
Until recently, the unified API was centered around the concept of a Hub
. Some SDKs are still using this concept, but it is being phased out in favor of the new scope-based approach lined out in this document. Read about the Hub based API.
New Sentry SDKs should follow the unified API, use consistent terms to refer to concepts. This documentation explains what the unified API is and why it exists.
Motivation
Sentry has a wide range of SDKs that have been developed over the years by different developers and based on different ideas. This has lead to the situation that the feature sets across the SDKs are different, use different concepts and terms which has lead to the situation that it's often not clear how to achieve the same thing on different platforms.
Additionally those SDKs were purely centered around error reporting through explicit clients which meant that certain integrations (such as breadcrumbs) were often not possible.
General Guidelines
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 3rd 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.).
Simplified Visualization
TODO: Graphic of the SDK pieces interacting
Terminology
scope: A scope holds data that should implicitly be sent with Sentry events. It can hold context data, extra parameters, level overrides, fingerprints etc. There are three different scopes (global, isolation and current). To understand scopes and the differences between those you should read the user-facing documentation on Scopes before continuing.
client: A client is an object that is configured once and is bound to a scope. 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 should generally use top-level methods that may call the client under the hood. The client is mostly responsible for building Sentry events and dispatching those to the transport.
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.
context: Contexts give extra data to Sentry. There are the custom contexts (set by the user) and predefined ones (e.g.
runtime
,os
,device
). Check out Contexts for some predefined keys.tags: Tags can be arbitrary string→string pairs by which events can be searched.
extra: Truly arbitrary data attached by client users. Extra should be avoided where possible, and instead structured
context
should be used - as these can be queried and visualized in a better way.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.
event processors: Callbacks that run for every event.
They can either modify and return the event, ornull
. Whennull
is returned, the SDK MUST discard the event and not process it further.See Event Pipeline for more information.
disabled SDK: Most of the SDK functionality depends on a configured and active client. Sentry considers the client active when it has a transport. Otherwise, the client is inactive, and the SDK is considered "disabled". In this case, the SDK MUST NOT send any events, and SHOULD NOT register any (global) handlers.
Static API (or Top-Level API)
The static API functions is the most common user facing API. A user just imports these functions and can start emitting events to Sentry or configuring scopes. These shortcut functions should be exported in the top-level namespace of your package. Behind the scenes they use scopes and clients (see Concurrency for more information).
The static APIs should cover the 95% use case for a common user. They should be easy to use and understand, and should be considered the primary way users interact with the Sentry SDK.
init(options)
This is the entry point for every SDK. Every SDK MUST have an init
method. Calling init
will create & setup a new client, and bind it to the current scope.
The options passed in are SDK specific, but typically include a DSN, release, environment, etc.
This method SHOULD return a stand-in that can be used to drain events (a disposable), or the created client.
It SHOULD be possible to call init
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. In this case, the SDK will be disabled.
Calling init
MUST set up all default & defined integrations, UNLESS the SDK is disabled. When the SDK is disabled, it SHOULD NOT set up any integrations.
get_client()
Returns the currently active client. This MAY return null
if init
was never called. If init
was called but the SDK is disabled, this SHOULD return the disabled client.
Instead of retuning null
if the SDK was never initialized, this method MAY return a disabled (or a no-op) client.
capture_event(event)
Takes an already assembled event and dispatches it to the current scope.
This SHOULD be equivalent to calling get_current_scope().capture_event(event)
.
The event object can be a plain dictionary or a typed object whatever makes more sense in the SDK. It should follow the native protocol as close as possible ignoring platform specific renames (case styles etc.).
capture_exception(error)
Report an error or exception object and dispatches it to the current scope.
This SHOULD be equivalent to calling get_current_scope().capture_exception(error)
.
Depending on the platform different parameters are possible. The most obvious version accepts just an error object but also variations are possible where no error is passed and the current exception is used.
capture_message(message, level)
Reports a message and dispatches it to the current scope.
This SHOULD be equivalent to calling get_current_scope().capture_message(message)
.
The level can be optional in languages with default parameters in which case it should default to info
.
add_breadcrumb(crumb)
Adds a new breadcrumb to the isolation scope. If the totalnumber of breadcrumbs exceeds the max_breadcrumbs
setting, the SDK should remove the oldest breadcrumb.
This SHOULD be equivalent to calling get_isolation_scope().add_breadcrumb(crumb)
.
If the SDK is disabled, it SHOULD ignore the breadcrumb.
set_tag(key, value)
Set a tag on the isolation scope.
This SHOULD be equivalent to calling get_isolation_scope().set_tag(key, value)
.
set_context(name, context)
Set a context on the isolation scope.
This SHOULD be equivalent to calling get_isolation_scope().set_context(name, context)
.
set_extra(key, value)
Set extra data on the isolation scope.
This SHOULD be equivalent to calling get_isolation_scope().set_extra(key, value)
.
set_user(user)
Set the user on the isolation scope.
This SHOULD be equivalent to calling get_isolation_scope().set_user(user)
.
If null
is passed in, the user SHOULD be reset.
with_scope(callback)
Creates a new current scope that is active for the duration of the callback. This is useful for attaching contextual data to a subset of events that is dispatched inside of the provided callback.
The callback MUST receive the new scope as an argument. The new scope MUST be forked from the current scope, and the current scope MUST be restored after the callback is executed.
Mutating the new scope MUST NOT affect the previous current scope.
with_isolation_scope(callback)
Creates a new isolation scope that is active for the duration of the callback. This is useful for attaching contextual data to a subset of events that is dispatched inside of the provided callback.
The callback MUST receive the new isolation scope as an argument. The new isolation scope MUST be forked from the current isolation scope, and the current isolation scope MUST be restored after the callback is executed.
Mutating the new isolation scope MUST NOT affect the previous isolation scope.
This method MAY also fork the current scope (similar to with_scope
) in addition to forking the isolation scope.
get_current_scope()
Returns the currently active scope. This is the scope that is used for all events that are dispatched.
get_isolation_scope()
Returns the currently active isolation scope. This is the scope that is used to store breadcrumbs and contextual data that should be attached to events.
get_global_scope()
Returns the global scope, which is applied to all events dispatched by the SDK. This scope MUST be a singleton and shared across all instances of the SDK.
last_event_id()
Returns the last event ID that was emitted. The last event ID SHOULD be kept on the isolation scope, and thus SHOULD be kept per isolated process. This is for instance used to implement user feedback dialogs.
start_session()
Stores a session on the current scope and starts tracking it. This SHOULD attach a brand new session to the isolation scope, and implicitly ends any already existing session.
end_session()
Ends the session, setting an appropriate status
and duration
, and enqueues it for sending to Sentry.
Concurrency
All SDKs SHOULD have the concept of concurrency safe context storage, if the language permits it. What this means depends on the language.
The goal is that, wherever possible, concurrent operations can be automatically isolated from each other, in order to prevent data to leak between different operations. A classic example for this is to isolate requests in a HTTP server - if a user sets a user in this request, it should be applied to any event captured in this request, but not to events captured in other requests (because they may have a different user).
The SDK SHOULD automatically manage this isolation, so that the user does not have to worry about it. For cases where the SDK cannot do that automatically, the SDK MUST provide a way for the user to manually manage the isolation (for example via with_isolation_scope()
).
In client-side applications (Browser JS or mobile), there MAY not be a way to isolate code from each other. In this case, the SDK MAY fall back to handling state globally.
This is implemented as a thread local stack in most languages, or as a different construct (e.g. async local storage in Node.js) in others.
How to store concurrency data
Each thread local SHOULD hold a reference to the current scope & isolation scope that is active for the current thread. Newly spawned threads SHOULD inherit the reference to the isolation scope from the parent thread, and SHOULD create a new scope that is a child of the parent current scope, and make this the current scope for the child thread.
The isolation scope MAY be forked (instead of using the same reference) for a new thread if the SDK identifies that the new thread is a new logical operation that should not share data with the parent thread. For example, if the new thread is a new HTTP request in a server, it SHOULD have a new isolation scope.
In some environments it MAY be necessary to retrospectively change the isolation scope - for example, when it is not possible to know at the time of thread spawning if the new thread should be isolated. In this case, the SDK MAY retrospectively update the isolation scope on the thread local.
const initialCurrentScope = getCurrentScope();
const initialIsolationScope = getIsolationScope();
myApp.get('/my-route', function() {
// current scope is a fork of initialCurrentScope
assert(getCurrentScope() !== initialCurrentScope);
// This is a HTTP route handler, which we want to isolate from other routes
// So the isolation scope is a fork of the initialIsolationScope
assert(getIsolationScope() !== initialIsolationScope);
});
Calling with_scope
or with_isolation_scope
SHOULD spawn a new thread local that is active for the duration of the callback, and SHOULD restore the previous thread local after the callback is executed.
Environments without process isolation
In environments that do not support process isolation (e.g. Browser JS), the isolation scope should be considered a singleton, similar to the global scope.
In this case, with_isolation_scope
SHOULD NOT fork the isolation scope, but instead just keep the current isolation scope.
The current scope MAY be kept as a simple stack that is pushed and popped as needed. This stack MAY not be async safe, and may leak between different parts of the code. This should be avoided, if possible.
const initialCurrentScope = getCurrentScope();
const initialIsolationScope = getIsolationScope();
document.querySelector('#my-button').addEventListener('click', function() {
Sentry.withScope(scope => {
assert(getCurrentScope() !== initialCurrentScope);
assert(getIsolationScope() === initialIsolationScope);
// scope is valid until the end of this callback
// there are no guarantees that scope may not bleed outside of this callback
});
});
Scope
A scope holds data that should implicitly be sent with Sentry events. It can hold context data, extra parameters, level overrides, fingerprints etc.
The user can modify the current scope (to set extra, tags, current user) directly, but the main way for users to interact with the scope is through the Static API.
const scope = Sentry.getCurrentScope();
scope.setExtra("character_name", "Mighty Fighter"));
scope.set_user(user)
: Shallow merges user configuration (email
,username
, …). Removing user data is SDK-defined, either with aremove_user
function or by passing nothing as data.scope.set_extra(key, value)
: Sets the extra key to an arbitrary value, overwriting a potential previous value. Removing a key is SDK-defined, either with aremove_extra
function or by passing nothing as data. This is deprecated functionality and users should be encouraged to use contexts instead.scope.set_extras(extras)
: Sets an object with key/value pairs, convenience function instead of multipleset_extra
calls. As withset_extra
this is considered deprecated functionality.scope.set_tag(key, value)
: Sets the tag to a string value, overwriting a potential previous value. Removing a key is SDK-defined, either with aremove_tag
function or by passing nothing as data.scope.set_tags(tags)
: Sets an object with key/value pairs, convenience function instead of multipleset_tag
calls.scope.set_context(key, value)
: Sets the context key to a value, overwriting a potential previous value. Removing a key is SDK-defined, either with aremove_context
function or by passing nothing as data. The types are sdk specified.scope.set_level(level)
: Sets the level of all events sent within this scope.scope.set_transaction(transaction_name)
: Sets the name of the current transaction.scope.set_fingerprint(fingerprint[])
: Sets the fingerprint to group specific events togetherscope.add_event_processor(processor)
: Registers an event processor function. It takes an event and returns a new event orNone
to drop it. This is the basis of many integrations.scope.add_error_processor(processor)
(optional): Registers an error processor function. It takes an event and exception object and returns a new event orNone
to drop it. This can be used to extract additional information out of an exception object that the SDK cannot extract itself.scope.clear()
: Resets a scope to default values while keeping all registered event processors. This does not affect either child or parent scopes.scope.add_breadcrumb(breadcrumb)
: Adds a breadcrumb to the current scope.scope.clear_breadcrumbs()
(optional): Deletes current breadcrumbs from the scope.scope.capture_exception(exception)
: Capture an exception, ensuring the scope's data is added to the event.scope.capture_message(message)
: Capture a message, ensuring the scope's data is added to the event.scope.capture_event(event)
: Capture an event, ensuring the scope's data is added to the event.
A client MAY have a scope attached to it. Generally, the global & isolation scopes SHOULD NOT have a client attached, but only the current scope. The client is inherited by child scopes. This way, each current scope (which is used to capture events) knows which client it should route events to.
Applying Scope Data to Events
See user-facing Scope Docs on details about how scopes should be applied.
Client
A Client is the part of the SDK that is responsible for event creation. To give an example, the Client should convert an exception to a Sentry event. The Client should be stateless, it gets the Scope injected and delegates the work of sending the event to the Transport.
Client::from_config(config)
: (alternatively normal constructor) This takes typically an object with options + dsn.Client::capture_event(event, scope)
: Captures the event by merging it with other data with defaults from the client. In addition, if a scope is passed to this system, the data from the scope passes it to the internal transport.Client::close(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 might block for timeout seconds. The client should be disabled or disposed after close is calledClient::flush(timeout)
: Same asclose
difference is that the client is NOT disposed after calling flush
When get_client
is called, it SHOULD return the client that is currently bound to the current scope.
If no client is bound to the current scope, it SHOULD either return null
, or a Non-Recording (disabled) client.
get_current_hub()
(Legacy)
Previously, users interacted with the SDK through a Hub
object. This object was responsible for managing the current scope, client, and other SDK state.
While we do not have a hub anymore, we want to keep providing a get_current_hub()
API for backwards compatibility.
This method SHOULD return a shim or similar that allows to keep using old hub methods. This SHOULD NOT exist in new SDKs, but only in SDKs that are transitioning from the old hub-based API to the new scope-based API.
All methods that can be shimmed SHOULD continue to be available. Methods (like push_scope
or pop_scope
) that cannot be shimmed MAY be removed or MAY no-op.
Hints
Optionally an additional parameter is supported to event capturing and breadcrumb adding: a hint.
A hint is SDK specific but provides high level information about the origin of the event. For instance if an exception was captured the hint might carry the original exception object. Not all SDKs are required to provide this. The parameter however is reserved for this purpose.
Event Pipeline
An event captured by capture_event
is processed in the following order.
Note: The event can be discarded at any of the stages, at which point no further
processing happens.
- If the SDK is disabled, Sentry discards the event right away.
- The client samples events as defined by the configured sample rate. Events may be discarded randomly, according to the sample rate.
- The scope is applied, using
apply_to_event
. The scope’s event processors are invoked in order. - Sentry invokes the before-send hook.
- Sentry passes the event to the configured transport. The transport can discard the event if it does not have a valid DSN; its internal queue is full; or due to rate limiting, as requested by the server.
Options
Many options are standardized across SDKs. For a list of these refer to the main options documentation.