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/README.md b/README.md index c68489b..a30f34e 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 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: @@ -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 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 @@ -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 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/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/__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: [] } diff --git a/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts b/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts index 900f5ed..0715186 100755 --- a/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts +++ b/packages/lib/src/integrations/__mocks__/@optimizely/optimizely-sdk.ts @@ -5,30 +5,22 @@ export const voidEventDispatcher = { export const addNotificationListenerMock = jest.fn() export const createInstanceMock = jest.fn(() => ({ - getEnabledFeatures: getEnabledFeaturesMock, isFeatureEnabled: isFeatureEnabledMock, notificationCenter: { addNotificationListener: addNotificationListenerMock }, - activate: activateMock + activate: activateMock, + createUserContext: optimizelyUserContextMock })) -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`] - } +export const decideMock = jest.fn((toggleKey) => ({ + enabled: toggleKey === 'foo' +})) +export const optimizelyUserContextMock = jest.fn(() => ({ + decide: decideMock +})) - return [] -}) +export const isFeatureEnabledMock = jest.fn((toggleId) => toggleId === 'foo') export const activateMock = jest.fn((toggleId, userId) => { const shouldReturnB = @@ -38,7 +30,10 @@ export const activateMock = jest.fn((toggleId, userId) => { return shouldReturnB && 'b' }) +const originalModule = jest.requireActual('@optimizely/optimizely-sdk') + const mock = { + ...originalModule, createInstance: createInstanceMock } diff --git a/packages/lib/src/integrations/optimizely.test.ts b/packages/lib/src/integrations/optimizely.test.ts index f66d8a8..7e0a59a 100644 --- a/packages/lib/src/integrations/optimizely.test.ts +++ b/packages/lib/src/integrations/optimizely.test.ts @@ -5,14 +5,11 @@ import { setUserId, setAudienceSegmentationAttributes, resetAudienceSegmentationAttributes, - booleanToggle, toggle, - forceToggles, - getEnabledFeatures + forceToggles } 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' @@ -24,9 +21,11 @@ import Optimizely, { // @ts-expect-error isFeatureEnabledMock, // @ts-expect-error - getEnabledFeaturesMock, + activateMock, // @ts-expect-error - activateMock + decideMock, + // @ts-expect-error + optimizelyUserContextMock } from '@optimizely/optimizely-sdk' // Re-used between toggle test suites @@ -123,9 +122,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 +136,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 +143,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 +171,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 +197,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", () => { // 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 +238,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 +249,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,53 +263,13 @@ 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) }) }) }) }) - - 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 92211e1..80cd526 100644 --- a/packages/lib/src/integrations/optimizely.ts +++ b/packages/lib/src/integrations/optimizely.ts @@ -1,6 +1,11 @@ -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 @@ -11,23 +16,21 @@ 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 -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: 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 + [key in ToggleIdType]: ExperimentToggleValueType } type ExperimentCacheType = {[key in ToggleIdType]: ExperimentToggleValueType} type CacheType = FeatureEnabledCacheType | ExperimentCacheType @@ -120,7 +123,7 @@ const voidEventDispatcher = { } export enum ExperimentType { - flag = 'feature', + flag = 'flag', mvt = 'feature-test' } @@ -131,19 +134,18 @@ 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 variationKey: VariantType } } -interface ActivateFlagNotificationPayload - extends OptimizelyLib.ListenerPayload { +interface ActivateFlagNotificationPayload extends ListenerPayload { type: ExperimentType.flag decisionInfo: { - featureKey: ToggleIdType - featureEnabled: boolean + flagKey: ToggleIdType + enabled: boolean } } @@ -163,9 +165,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,7 +184,7 @@ export const initialize = ( * @returns void */ export const addActivateListener = ( - listener: OptimizelyLib.NotificationListener + listener: NotificationListener ) => optimizelyClient.notificationCenter.addNotificationListener( NOTIFICATION_TYPES.DECISION, @@ -207,9 +209,9 @@ const validateUserId = (id) => { if (!id) throw new Error('Opticks: Fatal error: user id is not set') } -const getOrSetCachedFeatureEnabled = ( +const getToggleDecisionStatus = ( toggleId: ToggleIdType -): BooleanToggleValueType => { +): ExperimentToggleValueType => { validateUserId(userId) const DEFAULT = false @@ -219,11 +221,13 @@ const getOrSetCachedFeatureEnabled = ( return typeof value === 'boolean' ? value : DEFAULT } - return (featureEnabledCache[toggleId] = optimizelyClient.isFeatureEnabled( - toggleId, + userContext = optimizelyClient.createUserContext( userId, audienceSegmentationAttributes - )) + ) + const decision = userContext.decide(toggleId) + + return (featureEnabledCache[toggleId] = decision.enabled) } /** @@ -244,33 +248,29 @@ 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 +292,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 +313,7 @@ const getToggle = (toggleId: ToggleIdType): ExperimentToggleValueType => { } const convertBooleanToggleToFeatureVariant = (toggleId: ToggleIdType) => { - const isFeatureEnabled = getBooleanToggle(toggleId) + const isFeatureEnabled = getToggleDecisionStatus(toggleId) return isFeatureEnabled ? 'b' : 'a' } @@ -337,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) { @@ -350,20 +346,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 */ 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 1161f47..e02d12a 100755 --- a/packages/lib/src/integrations/simple.ts +++ b/packages/lib/src/integrations/simple.ts @@ -1,15 +1,7 @@ /** @deprecated */ -import type {BooleanToggleType, ToggleIdType, VariantType} from '../types' -import {booleanToggle as baseBooleanToggle} from '../core/booleanToggle' +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'} } @@ -17,27 +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 booleanToggle = baseBooleanToggle(getBooleanToggle) - export const getToggle = (toggleId: ToggleIdType): VariantType => { const toggle = toggleList[toggleId && toggleId.toLowerCase()] @@ -46,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 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