diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/basic/test.ts
new file mode 100644
index 000000000000..fc23f80927ff
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/basic/test.ts
@@ -0,0 +1,68 @@
+import { expect } from '@playwright/test';
+import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
+import { sentryTest } from '../../../../../../utils/fixtures';
+import {
+ envelopeRequestParser,
+ shouldSkipFeatureFlagsTest,
+ waitForErrorRequest,
+} from '../../../../../../utils/helpers';
+
+sentryTest('GrowthBook onError: basic eviction/update and no async tasks', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipFeatureFlagsTest()) {
+ sentryTest.skip();
+ }
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 'test-id' }) });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
+ await page.goto(url);
+
+ await page.evaluate(bufferSize => {
+ const gb = new (window as any).GrowthBook();
+
+ for (let i = 1; i <= bufferSize; i++) {
+ gb.isOn(`feat${i}`);
+ }
+
+ gb.__setOn(`feat${bufferSize + 1}`, true);
+ gb.isOn(`feat${bufferSize + 1}`); // eviction
+
+ gb.__setOn('feat3', true);
+ gb.isOn('feat3'); // update
+
+ // Test getFeatureValue with boolean values (should be captured)
+ gb.__setFeatureValue('bool-feat', true);
+ gb.getFeatureValue('bool-feat', false);
+
+ // Test getFeatureValue with non-boolean values (should be ignored)
+ gb.__setFeatureValue('string-feat', 'hello');
+ gb.getFeatureValue('string-feat', 'default');
+ gb.__setFeatureValue('number-feat', 42);
+ gb.getFeatureValue('number-feat', 0);
+ }, FLAG_BUFFER_SIZE);
+
+ const reqPromise = waitForErrorRequest(page);
+ await page.locator('#error').click();
+ const req = await reqPromise;
+ const event = envelopeRequestParser(req);
+
+ const values = event.contexts?.flags?.values || [];
+
+ // After the sequence of operations:
+ // 1. feat1-feat100 are added (100 items)
+ // 2. feat101 is added, evicts feat1 (100 items: feat2-feat100, feat101)
+ // 3. feat3 is updated to true, moves to end (100 items: feat2, feat4-feat100, feat101, feat3)
+ // 4. bool-feat is added, evicts feat2 (100 items: feat4-feat100, feat101, feat3, bool-feat)
+
+ const expectedFlags = [];
+ for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) {
+ expectedFlags.push({ flag: `feat${i}`, result: false });
+ }
+ expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true });
+ expectedFlags.push({ flag: 'feat3', result: true });
+ expectedFlags.push({ flag: 'bool-feat', result: true }); // Only boolean getFeatureValue should be captured
+
+ expect(values).toEqual(expectedFlags);
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js
new file mode 100644
index 000000000000..e7831a1c2c0b
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/init.js
@@ -0,0 +1,37 @@
+import * as Sentry from '@sentry/browser';
+
+// Minimal mock GrowthBook class for tests
+window.GrowthBook = class {
+ constructor() {
+ this._onFlags = Object.create(null);
+ this._featureValues = Object.create(null);
+ }
+
+ isOn(featureKey) {
+ return !!this._onFlags[featureKey];
+ }
+
+ getFeatureValue(featureKey, defaultValue) {
+ return Object.prototype.hasOwnProperty.call(this._featureValues, featureKey)
+ ? this._featureValues[featureKey]
+ : defaultValue;
+ }
+
+ // Helpers for tests
+ __setOn(featureKey, value) {
+ this._onFlags[featureKey] = !!value;
+ }
+
+ __setFeatureValue(featureKey, value) {
+ this._featureValues[featureKey] = value;
+ }
+};
+
+window.Sentry = Sentry;
+window.sentryGrowthBookIntegration = Sentry.growthbookIntegration({ growthbookClass: window.GrowthBook });
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ integrations: [window.sentryGrowthBookIntegration],
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/subject.js
new file mode 100644
index 000000000000..e6697408128c
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/subject.js
@@ -0,0 +1,3 @@
+document.getElementById('error').addEventListener('click', () => {
+ throw new Error('Button triggered error');
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/template.html
new file mode 100644
index 000000000000..da7d69a24c97
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/template.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/withScope/test.ts
new file mode 100644
index 000000000000..48fa4718b856
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onError/withScope/test.ts
@@ -0,0 +1,65 @@
+import { expect } from '@playwright/test';
+import type { Scope } from '@sentry/browser';
+import { sentryTest } from '../../../../../../utils/fixtures';
+import {
+ envelopeRequestParser,
+ shouldSkipFeatureFlagsTest,
+ waitForErrorRequest,
+} from '../../../../../../utils/helpers';
+
+sentryTest('GrowthBook onError: forked scopes are isolated', async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipFeatureFlagsTest()) {
+ sentryTest.skip();
+ }
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 'test-id' }) });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
+ await page.goto(url);
+
+ const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === true);
+ const mainReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === false);
+
+ await page.evaluate(() => {
+ const Sentry = (window as any).Sentry;
+ const errorButton = document.querySelector('#error') as HTMLButtonElement;
+ const gb = new (window as any).GrowthBook();
+
+ gb.__setOn('shared', true);
+ gb.__setOn('main', true);
+
+ gb.isOn('shared');
+
+ Sentry.withScope((scope: Scope) => {
+ gb.__setOn('forked', true);
+ gb.__setOn('shared', false);
+ gb.isOn('forked');
+ gb.isOn('shared');
+ scope.setTag('isForked', true);
+ errorButton.click();
+ });
+
+ gb.isOn('main');
+ Sentry.getCurrentScope().setTag('isForked', false);
+ errorButton.click();
+ return true;
+ });
+
+ const forkedReq = await forkedReqPromise;
+ const forkedEvent = envelopeRequestParser(forkedReq);
+
+ const mainReq = await mainReqPromise;
+ const mainEvent = envelopeRequestParser(mainReq);
+
+ expect(forkedEvent.contexts?.flags?.values).toEqual([
+ { flag: 'forked', result: true },
+ { flag: 'shared', result: false },
+ ]);
+
+ expect(mainEvent.contexts?.flags?.values).toEqual([
+ { flag: 'shared', result: true },
+ { flag: 'main', result: true },
+ ]);
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js
new file mode 100644
index 000000000000..d755d7a1d972
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/init.js
@@ -0,0 +1,39 @@
+import * as Sentry from '@sentry/browser';
+
+window.GrowthBook = class {
+ constructor() {
+ this._onFlags = Object.create(null);
+ this._featureValues = Object.create(null);
+ }
+
+ isOn(featureKey) {
+ return !!this._onFlags[featureKey];
+ }
+
+ getFeatureValue(featureKey, defaultValue) {
+ return Object.prototype.hasOwnProperty.call(this._featureValues, featureKey)
+ ? this._featureValues[featureKey]
+ : defaultValue;
+ }
+
+ __setOn(featureKey, value) {
+ this._onFlags[featureKey] = !!value;
+ }
+
+ __setFeatureValue(featureKey, value) {
+ this._featureValues[featureKey] = value;
+ }
+};
+
+window.Sentry = Sentry;
+window.sentryGrowthBookIntegration = Sentry.growthbookIntegration({ growthbookClass: window.GrowthBook });
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ tracesSampleRate: 1.0,
+ integrations: [
+ window.sentryGrowthBookIntegration,
+ Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }),
+ ],
+});
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/subject.js
new file mode 100644
index 000000000000..ad874b2bd697
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/subject.js
@@ -0,0 +1,16 @@
+const btnStartSpan = document.getElementById('btnStartSpan');
+const btnEndSpan = document.getElementById('btnEndSpan');
+const btnStartNestedSpan = document.getElementById('btnStartNestedSpan');
+const btnEndNestedSpan = document.getElementById('btnEndNestedSpan');
+
+window.withNestedSpans = callback => {
+ window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => {
+ window.traceId = rootSpan.spanContext().traceId;
+
+ window.Sentry.startSpan({ name: 'test-span' }, _span => {
+ window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => {
+ callback();
+ });
+ });
+ });
+};
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/template.html
new file mode 100644
index 000000000000..4efb91e75451
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/template.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts
new file mode 100644
index 000000000000..6661edc9723d
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/growthbook/onSpan/test.ts
@@ -0,0 +1,65 @@
+import { expect } from '@playwright/test';
+import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
+import { sentryTest } from '../../../../../utils/fixtures';
+import {
+ type EventAndTraceHeader,
+ eventAndTraceHeaderRequestParser,
+ getMultipleSentryEnvelopeRequests,
+ shouldSkipFeatureFlagsTest,
+ shouldSkipTracingTest,
+} from '../../../../../utils/helpers';
+
+sentryTest(
+ "GrowthBook onSpan: flags are added to active span's attributes on span end",
+ async ({ getLocalTestUrl, page }) => {
+ if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
+ sentryTest.skip();
+ }
+
+ await page.route('https://dsn.ingest.sentry.io/**/*', route => {
+ return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
+ });
+
+ const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
+ await page.goto(url);
+
+ const envelopeRequestPromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ {},
+ eventAndTraceHeaderRequestParser,
+ );
+
+ await page.evaluate(maxFlags => {
+ (window as any).withNestedSpans(() => {
+ const gb = new (window as any).GrowthBook();
+ for (let i = 1; i <= maxFlags; i++) {
+ gb.isOn(`feat${i}`);
+ }
+ gb.__setOn(`feat${maxFlags + 1}`, true);
+ gb.isOn(`feat${maxFlags + 1}`); // dropped
+ gb.__setOn('feat3', true);
+ gb.isOn('feat3'); // update
+ });
+ return true;
+ }, MAX_FLAGS_PER_SPAN);
+
+ const event = (await envelopeRequestPromise)[0][0];
+ const innerSpan = event.spans?.[0];
+ const outerSpan = event.spans?.[1];
+ const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) =>
+ key.startsWith('flag.evaluation'),
+ );
+ const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) =>
+ key.startsWith('flag.evaluation'),
+ );
+
+ expect(innerSpanFlags).toEqual([]);
+
+ const expectedOuterSpanFlags = [] as Array<[string, unknown]>;
+ for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
+ expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]);
+ }
+ expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort());
+ },
+);
diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json
index f8c294738065..118db71a6b98 100644
--- a/dev-packages/node-integration-tests/package.json
+++ b/dev-packages/node-integration-tests/package.json
@@ -26,6 +26,7 @@
"@anthropic-ai/sdk": "0.63.0",
"@aws-sdk/client-s3": "^3.552.0",
"@google/genai": "^1.20.0",
+ "@growthbook/growthbook": "^1.6.1",
"@hapi/hapi": "^21.3.10",
"@hono/node-server": "^1.19.4",
"@nestjs/common": "^11",
diff --git a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onError/basic/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onError/basic/scenario.ts
new file mode 100644
index 000000000000..f907e320696d
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onError/basic/scenario.ts
@@ -0,0 +1,89 @@
+import type { ClientOptions, UserContext } from '@growthbook/growthbook';
+import { GrowthBookClient } from '@growthbook/growthbook';
+import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
+import * as Sentry from '@sentry/node';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+
+// Wrapper class to instantiate GrowthBookClient
+class GrowthBookWrapper {
+ private _gbClient: GrowthBookClient;
+ private _userContext: UserContext = { attributes: { id: 'test-user-123' } };
+
+ public constructor(..._args: unknown[]) {
+ // Create GrowthBookClient with proper configuration
+ const clientOptions: ClientOptions = {
+ apiHost: 'https://cdn.growthbook.io',
+ clientKey: 'sdk-abc123',
+ };
+ this._gbClient = new GrowthBookClient(clientOptions);
+
+ // Create features for testing
+ const features = this._createTestFeatures();
+
+ this._gbClient.initSync({
+ payload: { features },
+ });
+ }
+
+ public isOn(featureKey: string, ..._rest: unknown[]): boolean {
+ return this._gbClient.isOn(featureKey, this._userContext);
+ }
+
+ public getFeatureValue(featureKey: string, defaultValue: unknown, ..._rest: unknown[]): unknown {
+ return this._gbClient.getFeatureValue(featureKey, defaultValue as boolean | string | number, this._userContext);
+ }
+
+ private _createTestFeatures(): Record {
+ const features: Record = {};
+
+ // Fill buffer with flags 1-100 (all false by default)
+ for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) {
+ features[`feat${i}`] = { defaultValue: false };
+ }
+
+ // Add feat101 (true), which should evict feat1
+ features[`feat${FLAG_BUFFER_SIZE + 1}`] = { defaultValue: true };
+
+ // Update feat3 to true, which should move it to the end
+ features['feat3'] = { defaultValue: true };
+
+ // Test features with boolean values (should be captured)
+ features['bool-feat'] = { defaultValue: true };
+
+ // Test features with non-boolean values (should NOT be captured)
+ features['string-feat'] = { defaultValue: 'hello' };
+ features['number-feat'] = { defaultValue: 42 };
+
+ return features;
+ }
+}
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ transport: loggingTransport,
+ integrations: [Sentry.growthbookIntegration({ growthbookClass: GrowthBookWrapper })],
+});
+
+// Create GrowthBookWrapper instance
+const gb = new GrowthBookWrapper();
+
+// Fill buffer with flags 1-100 (all false by default)
+for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) {
+ gb.isOn(`feat${i}`);
+}
+
+// Add feat101 (true), which should evict feat1
+gb.isOn(`feat${FLAG_BUFFER_SIZE + 1}`);
+
+// Update feat3 to true, which should move it to the end
+gb.isOn('feat3');
+
+// Test getFeatureValue with boolean values (should be captured)
+gb.getFeatureValue('bool-feat', false);
+
+// Test getFeatureValue with non-boolean values (should NOT be captured)
+gb.getFeatureValue('string-feat', 'default');
+gb.getFeatureValue('number-feat', 0);
+
+throw new Error('Test error');
diff --git a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onError/basic/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onError/basic/test.ts
new file mode 100644
index 000000000000..82e39eb62364
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onError/basic/test.ts
@@ -0,0 +1,31 @@
+import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
+import { afterAll, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('GrowthBook flags captured on error with eviction, update, and no async tasks', async () => {
+ const expectedFlags = [];
+ for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) {
+ expectedFlags.push({ flag: `feat${i}`, result: false });
+ }
+ expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true });
+ expectedFlags.push({ flag: 'feat3', result: true });
+ expectedFlags.push({ flag: 'bool-feat', result: true }); // Only boolean getFeatureValue should be captured
+
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ event: {
+ exception: { values: [{ type: 'Error', value: 'Test error' }] },
+ contexts: {
+ flags: {
+ values: expectedFlags,
+ },
+ },
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts
new file mode 100644
index 000000000000..b25f36f00951
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/scenario.ts
@@ -0,0 +1,64 @@
+import type { ClientOptions, InitSyncOptions, UserContext } from '@growthbook/growthbook';
+import { GrowthBookClient } from '@growthbook/growthbook';
+import * as Sentry from '@sentry/node';
+import { loggingTransport } from '@sentry-internal/node-integration-tests';
+
+// Wrapper class to instantiate GrowthBookClient
+class GrowthBookWrapper {
+ private _gbClient: GrowthBookClient;
+ private _userContext: UserContext = { attributes: { id: 'test-user-123' } };
+
+ public constructor(..._args: unknown[]) {
+ // Create GrowthBookClient and initialize it synchronously with payload
+ const clientOptions: ClientOptions = {
+ apiHost: 'https://cdn.growthbook.io',
+ clientKey: 'sdk-abc123',
+ };
+ this._gbClient = new GrowthBookClient(clientOptions);
+
+ // Create test features
+ const features = {
+ feat1: { defaultValue: true },
+ feat2: { defaultValue: false },
+ 'bool-feat': { defaultValue: true },
+ 'string-feat': { defaultValue: 'hello' },
+ };
+
+ // Initialize synchronously with payload
+ const initOptions: InitSyncOptions = {
+ payload: { features },
+ };
+
+ this._gbClient.initSync(initOptions);
+ }
+
+ public isOn(featureKey: string, ..._rest: unknown[]): boolean {
+ return this._gbClient.isOn(featureKey, this._userContext);
+ }
+
+ public getFeatureValue(featureKey: string, defaultValue: unknown, ..._rest: unknown[]): unknown {
+ return this._gbClient.getFeatureValue(featureKey, defaultValue as boolean | string | number, this._userContext);
+ }
+}
+
+const gb = new GrowthBookWrapper();
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ sampleRate: 1.0,
+ tracesSampleRate: 1.0,
+ transport: loggingTransport,
+ integrations: [Sentry.growthbookIntegration({ growthbookClass: GrowthBookWrapper })],
+});
+
+Sentry.startSpan({ name: 'test-span', op: 'function' }, () => {
+ // Evaluate feature flags during the span
+ gb.isOn('feat1');
+ gb.isOn('feat2');
+
+ // Test getFeatureValue with boolean values (should be captured)
+ gb.getFeatureValue('bool-feat', false);
+
+ // Test getFeatureValue with non-boolean values (should NOT be captured)
+ gb.getFeatureValue('string-feat', 'default');
+});
diff --git a/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts
new file mode 100644
index 000000000000..fbb084b98928
--- /dev/null
+++ b/dev-packages/node-integration-tests/suites/featureFlags/growthbook/onSpan/test.ts
@@ -0,0 +1,34 @@
+import { afterAll, expect, test } from 'vitest';
+import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
+
+afterAll(() => {
+ cleanupChildProcesses();
+});
+
+test('GrowthBook flags are added to active span attributes on span end', async () => {
+ await createRunner(__dirname, 'scenario.ts')
+ .expect({
+ transaction: {
+ contexts: {
+ trace: {
+ data: {
+ 'flag.evaluation.feat1': true,
+ 'flag.evaluation.feat2': false,
+ 'flag.evaluation.bool-feat': true,
+ // string-feat should NOT be here since it's not boolean
+ },
+ op: 'function',
+ origin: 'manual',
+ status: 'ok',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ spans: [],
+ transaction: 'test-span',
+ type: 'transaction',
+ },
+ })
+ .start()
+ .completed();
+});
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index f70d6e0a3573..15158bdbb7bc 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -159,6 +159,7 @@ export {
OpenFeatureIntegrationHook,
statsigIntegration,
unleashIntegration,
+ growthbookIntegration,
} from '@sentry/node';
export { init } from './server/sdk';
diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts
index ceb4fc6d8a51..b09a1cfa09d5 100644
--- a/packages/astro/src/index.types.ts
+++ b/packages/astro/src/index.types.ts
@@ -35,5 +35,6 @@ export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegra
export declare const OpenFeatureIntegrationHook: typeof clientSdk.OpenFeatureIntegrationHook;
export declare const statsigIntegration: typeof clientSdk.statsigIntegration;
export declare const unleashIntegration: typeof clientSdk.unleashIntegration;
+export declare const growthbookIntegration: typeof clientSdk.growthbookIntegration;
export default sentryAstro;
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index 5a608a925edb..5ff30f069486 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -145,6 +145,7 @@ export {
OpenFeatureIntegrationHook,
statsigIntegration,
unleashIntegration,
+ growthbookIntegration,
} from '@sentry/node';
export {
diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts
index 5e9924fe6da5..308caea7305f 100644
--- a/packages/browser/src/index.ts
+++ b/packages/browser/src/index.ts
@@ -73,6 +73,7 @@ export { browserSessionIntegration } from './integrations/browsersession';
export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly';
export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature';
export { unleashIntegration } from './integrations/featureFlags/unleash';
+export { growthbookIntegration } from './integrations/featureFlags/growthbook';
export { statsigIntegration } from './integrations/featureFlags/statsig';
export { diagnoseSdkConnectivity } from './diagnose-sdk';
export { webWorkerIntegration, registerWebWorker } from './integrations/webWorker';
diff --git a/packages/browser/src/integrations/featureFlags/growthbook/index.ts b/packages/browser/src/integrations/featureFlags/growthbook/index.ts
new file mode 100644
index 000000000000..a931e2376ab7
--- /dev/null
+++ b/packages/browser/src/integrations/featureFlags/growthbook/index.ts
@@ -0,0 +1 @@
+export { growthbookIntegration } from './integration';
diff --git a/packages/browser/src/integrations/featureFlags/growthbook/integration.ts b/packages/browser/src/integrations/featureFlags/growthbook/integration.ts
new file mode 100644
index 000000000000..560918535cce
--- /dev/null
+++ b/packages/browser/src/integrations/featureFlags/growthbook/integration.ts
@@ -0,0 +1,26 @@
+import type { IntegrationFn } from '@sentry/core';
+import { growthbookIntegration as coreGrowthbookIntegration } from '@sentry/core';
+import type { GrowthBookClass } from './types';
+
+/**
+ * Sentry integration for capturing feature flag evaluations from GrowthBook.
+ *
+ * See the feature flag documentation: https://develop.sentry.dev/sdk/expected-features/#feature-flags
+ *
+ * @example
+ * ```
+ * import { GrowthBook } from '@growthbook/growthbook';
+ * import * as Sentry from '@sentry/browser';
+ *
+ * Sentry.init({
+ * dsn: '___PUBLIC_DSN___',
+ * integrations: [Sentry.growthbookIntegration({ growthbookClass: GrowthBook })],
+ * });
+ *
+ * const gb = new GrowthBook();
+ * gb.isOn('my-feature');
+ * Sentry.captureException(new Error('something went wrong'));
+ * ```
+ */
+export const growthbookIntegration = (({ growthbookClass }: { growthbookClass: GrowthBookClass }) =>
+ coreGrowthbookIntegration({ growthbookClass })) satisfies IntegrationFn;
diff --git a/packages/browser/src/integrations/featureFlags/growthbook/types.ts b/packages/browser/src/integrations/featureFlags/growthbook/types.ts
new file mode 100644
index 000000000000..5a852d633da9
--- /dev/null
+++ b/packages/browser/src/integrations/featureFlags/growthbook/types.ts
@@ -0,0 +1,7 @@
+export interface GrowthBook {
+ isOn(this: GrowthBook, featureKey: string, ...rest: unknown[]): boolean;
+ 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;
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index 2775cbc0624e..5ec1568229e4 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -156,6 +156,7 @@ export {
wrapMcpServerWithSentry,
featureFlagsIntegration,
launchDarklyIntegration,
+ growthbookIntegration,
buildLaunchDarklyFlagUsedHandler,
openFeatureIntegration,
OpenFeatureIntegrationHook,
diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts
index d4afd80313b1..6f731cb8d980 100644
--- a/packages/cloudflare/src/index.ts
+++ b/packages/cloudflare/src/index.ts
@@ -97,6 +97,7 @@ export {
consoleLoggingIntegration,
createConsolaReporter,
featureFlagsIntegration,
+ growthbookIntegration,
logger,
} from '@sentry/core';
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 06be19c86774..2377e2ce86b0 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -113,6 +113,7 @@ export { zodErrorsIntegration } from './integrations/zoderrors';
export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter';
export { consoleIntegration } from './integrations/console';
export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags';
+export { growthbookIntegration } from './integrations/featureFlags';
export { profiler } from './profiling';
// eslint thinks the entire function is deprecated (while only one overload is actually deprecated)
diff --git a/packages/core/src/integrations/featureFlags/growthbook.ts b/packages/core/src/integrations/featureFlags/growthbook.ts
new file mode 100644
index 000000000000..eeb2b25341e9
--- /dev/null
+++ b/packages/core/src/integrations/featureFlags/growthbook.ts
@@ -0,0 +1,77 @@
+import type { Client } from '../../client';
+import { defineIntegration } from '../../integration';
+import type { Event, EventHint } from '../../types-hoist/event';
+import type { IntegrationFn } from '../../types-hoist/integration';
+import {
+ _INTERNAL_addFeatureFlagToActiveSpan,
+ _INTERNAL_copyFlagsFromScopeToEvent,
+ _INTERNAL_insertFlagToScope,
+} from '../../utils/featureFlags';
+import { fill } from '../../utils/object';
+
+interface GrowthBookLike {
+ isOn(this: GrowthBookLike, featureKey: string, ...rest: unknown[]): boolean;
+ getFeatureValue(this: GrowthBookLike, featureKey: string, defaultValue: unknown, ...rest: unknown[]): unknown;
+}
+
+export type GrowthBookClassLike = new (...args: unknown[]) => GrowthBookLike;
+
+/**
+ * Sentry integration for capturing feature flag evaluations from GrowthBook.
+ *
+ * Only boolean results are captured at this time.
+ *
+ * @example
+ * ```typescript
+ * import { GrowthBook } from '@growthbook/growthbook';
+ * import * as Sentry from '@sentry/browser'; // or '@sentry/node'
+ *
+ * Sentry.init({
+ * dsn: 'your-dsn',
+ * integrations: [
+ * Sentry.growthbookIntegration({ growthbookClass: GrowthBook })
+ * ]
+ * });
+ * ```
+ */
+export const growthbookIntegration: IntegrationFn = defineIntegration(
+ ({ growthbookClass }: { growthbookClass: GrowthBookClassLike }) => {
+ return {
+ name: 'GrowthBook',
+
+ setupOnce() {
+ const proto = growthbookClass.prototype as GrowthBookLike;
+
+ // Type guard and wrap isOn
+ if (typeof proto.isOn === 'function') {
+ fill(proto, 'isOn', _wrapAndCaptureBooleanResult);
+ }
+
+ // Type guard and wrap getFeatureValue
+ if (typeof proto.getFeatureValue === 'function') {
+ fill(proto, 'getFeatureValue', _wrapAndCaptureBooleanResult);
+ }
+ },
+
+ processEvent(event: Event, _hint: EventHint, _client: Client): Event {
+ return _INTERNAL_copyFlagsFromScopeToEvent(event);
+ },
+ };
+ },
+);
+
+function _wrapAndCaptureBooleanResult(
+ original: (this: GrowthBookLike, ...args: unknown[]) => unknown,
+): (this: GrowthBookLike, ...args: unknown[]) => unknown {
+ return function (this: GrowthBookLike, ...args: unknown[]): unknown {
+ const flagName = args[0];
+ const result = original.apply(this, args);
+
+ if (typeof flagName === 'string' && typeof result === 'boolean') {
+ _INTERNAL_insertFlagToScope(flagName, result);
+ _INTERNAL_addFeatureFlagToActiveSpan(flagName, result);
+ }
+
+ return result;
+ };
+}
diff --git a/packages/core/src/integrations/featureFlags/index.ts b/packages/core/src/integrations/featureFlags/index.ts
index 2106ee7accf0..f0ee5ece65b2 100644
--- a/packages/core/src/integrations/featureFlags/index.ts
+++ b/packages/core/src/integrations/featureFlags/index.ts
@@ -1 +1,2 @@
export { featureFlagsIntegration, type FeatureFlagsIntegration } from './featureFlagsIntegration';
+export { growthbookIntegration } from './growthbook';
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index 8f1d236f7877..db52cf357a16 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -140,6 +140,7 @@ export {
featureFlagsIntegration,
type FeatureFlagsIntegration,
launchDarklyIntegration,
+ growthbookIntegration,
buildLaunchDarklyFlagUsedHandler,
openFeatureIntegration,
OpenFeatureIntegrationHook,
diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts
index fe5a75bd5c8b..d982ebbc7559 100644
--- a/packages/nextjs/src/index.types.ts
+++ b/packages/nextjs/src/index.types.ts
@@ -142,6 +142,7 @@ export declare function wrapPageComponentWithSentry(WrappingTarget: C): C;
export { captureRequestError } from './common/captureRequestError';
+export declare const growthbookIntegration: typeof clientSdk.growthbookIntegration;
export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration;
export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler;
export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration;
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index 54a90dbfcd09..b599351b5124 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -34,6 +34,7 @@ export {
OpenFeatureIntegrationHook,
statsigIntegration,
unleashIntegration,
+ growthbookIntegration,
} from './integrations/featureFlagShims';
export { firebaseIntegration } from './integrations/tracing/firebase';
diff --git a/packages/node/src/integrations/featureFlagShims/growthbook.ts b/packages/node/src/integrations/featureFlagShims/growthbook.ts
new file mode 100644
index 000000000000..d86f3a0349bc
--- /dev/null
+++ b/packages/node/src/integrations/featureFlagShims/growthbook.ts
@@ -0,0 +1,7 @@
+import { growthbookIntegration as coreGrowthbookIntegration } from '@sentry/core';
+
+/**
+ * Re-export the core GrowthBook integration for Node.js usage.
+ * The core integration is runtime-agnostic and works in both browser and Node environments.
+ */
+export const growthbookIntegrationShim = coreGrowthbookIntegration;
diff --git a/packages/node/src/integrations/featureFlagShims/index.ts b/packages/node/src/integrations/featureFlagShims/index.ts
index 230dbaeeb7e8..ef90a562983f 100644
--- a/packages/node/src/integrations/featureFlagShims/index.ts
+++ b/packages/node/src/integrations/featureFlagShims/index.ts
@@ -11,3 +11,5 @@ export {
export { statsigIntegrationShim as statsigIntegration } from './statsig';
export { unleashIntegrationShim as unleashIntegration } from './unleash';
+
+export { growthbookIntegrationShim as growthbookIntegration } from './growthbook';
diff --git a/packages/nuxt/src/index.types.ts b/packages/nuxt/src/index.types.ts
index 4f006e0b5b07..7abb16d197e3 100644
--- a/packages/nuxt/src/index.types.ts
+++ b/packages/nuxt/src/index.types.ts
@@ -19,6 +19,7 @@ export declare const defaultStackParser: StackParser;
export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger;
+export declare const growthbookIntegration: typeof clientSdk.growthbookIntegration;
export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration;
export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler;
export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration;
diff --git a/packages/react-router/src/index.types.ts b/packages/react-router/src/index.types.ts
index 150fc45a1e63..58566ba214fe 100644
--- a/packages/react-router/src/index.types.ts
+++ b/packages/react-router/src/index.types.ts
@@ -19,6 +19,7 @@ export declare const getDefaultIntegrations: (options: Options) => Integration[]
export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger;
+export declare const growthbookIntegration: typeof clientSdk.growthbookIntegration;
export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration;
export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler;
export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration;
diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts
index d0df7397f612..cacbac00e591 100644
--- a/packages/remix/src/index.types.ts
+++ b/packages/remix/src/index.types.ts
@@ -33,6 +33,7 @@ export const close = runtime === 'client' ? clientSdk.close : serverSdk.close;
export const flush = runtime === 'client' ? clientSdk.flush : serverSdk.flush;
export const lastEventId = runtime === 'client' ? clientSdk.lastEventId : serverSdk.lastEventId;
+export declare const growthbookIntegration: typeof clientSdk.growthbookIntegration;
export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration;
export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler;
export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration;
diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts
index 7725d1ad3d3c..7f7528a0dddb 100644
--- a/packages/solidstart/src/index.types.ts
+++ b/packages/solidstart/src/index.types.ts
@@ -26,6 +26,7 @@ export declare function lastEventId(): string | undefined;
export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger;
+export declare const growthbookIntegration: typeof clientSdk.growthbookIntegration;
export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration;
export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler;
export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration;
diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts
index 108e262f9992..40c2f5ff848e 100644
--- a/packages/sveltekit/src/index.types.ts
+++ b/packages/sveltekit/src/index.types.ts
@@ -60,6 +60,7 @@ export declare function trackComponent(options: clientSdk.TrackingOptions): Retu
export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger;
+export declare const growthbookIntegration: typeof clientSdk.growthbookIntegration;
export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration;
export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler;
export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration;
diff --git a/packages/tanstackstart-react/src/index.types.ts b/packages/tanstackstart-react/src/index.types.ts
index 448ea35f637b..5a44af1b59d4 100644
--- a/packages/tanstackstart-react/src/index.types.ts
+++ b/packages/tanstackstart-react/src/index.types.ts
@@ -29,6 +29,7 @@ export declare const withErrorBoundary: typeof clientSdk.withErrorBoundary;
export declare const logger: typeof clientSdk.logger | typeof serverSdk.logger;
+export declare const growthbookIntegration: typeof clientSdk.growthbookIntegration;
export declare const launchDarklyIntegration: typeof clientSdk.launchDarklyIntegration;
export declare const buildLaunchDarklyFlagUsedHandler: typeof clientSdk.buildLaunchDarklyFlagUsedHandler;
export declare const openFeatureIntegration: typeof clientSdk.openFeatureIntegration;
diff --git a/yarn.lock b/yarn.lock
index 4d084a8cf3c7..5e9edc00f1df 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4350,6 +4350,13 @@
dependencies:
tslib "^2.4.0"
+"@growthbook/growthbook@^1.6.1":
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/@growthbook/growthbook/-/growthbook-1.6.1.tgz#4135c680397af3e5de8d2ab92defe2c6ed697fc5"
+ integrity sha512-GSvb7bNaBTfH54AZ0oQdnoyV/ZxN9NhDEIHOsRUiM+CSOPiodz0i8/+1O6Wg0wFEVgBxS5CGWffyd74fym43Xw==
+ dependencies:
+ dom-mutator "^0.6.0"
+
"@handlebars/parser@~2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-2.0.0.tgz#5e8b7298f31ff8f7b260e6b7363c7e9ceed7d9c5"
@@ -14376,6 +14383,11 @@ dom-element-descriptors@^0.5.0, dom-element-descriptors@^0.5.1:
resolved "https://registry.yarnpkg.com/dom-element-descriptors/-/dom-element-descriptors-0.5.1.tgz#3ebfcf64198f922dba928f84f7970bb571891317"
integrity sha512-DLayMRQ+yJaziF4JJX1FMjwjdr7wdTr1y9XvZ+NfHELfOMcYDnCHneAYXAS4FT1gLILh4V0juMZohhH1N5FsoQ==
+dom-mutator@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/dom-mutator/-/dom-mutator-0.6.0.tgz#079d7a4b3e8981a562cd777548b99baab51d65c5"
+ integrity sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg==
+
dom-serializer@^1.0.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"