Skip to content

Commit ea62f11

Browse files
author
Vinicius de Lacerda
committed
Adds proper support to MVT experiments and simplifies the usage of Optimizely's SDK after update to >v4.5
1 parent fc32e5b commit ea62f11

File tree

2 files changed

+70
-74
lines changed

2 files changed

+70
-74
lines changed

packages/lib/src/integrations/optimizely.ts

Lines changed: 67 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ type ExperimentCacheType = {[key in ToggleIdType]: ExperimentToggleValueType}
3636
type CacheType = FeatureEnabledCacheType | ExperimentCacheType
3737
type ForcedTogglesType = {[Tkey in ToggleIdType]: ToggleValueType}
3838

39-
let featureEnabledCache: FeatureEnabledCacheType = {}
4039
let experimentCache: ExperimentCacheType = {}
4140
const forcedToggles: ForcedTogglesType = {}
4241

@@ -54,7 +53,6 @@ export const registerLibrary = (lib) => {
5453
optimizely = lib
5554
}
5655

57-
const clearFeatureEnabledCache = () => (featureEnabledCache = {})
5856
const clearExperimentCache = () => (experimentCache = {})
5957

6058
/**
@@ -77,7 +75,6 @@ export const forceToggles = (toggleKeyValues: {
7775
}
7876

7977
const invalidateCaches = () => {
80-
clearFeatureEnabledCache()
8178
clearExperimentCache()
8279
}
8380

@@ -134,24 +131,13 @@ export enum ExperimentType {
134131
*
135132
* It would be best if Opticks abstracts this difference from the client in future versions.
136133
*/
137-
interface ActivateMVTNotificationPayload extends ListenerPayload {
134+
interface ActivateNotificationPayload extends ListenerPayload {
138135
type: ExperimentType.mvt
139136
decisionInfo: {
140137
experimentKey: ToggleIdType
141138
variationKey: VariantType
142139
}
143140
}
144-
interface ActivateFlagNotificationPayload extends ListenerPayload {
145-
type: ExperimentType.flag
146-
decisionInfo: {
147-
flagKey: ToggleIdType
148-
enabled: boolean
149-
}
150-
}
151-
152-
export type ActivateNotificationPayload =
153-
| ActivateMVTNotificationPayload
154-
| ActivateFlagNotificationPayload
155141

156142
/**
157143
* Initializes Opticks with the supplied Optimizely datafile,
@@ -185,11 +171,32 @@ export const initialize = (
185171
*/
186172
export const addActivateListener = (
187173
listener: NotificationListener<ActivateNotificationPayload>
188-
) =>
189-
optimizelyClient.notificationCenter.addNotificationListener(
174+
) => {
175+
const decisionListener = (payload: ActivateNotificationPayload) => {
176+
const {variationKey} = payload.decisionInfo
177+
const decision =
178+
variationKey === 'on' ? 'b' : variationKey === 'off' ? 'a' : variationKey
179+
180+
const updatedPayload = {
181+
...payload,
182+
decisionInfo: {...payload.decisionInfo, variationKey: decision}
183+
}
184+
185+
return listener(updatedPayload)
186+
}
187+
188+
/**
189+
* This is a temporary support for the initial convention defined during the migration that "on" === "b" and "off" === "a"
190+
* With the latest SDK version, A/B tests and target deliveries return a string key for the variation
191+
* We will migrate the current experiments to the new convention and remove this temporary support.
192+
* In the new convention we will always use the variation key as the decision.
193+
*/
194+
// TODO (@vlacerda) [2024-06-30]: By this time we should ping @vlacerda to evaluate again if the fix is still needed and remove it if not.
195+
return optimizelyClient.notificationCenter.addNotificationListener(
190196
NOTIFICATION_TYPES.DECISION,
191-
listener
197+
decisionListener
192198
)
199+
}
193200

194201
const isForcedOrCached = (toggleId: ToggleIdType, cache: CacheType): boolean =>
195202
forcedToggles.hasOwnProperty(toggleId) || cache.hasOwnProperty(toggleId)
@@ -209,27 +216,6 @@ const validateUserId = (id) => {
209216
if (!id) throw new Error('Opticks: Fatal error: user id is not set')
210217
}
211218

212-
const getToggleDecisionStatus = (
213-
toggleId: ToggleIdType
214-
): ExperimentToggleValueType => {
215-
validateUserId(userId)
216-
217-
const DEFAULT = false
218-
219-
if (isForcedOrCached(toggleId, featureEnabledCache)) {
220-
const value = getForcedOrCached(toggleId, featureEnabledCache)
221-
return typeof value === 'boolean' ? value : DEFAULT
222-
}
223-
224-
userContext = optimizelyClient.createUserContext(
225-
userId,
226-
audienceSegmentationAttributes
227-
)
228-
const decision = userContext.decide(toggleId)
229-
230-
return (featureEnabledCache[toggleId] = decision.enabled)
231-
}
232-
233219
/**
234220
* Determines whether a user satisfies the audience requirements for a toggle.
235221
@@ -241,34 +227,43 @@ const getToggleDecisionStatus = (
241227
export const isUserInRolloutAudience = (toggleId: ToggleIdType) => {
242228
// @ts-expect-error we're being naughty here and using internals
243229
const config = optimizelyClient.projectConfigManager.getConfig()
230+
// feature in the config object represents an a/b test
244231
const feature = config.featureKeyMap[toggleId]
232+
// rollout is a targeted delivery
245233
const rollout = config.rolloutIdMap[feature.rolloutId]
234+
// both a/b tests and targeted deliveries can have audiences
235+
const allRules = [...rollout.experiments]
236+
237+
/**
238+
* The feature object supplies ids through experimentIds.
239+
* We find the rules for each experiment and add them to the allRules array.
240+
*/
241+
if (feature.experimentIds.length > 0) {
242+
const {experimentIds} = feature
243+
const experimentRules = experimentIds.map(
244+
(experimentId) => config.experimentIdMap[experimentId]
245+
)
246+
allRules.push(...experimentRules)
247+
}
246248

247-
const endIndex = rollout.experiments.length - 1
248-
let index: number
249-
let isInAnyAudience = false
250-
251-
for (index = 0; index <= endIndex; index++) {
252-
const rolloutRule = rollout.experiments[index]
253-
249+
const isInAnyAudience = allRules.reduce((acc, rule) => {
254250
// Reference: https://github.com/optimizely/javascript-sdk/blob/851b06622fa6a0239500b3b65e2d3937334960de/lib/core/decision_service/index.ts#L403
255251
const decisionIfUserIsInAudience =
256252
// @ts-expect-error we're being naughty here and using internals
257253
optimizelyClient.decisionService.checkIfUserIsInAudience(
258254
config,
259-
rolloutRule,
255+
rule,
260256
'rule',
261257
userContext,
262258
audienceSegmentationAttributes,
263259
''
264260
)
265261

266-
if (
267-
decisionIfUserIsInAudience.result &&
268-
!isPausedBooleanToggle(rolloutRule)
269-
)
270-
isInAnyAudience = true
271-
}
262+
if (decisionIfUserIsInAudience.result && !isPausedBooleanToggle(rule))
263+
return true
264+
265+
return acc
266+
}, false)
272267

273268
return isInAnyAudience
274269
}
@@ -302,19 +297,26 @@ const getToggle = (toggleId: ToggleIdType): ExperimentToggleValueType => {
302297
return typeof value === 'string' ? value : DEFAULT
303298
}
304299

300+
userContext = optimizelyClient.createUserContext(
301+
userId,
302+
audienceSegmentationAttributes
303+
)
304+
305+
const variationKey = userContext.decide(toggleId).variationKey
306+
307+
/**
308+
* This is a temporary support for the initial convention defined during the migration that "on" === "b" and "off" === "a"
309+
* With the latest SDK version, A/B tests and target deliveries return a string key for the variation
310+
* We will migrate the current experiments to the new convention and remove this temporary support.
311+
* In the new convention we will always use the variation key as the decision.
312+
*/
313+
// TODO (@vlacerda) [2024-06-30]: By this time we should ping @vlacerda to evaluate again if the fix is still needed and remove it if not.
314+
const decision =
315+
variationKey === 'on' ? 'b' : variationKey === 'off' ? 'a' : variationKey
316+
305317
// Assuming the variation keys follow a, b, c, etc. convention
306318
// TODO: Enforce ^ ?
307-
return (experimentCache[toggleId] =
308-
optimizelyClient.activate(
309-
toggleId,
310-
userId,
311-
audienceSegmentationAttributes
312-
) || DEFAULT)
313-
}
314-
315-
const convertBooleanToggleToFeatureVariant = (toggleId: ToggleIdType) => {
316-
const isFeatureEnabled = getToggleDecisionStatus(toggleId)
317-
return isFeatureEnabled ? 'b' : 'a'
319+
return (experimentCache[toggleId] = decision || DEFAULT)
318320
}
319321

320322
/**
@@ -335,15 +337,7 @@ export function toggle<A extends any[]>(
335337
...variants: A
336338
): ToggleFuncReturnType<A>
337339
export function toggle(toggleId: ToggleIdType, ...variants) {
338-
// An A/B/C... test
339-
if (variants.length > 2) {
340-
return baseToggle(getToggle)(toggleId, ...variants)
341-
} else {
342-
return baseToggle(convertBooleanToggleToFeatureVariant)(
343-
toggleId,
344-
...variants
345-
)
346-
}
340+
return baseToggle(getToggle)(toggleId, ...variants)
347341
}
348342

349343
/**

packages/lib/src/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export type ToggleIdType = string
22

3-
export type VariantType = 'a' | 'b' | 'c' | 'd' | 'e' | 'f'
3+
// TODO (@vlacerda) [2024-06-30]: By this time we should ping @vlacerda to evaluate again if the fix is still needed and remove it if not.
4+
// For more context, look at the other comment in this file in optimizely.ts marked with the same TODO date.
5+
export type VariantType = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'off' | 'on'
46

57
// Value Toggle
68
export type ToggleType = {

0 commit comments

Comments
 (0)