Skip to content

feat(core): Add type & utility for function-based integrations #9818

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

Merged
merged 12 commits into from
Dec 18, 2023

Conversation

mydea
Copy link
Member

@mydea mydea commented Dec 13, 2023

This PR adds new types for function-based integrations, that eventually (in v8) should fully replace the class-based functions.

This also introduces a small helper function to make writing such integrations easier (as we need to set an id/name etc. on the integration). With this, you can write an integration like this:

const inboundFiltersIntegration = makeIntegrationFn(
  'InboundFilters', 
  (options: Partial<InboundFiltersOptions>) => {
  return {
    processEvent(event, _hint, client) {
      const clientOptions = client.getOptions();
      const mergedOptions = _mergeOptions(options, clientOptions);
      return _shouldDropEvent(event, mergedOptions) ? null : event;
    }
  }
});

And you get a fully typed integration ready to go!

For backwards compatibility, and so that we can actually start converting integrations in v7 already, this PR also adds a small utility convertIntegrationFnToClass() to convert such an integration to the "current" integration class syntax.

So we can actually already start porting integrations over like this:

/** Inbound filters configurable by the user */
// eslint-disable-next-line deprecation/deprecation
export const InboundFilters = convertIntegrationFnToClass(inboundFiltersIntegration);

Then, in v8 we only have to remove all the convertIntegrationFnToClass calls, export the integration functions directly, and update the overall integration types which can be passed to init() etc.

@mydea mydea self-assigned this Dec 13, 2023
Copy link
Contributor

github-actions bot commented Dec 13, 2023

size-limit report 📦

Path Size
@sentry/browser (incl. Tracing, Replay, Feedback) - Webpack (gzipped) 75.24 KB (-0.01% 🔽)
@sentry/browser (incl. Tracing, Replay) - Webpack (gzipped) 66.61 KB (-0.02% 🔽)
@sentry/browser (incl. Tracing, Replay) - Webpack with treeshaking flags (gzipped) 60.21 KB (-0.01% 🔽)
@sentry/browser (incl. Tracing) - Webpack (gzipped) 31.29 KB (-0.02% 🔽)
@sentry/browser (incl. Feedback) - Webpack (gzipped) 29.89 KB (-0.05% 🔽)
@sentry/browser - Webpack (gzipped) 21.56 KB (-0.01% 🔽)
@sentry/browser (incl. Tracing, Replay, Feedback) - ES6 CDN Bundle (gzipped) 72.63 KB (+0.08% 🔺)
@sentry/browser (incl. Tracing, Replay) - ES6 CDN Bundle (gzipped) 64.34 KB (+0.06% 🔺)
@sentry/browser (incl. Tracing) - ES6 CDN Bundle (gzipped) 30.6 KB (+0.1% 🔺)
@sentry/browser - ES6 CDN Bundle (gzipped) 22.66 KB (+0.19% 🔺)
@sentry/browser (incl. Tracing, Replay) - ES6 CDN Bundle (minified & uncompressed) 202.52 KB (+0.02% 🔺)
@sentry/browser (incl. Tracing) - ES6 CDN Bundle (minified & uncompressed) 92.65 KB (+0.01% 🔺)
@sentry/browser - ES6 CDN Bundle (minified & uncompressed) 67.65 KB (+0.04% 🔺)
@sentry/browser (incl. Tracing) - ES5 CDN Bundle (gzipped) 33.52 KB (+0.15% 🔺)
@sentry/react (incl. Tracing, Replay) - Webpack (gzipped) 66.96 KB (-0.02% 🔽)
@sentry/react - Webpack (gzipped) 21.59 KB (-0.06% 🔽)
@sentry/nextjs Client (incl. Tracing, Replay) - Webpack (gzipped) 83.76 KB (+0.06% 🔺)
@sentry/nextjs Client - Webpack (gzipped) 48.42 KB (+0.05% 🔺)
@sentry-internal/feedback - Webpack (gzipped) 16.19 KB (0%)

*/
public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void {
// noop
const inboundFiltersIntegration = makeIntegrationFn('InboundFilters', (options: Partial<InboundFiltersOptions>) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

I just converted a single integration for now to show this in "reality", but I can also extract this out into a follow up PR.

Copy link
Member

Choose a reason for hiding this comment

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

Let's just keep it here, the change is pretty small.

Copy link
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

I'm not sure if I'm missing something here but I'm thinking about the defineConfig patterns from Vite, Astro, Nuxt, etc and I'm wondering if we could simplify this helper function a little in a similar manner; or actually even more than that:

const inboundFilters =
  (options: Partial<InboundFiltersOptions>) => {
  // do whatever you need to do here e.g. assign default values
  return {
    name: 'InboundFilters',
    processEvent(event, _hint, client) {
      const clientOptions = client.getOptions();
      const mergedOptions = _mergeOptions(options, clientOptions);
      return _shouldDropEvent(event, mergedOptions) ? null : event;
    }
  }
});

I get that this simplification doesn't allow for name checks before initalization but I'm wondering if we even need that. Usually, instrumentation should happen in setup(once) anyway.

export {
getIntegrationsToSetup,
addIntegration,
// eslint-disable-next-line deprecation/deprecation
Copy link
Contributor

Choose a reason for hiding this comment

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

If we are using deprecated functions, I think we should disable this rule instead of overriding it everywhere

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 idea here was to make it easier to spot that this should be removed in v8 🤔 no strong feelings, I just wanted to make it clear that this is a temporary thing that will be removed in v8 again - generally, we should avoid using deprecated stuff ourselves, so I think the rule is good.

@@ -84,8 +64,7 @@ export function _mergeOptions(
};
}

/** JSDoc */
export function _shouldDropEvent(event: Event, options: Partial<InboundFiltersOptions>): boolean {
function _shouldDropEvent(event: Event, options: Partial<InboundFiltersOptions>): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this function start with underscore?

Copy link
Member Author

Choose a reason for hiding this comment

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

they are "private" - we are not super consistent there, sometimes we prefix them and sometimes not. but specifically here I did not change that, just removed the export as we don't actually use it anywhere!

@mydea
Copy link
Member Author

mydea commented Dec 13, 2023

I'm not sure if I'm missing something here but I'm thinking about the defineConfig patterns from Vite, Astro, Nuxt, etc and I'm wondering if we could simplify this helper function a little in a similar manner; or actually even more than that:

const inboundFilters =
  (options: Partial<InboundFiltersOptions>) => {
  // do whatever you need to do here e.g. assign default values
  return {
    name: 'InboundFilters',
    processEvent(event, _hint, client) {
      const clientOptions = client.getOptions();
      const mergedOptions = _mergeOptions(options, clientOptions);
      return _shouldDropEvent(event, mergedOptions) ? null : event;
    }
  }
});

I get that this simplification doesn't allow for name checks before initalization but I'm wondering if we even need that. Usually, instrumentation should happen in setup(once) anyway.

Hmm, can we get rid of the id? If so, then yeah we don't really need this - this is mostly to make this id/name stuff easier to handle. 🤔

Copy link
Member

@AbhiPrasad AbhiPrasad left a comment

Choose a reason for hiding this comment

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

Hmm, can we get rid of the id

I think we can because id is basically equivalent to name everywhere.

*/
public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void {
// noop
const inboundFiltersIntegration = makeIntegrationFn('InboundFilters', (options: Partial<InboundFiltersOptions>) => {
Copy link
Member

Choose a reason for hiding this comment

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

Let's just keep it here, the change is pretty small.

@mydea
Copy link
Member Author

mydea commented Dec 14, 2023

Hmm, if we get rid of the id, do we still want to have the util function? It doesn't really do anything, but makes typing much nicer...

Function:

function makeIntegrationFn<Fn extends IntegrationFn>(fn: Fn): Fn {
  return fn;
}

But using it allows to omit all types:

const inboundFiltersIntegration = makeIntegrationFn((options: Partial<InboundFiltersOptions>) => {
  return {
    name: INTEGRATION_NAME,
    // look mom, no types!
    processEvent(event, _hint, client) {
      const clientOptions = client.getOptions();
      const mergedOptions = _mergeOptions(options, clientOptions);
      return _shouldDropEvent(event, mergedOptions) ? null : event;
    },
  };
});

is this worth it??

Comment on lines 21 to 22
export type IntegrationFn<Fn extends (...rest: any[]) => IntegrationFnResult> = { id: string } & Fn;

export interface IntegrationFnResult {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think these are necessary or a good idea. I would prefer if we just have an Integration interface. These types seem overkill and don't add much.

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 thing is that for now these are not the same, as setupOnce is optional here. In v8 we can get rid of this and have just a single Integration interface again!

* This will ensure to add the given name both to the function definition (as id),
* as well as to the integration return value.
*/
export function makeIntegrationFn<
Copy link
Member

Choose a reason for hiding this comment

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

This function is imo not really doing anything. I think we (and eventually our users) are just better off defining integrations as follows.

export function MyCustomIntegration(): Integration {};

Easy, simple, well-typed. The rollup and Vite ecosystem is proof that this pattern works and is well understood.

Copy link
Member Author

@mydea mydea Dec 15, 2023

Choose a reason for hiding this comment

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

Will remove this!

@mydea mydea changed the title feat(core): Add utilities for function-based integrations feat(core): Add type & utility for function-based integrations Dec 15, 2023
@mydea mydea force-pushed the fn/integration-fn branch from 4db0b7a to 2a64201 Compare December 15, 2023 11:21
@mydea mydea force-pushed the fn/integration-fn branch from 2a64201 to 87446e9 Compare December 15, 2023 11:22
@mydea
Copy link
Member Author

mydea commented Dec 15, 2023

Updated this to remove the makeIntergrationFn util as we don't need that anymore. Now it's just the new types (to make setupOnce optional there), as well as the util to convert to a class for v7.

@mydea mydea merged commit 01a4cc9 into develop Dec 18, 2023
@mydea mydea deleted the fn/integration-fn branch December 18, 2023 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants