Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rfc(feature): SDK Lifecycle Hooks #34

Merged
merged 5 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This repository contains RFCs and DACIs. Lost?
- [0022-response-context](text/0022-response-context.md): Response context
- [0033-view-hierarchy](text/0033-view-hierarchy.md): View Hierarchy
- [0027-manual-disabling-of-flaky-tests](text/0027-manual-disabling-of-flaky-tests.md): Processes for manually disabling flaky tests in `sentry` and `getsentry`
- [00034-sdk-lifecycle](text/0034-sdk-lifecycle-hooks.md): SDK Lifecycle hooks
- [0036-auto-instrumentation-ui-thread](text/0036-auto-instrumentation-ui-thread.md): auto-instrumentation UI thread
- [0037-anr-rates](text/0037-anr-rates.md): Calculating accurate ANR rates
- [0038-scrubbing-sensitive-data](text/0038-scrubbing-sensitive-data.md): Scrubbing sensitive data - how to improve
Expand Down
135 changes: 135 additions & 0 deletions text/0034-sdk-lifecycle-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
- Start Date: 2022-11-03
- RFC Type: feature
- RFC PR: [https://github.com/getsentry/rfcs/pull/34](https://github.com/getsentry/rfcs/pull/34)
- RFC Status: approved
- RFC Driver: [Abhijeet Prasad](https://github.com/AbhiPrasad)

# Summary

This PR proposes the introduction of lifecycle hooks in the SDK, improving extensibility and usability of SDKs at Sentry.

To test out this RFC, we implemented it in the [JavaScript SDK](https://github.com/getsentry/sentry-javascript/blob/7aa20d04a3d61f30600ed6367ca7151d183a8fc9/packages/types/src/client.ts#L153) with great success, so now we are looking to propose and implement it in the other SDKs.

Lifecyle hooks can be registered on a Sentry client, and allow for integrations to have finely grained control over the event lifecycle in the SDK.

Currently hooks are meant to be completely internal to SDK implementors - but we can re-evaluate this in the future.

```ts
interface Client {
// ...

on(hookName: string, callback: (...args: unknown) => void) => void;

emit(hookName: string, ...args: unknown[]) => void;

// ...
}
```
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved

# Motivation

There are three main ways users can extend functionality in the SDKs right now.

At it's current form, the SDK is an event processing pipeline. It takes in some data (an error/message, a span, a profile), turns it into the event, attaches useful context to that event based on the current scope, and then sends that event to Sentry.
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved

```
| Error | ---> | Event | ---> | EventWithContext | ---> | Envelope | ---> | Transport | ---> | Sentry |
```

```
| TransactionStart | ---> | SpanStart | ---> | SpanFinish | ---> | TransactionFinish | --> | Event | ---> | EventWithContext | ---> | Envelope | ---> | Transport | ---> | Sentry |
```

```
| Session | ---> | Envelope | ---> | Transport | ---> | Sentry |
```

The SDKs provide a few ways to extend this pipeline:

1. Event Processors (what Integrations use)
2. `beforeSend` callback
3. `beforeBreadcrumb` callback
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved

But these are all top level options in someway, and are part of the unified API as a result. This means that in certain scenarios, they are not granular enough as extension points.

The following are some examples of how sdk hooks can unblock new features and integrations, but not a definitive list:

- Integrations want to add information to spans when they start or finish. This is what [RFC #75 is running into](https://github.com/getsentry/rfcs/pull/75), where they want to add thread information to each span.
- Integrations want to add information to envelopes before they are sent to Sentry.
- Integrations want to run code on transaction/span finish (to add additional spans to the transaction, for example).
- Integrations want to mutate an error on `captureException`
- Integrations want to override propagation behaviour (extracing/injecting outgoing headers)
Copy link
Member

@philipphofmann philipphofmann Mar 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Mobile, take a screenshot or attach the view hierarchy when capturing an exception. Could we replace our custom logic of attachment processors with that, @AbhiPrasad? We call some code that returns attachments and then bundle it into an envelope when capturing exceptions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! We could for sure replace that with this, will add your example to the list!


# Proposal

SDK hooks live on the client, and are **stateless**. They are called in the order they are registered. SDKs can opt-in to whatever hooks they use, and there can be hooks unique to an SDK.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AbhiPrasad, why did we put the hooks on the client and not the options as our existing hooks beforeSend, beforeBreadcrumb, etc.?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not meant to be replacements for the top level hooks we have for beforeSend or beforeBreadcrumb for the time being, since the hook names are dynamic strings.

The hooks are on the client so they can be registered dynamically. This is basically the event emitter pattern.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to put this info in the RFC or the develop docs, as this was unclear to me when reading the RFC.

I didn't write JavaScript for 6 years or so, but event emitter still rings a bell now. For statically typed languages, we might have to adapt them to the language. For Swift and Java, it would maybe be something like addObserver and notifyObserver, or maybe we just stick to the JavaScript syntax. Thanks for explaining.


Hooks are meant to be mostly internal APIs for integration authors, but we can also expose them to SDK users if there is a use case for it.

As hook callbacks are not processed by the client. Data passed into can be mutated, which can have side effects on the event pipeline, and consquences for hooks with async callbacks. As such, we recommend using hooks for synchronous operations only, but SDK authors can choose to implement them as async if they need to.

```ts
// Example implementation in JavaScript

type HookCallback = (...args: unknown[]): void;

class Client {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused, who implements this Client? What exactly the end-user interacts with?

Copy link
Member

@cleptric cleptric Mar 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for showing an interest in our RFC process! 🚀
This is meant to be the client implementation we have in each SDK, see https://develop.sentry.dev/sdk/unified-api/#client

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to think a layer higher and across multiple languages and clients. Sorry for the confusion, and thank you so much for your response!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cleptric, I also need some clarification. Does this mean I need to implement a client when I want to add lifecycle hooks? Can you share the code a user must write when implementing a lifecycle hook?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The on and emit hooks will be defined on the client interface (much like flush and capture_event are at the moment).

Usually users (if writing custom clients) are inheriting from a base client implementation anyway, so they should get the hooks functionality out of the box.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification, @AbhiPrasad 😃 .

On mobile, hardly anybody implements a custom client. Most users interact with the static API. Implementing a custom client and binding this to the hub, then binding the hub to the static API, will not be the cleanest API on Mobile. Our users usually only call SentrySDK.start with the options, and that's it.

hooks: {
AbhiPrasad marked this conversation as resolved.
Show resolved Hide resolved
[hookName: string]: HookCallback[];
};

on(hookName: string, callback: HookCallback): void {
this.hooks[hookName].push(callback);
}

emit(hookName: string, ...args: unknown[]): void {
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved
this.hooks[hookName].forEach(callback => callback(...args));
}
}
```

For languages that cannot use dynamic strings as hook names, alternate options should be considered. For example, we could use a `Hook` enum, or a `Hook` class with static properties.

The primary hook functions named `on` and `emit` can also change based on implementor SDK. For example, using `addObserver` and `notifyObserver` may work better in the Java and Apple SDKs (since those are the names of the observer pattern in those languages).

Hook callbacks are recommended to be forwards compatible, and forward any arguments they don't use. This allows us to add new arguments to hooks without breaking integrations. For example in Python we should catch and forward extra keyword arguments to the callback.

SDKs are expected to have a common set of hooks, but can also have SDK specific hooks.

## Hooks
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's about hooks for startSession and endSession?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not an exhaustive list, but I will add those.


These following are a set of example hooks that would unblock some use cases listed above. The names/schema of the hooks are not final, and are meant to be used as a starting point for discussion.

To document and approve new hooks, we will create a new page in the develop docs that lists all the hooks, and what they are used for.

`startTransaction`:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also add our existing hooks as beforeSend, beforeTransaction, beforeBreadcrumb, etc. to have one way to define all hooks, and we should keep the existing hooks for backward compatibility. Otherwise, users need to use two different APIs for a similar use case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense to me - can add some notes


```ts
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved
on('startTransaction', callback: (transaction: Transaction) => void) => void;
```

`finishTransaction`:

```ts
on('finishTransaction', callback: (transaction: Transaction) => void) => void;
```

`startSpan`:

```ts
on('startSpan', callback: (span: Span) => void) => void;
```

`finishSpan`:

```ts
on('finishSpan', callback: (span: Span) => void) => void;
```

`beforeEnvelope`:

```ts
on('beforeEnvelope', callback: (envelope: Envelope) => void) => void;
```
Comment on lines +129 to +133
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a hook for beforeEnvelope? Can we make it internal at least? AFAIK this is not exposed for clients and the whole envelope impl. is marked as internal in Mobile SDKs at least.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we can make some hooks internal. I will clarify this in the RFC.


Other possible hooks inlcude `beforeBreadcrumb`, `beforeSend`, `startSession` and `endSession`.