diff --git a/src/docs/sdk/hub-based-api/index.mdx b/src/docs/sdk/hub-based-api/index.mdx
new file mode 100644
index 0000000000..a539592827
--- /dev/null
+++ b/src/docs/sdk/hub-based-api/index.mdx
@@ -0,0 +1,274 @@
+---
+title: 'Hub Based API'
+---
+
+
+
+This page describes what used to be the "Unified API".
+Previously, the Unified API was centered around a Hub as a central unit of control over the SDK.
+
+However, we are now moving away from the Hub-based API to a system centered around Scopes.
+
+Some SDKs (like the JavaScript SDK) have already moved to the new system, while others are in the process of transitioning.
+
+Read more about how to migrate away from Hubs in an SDK.
+
+
+
+## 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
+
+
+
+## Terminology
+
+- **minimal**: A separate "facade" package that re-exports a subset of the SDK's functionality through interfaces or proxies. That package does not directly depend on the SDK, instead it should make every operation a noop if the SDK is not installed.
+
+ The purpose of such a package is to allow random libraries to record breadcrumbs and set context data while not having a hard dependency on the SDK.
+
+- **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.
+
+- **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. 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 special contexts (user and similar) and the generic ones (`runtime`, `os`, `device`), etc. Check out Contexts for some predefined keys - users can also add arbitrary context keys. *Note: In older SDKs, you might encounter an unrelated concept of context, which is now deprecated by scopes*
+
+- **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. Extra should be avoided where possible, and instead structured `context` or `tags` 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`.
+ Returning `null` will discard the event and not process further.
+
+ See [Event Pipeline](#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, certain callbacks, such as `configure_scope` or
+ *event processors*, may not be invoked.
+ As a result, breadcrumbs are not recorded.
+
+
+## "Static 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 hubs and scopes (see [Concurrency](#concurrency) for more information) if available on that platform.
+Note that all listed functions below are mostly aliases for `Hub::get_current().function`.
+
+- `init(options)`: This is the entry point for every SDK.
+
+This typically creates / reinitializes the global hub which is propagated to all new threads/execution contexts, or a hub is created per thread/execution context.
+
+Takes options (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.
+
+Additionally it also sets up all default integrations.
+
+- `capture_event(event)`: Takes an already assembled event and dispatches it to the currently active hub. 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. 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. The level can be optional in language with default parameters in which case it should default to `info`.
+
+- `add_breadcrumb(crumb)`: Adds a new breadcrumb to the scope. If the total
+ number of breadcrumbs exceeds the `max_breadcrumbs` setting, the SDK should
+ remove the oldest breadcrumb. This works like the Hub API with regards to what
+ `crumb` can be. If the SDK is disabled, it should ignore the breadcrumb.
+
+- `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.
+
+- `last_event_id()`: Should return the last event ID emitted by the current scope. This is for instance used to implement user feedback dialogs.
+
+- `start_session()`: Stores a session on the current scope and starts tracking it. This normally attaches a brand new session to the 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. What this means depends on the language. The basic idea is that a user of the SDK can call a method to safely provide additional context information for all events that are about to be recorded.
+
+This is implemented as a thread local stack in most languages, but in some (such as JavaScript) it might be global under the assumption that this is something that makes sense in the environment.
+
+Here are some common concurrency patterns:
+
+* **Thread bound hub**: In that pattern each thread gets its own "hub" which internally manages a stack of scopes. If that pattern is followed one thread (the one that calls `init()`) becomes the "main" hub which is used as the base for newly spawned threads which will get a hub that is based on the main hub (but otherwise independent).
+
+* **Internally scoped hub**: On some platforms such as .NET ambient data is available in which case the Hub can internally manage the scopes.
+
+* **Dummy hub**: On some platforms concurrency just doesn't inherently exist. In that case the hub might be entirely absent or just be a singleton without concurrency management.
+
+## Hub
+
+Under normal circumstances the hub consists of a stack of clients and scopes.
+
+The SDK maintains two variables: The *main hub* (a global variable) and the *current hub* (a variable local to the current thread or execution context, also sometimes known as async local or context local)
+
+- `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 (thread's) hub
+
+- `get_main_hub()` / `Hub::main()` / `Hub::get_main()`: In languages where the main thread is special ("Thread bound hub" model) 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. As an additional argument it also takes a Hint.
+
+ For the hint parameter see [hints](#hints).
+
+- `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. When the "internally scoped hub" concurrency model is used calls to this are often necessary as otherwise a scope might be accidentally incorrectly shared.
+
+- `Hub::with_scope(callback)` (optional): In Python this could be a context manager, in Ruby a block function. Pushes and pops a scope for integration work.
+
+- `Hub::pop_scope()` (optional): Only exists in languages without better resource management. Better to have this function on a return value of `push_scope` or to use `with_scope`. This is also sometimes called `pop_scope_unsafe` to indicate that this method should not be used directly.
+
+- `Hub::configure_scope(callback)`: Invokes the callback with a mutable reference to the scope for modifications. This can also be a `with` statement in languages that have it (Python). If no active client is bound to this hub, the SDK should not invoke the callback.
+
+- `Hub::add_breadcrumb(crumb, hint)`: 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 (optional)
+ - In languages where we do not have a basic form of overloading only a raw breadcrumb object should be accepted.
+
+ The SDK should ignore the breadcrumb if no active client is bound to this hub.
+
+ For the hint parameter see [hints](#hints).
+
+- `Hub::client()` / `Hub::get_client()` (optional): Accessor or getter that returns the current client or `None`.
+
+- `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.
+
+- `Hub::unbind_client()` (optional): Optional way to unbind for languages where `bind_client` does not accept nullables.
+
+- `Hub::last_event_id()`: Should return the last event ID emitted by the current scope. This is for instance used to implement user feedback dialogs.
+
+- `Hub::run(hub, callback)` `hub.run(callback)`, `run_in_hub(hub, callback)` (optional): Runs a callback with the hub bound as the current hub.
+
+### 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) through the global function `configure_scope`. `configure_scope` takes a callback function to which it passes the current scope.
+
+The reason for this callback-based API is efficiency. If the SDK is disabled,
+it should not invoke the callback, thus avoiding unnecessary work.
+
+```javascript
+Sentry.configureScope(scope =>
+ 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()`: Deletes current breadcrumbs from the scope.
+
+- `scope.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.
+
+## 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
+
+## 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](https://docs.sentry.io/error-reporting/configuration/).
diff --git a/src/docs/sdk/hub-based-api/unified-api.excalidraw b/src/docs/sdk/hub-based-api/unified-api.excalidraw
new file mode 100644
index 0000000000..0526c95d73
--- /dev/null
+++ b/src/docs/sdk/hub-based-api/unified-api.excalidraw
@@ -0,0 +1,796 @@
+{
+ "type": "excalidraw",
+ "version": 1,
+ "source": "https://excalidraw.com",
+ "elements": [
+ {
+ "id": "3qrrTZfINNLLc6KPzSIe5",
+ "type": "rectangle",
+ "x": 535.05859375,
+ "y": 349.3828125,
+ "width": 201.1640625,
+ "height": 129.2265625,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 2059412052,
+ "version": 159,
+ "versionNonce": 894658644,
+ "isDeleted": false
+ },
+ {
+ "id": "D8SCAgr6FsN23HLJOKMet",
+ "type": "text",
+ "x": 552.67578125,
+ "y": 390.64453125,
+ "width": 164,
+ "height": 34,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 1202240364,
+ "version": 494,
+ "versionNonce": 1994573780,
+ "isDeleted": false,
+ "text": "Static API",
+ "font": "28px Cascadia",
+ "textAlign": "center",
+ "baseline": 27
+ },
+ {
+ "id": "8zlaJQnroS_bdx0IB9kOu",
+ "type": "text",
+ "x": 519.68359375,
+ "y": 325.12109375,
+ "width": 235,
+ "height": 20,
+ "angle": 0,
+ "strokeColor": "#5f3dc4",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 1133652972,
+ "version": 132,
+ "versionNonce": 5577196,
+ "isDeleted": false,
+ "text": "User Entry Point / Public API",
+ "font": "16px Virgil",
+ "textAlign": "center",
+ "baseline": 14
+ },
+ {
+ "id": "lIn7gig0vxn7USQVnv-pw",
+ "type": "rectangle",
+ "x": 535.19140625,
+ "y": 522.875,
+ "width": 201.1640625,
+ "height": 129.2265625,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 1396089964,
+ "version": 270,
+ "versionNonce": 241673812,
+ "isDeleted": false
+ },
+ {
+ "id": "iPFO2eb42UnKozxmNvl7a",
+ "type": "text",
+ "x": 610.30859375,
+ "y": 564.13671875,
+ "width": 49,
+ "height": 34,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 474294356,
+ "version": 611,
+ "versionNonce": 682359148,
+ "isDeleted": false,
+ "text": "Hub",
+ "font": "28px Cascadia",
+ "textAlign": "center",
+ "baseline": 27
+ },
+ {
+ "id": "UNYMxoo4j7_hEpUW_HL1g",
+ "type": "arrow",
+ "x": 633.296875,
+ "y": 480.6171875,
+ "width": 1.4375,
+ "height": 38.80078125,
+ "angle": 0,
+ "strokeColor": "#5f3dc4",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 673782380,
+ "version": 283,
+ "versionNonce": 533790804,
+ "isDeleted": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -1.4375,
+ 38.80078125
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "o7UWhMo8dq-T4y2QWZ2pU",
+ "type": "text",
+ "x": 657.26953125,
+ "y": 500.28125,
+ "width": 77,
+ "height": 20,
+ "angle": 0,
+ "strokeColor": "#5f3dc4",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 1026058708,
+ "version": 56,
+ "versionNonce": 546222700,
+ "isDeleted": false,
+ "text": "The SDK",
+ "font": "16px Virgil",
+ "textAlign": "center",
+ "baseline": 14
+ },
+ {
+ "id": "I39fc5fWNUYNFRk-h9CVP",
+ "type": "rectangle",
+ "x": 323.375,
+ "y": 714.41015625,
+ "width": 201.1640625,
+ "height": 129.2265625,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 1970382572,
+ "version": 307,
+ "versionNonce": 130975060,
+ "isDeleted": false
+ },
+ {
+ "id": "0h_EsmP2wCWmULcvM8Muh",
+ "type": "text",
+ "x": 373.9921875,
+ "y": 755.671875,
+ "width": 98,
+ "height": 34,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 286363092,
+ "version": 657,
+ "versionNonce": 1990960236,
+ "isDeleted": false,
+ "text": "Client",
+ "font": "28px Cascadia",
+ "textAlign": "center",
+ "baseline": 27
+ },
+ {
+ "id": "Q3LK7K8CsdxtFd9ecVt6Y",
+ "type": "rectangle",
+ "x": 751.69921875,
+ "y": 719.10546875,
+ "width": 201.1640625,
+ "height": 129.2265625,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 1010018004,
+ "version": 332,
+ "versionNonce": 1551987180,
+ "isDeleted": false
+ },
+ {
+ "id": "g6M83zOXf9LSWWw5-tQb3",
+ "type": "text",
+ "x": 810.31640625,
+ "y": 760.3671875,
+ "width": 82,
+ "height": 34,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 257810540,
+ "version": 681,
+ "versionNonce": 373486700,
+ "isDeleted": false,
+ "text": "Scope",
+ "font": "28px Cascadia",
+ "textAlign": "center",
+ "baseline": 27
+ },
+ {
+ "id": "nYQDtinU4bPwIpD_cKVNs",
+ "type": "rectangle",
+ "x": 323.9921875,
+ "y": 926.56640625,
+ "width": 201.1640625,
+ "height": 129.2265625,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 295479508,
+ "version": 380,
+ "versionNonce": 482137812,
+ "isDeleted": false
+ },
+ {
+ "id": "HQCeUCvXALRSFQy0izHta",
+ "type": "text",
+ "x": 349.609375,
+ "y": 967.828125,
+ "width": 148,
+ "height": 34,
+ "angle": 0,
+ "strokeColor": "#000000",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 1652424300,
+ "version": 742,
+ "versionNonce": 1369455956,
+ "isDeleted": false,
+ "text": "Transport",
+ "font": "28px Cascadia",
+ "textAlign": "center",
+ "baseline": 27
+ },
+ {
+ "id": "TCWwdAZlVotm3d0HFIp9h",
+ "type": "text",
+ "x": 323.88671875,
+ "y": 690.86328125,
+ "width": 105,
+ "height": 20,
+ "angle": 0,
+ "strokeColor": "#5f3dc4",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 347216852,
+ "version": 189,
+ "versionNonce": 1280075220,
+ "isDeleted": false,
+ "text": "Builds Events",
+ "font": "16px Virgil",
+ "textAlign": "center",
+ "baseline": 14
+ },
+ {
+ "id": "EBgjmEW0iSesbG7mkt3dC",
+ "type": "text",
+ "x": 843.4453125,
+ "y": 696.54296875,
+ "width": 109,
+ "height": 20,
+ "angle": 0,
+ "strokeColor": "#5f3dc4",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 291326700,
+ "version": 327,
+ "versionNonce": 387599724,
+ "isDeleted": false,
+ "text": "Holds context",
+ "font": "16px Virgil",
+ "textAlign": "center",
+ "baseline": 14
+ },
+ {
+ "id": "bZicz-MfeDeIR4M_gRUyS",
+ "type": "text",
+ "x": 421.0078125,
+ "y": 903.125,
+ "width": 104,
+ "height": 20,
+ "angle": 0,
+ "strokeColor": "#5f3dc4",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 4,
+ "roughness": 0,
+ "opacity": 100,
+ "seed": 20829012,
+ "version": 270,
+ "versionNonce": 1277143508,
+ "isDeleted": false,
+ "text": "Sends Events",
+ "font": "16px Virgil",
+ "textAlign": "center",
+ "baseline": 14
+ },
+ {
+ "id": "Z3XFb6ovUu0X4rFsTqkgs",
+ "type": "arrow",
+ "x": 746.546875,
+ "y": 715.09765625,
+ "width": 42.40625,
+ "height": 56.92578125,
+ "angle": 0,
+ "strokeColor": "#5f3dc4",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 568160596,
+ "version": 59,
+ "versionNonce": 1629277012,
+ "isDeleted": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -42.40625,
+ -56.92578125
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "dfQiAjC3AGvW0IKLkjx17",
+ "type": "arrow",
+ "x": 562.2265625,
+ "y": 659.91796875,
+ "width": 42.40234375,
+ "height": 48.24609375,
+ "angle": 0,
+ "strokeColor": "#5f3dc4",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 1651380716,
+ "version": 180,
+ "versionNonce": 968224364,
+ "isDeleted": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -42.40234375,
+ 48.24609375
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "KpeVQFm6Ja78yz_vKDrew",
+ "type": "arrow",
+ "x": 355.5121527777771,
+ "y": 849.7065972222215,
+ "width": 0.38671875,
+ "height": 69.19921875,
+ "angle": 0,
+ "strokeColor": "#087f5b",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 2133897708,
+ "version": 104,
+ "versionNonce": 801041876,
+ "isDeleted": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.38671875,
+ 69.19921875
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "mzbRuT3zALH5vybgsp6dV",
+ "type": "text",
+ "x": 318.58550347222194,
+ "y": 372.38671874999966,
+ "width": 197,
+ "height": 19,
+ "angle": 0,
+ "strokeColor": "#1864ab",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 1947945068,
+ "version": 283,
+ "versionNonce": 1115983084,
+ "isDeleted": false,
+ "text": "e.g. captureMessage()",
+ "font": "16px Cascadia",
+ "textAlign": "center",
+ "baseline": 15
+ },
+ {
+ "id": "xRrKT4YHMckTHlHsfJ_m0",
+ "type": "arrow",
+ "x": 413.1584201388889,
+ "y": 406.8285590277777,
+ "width": 1.5525173611110858,
+ "height": 102.37065972222229,
+ "angle": 0,
+ "strokeColor": "#1864ab",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 1853976684,
+ "version": 1073,
+ "versionNonce": 334457452,
+ "isDeleted": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 1.5525173611110858,
+ 102.37065972222229
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "Z4JpDECxUwk06gSZT1Ccm",
+ "type": "text",
+ "x": 272.3671875,
+ "y": 523.78515625,
+ "width": 253,
+ "height": 19,
+ "angle": 0,
+ "strokeColor": "#1864ab",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 716437356,
+ "version": 475,
+ "versionNonce": 1997504596,
+ "isDeleted": false,
+ "text": "getCurrentHub().getClient()",
+ "font": "16px Cascadia",
+ "textAlign": "center",
+ "baseline": 15
+ },
+ {
+ "id": "SOB6n6jdQ91-EWiReaYIc",
+ "type": "arrow",
+ "x": 400.74913194444446,
+ "y": 553.6171875,
+ "width": 156.46788194444446,
+ "height": 149.41796875,
+ "angle": 0,
+ "strokeColor": "#1864ab",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 1813251540,
+ "version": 688,
+ "versionNonce": 822953196,
+ "isDeleted": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -156.46788194444446,
+ 149.41796875
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "keD-eEvlLxmthcsF4uLbH",
+ "type": "text",
+ "x": 152.44921875,
+ "y": 716.65234375,
+ "width": 150,
+ "height": 19,
+ "angle": 0,
+ "strokeColor": "#1864ab",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 785293548,
+ "version": 408,
+ "versionNonce": 722760172,
+ "isDeleted": false,
+ "text": "captureMessage()",
+ "font": "16px Cascadia",
+ "textAlign": "center",
+ "baseline": 15
+ },
+ {
+ "id": "vpUsAc-0bsh-k8FMce2Vu",
+ "type": "arrow",
+ "x": 236.17730034722223,
+ "y": 746.9331597222222,
+ "width": 0.45876736111108585,
+ "height": 168.59027777777783,
+ "angle": 0,
+ "strokeColor": "#1864ab",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 1630891604,
+ "version": 1235,
+ "versionNonce": 2046521708,
+ "isDeleted": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ 0.45876736111108585,
+ 168.59027777777783
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "T1H9e1vQU9ZxwHzpoj6Zd",
+ "type": "text",
+ "x": 183.07421875000006,
+ "y": 930.4166666666654,
+ "width": 103,
+ "height": 19,
+ "angle": 0,
+ "strokeColor": "#1864ab",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 1590749908,
+ "version": 511,
+ "versionNonce": 204931668,
+ "isDeleted": false,
+ "text": "sendEvent()",
+ "font": "16px Cascadia",
+ "textAlign": "center",
+ "baseline": 15
+ },
+ {
+ "id": "vZQrtY9rRmdLbNnvCMd3r",
+ "type": "text",
+ "x": 193.4244791666663,
+ "y": 583.0972222222213,
+ "width": 145,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1864ab",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 1500684780,
+ "version": 286,
+ "versionNonce": 35291476,
+ "isDeleted": false,
+ "text": "Hub passes scope \nto client",
+ "font": "16px Virgil",
+ "textAlign": "center",
+ "baseline": 34
+ },
+ {
+ "id": "fHx64-LJymubfAQsjQHhn",
+ "type": "text",
+ "x": 34.35677083333337,
+ "y": 799.8116319444445,
+ "width": 191,
+ "height": 40,
+ "angle": 0,
+ "strokeColor": "#1864ab",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 968432492,
+ "version": 320,
+ "versionNonce": 248667604,
+ "isDeleted": false,
+ "text": "Client applies scope and\nbuilds an event",
+ "font": "16px Virgil",
+ "textAlign": "center",
+ "baseline": 34
+ },
+ {
+ "id": "zT7r5L3f3IQDdZDRYvoYp",
+ "type": "arrow",
+ "x": 853.8064236111111,
+ "y": 683.8307291666667,
+ "width": 20.468750000000114,
+ "height": 20.785590277777942,
+ "angle": 0,
+ "strokeColor": "#087f5b",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 1624805972,
+ "version": 265,
+ "versionNonce": 1456339412,
+ "isDeleted": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -20.468750000000114,
+ -20.785590277777942
+ ]
+ ],
+ "lastCommittedPoint": null
+ },
+ {
+ "id": "VyMS4kBYw5lFIOTS2YcgA",
+ "type": "text",
+ "x": 786.0963541666661,
+ "y": 537.9322916666672,
+ "width": 112,
+ "height": 120,
+ "angle": 0,
+ "strokeColor": "#087f5b",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 1241057108,
+ "version": 349,
+ "versionNonce": 990088300,
+ "isDeleted": false,
+ "text": "- Breadcrumbs\n- Extra\n- Tags\n- User\n- Level\n....",
+ "font": "16px Virgil",
+ "textAlign": "left",
+ "baseline": 114
+ },
+ {
+ "id": "9HnhE8jRX0u_JTU-3-ysO",
+ "type": "text",
+ "x": 782.9722222222227,
+ "y": 266.33420138888835,
+ "width": 71,
+ "height": 25,
+ "angle": 0,
+ "strokeColor": "#087f5b",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 2104832468,
+ "version": 473,
+ "versionNonce": 1041646700,
+ "isDeleted": false,
+ "text": "Options",
+ "font": "20px Virgil",
+ "textAlign": "left",
+ "baseline": 18
+ },
+ {
+ "id": "2yFB3wT9XsLDHer8NE-pB",
+ "type": "arrow",
+ "x": 819.2361111111111,
+ "y": 296.4947916666667,
+ "width": 68.79340277777771,
+ "height": 76.04600694444446,
+ "angle": 0,
+ "strokeColor": "#087f5b",
+ "backgroundColor": "transparent",
+ "fillStyle": "hachure",
+ "strokeWidth": 2,
+ "roughness": 1,
+ "opacity": 100,
+ "seed": 2075483244,
+ "version": 181,
+ "versionNonce": 1931150956,
+ "isDeleted": false,
+ "points": [
+ [
+ 0,
+ 0
+ ],
+ [
+ -16.740451388888914,
+ 64.44010416666663
+ ],
+ [
+ -68.79340277777771,
+ 76.04600694444446
+ ]
+ ],
+ "lastCommittedPoint": [
+ -68.79340277777771,
+ 76.04600694444446
+ ]
+ }
+ ],
+ "appState": {
+ "viewBackgroundColor": "#ffffff"
+ }
+}
\ No newline at end of file
diff --git a/src/docs/sdk/hub-based-api/unified-api.png b/src/docs/sdk/hub-based-api/unified-api.png
new file mode 100644
index 0000000000..b35df07a0c
Binary files /dev/null and b/src/docs/sdk/hub-based-api/unified-api.png differ
diff --git a/src/docs/sdk/unified-api/index.mdx b/src/docs/sdk/unified-api/index.mdx
index c9dfc6b331..52c478c0a0 100644
--- a/src/docs/sdk/unified-api/index.mdx
+++ b/src/docs/sdk/unified-api/index.mdx
@@ -8,12 +8,10 @@ Everything written here sounds real nice. However, we live in a practical world,
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`.
-
-*Unified API Example:* We don't like Hubs and Scopes anymore, and we are redefining that, Hub & Scope Refactoring
+* *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
@@ -45,25 +43,19 @@ meant that certain integrations (such as breadcrumbs) were often not possible.
## Simplified Visualization
-
+TODO: Graphic of the SDK pieces interacting
## Terminology
-- **minimal**: A separate "facade" package that re-exports a subset of the SDK's functionality through interfaces or proxies. That package does not directly depend on the SDK, instead it should make every operation a noop if the SDK is not installed.
-
- The purpose of such a package is to allow random libraries to record breadcrumbs and set context data while not having a hard dependency on the SDK.
-
-- **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.
+- **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](https://docs.sentry.io/platforms/javascript/enriching-events/scopes/) before continuing.
-- **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. The client is mostly responsible for building Sentry events and dispatching those to the transport.
+- **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 special contexts (user and similar) and the generic ones (`runtime`, `os`, `device`), etc. Check out Contexts for some predefined keys - users can also add arbitrary context keys. *Note: In older SDKs, you might encounter an unrelated concept of context, which is now deprecated by scopes*
+- **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. Contexts are converted into tags.
+- **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.
@@ -73,7 +65,7 @@ meant that certain integrations (such as breadcrumbs) were often not possible.
- **event processors**: Callbacks that run for every event.
They can either modify and return the event, or `null`.
- Returning `null` will discard the event and not process further.
+ When `null` is returned, the SDK MUST discard the event and not process it further.
See [Event Pipeline](#event-pipeline) for more information.
@@ -81,128 +73,211 @@ meant that certain integrations (such as breadcrumbs) were often not possible.
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, certain callbacks, such as `configure_scope` or
- *event processors*, may not be invoked.
- As a result, breadcrumbs are not recorded.
-
+ In this case, the SDK MUST NOT send any events, and SHOULD NOT register any (global) handlers.
-## "Static API"
+## 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 hubs and scopes (see [Concurrency](#concurrency) for more information) if available on that platform.
-Note that all listed functions below are mostly aliases for `Hub::get_current().function`.
+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](#concurrency) for more information).
-- `init(options)`: This is the entry point for every SDK.
+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.
-This typically creates / reinitializes the global hub which is propagated to all new threads/execution contexts, or a hub is created per thread/execution context.
+### `init(options)`
-Takes options (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 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.
-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.
+The options passed in are SDK specific, but typically include a DSN, release, environment, etc.
-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.
+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. Might for instance be no parameter passed etc.
+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*.
-Additionally it also sets up all default integrations.
+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.
-- `capture_event(event)`: Takes an already assembled event and dispatches it to the currently active hub. 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.).
+### `get_client()`
-- `capture_exception(error)`: Report an error or exception object. 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.
+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.
-- `capture_message(message, level)`: Reports a message. The level can be optional in language with default parameters in which case it should default to `info`.
+Instead of retuning `null` if the SDK was never initialized, this method MAY return a disabled (or a no-op) client.
-- `add_breadcrumb(crumb)`: Adds a new breadcrumb to the scope. If the total
- number of breadcrumbs exceeds the `max_breadcrumbs` setting, the SDK should
- remove the oldest breadcrumb. This works like the Hub API with regards to what
- `crumb` can be. If the SDK is disabled, it should ignore the breadcrumb.
+### `capture_event(event)`
-- `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.
+Takes an already assembled event and dispatches it to the current scope.
-- `last_event_id()`: Should return the last event ID emitted by the current scope. This is for instance used to implement user feedback dialogs.
+This SHOULD be equivalent to calling `get_current_scope().capture_event(event)`.
-- `start_session()`: Stores a session on the current scope and starts tracking it. This normally attaches a brand new session to the scope, and implicitly ends any already existing session.
+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.).
-- `end_session()`: Ends the session, setting an appropriate `status` and `duration`, and enqueues it for sending to Sentry.
+### `capture_exception(error)`
-## Concurrency
+Report an error or exception object and dispatches it to the current scope.
-All SDKs should have the concept of concurrency safe context storage. What this means depends on the language. The basic idea is that a user of the SDK can call a method to safely provide additional context information for all events that are about to be recorded.
+This SHOULD be equivalent to calling `get_current_scope().capture_exception(error)`.
-This is implemented as a thread local stack in most languages, but in some (such as JavaScript) it might be global under the assumption that this is something that makes sense in the environment.
+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.
-Here are some common concurrency patterns:
+### `capture_message(message, level)`
-* **Thread bound hub**: In that pattern each thread gets its own "hub" which internally manages a stack of scopes. If that pattern is followed one thread (the one that calls `init()`) becomes the "main" hub which is used as the base for newly spawned threads which will get a hub that is based on the main hub (but otherwise independent).
+Reports a message and dispatches it to the current scope.
-* **Internally scoped hub**: On some platforms such as .NET ambient data is available in which case the Hub can internally manage the scopes.
+This SHOULD be equivalent to calling `get_current_scope().capture_message(message)`.
-* **Dummy hub**: On some platforms concurrency just doesn't inherently exist. In that case the hub might be entirely absent or just be a singleton without concurrency management.
+The level can be optional in languages with default parameters in which case it should default to `info`.
-## Hub
+### `add_breadcrumb(crumb)`
-Under normal circumstances the hub consists of a stack of clients and scopes.
+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.
-The SDK maintains two variables: The *main hub* (a global variable) and the *current hub* (a variable local to the current thread or execution context, also sometimes known as async local or context local)
+This SHOULD be equivalent to calling `get_isolation_scope().add_breadcrumb(crumb)`.
-- `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)
+If the SDK is disabled, it SHOULD ignore the breadcrumb.
-- `Hub::new_from_top(hub)` / alternatively native constructor overloads: Creates a new hub by cloning the top stack of another hub.
+### `set_tag(key, value)`
-- `get_current_hub()` / `Hub::current()` / `Hub::get_current()`: Global function or static function to return the current (thread's) hub
+Set a tag on the isolation scope.
-- `get_main_hub()` / `Hub::main()` / `Hub::get_main()`: In languages where the main thread is special ("Thread bound hub" model) this returns the main thread’s hub instead of the current thread’s hub. This might not exist in all languages.
+This SHOULD be equivalent to calling `get_isolation_scope().set_tag(key, value)`.
-- `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. As an additional argument it also takes a Hint.
+### `set_context(name, context)`
- For the hint parameter see [hints](#hints).
+Set a context on the isolation scope.
-- `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. When the "internally scoped hub" concurrency model is used calls to this are often necessary as otherwise a scope might be accidentally incorrectly shared.
+This SHOULD be equivalent to calling `get_isolation_scope().set_context(name, context)`.
-- `Hub::with_scope(callback)` (optional): In Python this could be a context manager, in Ruby a block function. Pushes and pops a scope for integration work.
+### `set_extra(key, value)`
-- `Hub::pop_scope()` (optional): Only exists in languages without better resource management. Better to have this function on a return value of `push_scope` or to use `with_scope`. This is also sometimes called `pop_scope_unsafe` to indicate that this method should not be used directly.
+Set extra data on the isolation scope.
-- `Hub::configure_scope(callback)`: Invokes the callback with a mutable reference to the scope for modifications. This can also be a `with` statement in languages that have it (Python). If no active client is bound to this hub, the SDK should not invoke the callback.
+This SHOULD be equivalent to calling `get_isolation_scope().set_extra(key, value)`.
-- `Hub::add_breadcrumb(crumb, hint)`: Adds a breadcrumb to the current scope.
+### `set_user(user)`
- - The argument supported should be:
- - function that creates a breadcrumb
- - an already created breadcrumb object
- - a list of breadcrumbs (optional)
- - In languages where we do not have a basic form of overloading only a raw breadcrumb object should be accepted.
+Set the user on the isolation scope.
- The SDK should ignore the breadcrumb if no active client is bound to this hub.
+This SHOULD be equivalent to calling `get_isolation_scope().set_user(user)`.
- For the hint parameter see [hints](#hints).
+If `null` is passed in, the user SHOULD be reset.
-- `Hub::client()` / `Hub::get_client()` (optional): Accessor or getter that returns the current client or `None`.
+### `with_scope(callback)`
-- `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.
+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.
-- `Hub::unbind_client()` (optional): Optional way to unbind for languages where `bind_client` does not accept nullables.
+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.
-- `Hub::last_event_id()`: Should return the last event ID emitted by the current scope. This is for instance used to implement user feedback dialogs.
+Mutating the new scope MUST NOT affect the previous current scope.
-- `Hub::run(hub, callback)` `hub.run(callback)`, `run_in_hub(hub, callback)` (optional): Runs a callback with the hub bound as the current hub.
+### `with_isolation_scope(callback)`
-### Scope
+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.
-A scope holds data that should implicitly be sent with Sentry events. It can hold context data, extra parameters, level overrides, fingerprints etc.
+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()`).
-The user can modify the current scope (to set extra, tags, current user) through the global function `configure_scope`. `configure_scope` takes a callback function to which it passes the current 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.
-The reason for this callback-based API is efficiency. If the SDK is disabled,
-it should not invoke the callback, thus avoiding unnecessary work.
+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.
+
+```javascript
+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.
```javascript
-Sentry.configureScope(scope =>
- scope.setExtra("character_name", "Mighty Fighter"));
+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](#static-api-or-top-level-api).
+
+```javascript
+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.
@@ -232,9 +307,20 @@ Sentry.configureScope(scope =>
- `scope.add_breadcrumb(breadcrumb)`: Adds a breadcrumb to the current scope.
-- `scope.clear_breadcrumbs()`: Deletes current breadcrumbs from the 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.
-- `scope.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.
+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](https://docs.sentry.io/platforms/javascript/enriching-events/scopes/) on details about how scopes should be applied.
## Client
@@ -248,6 +334,17 @@ A Client is the part of the SDK that is responsible for event creation. To give
- `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.