From e73cb22f870cc2316c7265fe4f4f80ee3d08aba8 Mon Sep 17 00:00:00 2001 From: Cody Coljee-Gray <68203+codr@users.noreply.github.com> Date: Fri, 26 Jun 2026 20:57:40 -0700 Subject: [PATCH] fix(browser): Accept precisely-typed GrowthBook class in growthbookIntegration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `growthbookIntegration({ growthbookClass: GrowthBook })` — the call shown in the integration's own docs — did not type-check against the real `@growthbook/growthbook` class, forcing consumers to cast. The parameter was typed as `new (...args: unknown[]) => GrowthBook`, and a narrow constructor (`constructor(options?: Options)`) is not assignable to an `unknown[]` constructor. The integration only reads `growthbookClass.prototype`, so type the parameter structurally as `{ prototype: GrowthBook }`. This accepts a real GrowthBook class with no cast, still rejects classes missing `isOn`/`getFeatureValue`, uses no `any`, and makes the internal prototype cast redundant. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../featureFlags/growthbook/types.ts | 4 +- .../growthbook/integration.test.ts | 37 +++++++++++++++++++ .../integrations/featureFlags/growthbook.ts | 4 +- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 packages/browser/test/integrations/featureFlags/growthbook/integration.test.ts 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') {