Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
e6ce870
add growthbook integration
madhuchavva Aug 21, 2025
7d2bec8
add handler
madhuchavva Aug 21, 2025
ff684eb
add handler, types and shim
madhuchavva Aug 21, 2025
b50ef20
capture evalFeature method
madhuchavva Aug 21, 2025
a6c3d73
add integration tests
madhuchavva Aug 21, 2025
92ba49c
fix fixture imports
madhuchavva Aug 21, 2025
0b38a7b
use withNestedSpans
madhuchavva Aug 21, 2025
819b5cb
fill buffer and assert ordered list
madhuchavva Aug 21, 2025
e07bab4
Merge branch 'develop' into feat/growthbook-integration
madhuchavva Aug 29, 2025
a79d0ed
move gb integration to core
madhuchavva Sep 23, 2025
8b2914f
update tests
madhuchavva Sep 23, 2025
dfdae98
remove evalFunction
madhuchavva Sep 24, 2025
0a6c795
Merge branch 'develop' into feat/growthbook-integration
madhuchavva Sep 24, 2025
996a37d
fix shim
madhuchavva Sep 24, 2025
94bb650
Merge remote-tracking branch 'refs/remotes/origin/feat/growthbook-int…
madhuchavva Sep 24, 2025
8341c65
Merge branch 'develop' into feat/growthbook-integration
madhuchavva Sep 24, 2025
3b0b82d
Merge branch 'develop' into feat/growthbook-integration
madhuchavva Sep 25, 2025
bbb6c8d
add node integration tests
madhuchavva Sep 25, 2025
ce4e27e
add exports to astro
madhuchavva Sep 25, 2025
b266010
export growthbook integration
madhuchavva Sep 25, 2025
f75e3f1
fix race condition
madhuchavva Sep 25, 2025
6f2e0aa
fix lint issues
madhuchavva Sep 26, 2025
7b5af7c
fix nuxt build issue
madhuchavva Sep 26, 2025
b351da2
resolve import conflict
madhuchavva Sep 26, 2025
2a5d25f
fix type cast warning
madhuchavva Sep 26, 2025
9fbd558
export growthbook integration
madhuchavva Sep 29, 2025
03437b1
Use actual growthbook instance for testing
madhuchavva Sep 29, 2025
e6193e3
Merge branch 'develop' into feat/growthbook-integration
madhuchavva Sep 29, 2025
0ffa0a6
fix linting issues
madhuchavva Sep 30, 2025
8fe30ac
Merge remote-tracking branch 'refs/remotes/origin/feat/growthbook-int…
madhuchavva Sep 30, 2025
dec0477
add growthbook integration
madhuchavva Aug 21, 2025
26f7d8d
add handler
madhuchavva Aug 21, 2025
440b426
add handler, types and shim
madhuchavva Aug 21, 2025
f04b046
capture evalFeature method
madhuchavva Aug 21, 2025
4256177
add integration tests
madhuchavva Aug 21, 2025
7018fe7
fix fixture imports
madhuchavva Aug 21, 2025
f245d4e
use withNestedSpans
madhuchavva Aug 21, 2025
909194b
fill buffer and assert ordered list
madhuchavva Aug 21, 2025
2a2ab11
move gb integration to core
madhuchavva Sep 23, 2025
f96d204
update tests
madhuchavva Sep 23, 2025
d9c8168
remove evalFunction
madhuchavva Sep 24, 2025
51e4716
fix shim
madhuchavva Sep 24, 2025
6e73250
add node integration tests
madhuchavva Sep 25, 2025
3e2d47b
add exports to astro
madhuchavva Sep 25, 2025
6b9d6a9
export growthbook integration
madhuchavva Sep 25, 2025
59fe2fd
fix race condition
madhuchavva Sep 25, 2025
3a9664d
fix lint issues
madhuchavva Sep 26, 2025
18864b3
fix nuxt build issue
madhuchavva Sep 26, 2025
958bd6d
resolve import conflict
madhuchavva Sep 26, 2025
709fc90
fix type cast warning
madhuchavva Sep 26, 2025
fcc38b8
export growthbook integration
madhuchavva Sep 29, 2025
a8aeeb2
Use actual growthbook instance for testing
madhuchavva Sep 29, 2025
e1379ca
fix linting issues
madhuchavva Sep 30, 2025
6d0c950
Merge remote-tracking branch 'refs/remotes/origin/feat/growthbook-int…
madhuchavva Oct 6, 2025
27933b5
Merge branch 'develop' into feat/growthbook-integration
madhuchavva Oct 6, 2025
99aea89
add growthbook dep for integration tests
madhuchavva Oct 6, 2025
827f318
Merge remote-tracking branch 'refs/remotes/origin/feat/growthbook-int…
madhuchavva Oct 6, 2025
1576bfc
Merge branch 'develop' into feat/growthbook-integration
madhuchavva Oct 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -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],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
document.getElementById('error').addEventListener('click', () => {
throw new Error('Button triggered error');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="error">Throw Error</button>
</body>
<script src="./subject.js"></script>
</html>


Original file line number Diff line number Diff line change
@@ -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 },
]);
});
Original file line number Diff line number Diff line change
@@ -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 }),
],
});
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="btnStartSpan">Start Span</button>
<button id="btnEndSpan">End Span</button>
<button id="btnStartNestedSpan">Start Nested Span</button>
<button id="btnEndNestedSpan">End Nested Span</button>
</body>
<script src="./subject.js"></script>
</html>


Original file line number Diff line number Diff line change
@@ -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<EventAndTraceHeader>(
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());
},
);
1 change: 1 addition & 0 deletions dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading