diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index a9a241c35941..83aeb475a390 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -207,7 +207,7 @@ export abstract class BaseClient implement * Sets up the integrations */ public setupIntegrations(): void { - if (this._isEnabled()) { + if (this._isEnabled() && !this._integrations.initialized) { this._integrations = setupIntegrations(this._options); } } diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 82c310ddef4e..1eea0a2d1084 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -5,9 +5,9 @@ import { logger } from '@sentry/utils'; export const installedIntegrations: string[] = []; /** Map of integrations assigned to a client */ -export interface IntegrationIndex { +export type IntegrationIndex = { [key: string]: Integration; -} +} & { initialized?: boolean }; /** * @private @@ -74,5 +74,9 @@ export function setupIntegrations(options: O): IntegrationInd integrations[integration.name] = integration; setupIntegration(integration); }); + // set the `initialized` flag so we don't run through the process again unecessarily; use `Object.defineProperty` + // because by default it creates a property which is nonenumerable, which we want since `initialized` shouldn't be + // considered a member of the index the way the actual integrations are + Object.defineProperty(integrations, 'initialized', { value: true }); return integrations; } diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 61e961d214d9..db9fbeb09664 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -2,6 +2,7 @@ import { Hub, Scope, Session } from '@sentry/hub'; import { Event, Severity, Span } from '@sentry/types'; import { logger, SentryError, SyncPromise } from '@sentry/utils'; +import * as integrationModule from '../../src/integration'; import { TestBackend } from '../mocks/backend'; import { TestClient } from '../mocks/client'; import { TestIntegration } from '../mocks/integration'; @@ -872,6 +873,26 @@ describe('BaseClient', () => { expect(Object.keys((client as any)._integrations).length).toBe(0); expect(client.getIntegration(TestIntegration)).toBeFalsy(); }); + + test('skips installation if integrations are already installed', () => { + expect.assertions(4); + const client = new TestClient({ + dsn: PUBLIC_DSN, + integrations: [new TestIntegration()], + }); + // note: not the `Client` method `setupIntegrations`, but the free-standing function which that method calls + const setupIntegrationsHelper = jest.spyOn(integrationModule, 'setupIntegrations'); + + // it should install the first time, because integrations aren't yet installed... + client.setupIntegrations(); + expect(Object.keys((client as any)._integrations).length).toBe(1); + expect(client.getIntegration(TestIntegration)).toBeTruthy(); + expect(setupIntegrationsHelper).toHaveBeenCalledTimes(1); + + // ...but it shouldn't try to install a second time + client.setupIntegrations(); + expect(setupIntegrationsHelper).toHaveBeenCalledTimes(1); + }); }); describe('flush/close', () => { diff --git a/packages/hub/src/hub.ts b/packages/hub/src/hub.ts index 1b14f6f5b899..a2680ad19157 100644 --- a/packages/hub/src/hub.ts +++ b/packages/hub/src/hub.ts @@ -103,7 +103,9 @@ export class Hub implements HubInterface { */ public constructor(client?: Client, scope: Scope = new Scope(), private readonly _version: number = API_VERSION) { this.getStackTop().scope = scope; - this.bindClient(client); + if (client) { + this.bindClient(client); + } } /**