From a00b78e2a145dd588fab6f7d3e950c2e86d077e6 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 20 Sep 2024 14:16:36 +0200 Subject: [PATCH] feat(develop): Add docs about integrations in the SDK --- develop-docs/sdk/unified-api/index.mdx | 174 +++++++++++++++++++------ 1 file changed, 133 insertions(+), 41 deletions(-) diff --git a/develop-docs/sdk/unified-api/index.mdx b/develop-docs/sdk/unified-api/index.mdx index bbdc86b319dec..9088ae655d194 100644 --- a/develop-docs/sdk/unified-api/index.mdx +++ b/develop-docs/sdk/unified-api/index.mdx @@ -8,21 +8,21 @@ 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. +_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`. +_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 +_Unified API Example:_ We don't like Hubs and Scopes anymore, and we are redefining that, Hub & Scope Refactoring -New Sentry SDKs should follow the unified API, use consistent terms to refer to concepts. This +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 +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. @@ -57,11 +57,11 @@ meant that certain integrations (such as breadcrumbs) were often not possible. - **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 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* +- **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. @@ -79,18 +79,17 @@ meant that certain integrations (such as breadcrumbs) were often not possible. - **disabled SDK**: Most of the SDK functionality depends on a configured and active client. - Sentry considers the client active when it has a *transport*. + 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. + _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. +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. @@ -110,9 +109,9 @@ A user has to call `init` once but it’s permissible to call this with a disabl 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_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_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`. @@ -137,19 +136,19 @@ This is implemented as a thread local stack in most languages, but in some (such 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). +- **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. +- **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. +- **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) +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(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. @@ -161,11 +160,11 @@ The SDK maintains two variables: The *main hub* (a global variable) and the *cur 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::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::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. @@ -187,35 +186,36 @@ The SDK maintains two variables: The *main hub* (a global variable) and the *cur - `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::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. +- `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 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")); +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_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_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_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_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_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. @@ -223,9 +223,9 @@ Sentry.configureScope(scope => - `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_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.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. @@ -234,11 +234,11 @@ Sentry.configureScope(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. +- `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. +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. @@ -253,8 +253,8 @@ A Client is the part of the SDK that is responsible for event creation. To give 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 +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 @@ -266,13 +266,105 @@ 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* +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. +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/). +Many options are standardized across SDKs. For a list of these refer to [the main options documentation](https://docs.sentry.io/error-reporting/configuration/). + +## Integrations + +Integrations allow you to configure and extend SDK functionality in a modular way. We generally recommend adding SDK features (like breadcrumbs, context, etc.) through integrations rather than directly in the SDK so they can be enabled or disabled as needed. + +### Integration Public API + +An integration should be configurable through the SDK's options. This is typically using a `integrations` key in the SDK options, which accepts an array of integration instances. The integration instances should be created with the necessary configuration and then passed to the SDK. + +```javascript +Sentry.init({ + integrations: [Sentry.browserTracingIntegration()], +}); +``` + +By default, initializing the Sentry SDK will automatically enable a set of default integrations that define baseline functionality. SDKs should expose a way to disable these default integrations, or to individually enable or disable specific default integrations. + +Under the hood, the `client` should own the integration instances and be responsible for calling the integration's methods at the appropriate times. + +The API surface of integrations does not follow a common interface in the SDKs. Below is an example of what the integration interface looks like in the JavaScript SDKs, which is a good starting point to design an integration API in other SDKs. + +```ts +/** Integration interface */ +export interface Integration { + /** + * The name of the integration. Should be unique to avoid conflicts with other integrations. + */ + name: string; + + /** + * This hook is only called once, even if multiple clients are created. + * It does not receives any arguments, and should only use for e.g. global monkey patching and similar things. + */ + setupOnce?(): void; + + /** + * Set up an integration for the given client. + * Receives the client as argument. + * + * Whenever possible, prefer this over `setupOnce`, as that is only run for the first client, + * whereas `setup` runs for each client. Only truly global things (e.g. registering global handlers) + * should be done in `setupOnce`. + */ + setup?(client: Client): void; + + /** + * This hook is triggered after `setupOnce()` and `setup()` have been called for all integrations. + * You can use it if it is important that all other integrations have been run before. + */ + afterAllSetup?(client: Client): void; + + /** + * An optional hook that allows to preprocess an event _before_ it is passed to all other event processors. + */ + preprocessEvent?( + event: Event, + hint: EventHint | undefined, + client: Client + ): void; + + /** + * An optional hook that allows to process an event. + * Return `null` to drop the event, or mutate the event & return it. + * This receives the client that the integration was installed for as third argument. + */ + processEvent?( + event: Event, + hint: EventHint, + client: Client + ): Event | null | PromiseLike; +} +``` + +### Adding new default integrations + +Adding new behaviour to an SDK (like new instrumentation or event processing functionality) should be done via an integration if possible. This integration can either be default (opt-out) or opt-in depending on the use case. + +The biggest thing that influences if a new integration should be default or opt-in is the overhead it adds. Default integrations should be lightweight and not add significant overhead to the SDK. If a piece of instrumentation can cause performance issues, it should be opt-in, and the user should be made aware of the potential impact. + +In addition, default integrations should be well-tested. If a new integration is default, it should be tested in all supported environments and configurations to ensure it doesn't cause issues. We should not be adding new default integrations without proper testing. + +If an default integration creates a high volume of new data (for example a lot spans or transactions), it should be opt-in and only made default in a new major version of the SDK. This is to prevent users from being surprised by the new data and to prevent potential performance issues. In the case of spans we especially have to take care to ensure that the spans generated by a new integration don't make traces too noisy, which hurts their usefulness. + +In general we can examine the following questions to determine if a new integration should be default or opt-in: + +- Does the integration add significant overhead to the SDK? +- Does the integration create a high volume of new data? +- Does the integration have potential compatibility issues with other integrations or the SDK itself? + +Integration authors and SDK maintainers should work together to determine the best approach for new integrations based on the answers to these questions. + +If an integration is introduced as opt-in, it can be made default in a future version of the SDK if it is well-tested and doesn't cause issues.