Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(core): Fix integration deduping (#5696)
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.
- Loading branch information