Skip to content

Commit a6c3d73

Browse files
committed
add integration tests
1 parent b50ef20 commit a6c3d73

File tree

13 files changed

+289
-9
lines changed

13 files changed

+289
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../../utils/fixtures';
3+
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
4+
import { FLAG_BUFFER_SIZE } from '../../constants';
5+
6+
sentryTest('GrowthBook onError: basic eviction/update and mixed values', async ({ getLocalTestUrl, page }) => {
7+
if (shouldSkipFeatureFlagsTest()) {
8+
sentryTest.skip();
9+
}
10+
11+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
12+
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 'test-id' }) });
13+
});
14+
15+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
16+
await page.goto(url);
17+
18+
await page.evaluate(bufferSize => {
19+
const gb = new (window as any).GrowthBook();
20+
21+
gb.__setOn('onTrue', true);
22+
gb.__setOn('onFalse', false);
23+
gb.__setFeatureValue('strVal', 'hello');
24+
gb.__setFeatureValue('numVal', 42);
25+
gb.__setFeatureValue('objVal', { a: 1, b: 'c' });
26+
27+
gb.isOn('onTrue');
28+
gb.isOn('onFalse');
29+
gb.getFeatureValue('strVal', '');
30+
gb.getFeatureValue('numVal', 0);
31+
gb.getFeatureValue('objVal', {});
32+
33+
for (let i = 1; i <= bufferSize; i++) {
34+
gb.isOn(`feat${i}`);
35+
}
36+
37+
gb.__setOn(`feat${bufferSize + 1}`, true);
38+
gb.isOn(`feat${bufferSize + 1}`);
39+
gb.isOn('feat3');
40+
}, FLAG_BUFFER_SIZE);
41+
42+
const reqPromise = waitForErrorRequest(page);
43+
await page.locator('#error').click();
44+
const req = await reqPromise;
45+
const event = envelopeRequestParser(req);
46+
47+
const values = event.contexts?.flags?.values || [];
48+
expect(values).toEqual(
49+
expect.arrayContaining([
50+
{ flag: 'onTrue', result: true },
51+
{ flag: 'onFalse', result: false },
52+
{ flag: 'strVal', result: 'hello' },
53+
{ flag: 'numVal', result: 42 },
54+
{ flag: 'objVal', result: { a: 1, b: 'c' } },
55+
]),
56+
);
57+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
// Minimal mock GrowthBook class for tests
4+
window.GrowthBook = class {
5+
constructor() {
6+
this._onFlags = Object.create(null);
7+
this._featureValues = Object.create(null);
8+
}
9+
10+
isOn(featureKey) {
11+
return !!this._onFlags[featureKey];
12+
}
13+
14+
getFeatureValue(featureKey, defaultValue) {
15+
return Object.prototype.hasOwnProperty.call(this._featureValues, featureKey)
16+
? this._featureValues[featureKey]
17+
: defaultValue;
18+
}
19+
20+
// Helpers for tests
21+
__setOn(featureKey, value) {
22+
this._onFlags[featureKey] = !!value;
23+
}
24+
25+
__setFeatureValue(featureKey, value) {
26+
this._featureValues[featureKey] = value;
27+
}
28+
};
29+
30+
window.Sentry = Sentry;
31+
window.sentryGrowthBookIntegration = Sentry.growthbookIntegration({ growthbookClass: window.GrowthBook });
32+
33+
Sentry.init({
34+
dsn: 'https://[email protected]/1337',
35+
sampleRate: 1.0,
36+
integrations: [window.sentryGrowthBookIntegration],
37+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
document.getElementById('error').addEventListener('click', () => {
2+
throw new Error('Button triggered error');
3+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="error">Throw Error</button>
8+
</body>
9+
<script src="./subject.js"></script>
10+
</html>
11+
12+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { expect } from '@playwright/test';
2+
import type { Scope } from '@sentry/browser';
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
5+
6+
sentryTest('GrowthBook onError: forked scopes are isolated', async ({ getLocalTestUrl, page }) => {
7+
if (shouldSkipFeatureFlagsTest()) {
8+
sentryTest.skip();
9+
}
10+
11+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
12+
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ id: 'test-id' }) });
13+
});
14+
15+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
16+
await page.goto(url);
17+
18+
const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === true);
19+
const mainReqPromise = waitForErrorRequest(page, event => !!event.tags?.isForked === false);
20+
21+
await page.evaluate(() => {
22+
const Sentry = (window as any).Sentry;
23+
const errorButton = document.querySelector('#error') as HTMLButtonElement;
24+
const gb = new (window as any).GrowthBook();
25+
26+
gb.__setOn('shared', true);
27+
gb.__setOn('main', true);
28+
29+
gb.isOn('shared');
30+
31+
Sentry.withScope((scope: Scope) => {
32+
gb.__setOn('forked', true);
33+
gb.__setOn('shared', false);
34+
gb.isOn('forked');
35+
gb.isOn('shared');
36+
scope.setTag('isForked', true);
37+
errorButton.click();
38+
});
39+
40+
gb.isOn('main');
41+
Sentry.getCurrentScope().setTag('isForked', false);
42+
errorButton.click();
43+
return true;
44+
});
45+
46+
const forkedReq = await forkedReqPromise;
47+
const forkedEvent = envelopeRequestParser(forkedReq);
48+
49+
const mainReq = await mainReqPromise;
50+
const mainEvent = envelopeRequestParser(mainReq);
51+
52+
expect(forkedEvent.contexts?.flags?.values).toEqual([
53+
{ flag: 'forked', result: true },
54+
{ flag: 'shared', result: false },
55+
]);
56+
57+
expect(mainEvent.contexts?.flags?.values).toEqual([
58+
{ flag: 'shared', result: true },
59+
{ flag: 'main', result: true },
60+
]);
61+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.GrowthBook = class {
4+
constructor() {
5+
this._onFlags = Object.create(null);
6+
this._featureValues = Object.create(null);
7+
}
8+
9+
isOn(featureKey) {
10+
return !!this._onFlags[featureKey];
11+
}
12+
13+
getFeatureValue(featureKey, defaultValue) {
14+
return Object.prototype.hasOwnProperty.call(this._featureValues, featureKey)
15+
? this._featureValues[featureKey]
16+
: defaultValue;
17+
}
18+
19+
__setOn(featureKey, value) {
20+
this._onFlags[featureKey] = !!value;
21+
}
22+
23+
__setFeatureValue(featureKey, value) {
24+
this._featureValues[featureKey] = value;
25+
}
26+
};
27+
28+
window.Sentry = Sentry;
29+
window.sentryGrowthBookIntegration = Sentry.growthbookIntegration({ growthbookClass: window.GrowthBook });
30+
31+
Sentry.init({
32+
dsn: 'https://[email protected]/1337',
33+
sampleRate: 1.0,
34+
tracesSampleRate: 1.0,
35+
integrations: [
36+
window.sentryGrowthBookIntegration,
37+
Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }),
38+
],
39+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
document.getElementById('error').addEventListener('click', () => {
2+
// no-op
3+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="error">Throw Error</button>
8+
</body>
9+
<script src="./subject.js"></script>
10+
</html>
11+
12+
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect } from '@playwright/test';
2+
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
import {
5+
type EventAndTraceHeader,
6+
eventAndTraceHeaderRequestParser,
7+
getMultipleSentryEnvelopeRequests,
8+
shouldSkipFeatureFlagsTest,
9+
shouldSkipTracingTest,
10+
} from '../../../../../utils/helpers';
11+
12+
sentryTest(
13+
"GrowthBook onSpan: flags are added to active span's attributes on span end",
14+
async ({ getLocalTestUrl, page }) => {
15+
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
16+
sentryTest.skip();
17+
}
18+
19+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
20+
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
21+
});
22+
23+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
24+
await page.goto(url);
25+
26+
const envelopeRequestPromise = getMultipleSentryEnvelopeRequests<EventAndTraceHeader>(
27+
page,
28+
1,
29+
{},
30+
eventAndTraceHeaderRequestParser,
31+
);
32+
33+
await page.evaluate(maxFlags => {
34+
(window as any).withNestedSpans(() => {
35+
const gb = new (window as any).GrowthBook();
36+
for (let i = 1; i <= maxFlags; i++) {
37+
gb.isOn(`feat${i}`);
38+
}
39+
gb.__setOn(`feat${maxFlags + 1}`, true);
40+
gb.isOn(`feat${maxFlags + 1}`); // dropped
41+
gb.__setOn('feat3', true);
42+
gb.isOn('feat3'); // update
43+
});
44+
return true;
45+
}, MAX_FLAGS_PER_SPAN);
46+
47+
const event = (await envelopeRequestPromise)[0][0];
48+
const innerSpan = event.spans?.[0];
49+
const outerSpan = event.spans?.[1];
50+
const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) =>
51+
key.startsWith('flag.evaluation'),
52+
);
53+
const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) =>
54+
key.startsWith('flag.evaluation'),
55+
);
56+
57+
expect(innerSpanFlags).toEqual([]);
58+
59+
const expectedOuterSpanFlags = [] as Array<[string, unknown]>;
60+
for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
61+
expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]);
62+
}
63+
expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort());
64+
},
65+
);
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
export { growthbookIntegration } from './integration';
2-
3-

0 commit comments

Comments
 (0)