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

fix(core): Fix integration deduping #5696

Merged
merged 5 commits into from
Sep 7, 2022

Conversation

lobsterkatie
Copy link
Member

@lobsterkatie lobsterkatie commented Sep 6, 2022

When the SDK is initialized, we set up a combination of default and user-provided integrations, controlled by two Sentry.init() options:

  • defaultIntegrations - can be set to false to disable defaults
  • integrations - can be an array of integrations (to be merged with the existing array of defaults) or a function (defaults: Integration[]) => Integration[] (to replace the entire array of defaults with a new array)

The first option works correctly, but the second has long had two bugs, both relating to how duplicates are handled. Specifically, when two instances of the same integration are present, the following two principles should (but don't) always hold: 1) a user-provided instance should always beat a default instance, and within that, 2) a more recent instance should always beat a less recent one.

To understand why this happens, it helps to know the basics of the integration set-up flow, which goes like this:

  • Default integrations are set by the SDK
  • The user either does or (much more often) doesn't disable them
  • The user either does or doesn't provide either an array or a function to specify their own integrations
  • The SDK combines the sets of default and user integrations using the getIntegrationsToSetup function, which returns an array of integrations to install
  • As each integration is installed, it is added to a registry (which lives in a closure)
  • If multiple instances of the same integration are installed, every installation after the first is a no-op (because we check the registry, see that that integration has already been installed, and bail)

Because of the last point, if getIntegrationsToSetup returns a list containing duplicates, unexpected things can happen. For example, consider the following getIntegrationsToSetup return values under the current implementation:

  • [UserProvidedCopyofIntegration, DefaultCopyOfSameIntegration] - the user's copy should win, and does
  • [DefaultCopyofIntegration, UserProvidedCopyOfSameIntegration] - the user's copy should win, but doesn't
  • [DefaultCopyAofIntegration, DefaultCopyBofIntegration] - copy B should win, but doesn't
  • [UserCopyAofIntegration, UserCopyBofIntegration] - copy B should win, but doesn't

The most straightforward way to fix this would be to make it so that installing an existing integration would overwrite the existing copy with the new copy, but that would change the end result not just in the above situations (which all involve a single Sentry.init() call) but also in situations involving competing Sentry.init() calls and in situations where a second (third, fourth, etc) client or hub is being initialized directly. In those latter cases, we do want the first copy installed to take precedence, because it presumably corresponds to the "main" Sentry instance.

In order not to cause that larger behavior shift, it's therefore better to fix the aforementinoed bugs by making sure that a) getIntegrationsToSetup never returns duplicates, and b) its deduping process preserves the correct copy of each integration.

This both makes that change and introduces a new (hopefully more comprehensive) test suite.

The roots of the problem in the current code are:

  • When the user provides a function rather than an array, no deduping is done, so neither duplicate user integrations nor user integrations which duplicate default integrations are caught and dealt with.
  • The deduping prefers first instance over last instance.

To solve this, the following changes have been made:

  • The entire combining-default-with-user-integrations algorithm has been reworked, so that deduping now happens only once, at the end, after all default and user-provided integrations have been collected. This guarantees that the deduping applies equally to default and user-provided integrations, and applies whether the user has provided an array or a function.
  • The deduping now prefers last over first, by using an object keyed by integration name to store integration instances. It also now takes into account whether an integration instance is the default one or a user-provided one, so that a user-provided function can return defaults before or after custom instances without changing the eventual outcome.

Note that in order to show that this change doesn't break existing behavior, for the moment the original tests (other than the one explicitly testing wrong behavior) have been retained for now. Since the new tests cover everything the existing tests cover, however, the existing tests can (and will) be removed in the future.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 6, 2022

size-limit report 📦

Path Size
@sentry/browser - ES5 CDN Bundle (gzipped + minified) 19.41 KB (-0.04% 🔽)
@sentry/browser - ES5 CDN Bundle (minified) 60.02 KB (-0.1% 🔽)
@sentry/browser - ES6 CDN Bundle (gzipped + minified) 18.01 KB (+0.13% 🔺)
@sentry/browser - ES6 CDN Bundle (minified) 52.95 KB (+0.02% 🔺)
@sentry/browser - Webpack (gzipped + minified) 19.8 KB (+0.1% 🔺)
@sentry/browser - Webpack (minified) 64.33 KB (+0.02% 🔺)
@sentry/react - Webpack (gzipped + minified) 19.83 KB (+0.09% 🔺)
@sentry/nextjs Client - Webpack (gzipped + minified) 44.67 KB (+0.01% 🔺)
@sentry/browser + @sentry/tracing - ES5 CDN Bundle (gzipped + minified) 25.93 KB (+0.05% 🔺)
@sentry/browser + @sentry/tracing - ES6 CDN Bundle (gzipped + minified) 24.31 KB (+0.11% 🔺)

Copy link
Member

@lforst lforst left a comment

Choose a reason for hiding this comment

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

Super clean! I'm wondering though if this isn't breaking - considering we had a test that specifically checked that the first integration in the array always wins.

Comment on lines +19 to +20
* Remove duplicates from the given array, preferring the last instance of any duplicate. Not guaranteed to
* preseve the order of integrations in the array.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Remove duplicates from the given array, preferring the last instance of any duplicate. Not guaranteed to
* preseve the order of integrations in the array.
* Remove duplicates from the given array, preferring the last instance of duplicate instances, and user instances over
* default instances. Not guaranteed to preseve the order of integrations in the array.

Copy link
Member Author

Choose a reason for hiding this comment

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

Technically? Yes. But I'd argue that

a) it falls under the heading of "fixing bugs is always technically breaking", and

b) coming back to the examples from the description, we have

  • [UserProvidedCopyofIntegration, DefaultCopyOfSameIntegration] - the user's copy should win, and already does (no behavior change here)
  • [DefaultCopyofIntegration, UserProvidedCopyOfSameIntegration] - the user's copy should win, but currently doesn't (this seems wrong on its face and also, why would anyone rely on us ignoring their copy?)
  • [DefaultCopyAofIntegration, DefaultCopyBofIntegration] - copy B should win, but currently doesn't (we have total control here, though I don't think it actually comes up anywhere)
  • [UserCopyAofIntegration, UserCopyBofIntegration] - copy B should win, but currently doesn't (the one breaking, not-currently-quite-as-obviously-wrong change, but I'm wagering that the number of people actively providing two copies of the same integration is vanishingly small)

I honestly have no idea why we were preferring first to begin with (the PR which introduced both the logic and the test unfortunately says absolutely nothing about it), and I can't find any place where we rely on that behavior, so I feel okay changing it.

WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

lol at the xkcd

I am totally fine with the changes. If people report in that they see problems, we can tell them to change the order to match the fixed behavior!

Copy link
Member Author

Choose a reason for hiding this comment

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

Whoops - completely forgot to include your good suggestion above before I merged. I'll throw it into the pile for the next comments-fix PR.

@lobsterkatie lobsterkatie merged commit 781485e into master Sep 7, 2022
@lobsterkatie lobsterkatie deleted the kmclb-fix-integration-deduping branch September 7, 2022 19:38
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.

None yet

2 participants