From 8c7dd431a2becd3be3aaa40ef234284d63d6afe0 Mon Sep 17 00:00:00 2001 From: Vinicius de Lacerda Date: Wed, 5 Jun 2024 17:05:40 +0200 Subject: [PATCH 1/8] Update Opticks to support Optimizely's JS SDK v5.3.2 --- packages/lib/package.json | 4 +- packages/lib/src/core/booleanToggle.test.ts | 58 --------- packages/lib/src/core/booleanToggle.ts | 24 ---- .../__mocks__/@optimizely/optimizely-sdk.ts | 15 ++- .../lib/src/integrations/optimizely.test.ts | 56 +++------ packages/lib/src/integrations/optimizely.ts | 63 ++++------ packages/lib/src/integrations/simple.ts | 3 - yarn.lock | 110 ++++++------------ 8 files changed, 91 insertions(+), 242 deletions(-) delete mode 100755 packages/lib/src/core/booleanToggle.test.ts delete mode 100755 packages/lib/src/core/booleanToggle.ts diff --git a/packages/lib/package.json b/packages/lib/package.json index 5017171..6448337 100755 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -33,10 +33,10 @@ "author": "Jop de Klein", "license": "ISC", "peerDependencies": { - "@optimizely/optimizely-sdk": "~4.4.3" + "@optimizely/optimizely-sdk": "5.3.2" }, "devDependencies": { - "@optimizely/optimizely-sdk": "~4.4.3", + "@optimizely/optimizely-sdk": "5.3.2", "@types/jest": "^29.4.0", "jest": "^29.4.0", "ts-jest": "^29.0.5", diff --git a/packages/lib/src/core/booleanToggle.test.ts b/packages/lib/src/core/booleanToggle.test.ts deleted file mode 100755 index 6e22183..0000000 --- a/packages/lib/src/core/booleanToggle.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {booleanToggle as baseBooleanToggle} from './booleanToggle' - -describe('Boolean Toggles', () => { - let booleanToggle - - beforeEach(() => { - const dummyGetToggle = jest.fn( - // only 'foo' is considered true for the tests - (toggleId) => (toggleId && toggleId.toLowerCase() === 'foo') || false - ) - - booleanToggle = baseBooleanToggle(dummyGetToggle) - }) - - describe('Simple boolean return', () => { - it('is case insensitive', () => { - expect(booleanToggle('fOO')).toEqual(true) - expect(booleanToggle('bAR')).toEqual(false) - }) - }) - - describe('On Toggle Execution', () => { - it('Executes if toggle value is a function', () => { - expect(booleanToggle('foo', () => 'returnForOn')).toEqual('returnForOn') - }) - - it('Always returns false for toggles that are off', () => { - expect(booleanToggle('bar', () => 'foo')).toEqual(false) - expect(booleanToggle('bar', true)).toEqual(false) - expect(booleanToggle('bar', false)).toEqual(false) - }) - - it('Returns false for non-existent toggles', () => { - expect(booleanToggle('baz', 'foo')).toEqual(false) - }) - }) - - describe('When both Off and On toggles are specified', () => { - it('Returns value for active side of the toggle', () => { - expect(booleanToggle('foo', 'toggleIsOff', 'toggleIsOn')).toEqual( - 'toggleIsOn' - ) - expect(booleanToggle('bar', 'toggleIsOff', 'toggleIsOn')).toEqual( - 'toggleIsOff' - ) - }) - it('Executes value for active side of the toggle if it is a function', () => { - expect( - booleanToggle( - 'foo', - () => 'toggleIsOff', - () => 'toggleIsOn' - ) - ).toEqual('toggleIsOn') - expect(booleanToggle('bar', null, () => 'toggleIsOn')).toEqual(null) - }) - }) -}) diff --git a/packages/lib/src/core/booleanToggle.ts b/packages/lib/src/core/booleanToggle.ts deleted file mode 100755 index 3d4b00d..0000000 --- a/packages/lib/src/core/booleanToggle.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** @deprecated */ -import type {ToggleIdType, TogglerGetterType} from '../types' -import {handleToggleVariant} from '../variantUtils' - -export const booleanToggle = - (getToggle: TogglerGetterType) => - (toggleId: ToggleIdType, ...variants: Array): any | boolean => { - switch (variants.length) { - // supplied both 'off' and 'on' variants - case 2: { - const [toggleOff, toggleOn] = variants - return handleToggleVariant(getToggle(toggleId) ? toggleOn : toggleOff) - } - // supplied only 'on' variant - case 1: { - const [toggleOn] = variants - return getToggle(toggleId) ? handleToggleVariant(toggleOn) : false - } - default: - // no (or incorrect) variants supplied, just return the value of the - // toggle itself - return getToggle(toggleId) - } - } diff --git a/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts b/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts index 900f5ed..0ff7d4c 100755 --- a/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts +++ b/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts @@ -10,9 +10,18 @@ export const createInstanceMock = jest.fn(() => ({ notificationCenter: { addNotificationListener: addNotificationListenerMock }, - activate: activateMock + activate: activateMock, + createUserContext: optimizelyUserContextMock })) +export const decideMock = jest.fn((toggleKey) => ({ + enabled: toggleKey === "foo" +})) +export const optimizelyUserContextMock = jest.fn((userId, attributes) => ({ + decide: decideMock +})) + + export const isFeatureEnabledMock = jest.fn((toggleId) => toggleId === 'foo') export const getEnabledFeaturesMock = jest.fn((userId, attributes) => { @@ -38,8 +47,12 @@ export const activateMock = jest.fn((toggleId, userId) => { return shouldReturnB && 'b' }) +const originalModule = jest.requireActual('@optimizely/optimizely-sdk') + const mock = { + ...originalModule, createInstance: createInstanceMock } + export default mock diff --git a/packages/lib/src/integrations/optimizely.test.ts b/packages/lib/src/integrations/optimizely.test.ts index f66d8a8..36fd58c 100644 --- a/packages/lib/src/integrations/optimizely.test.ts +++ b/packages/lib/src/integrations/optimizely.test.ts @@ -5,14 +5,12 @@ import { setUserId, setAudienceSegmentationAttributes, resetAudienceSegmentationAttributes, - booleanToggle, toggle, forceToggles, getEnabledFeatures } from './optimizely' // During the tests: -// for booleanToggle 'foo' yields true and 'bar' yields false, unless forced // for toggle 'foo' yields 'b' and 'bar' yields 'a', unless forced import datafile from './__fixtures__/dataFile' @@ -26,7 +24,11 @@ import Optimizely, { // @ts-expect-error getEnabledFeaturesMock, // @ts-expect-error - activateMock + activateMock, + // @ts-expect-error + decideMock, + // @ts-expect-error + optimizelyUserContextMock } from '@optimizely/optimizely-sdk' // Re-used between toggle test suites @@ -123,9 +125,6 @@ describe('Optimizely Integration', () => { expect(() => toggle('foo')).toThrow( 'Opticks: Fatal error: user id is not set' ) - expect(() => booleanToggle('foo')).toThrow( - 'Opticks: Fatal error: user id is not set' - ) }) }) @@ -140,8 +139,6 @@ describe('Optimizely Integration', () => { expect(toggle('bax', 'a', 'b', 'c')).toEqual('c') expect(toggle('foo')).toEqual('b') expect(toggle('bar')).toEqual('a') - expect(booleanToggle('foo')).toEqual(true) - expect(booleanToggle('bar')).toEqual(false) }) */ @@ -149,16 +146,11 @@ describe('Optimizely Integration', () => { toggle('foo', 'a', 'b', 'c') expect(activateMock).toHaveBeenCalledWith('foo', 'fooBSide', {}) toggle('foo') - expect(isFeatureEnabledMock).toHaveBeenCalledWith( - 'foo', - 'fooBSide', - {} + expect(decideMock).toHaveBeenCalledWith( + 'foo' ) - booleanToggle('foo') - expect(isFeatureEnabledMock).toHaveBeenCalledWith( - 'foo', - 'fooBSide', - {} + expect(optimizelyUserContextMock).toHaveBeenCalledWith( + 'fooBSide', {} ) }) }) @@ -182,16 +174,11 @@ describe('Optimizely Integration', () => { deviceType: 'mobile', isLoggedIn: false }) + }) + it('Forwards toggle reading and audienceSegmentationAttributes to Optimizely', () => { toggle('foo') - expect(isFeatureEnabledMock).toHaveBeenCalledWith('foo', 'fooBSide', { - thisWillNotBeOverwritten: 'foo', - deviceType: 'mobile', - isLoggedIn: false - }) - - booleanToggle('foo') - expect(isFeatureEnabledMock).toHaveBeenCalledWith('foo', 'fooBSide', { + expect(optimizelyUserContextMock).toHaveBeenCalledWith('fooBSide', { thisWillNotBeOverwritten: 'foo', deviceType: 'mobile', isLoggedIn: false @@ -213,26 +200,23 @@ describe('Optimizely Integration', () => { }) toggle('foo', 'a', 'b') - expect(isFeatureEnabledMock).toHaveBeenCalledWith('foo', 'fooBSide', { + expect(optimizelyUserContextMock).toHaveBeenCalledWith('fooBSide', { valueAfterReset: true }) toggle('foo') - expect(isFeatureEnabledMock).toHaveBeenCalledWith('foo', 'fooBSide', { + expect(optimizelyUserContextMock).toHaveBeenCalledWith('fooBSide', { valueAfterReset: true }) }) }) testAudienceSegmentationCacheBusting(toggle, activateMock) - testAudienceSegmentationCacheBusting(booleanToggle, isFeatureEnabledMock) - it("Returns Optimizely's value when no arguments supplied using booleanToggle", () => { + it("Returns Optimizely's value when no arguments supplied using", () => { // maps to a, b, c expect(toggle('foo')).toEqual('b') expect(toggle('bar')).toEqual('a') - expect(booleanToggle('foo')).toBeTruthy() - expect(booleanToggle('bar')).toBeFalsy() }) it('Maps Optimizely value to a, b, c indexed arguments', () => { @@ -257,13 +241,10 @@ describe('Optimizely Integration', () => { expect(toggle('bax', 'a', 'b', 'c')).toEqual('c') expect(toggle('bar')).toEqual('a') expect(toggle('baz')).toEqual('b') - expect(booleanToggle('bar')).toEqual(false) - expect(booleanToggle('baz')).toEqual(true) }) it('allows you to invent non-existing experiments', () => { expect(toggle('bax', 'a', 'b', 'c')).toEqual('c') - expect(booleanToggle('baz')).toEqual(true) }) it('persist after setAudienceSegmentationAttributes is called', () => { @@ -271,13 +252,10 @@ describe('Optimizely Integration', () => { setAudienceSegmentationAttributes({foo: 'bar'}) expect(toggle('foo', 'a', 'b', 'c')).toEqual('a') expect(toggle('bax', 'a', 'b', 'c')).toEqual('c') - expect(booleanToggle('bar')).toEqual(false) - expect(booleanToggle('baz')).toEqual(true) }) it('makes sure Toggles return defaults if forced values are of wrong type', () => { expect(toggle('baz', 'a', 'b', 'c')).toEqual('a') - expect(booleanToggle('bax')).toEqual(false) }) describe('Clearing forced toggles', () => { @@ -288,15 +266,11 @@ describe('Optimizely Integration', () => { it('should yield real values for cleared toggles', () => { expect(toggle('foo', 'a', 'b', 'c')).toEqual('b') expect(toggle('bar', 'a', 'b', 'c')).toEqual('a') - expect(booleanToggle('foo')).toEqual(true) - expect(booleanToggle('bar')).toEqual(false) }) it('should keep the non-cleared forced toggles and other defaults', () => { expect(toggle('bax', 'a', 'b', 'c')).toEqual('c') - expect(booleanToggle('baz')).toEqual(true) expect(toggle('nonexistent', 'a', 'b', 'c')).toEqual('a') - expect(booleanToggle('nonexistent')).toEqual(false) }) }) }) diff --git a/packages/lib/src/integrations/optimizely.ts b/packages/lib/src/integrations/optimizely.ts index 92211e1..e1b2a6c 100644 --- a/packages/lib/src/integrations/optimizely.ts +++ b/packages/lib/src/integrations/optimizely.ts @@ -1,6 +1,5 @@ -import OptimizelyLib, {EventDispatcher} from '@optimizely/optimizely-sdk' +import OptimizelyLib, {EventDispatcher, Client, OptimizelyUserContext, NotificationListener, ListenerPayload} from '@optimizely/optimizely-sdk' import {ToggleFuncReturnType, ToggleIdType, VariantType} from '../types' -import {booleanToggle as baseBooleanToggle} from '../core/booleanToggle' import {toggle as baseToggle} from '../core/toggle' type UserIdType = string @@ -22,9 +21,10 @@ export const NOTIFICATION_TYPES = { } let optimizely = OptimizelyLib // reference to injected Optimizely library -let optimizelyClient: OptimizelyLib.Client | null // reference to active Optimizely instance +let optimizelyClient: Client | null // reference to active Optimizely instance let userId: UserIdType let audienceSegmentationAttributes: AudienceSegmentationAttributesType = {} +let userContext: OptimizelyUserContext type FeatureEnabledCacheType = { [key in ToggleIdType]: BooleanToggleValueType @@ -120,7 +120,7 @@ const voidEventDispatcher = { } export enum ExperimentType { - flag = 'feature', + flag = 'flag', mvt = 'feature-test' } @@ -131,7 +131,7 @@ export enum ExperimentType { * * It would be best if Opticks abstracts this difference from the client in future versions. */ -interface ActivateMVTNotificationPayload extends OptimizelyLib.ListenerPayload { +interface ActivateMVTNotificationPayload extends ListenerPayload { type: ExperimentType.mvt decisionInfo: { experimentKey: ToggleIdType @@ -139,7 +139,7 @@ interface ActivateMVTNotificationPayload extends OptimizelyLib.ListenerPayload { } } interface ActivateFlagNotificationPayload - extends OptimizelyLib.ListenerPayload { + extends ListenerPayload { type: ExperimentType.flag decisionInfo: { featureKey: ToggleIdType @@ -163,9 +163,9 @@ export type ActivateNotificationPayload = */ export const initialize = ( datafile: OptimizelyDatafileType, - onExperimentDecision: OptimizelyLib.NotificationListener = voidActivateHandler, + onExperimentDecision: NotificationListener = voidActivateHandler, eventDispatcher: EventDispatcher = voidEventDispatcher -): OptimizelyLib.Client => { +): Client => { optimizelyClient = optimizely.createInstance({ datafile, eventDispatcher: eventDispatcher @@ -182,10 +182,10 @@ export const initialize = ( * @returns void */ export const addActivateListener = ( - listener: OptimizelyLib.NotificationListener + listener: NotificationListener ) => optimizelyClient.notificationCenter.addNotificationListener( - NOTIFICATION_TYPES.DECISION, + OptimizelyLib.enums.NOTIFICATION_TYPES.DECISION, listener ) @@ -207,7 +207,7 @@ const validateUserId = (id) => { if (!id) throw new Error('Opticks: Fatal error: user id is not set') } -const getOrSetCachedFeatureEnabled = ( +const getToggleDecisionStatus = ( toggleId: ToggleIdType ): BooleanToggleValueType => { validateUserId(userId) @@ -219,11 +219,10 @@ const getOrSetCachedFeatureEnabled = ( return typeof value === 'boolean' ? value : DEFAULT } - return (featureEnabledCache[toggleId] = optimizelyClient.isFeatureEnabled( - toggleId, - userId, - audienceSegmentationAttributes - )) + userContext = optimizelyClient.createUserContext(userId, audienceSegmentationAttributes) + const decision = userContext.decide(toggleId) + + return (featureEnabledCache[toggleId] = decision.enabled) } /** @@ -244,33 +243,26 @@ export const isUserInRolloutAudience = (toggleId: ToggleIdType) => { let index: number let isInAnyAudience = false - for (index = 0; index < endIndex; index++) { - const rolloutRule = config.experimentKeyMap[rollout.experiments[index].key] + for (index = 0; index <= endIndex; index++) { + const rolloutRule = rollout.experiments[index] - // The method `__checkIfUserIsInAudience()` will return `{result, reasons}` for versions > 4.5.0 - // For versions < 4.5.0 it will return `result` only. - // - // Reference: https://github.com/optimizely/javascript-sdk/blob/v4.4.3/packages/optimizely-sdk/lib/core/decision_service/index.js#L230 + // Reference: https://github.com/optimizely/javascript-sdk/blob/851b06622fa6a0239500b3b65e2d3937334960de/lib/core/decision_service/index.ts#L403 const decisionIfUserIsInAudience = // @ts-expect-error we're being naughty here and using internals - optimizelyClient.decisionService.__checkIfUserIsInAudience( + optimizelyClient.decisionService.checkIfUserIsInAudience( config, - rolloutRule.key, + rolloutRule, 'rule', - userId, + userContext, audienceSegmentationAttributes, '' ) - if (decisionIfUserIsInAudience && !isPausedBooleanToggle(rolloutRule)) + if (decisionIfUserIsInAudience.result && !isPausedBooleanToggle(rolloutRule)) isInAnyAudience = true } - const isEveryoneElseRulePaused = isPausedBooleanToggle( - config.experimentKeyMap[rollout.experiments[endIndex].key] - ) - - return isInAnyAudience || !isEveryoneElseRulePaused + return isInAnyAudience } /** @@ -292,13 +284,6 @@ const isPausedBooleanToggle = (rolloutRule: { ) } -const getBooleanToggle = getOrSetCachedFeatureEnabled - -/** - * @deprecated Since the `toggle` function supports both flags and MVT experiments - */ -export const booleanToggle = baseBooleanToggle(getBooleanToggle) - const getToggle = (toggleId: ToggleIdType): ExperimentToggleValueType => { validateUserId(userId) @@ -320,7 +305,7 @@ const getToggle = (toggleId: ToggleIdType): ExperimentToggleValueType => { } const convertBooleanToggleToFeatureVariant = (toggleId: ToggleIdType) => { - const isFeatureEnabled = getBooleanToggle(toggleId) + const isFeatureEnabled = getToggleDecisionStatus(toggleId) return isFeatureEnabled ? 'b' : 'a' } diff --git a/packages/lib/src/integrations/simple.ts b/packages/lib/src/integrations/simple.ts index 1161f47..9584568 100755 --- a/packages/lib/src/integrations/simple.ts +++ b/packages/lib/src/integrations/simple.ts @@ -1,6 +1,5 @@ /** @deprecated */ import type {BooleanToggleType, ToggleIdType, VariantType} from '../types' -import {booleanToggle as baseBooleanToggle} from '../core/booleanToggle' import {toggle as baseToggle} from '../core/toggle' // This implementation expects you to populate a list of boolean toggles in @@ -36,8 +35,6 @@ export const getBooleanToggle = (toggleId: ToggleIdType) => { : false } -export const booleanToggle = baseBooleanToggle(getBooleanToggle) - export const getToggle = (toggleId: ToggleIdType): VariantType => { const toggle = toggleList[toggleId && toggleId.toLowerCase()] diff --git a/yarn.lock b/yarn.lock index 910e91e..27567ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1491,70 +1491,25 @@ __metadata: languageName: node linkType: hard -"@optimizely/js-sdk-datafile-manager@npm:^0.8.0": - version: 0.8.1 - resolution: "@optimizely/js-sdk-datafile-manager@npm:0.8.1" +"@optimizely/optimizely-sdk@npm:5.3.2": + version: 5.3.2 + resolution: "@optimizely/optimizely-sdk@npm:5.3.2" dependencies: - "@optimizely/js-sdk-logging": "npm:^0.1.0" - "@optimizely/js-sdk-utils": "npm:^0.4.0" decompress-response: "npm:^4.2.1" + json-schema: "npm:^0.4.0" + murmurhash: "npm:^2.0.1" + ua-parser-js: "npm:^1.0.37" + uuid: "npm:^9.0.1" peerDependencies: - "@react-native-async-storage/async-storage": ^1.2.0 - checksum: 2a4eb71dda778bb2354baed8d20d42da0f226f456e36329a38d1ef326406d646921692b51d5a5d258d0c4106ff7c5f3b78b743d47f8b9d0003c4158245baeb46 - languageName: node - linkType: hard - -"@optimizely/js-sdk-event-processor@npm:^0.8.0": - version: 0.8.2 - resolution: "@optimizely/js-sdk-event-processor@npm:0.8.2" - dependencies: - "@optimizely/js-sdk-logging": "npm:^0.1.0" - "@optimizely/js-sdk-utils": "npm:^0.4.0" - peerDependencies: + "@babel/runtime": ^7.0.0 "@react-native-async-storage/async-storage": ^1.2.0 "@react-native-community/netinfo": 5.9.4 - checksum: b03f9668da7a9598fadae5b326764451fc556afd5cb3004741b0f5ca74d49713802751fe5eb939689bd21f95db308ec32621f514500e5124a400c48412637c95 - languageName: node - linkType: hard - -"@optimizely/js-sdk-logging@npm:^0.1.0": - version: 0.1.0 - resolution: "@optimizely/js-sdk-logging@npm:0.1.0" - dependencies: - "@optimizely/js-sdk-utils": "npm:^0.1.0" - checksum: 3bd71cf397aba639bf73df554b2c6d392230fa9c482947011554bac6259b4b707672586b08680c7778933f3e5bbba437b4fb0d5772171ec7e782be94f7be7b73 - languageName: node - linkType: hard - -"@optimizely/js-sdk-utils@npm:^0.1.0": - version: 0.1.0 - resolution: "@optimizely/js-sdk-utils@npm:0.1.0" - dependencies: - uuid: "npm:^3.3.2" - checksum: 614e1bbbed0f0b58e510a5fa91c0c0a9dba0bd0591b436fb1079d104b54835f87b985002f9e64a80d7f7fe97bbe33392906537316985580e4b305c29b0c596b8 - languageName: node - linkType: hard - -"@optimizely/js-sdk-utils@npm:^0.4.0": - version: 0.4.0 - resolution: "@optimizely/js-sdk-utils@npm:0.4.0" - dependencies: - uuid: "npm:^3.3.2" - checksum: 18b63d9b42a062e0064c4b646ddf2d0f0d81f7563f7bdfd83200be4ce0f01d05cc3c128cd3bd38e8d3a35cce8b76a5ab308d9078eeacf2544fa88c2ee090f640 - languageName: node - linkType: hard - -"@optimizely/optimizely-sdk@npm:~4.4.3": - version: 4.4.3 - resolution: "@optimizely/optimizely-sdk@npm:4.4.3" - dependencies: - "@optimizely/js-sdk-datafile-manager": "npm:^0.8.0" - "@optimizely/js-sdk-event-processor": "npm:^0.8.0" - "@optimizely/js-sdk-logging": "npm:^0.1.0" - "@optimizely/js-sdk-utils": "npm:^0.4.0" - json-schema: "npm:^0.2.3" - murmurhash: "npm:0.0.2" - checksum: b2612ad8a7a411e8187e15b4be3b50a3a94205e108f110cb384090bcd177393429a91989cc3e5be7a08f89197892afe08ccc8dd0f0421d5a7cfec991562f3c8d + peerDependenciesMeta: + "@react-native-async-storage/async-storage": + optional: true + "@react-native-community/netinfo": + optional: true + checksum: 04623ae4deaefc1546dec8c62699a18146b01cb937baa66a51d59148df8b62671f06aa4381cd6fed0207e28b6cb88118c2253826048b172677c763a0184183b6 languageName: node linkType: hard @@ -5281,10 +5236,10 @@ __metadata: languageName: node linkType: hard -"json-schema@npm:^0.2.3": - version: 0.2.5 - resolution: "json-schema@npm:0.2.5" - checksum: 7d9c64351be318897bf56a99360dfd3274a98ee55a2491e68fc6557a87188e48cc725a29ec99d89b62933598779b8545d47214aeb8be3c044a5fb571c25b99f7 +"json-schema@npm:^0.4.0": + version: 0.4.0 + resolution: "json-schema@npm:0.4.0" + checksum: d4a637ec1d83544857c1c163232f3da46912e971d5bf054ba44fdb88f07d8d359a462b4aec46f2745efbc57053365608d88bc1d7b1729f7b4fc3369765639ed3 languageName: node linkType: hard @@ -5866,10 +5821,10 @@ __metadata: languageName: node linkType: hard -"murmurhash@npm:0.0.2": - version: 0.0.2 - resolution: "murmurhash@npm:0.0.2" - checksum: f12bdf9702a78c5abae19253b9c7de120ce1541b86d5c9b80f608809ebdf0dc41934c08cbf73d29113785242c5ca977779e07e45559d03849ced3faceac4bcd2 +"murmurhash@npm:^2.0.1": + version: 2.0.1 + resolution: "murmurhash@npm:2.0.1" + checksum: f6c7cb12d6ebc9c1cfd232fe9406089e1ceb128d24245e852866ba28967271925d915140f77fef7c92ee29b13165f4537ce80a85c3d0550b1b5cdb9f8bcaa19f languageName: node linkType: hard @@ -6154,14 +6109,14 @@ __metadata: version: 0.0.0-use.local resolution: "opticks@workspace:packages/lib" dependencies: - "@optimizely/optimizely-sdk": "npm:~4.4.3" + "@optimizely/optimizely-sdk": "npm:5.3.2" "@types/jest": "npm:^29.4.0" jest: "npm:^29.4.0" ts-jest: "npm:^29.0.5" tsup: "npm:^6.5.0" typescript: "npm:^5.0.4" peerDependencies: - "@optimizely/optimizely-sdk": ~4.4.3 + "@optimizely/optimizely-sdk": 5.3.2 languageName: unknown linkType: soft @@ -7905,6 +7860,13 @@ __metadata: languageName: node linkType: hard +"ua-parser-js@npm:^1.0.37": + version: 1.0.38 + resolution: "ua-parser-js@npm:1.0.38" + checksum: b1dd11b87e1784c79f7129e9aec679753fccf8a9b22f5202b79b19492635b5b46b779607a3cfae0270999a0d48da223bf94015642d2abee69d83c9069ab37bd0 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.0.2": version: 1.0.2 resolution: "unbox-primitive@npm:1.0.2" @@ -7986,12 +7948,12 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^3.3.2": - version: 3.4.0 - resolution: "uuid@npm:3.4.0" +"uuid@npm:^9.0.1": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" bin: - uuid: ./bin/uuid - checksum: 1c13950df865c4f506ebfe0a24023571fa80edf2e62364297a537c80af09c618299797bbf2dbac6b1f8ae5ad182ba474b89db61e0e85839683991f7e08795347 + uuid: dist/bin/uuid + checksum: 1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b languageName: node linkType: hard From 38ab4b6d0ee66b13636292fedd2f64f28a2578db Mon Sep 17 00:00:00 2001 From: Vinicius de Lacerda Date: Wed, 5 Jun 2024 17:50:02 +0200 Subject: [PATCH 2/8] Updated README --- README.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c68489b..16e9420 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ The library consists of two related concepts: At the heart of our experimentation framework is the `toggle` function. -- `toggle` toggles that switch between multiple experiment variants (a/b/c/...) -- `booleanToggle` toggles that turn functionality on or off (feature flags) +A toggle allow you to switch between multiple experiment variants (a/b/c/...) +and also turn functionality on or off (feature flags) It can be used in a variety of ways: @@ -23,15 +23,9 @@ It can be used in a variety of ways: 1. Execute code or for a variant of a multi toggle 1. Execute code when a boolean toggle is on -We use React at FindHotel and some of the code examples use JSX, but the code +We use React at vio.com and some of the code examples use JSX, but the code and concept is compatible with any front-end framework or architecture. -The `booleanToggle` is the simplest toggle type to use for feature flagging, but -it's also the least flexible. As of version 2.0 `toggle` is the default and -recommended for both a/b/c experiments and feature flags. If you're only ever -interested in feature flags, read more about [Boolean -Toggles](docs/booleanToggles.md). - ### Opticks vs other experimentation frameworks The main reason for using the Opticks library is to be able to clean your code @@ -64,9 +58,9 @@ See the [Optimizely integration documentation](docs/optimizely-integration.md). ## Toggles -Toggles can be used to implement a/b/c style testing, instead of on/off values -as with `booleanToggle`, we specify multiple variants of which one is active at -any time. By convention the variants are named `a` (control), `b`, `c` etc. +Toggles can be used to implement a/b/c style testing and on/off values as well. +We specify multiple variants of which one is active at any time. +By convention the variants are named `a` (control), `b`, `c` etc. ### Reading values @@ -133,7 +127,7 @@ experimentId, then the values for `a`, `b`, etc. For instance: ``` -// simple boolean switch: (you could use a BooleanToggle as well) +// simple boolean switch const shouldDoSomething = toggle('foo', false, true) // multiple variants as strings From 4c2ff79b144e88d2866403bca01deed3053aacfe Mon Sep 17 00:00:00 2001 From: Vinicius de Lacerda Date: Thu, 6 Jun 2024 12:38:47 +0200 Subject: [PATCH 3/8] Update README.md Co-authored-by: Gerrit Burger --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16e9420..ebecd70 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The library consists of two related concepts: At the heart of our experimentation framework is the `toggle` function. -A toggle allow you to switch between multiple experiment variants (a/b/c/...) +A toggle allows you to switch between multiple experiment variants (a/b/c/...) and also turn functionality on or off (feature flags) It can be used in a variety of ways: From 28ff2e4cef9ff2438ef58d6d03822c60bb9801f5 Mon Sep 17 00:00:00 2001 From: Vinicius de Lacerda Date: Thu, 6 Jun 2024 13:02:52 +0200 Subject: [PATCH 4/8] Apply suggestions from code review Co-authored-by: Gerrit Burger --- packages/lib/src/integrations/optimizely.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/integrations/optimizely.test.ts b/packages/lib/src/integrations/optimizely.test.ts index 36fd58c..ac931d3 100644 --- a/packages/lib/src/integrations/optimizely.test.ts +++ b/packages/lib/src/integrations/optimizely.test.ts @@ -213,7 +213,7 @@ describe('Optimizely Integration', () => { testAudienceSegmentationCacheBusting(toggle, activateMock) - it("Returns Optimizely's value when no arguments supplied using", () => { + it("Returns Optimizely's value when no arguments supplied", () => { // maps to a, b, c expect(toggle('foo')).toEqual('b') expect(toggle('bar')).toEqual('a') From 8c68cef6419573163a274e72ea7cc4bef1668cef Mon Sep 17 00:00:00 2001 From: Vinicius de Lacerda Date: Thu, 6 Jun 2024 13:04:06 +0200 Subject: [PATCH 5/8] Code review --- .../__mocks__/@optimizely/optimizely-sdk.ts | 18 +------- .../lib/src/integrations/optimizely.test.ts | 41 +------------------ packages/lib/src/integrations/optimizely.ts | 20 +-------- 3 files changed, 4 insertions(+), 75 deletions(-) diff --git a/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts b/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts index 0ff7d4c..ecce5cc 100755 --- a/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts +++ b/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts @@ -5,7 +5,6 @@ export const voidEventDispatcher = { export const addNotificationListenerMock = jest.fn() export const createInstanceMock = jest.fn(() => ({ - getEnabledFeatures: getEnabledFeaturesMock, isFeatureEnabled: isFeatureEnabledMock, notificationCenter: { addNotificationListener: addNotificationListenerMock @@ -17,28 +16,13 @@ export const createInstanceMock = jest.fn(() => ({ export const decideMock = jest.fn((toggleKey) => ({ enabled: toggleKey === "foo" })) -export const optimizelyUserContextMock = jest.fn((userId, attributes) => ({ +export const optimizelyUserContextMock = jest.fn(() => ({ decide: decideMock })) export const isFeatureEnabledMock = jest.fn((toggleId) => toggleId === 'foo') -export const getEnabledFeaturesMock = jest.fn((userId, attributes) => { - if (userId) { - if (attributes.deviceType) { - return [ - `${userId}-${attributes.deviceType}-test-1`, - `${userId}-${attributes.deviceType}-test-2` - ] - } - - return [`${userId}-test-1`, `${userId}-test-2`] - } - - return [] -}) - export const activateMock = jest.fn((toggleId, userId) => { const shouldReturnB = (toggleId === 'foo' && userId === 'fooBSide') || diff --git a/packages/lib/src/integrations/optimizely.test.ts b/packages/lib/src/integrations/optimizely.test.ts index ac931d3..7e0a59a 100644 --- a/packages/lib/src/integrations/optimizely.test.ts +++ b/packages/lib/src/integrations/optimizely.test.ts @@ -6,8 +6,7 @@ import { setAudienceSegmentationAttributes, resetAudienceSegmentationAttributes, toggle, - forceToggles, - getEnabledFeatures + forceToggles } from './optimizely' // During the tests: @@ -22,8 +21,6 @@ import Optimizely, { // @ts-expect-error isFeatureEnabledMock, // @ts-expect-error - getEnabledFeaturesMock, - // @ts-expect-error activateMock, // @ts-expect-error decideMock, @@ -275,40 +272,4 @@ describe('Optimizely Integration', () => { }) }) }) - - describe('getEnabledFeatures', () => { - beforeEach(() => { - setUserId('chewbacca') - setAudienceSegmentationAttributes({ - deviceType: 'desktop' - }) - }) - - it('should return enabled features for R2-D2 user', () => { - setUserId('R2-D2') - expect(getEnabledFeatures()).toEqual([ - 'R2-D2-desktop-test-1', - 'R2-D2-desktop-test-2' - ]) - }) - - it('should return enabled features for C-3PO user', () => { - setUserId('C-3PO') - expect(getEnabledFeatures()).toEqual([ - 'C-3PO-desktop-test-1', - 'C-3PO-desktop-test-2' - ]) - }) - - it('should return enabled features for C-3PO user for mobile', () => { - setUserId('C-3PO') - setAudienceSegmentationAttributes({ - deviceType: 'mobile' - }) - expect(getEnabledFeatures()).toEqual([ - 'C-3PO-mobile-test-1', - 'C-3PO-mobile-test-2' - ]) - }) - }) }) diff --git a/packages/lib/src/integrations/optimizely.ts b/packages/lib/src/integrations/optimizely.ts index e1b2a6c..7c4e9b4 100644 --- a/packages/lib/src/integrations/optimizely.ts +++ b/packages/lib/src/integrations/optimizely.ts @@ -16,9 +16,7 @@ type ToggleValueType = ExperimentToggleValueType | BooleanToggleValueType export type OptimizelyDatafileType = object -export const NOTIFICATION_TYPES = { - DECISION: 'DECISION:type, userId, attributes, decisionInfo' -} +export const NOTIFICATION_TYPES = OptimizelyLib.enums.NOTIFICATION_TYPES let optimizely = OptimizelyLib // reference to injected Optimizely library let optimizelyClient: Client | null // reference to active Optimizely instance @@ -185,7 +183,7 @@ export const addActivateListener = ( listener: NotificationListener ) => optimizelyClient.notificationCenter.addNotificationListener( - OptimizelyLib.enums.NOTIFICATION_TYPES.DECISION, + NOTIFICATION_TYPES.DECISION, listener ) @@ -335,20 +333,6 @@ export function toggle(toggleId: ToggleIdType, ...variants) { } } -/** - * Get all enabled features for the user - * - * @deprecated - */ -export const getEnabledFeatures = () => { - validateUserId(userId) - - return optimizelyClient.getEnabledFeatures( - userId, - audienceSegmentationAttributes - ) -} - /** * Export imported types */ From d750ed274b0ed02c0a85acf1e782aa3fc9f95196 Mon Sep 17 00:00:00 2001 From: Vinicius de Lacerda Date: Thu, 6 Jun 2024 13:16:36 +0200 Subject: [PATCH 6/8] Code review --- .../src/integrations/__fixtures__/dataFile.ts | 122 +++++++++++++----- 1 file changed, 91 insertions(+), 31 deletions(-) diff --git a/packages/lib/src/integrations/__fixtures__/dataFile.ts b/packages/lib/src/integrations/__fixtures__/dataFile.ts index 292932f..7313f92 100755 --- a/packages/lib/src/integrations/__fixtures__/dataFile.ts +++ b/packages/lib/src/integrations/__fixtures__/dataFile.ts @@ -1,10 +1,98 @@ export default { accountId: '12345', - anonymizeIP: false, - botFiltering: false, projectId: '23456', revision: '6', + attributes: [ + {id: 'trafficSource', key: 'trafficSource'}, + {id: 'hasDefaultDates', key: 'hasDefaultDates'}, + {id: 'deviceType', key: 'deviceType'} + ], + audiences: [ + { + id: 'foo-default-dates', + name: 'Foo Traffic', + conditions: + '[ "and", { "name": "trafficSource", "value": "foo", "type": "custom_attribute" }, { "name": "hasDefaultDates", "value": true, "type": "custom_attribute" }, ["not", { "name": "deviceType", "value": "mobile", "type": "custom_attribute" } ] ]' + } + ], version: '4', + events: [], + integrations: [], + anonymizeIP: false, + botFiltering: false, + typedAudiences: [], + variables: [], + environmentKey: 'production', + sdkKey: '12345', + featureFlags: [ + { + experimentIds: ['foo'], + id: 'foo', + rolloutId: 'rollout-1234', + key: 'foo', + variables: [] + }, + { + experimentIds: ['bar'], + id: 'bar', + rolloutId: 'rollout-456', + key: 'bar', + variables: [] + } + ], + rollouts: [ + { + id: 'rollout-1234', + experiments: [ + { + id: '12345', + key: 'foo-exp', + status: 'Running', + layerId: '1234', + variations: [ + { + id: '12345', + key: 'on', + featureEnabled: true, + variables: [] + } + ], + trafficAllocation: [ + { + entityId: '12345', + endOfRange: 5000 + } + ], + forcedVariations: {}, + audienceIds: [], + audienceConditions: [] + }, + { + id: 'default-foo', + key: 'default-foo', + status: 'Running', + layerId: 'default-foo', + variations: [ + { + id: '624542', + key: 'off', + featureEnabled: false, + variables: [] + } + ], + trafficAllocation: [ + { + entityId: '624542', + endOfRange: 10000 + } + ], + forcedVariations: {}, + audienceIds: [], + audienceConditions: [] + } + ] + } + ], experiments: [ { id: 'foo', @@ -67,33 +155,5 @@ export default { forcedVariations: [] } ], - featureFlags: [ - { - experimentIds: ['foo'], - id: 'foo', - key: 'foo' - }, - { - experimentIds: ['bar'], - id: 'bar', - key: 'bar' - } - ], - events: [], - audiences: [ - { - id: 'foo-default-dates', - name: 'Foo Traffic', - conditions: - '[ "and", { "name": "trafficSource", "value": "foo", "type": "custom_attribute" }, { "name": "hasDefaultDates", "value": true, "type": "custom_attribute" }, ["not", { "name": "deviceType", "value": "mobile", "type": "custom_attribute" } ] ]' - } - ], - attributes: [ - {id: 'trafficSource', key: 'trafficSource'}, - {id: 'hasDefaultDates', key: 'hasDefaultDates'}, - {id: 'deviceType', key: 'deviceType'} - ], - groups: [], - rollouts: [], - variables: [] + groups: [] } From 9b32a6a9b7589dfe4f3491229c6f78a43ce5193a Mon Sep 17 00:00:00 2001 From: Vinicius de Lacerda Date: Thu, 6 Jun 2024 16:20:38 +0200 Subject: [PATCH 7/8] Remove boolean toggle remnants --- .changeset/silly-schools-hunt.md | 14 ++++ docs/booleanToggles.md | 82 -------------------- docs/dead-code-removal.md | 28 ++----- docs/optimizely-integration.md | 6 +- docs/simple-integration.md | 10 --- packages/lib/src/integrations/optimizely.ts | 39 ++++++---- packages/lib/src/integrations/simple.test.ts | 26 +------ packages/lib/src/integrations/simple.ts | 30 +------ packages/lib/src/types.ts | 3 - 9 files changed, 53 insertions(+), 185 deletions(-) create mode 100644 .changeset/silly-schools-hunt.md delete mode 100644 docs/booleanToggles.md diff --git a/.changeset/silly-schools-hunt.md b/.changeset/silly-schools-hunt.md new file mode 100644 index 0000000..0bfac80 --- /dev/null +++ b/.changeset/silly-schools-hunt.md @@ -0,0 +1,14 @@ +--- +'opticks': major +--- + +Upgrade Opticks optimizely integration to support Optimizely's JS SDK v5.3.2 + +Non-breaking changes: + +- `getOrSetCachedFeatureEnabled` is renamed to `getToggleDecisionStatus` + +Breaking changes: + +- `__checkIfUserIsInAudience`, an internal from Optimizely got updated to `checkIfUserIsInAudience`. Hence you need the version 5.3.2 of the JS SDK to run this version of opticks properly +- Removed deprecated `booleanToggle` and `getEnabledFeatures` and all mentions of it diff --git a/docs/booleanToggles.md b/docs/booleanToggles.md deleted file mode 100644 index dab353c..0000000 --- a/docs/booleanToggles.md +++ /dev/null @@ -1,82 +0,0 @@ -# Boolean Toggles - -## Boolean Toggles: Reading values - -Boolean Toggles as the name suggest, are either on or off. - -``` -booleanToggle('shouldShowSomething') // true or false -``` - -While these are simple to implement, using them directly tends to create code -that's hard to clean up fully. - -Consider the following toggle implementation: - -``` -// before -if (booleanToggle('foo')) foo() -if (booleanToggle('bar')) bar() -``` - -After the experiments concluded with `foo` winning and `bar` losing: - -``` -// after -if (true) foo() -if (false) bar() // never needed -``` - -While it works functionally, and code optimizers can eliminate any dead code -when generating a build, it still clutters your source code requiring manual -clean up efforts. - -You could assign a variable to add semantics: - -``` -// before -const shouldShowWarning = booleanToggle('warningToggle') -// ... -shouldShowWarning && renderWarning() -``` - -After toggle is true: - -``` -// after -const shouldShowWarning = true -// ... -shouldShowWarning && renderWarning() -``` - -Slightly better, but it would be great if we can prune dead code altogether, -considering the following would remain if the flag is false: - -``` -// after -const shouldShowWarning = false -// ... -shouldShowWarning && renderWarning() -``` - -## Boolean Toggles: Executing code - -You can pass a function to execute when a `toggle` is true. This can reduce the -amount of dangling leftover code after cleanup. - -``` -// before -always() -booleanToggle('warningToggle', () => renderWarning()) -``` - -``` -// after toggle is true -always() -renderWarning() -``` - -``` -// after toggle is false -always() -``` diff --git a/docs/dead-code-removal.md b/docs/dead-code-removal.md index 87df434..b08cd51 100644 --- a/docs/dead-code-removal.md +++ b/docs/dead-code-removal.md @@ -14,7 +14,7 @@ winners: - winning values are kept - for functions, the function body is kept -For the losing boolean toggles and losing multi toggle variants: +For the losing multi toggle variants: - losing toggles are pruned - if the losing side is a JSXExpression, we clean it up including the variables @@ -36,15 +36,13 @@ information. ## Running the codemods -There two codemods supplied with Opticks, one for Boolean Toggles, one for -Toggles, they can be found in the `src/transform` directory. +There is a codemod supplied with Opticks, that can be found in the `src/transform` directory. In order to clean all "losing" branches of the code, the codemods need to know -which toggle you're modifying, whether the toggle (for Boolean Toggles) or which -variation (for Multi Toggles) "won". +which toggle you're modifying, and which variation "won". Assuming you're running the codemods directly from the Opticks directory in your -`node_modules`, to declare the `b` side of the `foo` Multi Toggle the winner and +`node_modules`, to declare the `b` side of the `foo` Toggle the winner and prune all losing code in the `src` directory, run the script as follows: ```shell @@ -74,28 +72,17 @@ npm run clean:toggle -- --toggle=foo --winner=b The codemods are designed to work with TypeScript and they expect the `tsx` parser to be used. You can override the parser option from the consuming project to parse code other than TypeScript, but not all patterns might be cleaned up as intended. -### Boolean Toggles - -Boolean Toggle clean up works in a similar way, noting the winner accepts a -string value `'true'` or `'false'`: - -```shell -npm run clean:booleanToggle -- --toggle=foo --winner='true' -``` - ### Overriding the library import settings By default the codemods make assumptions on the name of the imports to clean, namely: ```typescript -import {booleanToggle} from 'opticks' -// or import {toggle} from 'opticks' ``` You can override these values via: -`--functionName=myLocalNameForMultiOrBooleanToggle` and +`--functionName=myLocalNameToggle` and `--packageName=myNameForOpticks` ## Cleaning Examples and Recipes @@ -132,7 +119,7 @@ const result = toggle('toggleFoo') // 'b' ``` A more interesting experiment would execute conditional code based on a multi -toggle. The trick here is that like Boolean Toggles, toggles accepts +toggle. The trick here is that toggles accept functions that will be executed only for a particular experiment branch. For example: @@ -225,8 +212,7 @@ of business logic. ### Conditional rendering The simplest way to render something conditionally is to assign a boolean to a -variable. Though this is probably better done with a boolean toggle, it's -possible with a toggle as well: +variable. ```typescript const shouldRenderIcon = toggle('SomethingWithIcon', false, true) diff --git a/docs/optimizely-integration.md b/docs/optimizely-integration.md index 7f026b9..36b84f0 100644 --- a/docs/optimizely-integration.md +++ b/docs/optimizely-integration.md @@ -93,11 +93,9 @@ forwarded to Optimizely with each call. ### The DataFile The Opticks Optimizely integration makes some assumptions on how the experiments -are set up. Optimizely supports two types of flags, "Feature Flags" (Boolean -Toggles in Opticks) and Experiments (Multi Toggles in Opticks). +are set up. Optimizely supports two types of flags, "Feature Flags" and Experiments . The Opticks library uses certain conventions to wrap both concepts in a -predictable API, where experiment variations are in the `a`, `b`, `c` format and -Boolean Toggles return only `true` or `false`. +predictable API, where experiment variations are in the `a`, `b`, `c` format. The following is subject to change, but right now Opticks uses both Feature Flags and the Experiments concepts of the Optimizely SDK which means you'll need diff --git a/docs/simple-integration.md b/docs/simple-integration.md index da19363..4f2429b 100644 --- a/docs/simple-integration.md +++ b/docs/simple-integration.md @@ -7,16 +7,6 @@ concern itself with generating a list of toggles or experiments, as there are many libraries and services out there doing it well already. It should be easy to write an adapter for whichever experimentation service or tool you're using. -## Boolean Toggles - -### Setting Toggles - -``` -setBooleanToggles({ foo: true, bar: false }) -``` - -## Multi Toggles - ### Setting Toggles ``` diff --git a/packages/lib/src/integrations/optimizely.ts b/packages/lib/src/integrations/optimizely.ts index 7c4e9b4..80cd526 100644 --- a/packages/lib/src/integrations/optimizely.ts +++ b/packages/lib/src/integrations/optimizely.ts @@ -1,4 +1,10 @@ -import OptimizelyLib, {EventDispatcher, Client, OptimizelyUserContext, NotificationListener, ListenerPayload} from '@optimizely/optimizely-sdk' +import OptimizelyLib, { + EventDispatcher, + Client, + OptimizelyUserContext, + NotificationListener, + ListenerPayload +} from '@optimizely/optimizely-sdk' import {ToggleFuncReturnType, ToggleIdType, VariantType} from '../types' import {toggle as baseToggle} from '../core/toggle' @@ -10,9 +16,8 @@ type AudienceSegmentationAttributesType = { [key in AudienceSegmentationAttributeKeyType]?: AudienceSegmentationAttributeValueType } -type BooleanToggleValueType = boolean -type ExperimentToggleValueType = string -type ToggleValueType = ExperimentToggleValueType | BooleanToggleValueType +type ExperimentToggleValueType = boolean | string +type ToggleValueType = ExperimentToggleValueType export type OptimizelyDatafileType = object @@ -25,7 +30,7 @@ let audienceSegmentationAttributes: AudienceSegmentationAttributesType = {} let userContext: OptimizelyUserContext type FeatureEnabledCacheType = { - [key in ToggleIdType]: BooleanToggleValueType + [key in ToggleIdType]: ExperimentToggleValueType } type ExperimentCacheType = {[key in ToggleIdType]: ExperimentToggleValueType} type CacheType = FeatureEnabledCacheType | ExperimentCacheType @@ -136,12 +141,11 @@ interface ActivateMVTNotificationPayload extends ListenerPayload { variationKey: VariantType } } -interface ActivateFlagNotificationPayload - extends ListenerPayload { +interface ActivateFlagNotificationPayload extends ListenerPayload { type: ExperimentType.flag decisionInfo: { - featureKey: ToggleIdType - featureEnabled: boolean + flagKey: ToggleIdType + enabled: boolean } } @@ -207,7 +211,7 @@ const validateUserId = (id) => { const getToggleDecisionStatus = ( toggleId: ToggleIdType -): BooleanToggleValueType => { +): ExperimentToggleValueType => { validateUserId(userId) const DEFAULT = false @@ -217,7 +221,10 @@ const getToggleDecisionStatus = ( return typeof value === 'boolean' ? value : DEFAULT } - userContext = optimizelyClient.createUserContext(userId, audienceSegmentationAttributes) + userContext = optimizelyClient.createUserContext( + userId, + audienceSegmentationAttributes + ) const decision = userContext.decide(toggleId) return (featureEnabledCache[toggleId] = decision.enabled) @@ -256,7 +263,10 @@ export const isUserInRolloutAudience = (toggleId: ToggleIdType) => { '' ) - if (decisionIfUserIsInAudience.result && !isPausedBooleanToggle(rolloutRule)) + if ( + decisionIfUserIsInAudience.result && + !isPausedBooleanToggle(rolloutRule) + ) isInAnyAudience = true } @@ -320,7 +330,10 @@ const convertBooleanToggleToFeatureVariant = (toggleId: ToggleIdType) => { * @param variants */ -export function toggle(toggleId: ToggleIdType, ...variants: A): ToggleFuncReturnType; +export function toggle( + toggleId: ToggleIdType, + ...variants: A +): ToggleFuncReturnType export function toggle(toggleId: ToggleIdType, ...variants) { // An A/B/C... test if (variants.length > 2) { diff --git a/packages/lib/src/integrations/simple.test.ts b/packages/lib/src/integrations/simple.test.ts index 30b2b31..8e7b379 100755 --- a/packages/lib/src/integrations/simple.test.ts +++ b/packages/lib/src/integrations/simple.test.ts @@ -1,29 +1,7 @@ -import {initialize, getBooleanToggle, getToggle} from './simple' +import {initialize, getToggle} from './simple' describe('Simple Integration', () => { - describe('Boolean Toggles', () => { - beforeEach(() => { - initialize({ - booleanToggles: { - foo: true, - bar: false - } - }) - }) - - it('Gets boolean toggles by id', () => { - expect(getBooleanToggle('foo')).toBeTruthy() - expect(getBooleanToggle('bar')).toBeFalsy() - }) - - it('defaults to false when toggle cannot be found', () => { - expect(getBooleanToggle('baz')).toEqual(false) - // @ts-expect-error invalid API call for testing purpose - expect(getBooleanToggle()).toEqual(false) - }) - }) - - describe('Multi Toggles', () => { + describe('Toggles', () => { beforeEach(() => { initialize({ toggles: { diff --git a/packages/lib/src/integrations/simple.ts b/packages/lib/src/integrations/simple.ts index 9584568..e02d12a 100755 --- a/packages/lib/src/integrations/simple.ts +++ b/packages/lib/src/integrations/simple.ts @@ -1,14 +1,7 @@ /** @deprecated */ -import type {BooleanToggleType, ToggleIdType, VariantType} from '../types' +import type {ToggleIdType, VariantType} from '../types' import {toggle as baseToggle} from '../core/toggle' -// This implementation expects you to populate a list of boolean toggles in -// advance, in the following format: -// { foo: true, bar: false } -export type BooleanToggleListType = { - [key in ToggleIdType]?: BooleanToggleType -} - // This implementation expects you to populate a list of multi toggle objects in // advance, in the following format: // { fooExperiment: {variant: 'a'}, barExperiment: {variant: 'b'} } @@ -16,25 +9,13 @@ export type toggleListType = { [key in ToggleIdType]?: {variant: VariantType} } -let booleanToggleList: BooleanToggleListType = {} let toggleList: toggleListType = {} -export const setBooleanToggles = (toggles: BooleanToggleListType) => { - booleanToggleList = toggles -} - // FIXME: FlowType export const setToggles = (toggles: any) => { toggleList = toggles } -export const getBooleanToggle = (toggleId: ToggleIdType) => { - const lowerCaseToggleId = toggleId && toggleId.toLowerCase() - return booleanToggleList.hasOwnProperty(lowerCaseToggleId) - ? booleanToggleList[lowerCaseToggleId] - : false -} - export const getToggle = (toggleId: ToggleIdType): VariantType => { const toggle = toggleList[toggleId && toggleId.toLowerCase()] @@ -43,13 +24,6 @@ export const getToggle = (toggleId: ToggleIdType): VariantType => { export const toggle = baseToggle(getToggle) -export const initialize = ({ - booleanToggles, - toggles -}: { - booleanToggles?: BooleanToggleListType - toggles?: toggleListType -}) => { - booleanToggles && setBooleanToggles(booleanToggles) +export const initialize = ({toggles}: {toggles?: toggleListType}) => { toggles && setToggles(toggles) } diff --git a/packages/lib/src/types.ts b/packages/lib/src/types.ts index ae680b7..d2fe220 100755 --- a/packages/lib/src/types.ts +++ b/packages/lib/src/types.ts @@ -7,9 +7,6 @@ export type ToggleType = { variant: VariantType } -// Boolean Toggle -export type BooleanToggleType = boolean - export type TogglerGetterType = (ToggleIdType) => any // Return type of `toggle` function From 5e2c595371c7263f944a5fbf6e885a3ad2f6cace Mon Sep 17 00:00:00 2001 From: Vinicius de Lacerda Date: Thu, 6 Jun 2024 16:40:44 +0200 Subject: [PATCH 8/8] Code review --- README.md | 4 ++-- .../src/integrations/__mocks__/@optimizely/optimizely-sdk.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ebecd70..a30f34e 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,8 @@ See the [Optimizely integration documentation](docs/optimizely-integration.md). ## Toggles -Toggles can be used to implement a/b/c style testing and on/off values as well. -We specify multiple variants of which one is active at any time. +Toggles can be used to implement a/b/c style MVT testing and on/off feature flags as well. +We specify multiple variants of which only one is active at any time. By convention the variants are named `a` (control), `b`, `c` etc. ### Reading values diff --git a/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts b/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts index ecce5cc..0715186 100755 --- a/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts +++ b/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts @@ -14,12 +14,11 @@ export const createInstanceMock = jest.fn(() => ({ })) export const decideMock = jest.fn((toggleKey) => ({ - enabled: toggleKey === "foo" + enabled: toggleKey === 'foo' })) export const optimizelyUserContextMock = jest.fn(() => ({ decide: decideMock })) - export const isFeatureEnabledMock = jest.fn((toggleId) => toggleId === 'foo') @@ -38,5 +37,4 @@ const mock = { createInstance: createInstanceMock } - export default mock