Skip to content

Commit 9de3ca9

Browse files
authored
ref(flags/v8): rename unleash integration param (#15345)
Renaming this parameter to keep the name consistent with #15319. We can reuse this generic name for future FF integrations. The old name is still supported but now marked as deprecated. As a result both params have to be optional, the constructor throws if neither is given. The old name will not be supported in #15343.
1 parent 9f1a17e commit 9de3ca9

File tree

10 files changed

+326
-6
lines changed

10 files changed

+326
-6
lines changed

dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ window.UnleashClient = class {
77
};
88

99
window.Sentry = Sentry;
10-
window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient });
10+
window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient });
1111

1212
Sentry.init({
1313
dsn: 'https://[email protected]/1337',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.UnleashClient = class {
4+
isEnabled(x) {
5+
return x;
6+
}
7+
};
8+
9+
window.Sentry = Sentry;
10+
window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient });
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
sampleRate: 1.0,
15+
integrations: [window.sentryUnleashIntegration],
16+
debug: true, // Required to test logging.
17+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
5+
import { shouldSkipFeatureFlagsTest } from '../../../../../utils/helpers';
6+
7+
sentryTest('Logs and returns if isEnabled does not match expected signature', async ({ getLocalTestUrl, page }) => {
8+
if (shouldSkipFeatureFlagsTest()) {
9+
sentryTest.skip();
10+
}
11+
const bundleKey = process.env.PW_BUNDLE || '';
12+
const hasDebug = !bundleKey.includes('_min');
13+
14+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
15+
return route.fulfill({
16+
status: 200,
17+
contentType: 'application/json',
18+
body: JSON.stringify({ id: 'test-id' }),
19+
});
20+
});
21+
22+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
23+
await page.goto(url);
24+
25+
const errorLogs: string[] = [];
26+
page.on('console', msg => {
27+
if (msg.type() == 'error') {
28+
errorLogs.push(msg.text());
29+
}
30+
});
31+
32+
const results = await page.evaluate(() => {
33+
const unleash = new (window as any).UnleashClient();
34+
const res1 = unleash.isEnabled('my-feature');
35+
const res2 = unleash.isEnabled(999);
36+
const res3 = unleash.isEnabled({});
37+
return [res1, res2, res3];
38+
});
39+
40+
// Test that the expected results are still returned. Note isEnabled is identity function for this test.
41+
expect(results).toEqual(['my-feature', 999, {}]);
42+
43+
// Expected error logs.
44+
if (hasDebug) {
45+
expect(errorLogs).toEqual(
46+
expect.arrayContaining([
47+
expect.stringContaining(
48+
'[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: my-feature (string), result: my-feature (string)',
49+
),
50+
expect.stringContaining(
51+
'[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: 999 (number), result: 999 (number)',
52+
),
53+
expect.stringContaining(
54+
'[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: [object Object] (object), result: [object Object] (object)',
55+
),
56+
]),
57+
);
58+
}
59+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
window.Sentry = Sentry;
44+
window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient });
45+
46+
Sentry.init({
47+
dsn: 'https://[email protected]/1337',
48+
sampleRate: 1.0,
49+
integrations: [window.sentryUnleashIntegration],
50+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
5+
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
6+
7+
const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils.
8+
9+
sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
10+
if (shouldSkipFeatureFlagsTest()) {
11+
sentryTest.skip();
12+
}
13+
14+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
15+
return route.fulfill({
16+
status: 200,
17+
contentType: 'application/json',
18+
body: JSON.stringify({ id: 'test-id' }),
19+
});
20+
});
21+
22+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
23+
await page.goto(url);
24+
25+
await page.evaluate(bufferSize => {
26+
const client = new (window as any).UnleashClient();
27+
28+
client.isEnabled('feat1');
29+
client.isEnabled('strFeat');
30+
client.isEnabled('noPayloadFeat');
31+
client.isEnabled('jsonFeat');
32+
client.isEnabled('noVariantFeat');
33+
client.isEnabled('disabledFeat');
34+
35+
for (let i = 7; i <= bufferSize; i++) {
36+
client.isEnabled(`feat${i}`);
37+
}
38+
client.isEnabled(`feat${bufferSize + 1}`); // eviction
39+
client.isEnabled('noPayloadFeat'); // update (move to tail)
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 expectedFlags = [{ flag: 'strFeat', result: true }];
48+
expectedFlags.push({ flag: 'jsonFeat', result: true });
49+
expectedFlags.push({ flag: 'noVariantFeat', result: true });
50+
expectedFlags.push({ flag: 'disabledFeat', result: false });
51+
for (let i = 7; i <= FLAG_BUFFER_SIZE; i++) {
52+
expectedFlags.push({ flag: `feat${i}`, result: false });
53+
}
54+
expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: false });
55+
expectedFlags.push({ flag: 'noPayloadFeat', result: true });
56+
57+
expect(event.contexts?.flags?.values).toEqual(expectedFlags);
58+
});

dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ window.UnleashClient = class {
4141
};
4242

4343
window.Sentry = Sentry;
44-
window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient });
44+
window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient });
4545

4646
Sentry.init({
4747
dsn: 'https://[email protected]/1337',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
window.Sentry = Sentry;
44+
window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient });
45+
46+
Sentry.init({
47+
dsn: 'https://[email protected]/1337',
48+
sampleRate: 1.0,
49+
integrations: [window.sentryUnleashIntegration],
50+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../../utils/fixtures';
4+
5+
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
6+
7+
import type { Scope } from '@sentry/browser';
8+
9+
sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => {
10+
if (shouldSkipFeatureFlagsTest()) {
11+
sentryTest.skip();
12+
}
13+
14+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
15+
return route.fulfill({
16+
status: 200,
17+
contentType: 'application/json',
18+
body: JSON.stringify({ id: 'test-id' }),
19+
});
20+
});
21+
22+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
23+
await page.goto(url);
24+
25+
const forkedReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === true);
26+
const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false);
27+
28+
await page.evaluate(() => {
29+
const Sentry = (window as any).Sentry;
30+
const errorButton = document.querySelector('#error') as HTMLButtonElement;
31+
const unleash = new (window as any).UnleashClient();
32+
33+
unleash.isEnabled('strFeat');
34+
35+
Sentry.withScope((scope: Scope) => {
36+
unleash.isEnabled('disabledFeat');
37+
unleash.isEnabled('strFeat');
38+
scope.setTag('isForked', true);
39+
if (errorButton) {
40+
errorButton.click();
41+
}
42+
});
43+
44+
unleash.isEnabled('noPayloadFeat');
45+
Sentry.getCurrentScope().setTag('isForked', false);
46+
errorButton.click();
47+
return true;
48+
});
49+
50+
const forkedReq = await forkedReqPromise;
51+
const forkedEvent = envelopeRequestParser(forkedReq);
52+
53+
const mainReq = await mainReqPromise;
54+
const mainEvent = envelopeRequestParser(mainReq);
55+
56+
expect(forkedEvent.contexts?.flags?.values).toEqual([
57+
{ flag: 'disabledFeat', result: false },
58+
{ flag: 'strFeat', result: true },
59+
]);
60+
61+
expect(mainEvent.contexts?.flags?.values).toEqual([
62+
{ flag: 'strFeat', result: true },
63+
{ flag: 'noPayloadFeat', result: true },
64+
]);
65+
});

packages/browser/src/integrations/featureFlags/unleash/integration.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import { DEBUG_BUILD } from '../../../debug-build';
55
import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags';
66
import type { UnleashClient, UnleashClientClass } from './types';
77

8+
type UnleashIntegrationOptions = {
9+
featureFlagClientClass?: UnleashClientClass;
10+
11+
/**
12+
* @deprecated Use `featureFlagClientClass` instead.
13+
*/
14+
unleashClientClass?: UnleashClientClass;
15+
};
16+
817
/**
918
* Sentry integration for capturing feature flag evaluations from the Unleash SDK.
1019
*
@@ -17,19 +26,24 @@ import type { UnleashClient, UnleashClientClass } from './types';
1726
*
1827
* Sentry.init({
1928
* dsn: '___PUBLIC_DSN___',
20-
* integrations: [Sentry.unleashIntegration({unleashClientClass: UnleashClient})],
29+
* integrations: [Sentry.unleashIntegration({featureFlagClientClass: UnleashClient})],
2130
* });
2231
*
2332
* const unleash = new UnleashClient(...);
2433
* unleash.start();
2534
*
2635
* unleash.isEnabled('my-feature');
27-
* unleash.getVariant('other-feature');
2836
* Sentry.captureException(new Error('something went wrong'));
2937
* ```
3038
*/
3139
export const unleashIntegration = defineIntegration(
32-
({ unleashClientClass }: { unleashClientClass: UnleashClientClass }) => {
40+
// eslint-disable-next-line deprecation/deprecation
41+
({ featureFlagClientClass, unleashClientClass }: UnleashIntegrationOptions) => {
42+
const _unleashClientClass = featureFlagClientClass ? featureFlagClientClass : unleashClientClass;
43+
if (!_unleashClientClass) {
44+
throw new Error('featureFlagClientClass option is required');
45+
}
46+
3347
return {
3448
name: 'Unleash',
3549

@@ -38,7 +52,7 @@ export const unleashIntegration = defineIntegration(
3852
},
3953

4054
setupOnce() {
41-
const unleashClientPrototype = unleashClientClass.prototype as UnleashClient;
55+
const unleashClientPrototype = _unleashClientClass.prototype as UnleashClient;
4256
fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled);
4357
},
4458
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { unleashIntegration } from '../../../src';
2+
3+
describe('Unleash', () => {
4+
it('Throws error if given empty options', () => {
5+
expect(() => unleashIntegration({})).toThrow('featureFlagClientClass option is required');
6+
});
7+
});

0 commit comments

Comments
 (0)