Skip to content

Commit 2459a11

Browse files
committed
Add of, stat, unleash tests
1 parent e6da588 commit 2459a11

File tree

12 files changed

+421
-0
lines changed

12 files changed

+421
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration();
5+
6+
Sentry.init({
7+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
8+
sampleRate: 1.0,
9+
tracesSampleRate: 1.0,
10+
integrations: [
11+
window.sentryOpenFeatureIntegration,
12+
Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }),
13+
],
14+
});
15+
16+
window.initialize = () => {
17+
return {
18+
getBooleanValue(flag, value) {
19+
let hook = new Sentry.OpenFeatureIntegrationHook();
20+
hook.after(null, { flagKey: flag, value: value });
21+
return value;
22+
},
23+
};
24+
};
25+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const btnStartSpan = document.getElementById('btnStartSpan');
2+
const btnEndSpan = document.getElementById('btnEndSpan');
3+
const btnStartNestedSpan = document.getElementById('btnStartNestedSpan');
4+
const btnEndNestedSpan = document.getElementById('btnEndNestedSpan');
5+
6+
window.withNestedSpans = callback => {
7+
window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => {
8+
window.traceId = rootSpan.spanContext().traceId;
9+
10+
window.Sentry.startSpan({ name: 'test-span' }, _span => {
11+
window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => {
12+
callback();
13+
});
14+
});
15+
});
16+
};
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="btnStartSpan">Start Span</button>
8+
<button id="btnEndSpan">End Span</button>
9+
<button id="btnStartNestedSpan">Start Nested Span</button>
10+
<button id="btnEndNestedSpan">End Nested Span</button>
11+
</body>
12+
</html>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../../utils/fixtures';
3+
import {
4+
type EventAndTraceHeader,
5+
eventAndTraceHeaderRequestParser,
6+
getMultipleSentryEnvelopeRequests,
7+
shouldSkipFeatureFlagsTest,
8+
shouldSkipTracingTest,
9+
} from '../../../../../utils/helpers';
10+
import { MAX_FLAGS_PER_SPAN } from '../../constants';
11+
12+
sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
13+
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
14+
sentryTest.skip();
15+
}
16+
17+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
18+
return route.fulfill({
19+
status: 200,
20+
contentType: 'application/json',
21+
body: JSON.stringify({}),
22+
});
23+
});
24+
25+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
26+
await page.goto(url);
27+
28+
const envelopeRequestPromise = getMultipleSentryEnvelopeRequests<EventAndTraceHeader>(
29+
page,
30+
1,
31+
{},
32+
eventAndTraceHeaderRequestParser,
33+
);
34+
35+
// withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span.
36+
await page.evaluate(maxFlags => {
37+
(window as any).withNestedSpans(() => {
38+
const client = (window as any).initialize();
39+
for (let i = 1; i <= maxFlags; i++) {
40+
client.getBooleanValue(`feat${i}`, false);
41+
}
42+
client.getBooleanValue(`feat${maxFlags + 1}`, true); // drop
43+
client.getBooleanValue('feat3', true); // update
44+
});
45+
return true;
46+
}, MAX_FLAGS_PER_SPAN);
47+
48+
const event = (await envelopeRequestPromise)[0][0];
49+
const innerSpan = event.spans?.[0];
50+
const outerSpan = event.spans?.[1];
51+
const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) =>
52+
key.startsWith('flag.evaluation'),
53+
);
54+
const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) =>
55+
key.startsWith('flag.evaluation'),
56+
);
57+
58+
expect(innerSpanFlags).toEqual([]);
59+
60+
const expectedOuterSpanFlags = [];
61+
for (let i = 1; i <= 2; i++) {
62+
expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]);
63+
}
64+
for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) {
65+
expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]);
66+
}
67+
expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]);
68+
expect(outerSpanFlags).toEqual(expectedOuterSpanFlags);
69+
});
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+
class MockStatsigClient {
4+
constructor() {
5+
this._gateEvaluationListeners = [];
6+
this._mockGateValues = {};
7+
}
8+
9+
on(event, listener) {
10+
this._gateEvaluationListeners.push(listener);
11+
}
12+
13+
checkGate(name) {
14+
const value = this._mockGateValues[name] || false; // unknown features default to false.
15+
this._gateEvaluationListeners.forEach(listener => {
16+
listener({ gate: { name, value } });
17+
});
18+
return value;
19+
}
20+
21+
setMockGateValue(name, value) {
22+
this._mockGateValues[name] = value;
23+
}
24+
}
25+
26+
window.statsigClient = new MockStatsigClient();
27+
28+
window.Sentry = Sentry;
29+
window.sentryStatsigIntegration = Sentry.statsigIntegration({ featureFlagClient: window.statsigClient });
30+
31+
Sentry.init({
32+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
33+
sampleRate: 1.0,
34+
tracesSampleRate: 1.0,
35+
integrations: [
36+
window.sentryStatsigIntegration,
37+
Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }),
38+
],
39+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const btnStartSpan = document.getElementById('btnStartSpan');
2+
const btnEndSpan = document.getElementById('btnEndSpan');
3+
const btnStartNestedSpan = document.getElementById('btnStartNestedSpan');
4+
const btnEndNestedSpan = document.getElementById('btnEndNestedSpan');
5+
6+
window.withNestedSpans = callback => {
7+
window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => {
8+
window.traceId = rootSpan.spanContext().traceId;
9+
10+
window.Sentry.startSpan({ name: 'test-span' }, _span => {
11+
window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => {
12+
callback();
13+
});
14+
});
15+
});
16+
};
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="btnStartSpan">Start Span</button>
8+
<button id="btnEndSpan">End Span</button>
9+
<button id="btnStartNestedSpan">Start Nested Span</button>
10+
<button id="btnEndNestedSpan">End Nested Span</button>
11+
</body>
12+
</html>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../../utils/fixtures';
3+
import {
4+
type EventAndTraceHeader,
5+
eventAndTraceHeaderRequestParser,
6+
getMultipleSentryEnvelopeRequests,
7+
shouldSkipFeatureFlagsTest,
8+
shouldSkipTracingTest,
9+
} from '../../../../../utils/helpers';
10+
import { MAX_FLAGS_PER_SPAN } from '../../constants';
11+
12+
sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
13+
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
14+
sentryTest.skip();
15+
}
16+
17+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
18+
return route.fulfill({
19+
status: 200,
20+
contentType: 'application/json',
21+
body: JSON.stringify({}),
22+
});
23+
});
24+
25+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
26+
await page.goto(url);
27+
28+
const envelopeRequestPromise = getMultipleSentryEnvelopeRequests<EventAndTraceHeader>(
29+
page,
30+
1,
31+
{},
32+
eventAndTraceHeaderRequestParser,
33+
);
34+
35+
// withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span.
36+
await page.evaluate(maxFlags => {
37+
(window as any).withNestedSpans(() => {
38+
const client = (window as any).statsigClient;
39+
for (let i = 1; i <= maxFlags; i++) {
40+
client.checkGate(`feat${i}`); // values default to false
41+
}
42+
43+
client.setMockGateValue(`feat${maxFlags + 1}`, true);
44+
client.checkGate(`feat${maxFlags + 1}`); // dropped
45+
46+
client.setMockGateValue('feat3', true);
47+
client.checkGate('feat3'); // update
48+
});
49+
return true;
50+
}, MAX_FLAGS_PER_SPAN);
51+
52+
const event = (await envelopeRequestPromise)[0][0];
53+
const innerSpan = event.spans?.[0];
54+
const outerSpan = event.spans?.[1];
55+
const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) =>
56+
key.startsWith('flag.evaluation'),
57+
);
58+
const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) =>
59+
key.startsWith('flag.evaluation'),
60+
);
61+
62+
expect(innerSpanFlags).toEqual([]);
63+
64+
const expectedOuterSpanFlags = [];
65+
for (let i = 1; i <= 2; i++) {
66+
expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]);
67+
}
68+
for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) {
69+
expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]);
70+
}
71+
expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]);
72+
expect(outerSpanFlags).toEqual(expectedOuterSpanFlags);
73+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.UnleashClient = class {
4+
constructor() {
5+
this._featureToVariant = {
6+
strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } },
7+
noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true },
8+
jsonFeat: {
9+
name: 'paid-orgs',
10+
enabled: true,
11+
feature_enabled: true,
12+
payload: {
13+
type: 'json',
14+
value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}',
15+
},
16+
},
17+
18+
// Enabled feature with no configured variants.
19+
noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true },
20+
21+
// Disabled feature.
22+
disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false },
23+
};
24+
25+
// Variant returned for features that don't exist.
26+
// `feature_enabled` may be defined in prod, but we want to test the undefined case.
27+
this._fallbackVariant = {
28+
name: 'disabled',
29+
enabled: false,
30+
};
31+
}
32+
33+
isEnabled(toggleName) {
34+
const variant = this._featureToVariant[toggleName] || this._fallbackVariant;
35+
return variant.feature_enabled || false;
36+
}
37+
38+
getVariant(toggleName) {
39+
return this._featureToVariant[toggleName] || this._fallbackVariant;
40+
}
41+
};
42+
43+
// Not a mock UnleashClient class method since it needs to match the signature of the actual UnleashClient.
44+
window.setVariant = (client, featureName, variantName, isEnabled) => {
45+
client._featureToVariant[featureName] = { name: variantName, enabled: isEnabled, feature_enabled: isEnabled };
46+
}
47+
48+
window.Sentry = Sentry;
49+
window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient });
50+
51+
Sentry.init({
52+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
53+
sampleRate: 1.0,
54+
tracesSampleRate: 1.0,
55+
integrations: [
56+
window.sentryUnleashIntegration,
57+
Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }),
58+
],
59+
});
60+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const btnStartSpan = document.getElementById('btnStartSpan');
2+
const btnEndSpan = document.getElementById('btnEndSpan');
3+
const btnStartNestedSpan = document.getElementById('btnStartNestedSpan');
4+
const btnEndNestedSpan = document.getElementById('btnEndNestedSpan');
5+
6+
window.withNestedSpans = callback => {
7+
window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => {
8+
window.traceId = rootSpan.spanContext().traceId;
9+
10+
window.Sentry.startSpan({ name: 'test-span' }, _span => {
11+
window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => {
12+
callback();
13+
});
14+
});
15+
});
16+
};

0 commit comments

Comments
 (0)