Unified 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, or null. When null 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.

Copied
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.

Copied
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.

Copied
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 a remove_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 a remove_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 multiple set_extra calls. As with set_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 a remove_tag function or by passing nothing as data.

  • scope.set_tags(tags): Sets an object with key/value pairs, convenience function instead of multiple set_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 a remove_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 together

  • scope.add_event_processor(processor): Registers an event processor function. It takes an event and returns a new event or None 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 or None 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 called

  • Client::flush(timeout): Same as close 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.

  1. If the SDK is disabled, Sentry discards the event right away.
  2. The client samples events as defined by the configured sample rate. Events may be discarded randomly, according to the sample rate.
  3. The scope is applied, using apply_to_event. The scope’s event processors are invoked in order.
  4. Sentry invokes the before-send hook.
  5. 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.

You can edit this page on GitHub.