From 6440867cf13bf9e2fc16b0baa8f3d187ccaa8a63 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 1/3] 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') { From 9dc943c012d590508fe11d62ecbaf0644495d295 Mon Sep 17 00:00:00 2001 From: Cody Coljee-Gray <68203+codr@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:53:40 -0700 Subject: [PATCH 2/3] fix(browser): Use any[] constructor shape for GrowthBook class type Address review feedback: keep the constructor shape and switch the contravariant unknown[] to any[] (with an oxlint-disable for no-explicit-any) rather than the structural { prototype } type. This restores the as GrowthBookLike cast at the read site, since a bare construct-signature type resolves .prototype to any. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/integrations/featureFlags/growthbook/types.ts | 6 ++++-- packages/core/src/integrations/featureFlags/growthbook.ts | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/integrations/featureFlags/growthbook/types.ts b/packages/browser/src/integrations/featureFlags/growthbook/types.ts index 94d5a8ab2294..74e3360f5115 100644 --- a/packages/browser/src/integrations/featureFlags/growthbook/types.ts +++ b/packages/browser/src/integrations/featureFlags/growthbook/types.ts @@ -3,5 +3,7 @@ export interface GrowthBook { getFeatureValue(this: GrowthBook, featureKey: string, defaultValue: unknown, ...rest: unknown[]): unknown; } -// We only depend on the surface we wrap, so accept any class whose prototype matches. -export type GrowthBookClass = { prototype: GrowthBook }; +// We only depend on the surface we wrap; constructor args are irrelevant here. +// `unknown[]` is contravariantly incompatible with real constructors (e.g. GrowthBook's), so use `any[]`. +// oxlint-disable-next-line typescript-eslint/no-explicit-any +export type GrowthBookClass = new (...args: any[]) => GrowthBook; diff --git a/packages/core/src/integrations/featureFlags/growthbook.ts b/packages/core/src/integrations/featureFlags/growthbook.ts index 6193e271e9cc..40201fc7db28 100644 --- a/packages/core/src/integrations/featureFlags/growthbook.ts +++ b/packages/core/src/integrations/featureFlags/growthbook.ts @@ -14,7 +14,8 @@ interface GrowthBookLike { getFeatureValue(this: GrowthBookLike, featureKey: string, defaultValue: unknown, ...rest: unknown[]): unknown; } -export type GrowthBookClassLike = { prototype: GrowthBookLike }; +// oxlint-disable-next-line typescript-eslint/no-explicit-any +export type GrowthBookClassLike = new (...args: any[]) => GrowthBookLike; /** * Sentry integration for capturing feature flag evaluations from GrowthBook. @@ -40,7 +41,7 @@ export const growthbookIntegration: IntegrationFn = defineIntegration( name: 'GrowthBook' as const, setupOnce() { - const proto = growthbookClass.prototype; + const proto = growthbookClass.prototype as GrowthBookLike; // Type guard and wrap isOn if (typeof proto.isOn === 'function') { From cabce32eb7e12a21c14947fa227b21ec0e8a5b5e Mon Sep 17 00:00:00 2001 From: Cody Coljee-Gray <68203+codr@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:09:45 -0700 Subject: [PATCH 3/3] fix(core): Explain any[] rationale on GrowthBookClassLike Add the contravariance rationale comment beside the explicit any, matching the browser types.ts change, per Cursor bot review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/src/integrations/featureFlags/growthbook.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/integrations/featureFlags/growthbook.ts b/packages/core/src/integrations/featureFlags/growthbook.ts index 40201fc7db28..5c2848b07365 100644 --- a/packages/core/src/integrations/featureFlags/growthbook.ts +++ b/packages/core/src/integrations/featureFlags/growthbook.ts @@ -14,6 +14,7 @@ interface GrowthBookLike { getFeatureValue(this: GrowthBookLike, featureKey: string, defaultValue: unknown, ...rest: unknown[]): unknown; } +// `unknown[]` is contravariantly incompatible with real constructors (e.g. GrowthBook's), so use `any[]`. // oxlint-disable-next-line typescript-eslint/no-explicit-any export type GrowthBookClassLike = new (...args: any[]) => GrowthBookLike;