diff --git a/packages/browser/src/integrations/featureFlags/growthbook/types.ts b/packages/browser/src/integrations/featureFlags/growthbook/types.ts index 5a852d633da9..94d5a8ab2294 100644 --- a/packages/browser/src/integrations/featureFlags/growthbook/types.ts +++ b/packages/browser/src/integrations/featureFlags/growthbook/types.ts @@ -3,5 +3,5 @@ export interface GrowthBook { getFeatureValue(this: GrowthBook, featureKey: string, defaultValue: unknown, ...rest: unknown[]): unknown; } -// We only depend on the surface we wrap; constructor args are irrelevant here. -export type GrowthBookClass = new (...args: unknown[]) => GrowthBook; +// We only depend on the surface we wrap, so accept any class whose prototype matches. +export type GrowthBookClass = { prototype: GrowthBook }; diff --git a/packages/browser/test/integrations/featureFlags/growthbook/integration.test.ts b/packages/browser/test/integrations/featureFlags/growthbook/integration.test.ts new file mode 100644 index 000000000000..b689d0a436a8 --- /dev/null +++ b/packages/browser/test/integrations/featureFlags/growthbook/integration.test.ts @@ -0,0 +1,37 @@ +import { getCurrentScope } from '@sentry/core/browser'; +import { afterEach, describe, expect, it } from 'vitest'; +import { growthbookIntegration } from '../../../../src/integrations/featureFlags/growthbook'; + +describe('growthbookIntegration', () => { + afterEach(() => { + getCurrentScope().clear(); + }); + + it('accepts a precisely-typed GrowthBook class without a cast and captures boolean evaluations', () => { + class MockGrowthBook { + public constructor(_options?: { apiHost: string }) {} + + public isOn(_key: string): boolean { + return true; + } + + public getFeatureValue(_key: string, _defaultValue: unknown): unknown { + return false; + } + } + + const integration = growthbookIntegration({ + growthbookClass: MockGrowthBook, + }); + integration.setupOnce?.(); + + const growthbook = new MockGrowthBook(); + growthbook.isOn('my-feature'); + growthbook.getFeatureValue('my-other-feature', true); + + expect(getCurrentScope().getScopeData().contexts.flags?.values).toEqual([ + { flag: 'my-feature', result: true }, + { flag: 'my-other-feature', result: false }, + ]); + }); +}); diff --git a/packages/core/src/integrations/featureFlags/growthbook.ts b/packages/core/src/integrations/featureFlags/growthbook.ts index 6f3d4bfe73fd..6193e271e9cc 100644 --- a/packages/core/src/integrations/featureFlags/growthbook.ts +++ b/packages/core/src/integrations/featureFlags/growthbook.ts @@ -14,7 +14,7 @@ interface GrowthBookLike { getFeatureValue(this: GrowthBookLike, featureKey: string, defaultValue: unknown, ...rest: unknown[]): unknown; } -export type GrowthBookClassLike = new (...args: unknown[]) => GrowthBookLike; +export type GrowthBookClassLike = { prototype: GrowthBookLike }; /** * Sentry integration for capturing feature flag evaluations from GrowthBook. @@ -40,7 +40,7 @@ export const growthbookIntegration: IntegrationFn = defineIntegration( name: 'GrowthBook' as const, setupOnce() { - const proto = growthbookClass.prototype as GrowthBookLike; + const proto = growthbookClass.prototype; // Type guard and wrap isOn if (typeof proto.isOn === 'function') {