-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(flags/v8): Add Unleash integration #14948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
90714e1
Init[WIP]
aliu39 29f5bac
Merge branch 'v8' into aliu/unleash
aliu39 e714b9e
Working patch and basic test for isEnabled
aliu39 e02b91b
Biome check
aliu39 e464187
Merge branch 'v8' into aliu/unleash
aliu39 7ec8d02
Working patch and test for getVariant
aliu39 f1a5574
Fix docstr sample code
aliu39 34af880
Fix patching for correct access to
aliu39 c8b0078
ini.js format
aliu39 2543733
init.js format2
aliu39 211a1ab
Disable eslint/unbound-method
aliu39 2898386
Rm getVariant code
aliu39 b972f6e
Add withScope test
aliu39 dd64f14
Merge branch 'v8' into aliu/unleash
aliu39 8c07c27
Merge branch 'v8' into aliu/unleash
aliu39 dbf0f64
Rewrite patching with fill()
aliu39 ba090a2
Change integration input to an options dict
aliu39 bba93f7
Update wrapper docstr
aliu39 776bb58
Add badSignature test
aliu39 fae120b
Fix formatting (yarn run biome check --apply)
aliu39 6160f07
Merge branch 'v8' into aliu/unleash
aliu39 bcc0b86
Only log/test if debug build
aliu39 c142b4f
Make UnleashClientClass type more specific
aliu39 dcf2595
Simplify docstr
aliu39 04cc919
Merge branch 'v8' into aliu/unleash
aliu39 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
17 changes: 17 additions & 0 deletions
17
...s/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/init.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
|
||
window.UnleashClient = class { | ||
isEnabled(x) { | ||
return x; | ||
} | ||
}; | ||
|
||
window.Sentry = Sentry; | ||
window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient }); | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
sampleRate: 1.0, | ||
integrations: [window.sentryUnleashIntegration], | ||
debug: true, // Required to test logging. | ||
}); |
59 changes: 59 additions & 0 deletions
59
...s/browser-integration-tests/suites/integrations/featureFlags/unleash/badSignature/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { expect } from '@playwright/test'; | ||
|
||
import { sentryTest } from '../../../../../utils/fixtures'; | ||
|
||
import { shouldSkipFeatureFlagsTest } from '../../../../../utils/helpers'; | ||
|
||
sentryTest('Logs and returns if isEnabled does not match expected signature', async ({ getLocalTestUrl, page }) => { | ||
if (shouldSkipFeatureFlagsTest()) { | ||
sentryTest.skip(); | ||
} | ||
const bundleKey = process.env.PW_BUNDLE || ''; | ||
const hasDebug = !bundleKey.includes('_min'); | ||
|
||
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 errorLogs: string[] = []; | ||
page.on('console', msg => { | ||
if (msg.type() == 'error') { | ||
errorLogs.push(msg.text()); | ||
} | ||
}); | ||
|
||
const results = await page.evaluate(() => { | ||
const unleash = new (window as any).UnleashClient(); | ||
const res1 = unleash.isEnabled('my-feature'); | ||
const res2 = unleash.isEnabled(999); | ||
const res3 = unleash.isEnabled({}); | ||
return [res1, res2, res3]; | ||
}); | ||
|
||
// Test that the expected results are still returned. Note isEnabled is identity function for this test. | ||
expect(results).toEqual(['my-feature', 999, {}]); | ||
|
||
// Expected error logs. | ||
if (hasDebug) { | ||
expect(errorLogs).toEqual( | ||
expect.arrayContaining([ | ||
expect.stringContaining( | ||
'[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: my-feature (string), result: my-feature (string)', | ||
), | ||
expect.stringContaining( | ||
'[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: 999 (number), result: 999 (number)', | ||
), | ||
expect.stringContaining( | ||
'[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: [object Object] (object), result: [object Object] (object)', | ||
), | ||
]), | ||
); | ||
} | ||
}); |
58 changes: 58 additions & 0 deletions
58
...packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { expect } from '@playwright/test'; | ||
|
||
import { sentryTest } from '../../../../../utils/fixtures'; | ||
|
||
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; | ||
|
||
const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. | ||
|
||
sentryTest('Basic test with 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 client = new (window as any).UnleashClient(); | ||
|
||
client.isEnabled('feat1'); | ||
client.isEnabled('strFeat'); | ||
client.isEnabled('noPayloadFeat'); | ||
client.isEnabled('jsonFeat'); | ||
client.isEnabled('noVariantFeat'); | ||
client.isEnabled('disabledFeat'); | ||
|
||
for (let i = 7; i <= bufferSize; i++) { | ||
client.isEnabled(`feat${i}`); | ||
} | ||
client.isEnabled(`feat${bufferSize + 1}`); // eviction | ||
client.isEnabled('noPayloadFeat'); // update (move to tail) | ||
}, FLAG_BUFFER_SIZE); | ||
|
||
const reqPromise = waitForErrorRequest(page); | ||
await page.locator('#error').click(); | ||
const req = await reqPromise; | ||
const event = envelopeRequestParser(req); | ||
|
||
const expectedFlags = [{ flag: 'strFeat', result: true }]; | ||
expectedFlags.push({ flag: 'jsonFeat', result: true }); | ||
expectedFlags.push({ flag: 'noVariantFeat', result: true }); | ||
expectedFlags.push({ flag: 'disabledFeat', result: false }); | ||
for (let i = 7; i <= FLAG_BUFFER_SIZE; i++) { | ||
expectedFlags.push({ flag: `feat${i}`, result: false }); | ||
} | ||
expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: false }); | ||
expectedFlags.push({ flag: 'noPayloadFeat', result: true }); | ||
|
||
expect(event.contexts?.flags?.values).toEqual(expectedFlags); | ||
}); |
50 changes: 50 additions & 0 deletions
50
dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
|
||
window.UnleashClient = class { | ||
constructor() { | ||
this._featureToVariant = { | ||
strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } }, | ||
noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true }, | ||
jsonFeat: { | ||
name: 'paid-orgs', | ||
enabled: true, | ||
feature_enabled: true, | ||
payload: { | ||
type: 'json', | ||
value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}', | ||
}, | ||
}, | ||
|
||
// Enabled feature with no configured variants. | ||
noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true }, | ||
|
||
// Disabled feature. | ||
disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false }, | ||
}; | ||
|
||
// Variant returned for features that don't exist. | ||
// `feature_enabled` may be defined in prod, but we want to test the undefined case. | ||
this._fallbackVariant = { | ||
name: 'disabled', | ||
enabled: false, | ||
}; | ||
} | ||
|
||
isEnabled(toggleName) { | ||
const variant = this._featureToVariant[toggleName] || this._fallbackVariant; | ||
return variant.feature_enabled || false; | ||
} | ||
|
||
getVariant(toggleName) { | ||
return this._featureToVariant[toggleName] || this._fallbackVariant; | ||
} | ||
}; | ||
|
||
window.Sentry = Sentry; | ||
window.sentryUnleashIntegration = Sentry.unleashIntegration({ unleashClientClass: window.UnleashClient }); | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
sampleRate: 1.0, | ||
integrations: [window.sentryUnleashIntegration], | ||
}); |
3 changes: 3 additions & 0 deletions
3
dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); |
9 changes: 9 additions & 0 deletions
9
...packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8" /> | ||
</head> | ||
<body> | ||
<button id="error">Throw Error</button> | ||
</body> | ||
</html> |
65 changes: 65 additions & 0 deletions
65
...ages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { expect } from '@playwright/test'; | ||
|
||
import { sentryTest } from '../../../../../utils/fixtures'; | ||
|
||
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; | ||
|
||
import type { Scope } from '@sentry/browser'; | ||
|
||
sentryTest('Flag evaluations in forked scopes are stored separately.', 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 && event.tags.isForked === true); | ||
const mainReqPromise = waitForErrorRequest(page, event => !!event.tags && event.tags.isForked === false); | ||
|
||
await page.evaluate(() => { | ||
const Sentry = (window as any).Sentry; | ||
const errorButton = document.querySelector('#error') as HTMLButtonElement; | ||
const unleash = new (window as any).UnleashClient(); | ||
|
||
unleash.isEnabled('strFeat'); | ||
|
||
Sentry.withScope((scope: Scope) => { | ||
unleash.isEnabled('disabledFeat'); | ||
unleash.isEnabled('strFeat'); | ||
scope.setTag('isForked', true); | ||
if (errorButton) { | ||
errorButton.click(); | ||
} | ||
}); | ||
|
||
unleash.isEnabled('noPayloadFeat'); | ||
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: 'disabledFeat', result: false }, | ||
{ flag: 'strFeat', result: true }, | ||
]); | ||
|
||
expect(mainEvent.contexts?.flags?.values).toEqual([ | ||
{ flag: 'strFeat', result: true }, | ||
{ flag: 'noPayloadFeat', result: true }, | ||
]); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
packages/browser/src/integrations/featureFlags/unleash/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { unleashIntegration } from './integration'; |
75 changes: 75 additions & 0 deletions
75
packages/browser/src/integrations/featureFlags/unleash/integration.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; | ||
|
||
import { defineIntegration, fill, logger } from '@sentry/core'; | ||
import { DEBUG_BUILD } from '../../../debug-build'; | ||
import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; | ||
import type { UnleashClient, UnleashClientClass } from './types'; | ||
|
||
/** | ||
* Sentry integration for capturing feature flag evaluations from the Unleash SDK. | ||
* | ||
* See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. | ||
* | ||
* @example | ||
* ``` | ||
* import { UnleashClient } from 'unleash-proxy-client'; | ||
* import * as Sentry from '@sentry/browser'; | ||
* | ||
* const unleashIntegration = Sentry.unleashIntegration({unleashClientClass: UnleashClient}); | ||
* | ||
* Sentry.init({ | ||
* dsn: '___PUBLIC_DSN___', | ||
* integrations: [unleashIntegration], | ||
* }); | ||
* | ||
* const unleash = new UnleashClient(...); | ||
* unleash.start(); | ||
* | ||
* unleash.isEnabled('my-feature'); | ||
* unleash.getVariant('other-feature'); | ||
* Sentry.captureException(new Error('something went wrong')); | ||
* ``` | ||
*/ | ||
export const unleashIntegration = defineIntegration( | ||
({ unleashClientClass }: { unleashClientClass: UnleashClientClass }) => { | ||
return { | ||
name: 'Unleash', | ||
|
||
processEvent(event: Event, _hint: EventHint, _client: Client): Event { | ||
return copyFlagsFromScopeToEvent(event); | ||
}, | ||
|
||
setupOnce() { | ||
const unleashClientPrototype = unleashClientClass.prototype as UnleashClient; | ||
fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled); | ||
}, | ||
}; | ||
}, | ||
) satisfies IntegrationFn; | ||
|
||
/** | ||
* Wraps the UnleashClient.isEnabled method to capture feature flag evaluations. Its only side effect is writing to Sentry scope. | ||
* | ||
* This wrapper is safe for all isEnabled signatures. If the signature does not match (this: UnleashClient, toggleName: string, ...args: unknown[]) => boolean, | ||
* we log an error and return the original result. | ||
* | ||
* @param original - The original method. | ||
* @returns Wrapped method. Results should match the original. | ||
*/ | ||
function _wrappedIsEnabled( | ||
original: (this: UnleashClient, ...args: unknown[]) => unknown, | ||
): (this: UnleashClient, ...args: unknown[]) => unknown { | ||
aliu39 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return function (this: UnleashClient, ...args: unknown[]): unknown { | ||
const toggleName = args[0]; | ||
const result = original.apply(this, args); | ||
|
||
if (typeof toggleName === 'string' && typeof result === 'boolean') { | ||
insertFlagToScope(toggleName, result); | ||
} else if (DEBUG_BUILD) { | ||
logger.error( | ||
chargome marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: ${toggleName} (${typeof toggleName}), result: ${result} (${typeof result})`, | ||
); | ||
} | ||
return result; | ||
}; | ||
} |
16 changes: 16 additions & 0 deletions
16
packages/browser/src/integrations/featureFlags/unleash/types.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export interface IVariant { | ||
name: string; | ||
enabled: boolean; | ||
feature_enabled?: boolean; | ||
payload?: { | ||
type: string; | ||
value: string; | ||
}; | ||
} | ||
|
||
export interface UnleashClient { | ||
isEnabled(this: UnleashClient, featureName: string): boolean; | ||
getVariant(this: UnleashClient, featureName: string): IVariant; | ||
} | ||
|
||
export type UnleashClientClass = new (...args: unknown[]) => UnleashClient; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.