UNPKG

opticks

Version:

FindHotel Toggle Flag JavaScript SDK

1 lines 15.7 kB
{"version":3,"sources":["../src/integrations/optimizely.ts"],"sourcesContent":["import OptimizelyLib, {\n EventDispatcher,\n Client,\n OptimizelyUserContext,\n NotificationListener,\n ListenerPayload\n} from '@optimizely/optimizely-sdk'\nimport {ToggleFuncReturnType, ToggleIdType, VariantType} from '../types'\nimport {toggle as baseToggle} from '../core/toggle'\n\ntype UserIdType = string\ntype AudienceSegmentationAttributeKeyType = string\ntype AudienceSegmentationAttributeValueType = string | boolean\n\ntype AudienceSegmentationAttributesType = {\n [key in AudienceSegmentationAttributeKeyType]?: AudienceSegmentationAttributeValueType\n}\n\ntype ExperimentToggleValueType = string | boolean\ntype ToggleValueType = ExperimentToggleValueType\n\nexport type OptimizelyDatafileType = object\n\nexport const NOTIFICATION_TYPES = OptimizelyLib.enums.NOTIFICATION_TYPES\n\nlet optimizely = OptimizelyLib // reference to injected Optimizely library\nlet optimizelyClient: Client | null // reference to active Optimizely instance\nlet userId: UserIdType\nlet audienceSegmentationAttributes: AudienceSegmentationAttributesType = {}\nlet userContext: OptimizelyUserContext\n\ntype FeatureEnabledCacheType = {\n [key in ToggleIdType]: ExperimentToggleValueType\n}\ntype ExperimentCacheType = {[key in ToggleIdType]: ExperimentToggleValueType}\ntype CacheType = FeatureEnabledCacheType | ExperimentCacheType\ntype ForcedTogglesType = {[Tkey in ToggleIdType]: ToggleValueType}\n\nlet experimentCache: ExperimentCacheType = {}\nconst forcedToggles: ForcedTogglesType = {}\n\n/**\n * Registers an externally passed in OptimizelySDK library to use.\n * This is meant to give flexibility in which SDK to use, but since Opticks\n * now relies on a specific version this will be bundled in future versions.\n *\n * @param lib OptimizelySDK\n */\nexport const registerLibrary = (lib) => {\n // TODO: Double-check if this works with server environments\n // Since they load the module in memory, make sure they are not persisted\n // across different requests\n optimizely = lib\n}\n\nconst clearExperimentCache = () => {\n experimentCache = {}\n}\n\n/**\n * Adds / removes Toggles to force from the forcedToggles list\n *\n * @param toggleKeyValues\n */\nexport const forceToggles = (toggleKeyValues: {\n [key in ToggleIdType]: ToggleValueType | null\n}) => {\n Object.keys(toggleKeyValues).forEach((toggleId) => {\n const value = toggleKeyValues[toggleId]\n\n /**\n * Keeping the old behaviour of using boolean values for toggles to represent `a` and `b` versions\n */\n const isBoolean = typeof value === 'boolean'\n if (isBoolean) {\n forcedToggles[toggleId] = value ? 'b' : 'a'\n return\n }\n\n if (value === null) {\n delete forcedToggles[toggleId]\n } else {\n forcedToggles[toggleId] = value\n }\n })\n}\n\nconst invalidateCaches = () => {\n clearExperimentCache()\n}\n\n/**\n * Sets the userId and invalidates caches so each toggle will\n * be re-evaluated in the next call.\n *\n * @param id\n */\nexport const setUserId = (id: UserIdType) => {\n if (userId !== id) invalidateCaches()\n userId = id\n}\n\n/**\n * Sets audience segmentation attributes and invalidates caches\n * so each toggle will be re-evaluated in the next call.\n *\n * @param attributes\n */\nexport const setAudienceSegmentationAttributes = (\n attributes: AudienceSegmentationAttributesType = {}\n) => {\n const newSegmentationAttributes = {\n ...audienceSegmentationAttributes,\n ...attributes\n }\n\n const shouldInvalidateCache =\n JSON.stringify(newSegmentationAttributes) !==\n JSON.stringify(audienceSegmentationAttributes || {})\n\n if (shouldInvalidateCache) {\n invalidateCaches()\n }\n\n audienceSegmentationAttributes = newSegmentationAttributes\n}\n\n/**\n * Clears all audience segmentation attributes and invalidates caches\n * so each toggle will be re-evaluated in the next call.\n */\nexport const resetAudienceSegmentationAttributes = () => {\n invalidateCaches()\n audienceSegmentationAttributes = {}\n}\n\nconst voidActivateHandler = () => null\nconst voidEventDispatcher = {\n dispatchEvent: () => null\n}\n\nexport enum ExperimentType {\n flag = 'flag',\n mvt = 'feature-test'\n}\n\n/**\n * REVIEW: This decisionInfo payload cannot be found anywhere in the types in the SDK.\n * However the information typed here is sent, the payload changes based on whether it is a feature flag or MVT, so typing it here.\n * https://github.com/optimizely/javascript-sdk/blob/625cb7c3835e31e25b16fa34b5f25a2bda42ed57/packages/optimizely-sdk/lib/optimizely/index.ts#L1577\n *\n * It would be best if Opticks abstracts this difference from the client in future versions.\n */\nexport interface ActivateNotificationPayload extends ListenerPayload {\n type: ExperimentType\n decisionInfo: {\n flagKey: ToggleIdType\n variationKey: VariantType\n }\n}\n\n/**\n * Initializes Opticks with the supplied Optimizely datafile,\n * and allows for registering experimentDecision handlers and custom\n * event dispatcher.\n *\n * @param datafile Optimizely Rollouts JSON datafile\n * @param onExperimentDecision Experiment decision listener\n * @param eventDispatcher Custom event dispatcher\n * @returns Optimizely Instance\n */\nexport const initialize = (\n datafile: OptimizelyDatafileType,\n onExperimentDecision: NotificationListener<ActivateNotificationPayload> = voidActivateHandler,\n eventDispatcher: EventDispatcher = voidEventDispatcher\n): Client => {\n optimizelyClient = optimizely.createInstance({\n datafile,\n eventDispatcher: eventDispatcher\n })\n\n addActivateListener(onExperimentDecision)\n return optimizelyClient\n}\n\n/**\n * Registers custom decision listener\n *\n * @param listener\n * @returns void\n */\nexport const addActivateListener = (\n listener: NotificationListener<ActivateNotificationPayload>\n) => {\n const decisionListener = (payload: ActivateNotificationPayload) => {\n const {variationKey} = payload.decisionInfo\n const decision =\n variationKey === 'on' ? 'b' : variationKey === 'off' ? 'a' : variationKey\n\n const updatedPayload = {\n ...payload,\n decisionInfo: {...payload.decisionInfo, variationKey: decision}\n }\n\n return listener(updatedPayload)\n }\n\n /**\n * This is a temporary support for the initial convention defined during the migration that \"on\" === \"b\" and \"off\" === \"a\"\n * With the latest SDK version, A/B tests and target deliveries return a string key for the variation\n * We will migrate the current experiments to the new convention and remove this temporary support.\n * In the new convention we will always use the variation key as the decision.\n */\n // 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.\n return optimizelyClient.notificationCenter.addNotificationListener(\n NOTIFICATION_TYPES.DECISION,\n decisionListener\n )\n}\n\nconst isForcedOrCached = (toggleId: ToggleIdType, cache: CacheType): boolean =>\n forcedToggles.hasOwnProperty(toggleId) || cache.hasOwnProperty(toggleId)\n\nconst getForcedOrCached = (\n toggleId: ToggleIdType,\n cache: CacheType\n): ToggleValueType => {\n const register = forcedToggles.hasOwnProperty(toggleId)\n ? forcedToggles\n : cache\n\n return register[toggleId]\n}\n\nconst validateUserId = (id) => {\n if (!id) throw new Error('Opticks: Fatal error: user id is not set')\n}\n\n/**\n * Determines whether a user satisfies the audience requirements for a toggle.\n\n * Since it uses an internal method of the SDK this unfortunately ties Opticks to\n * the specific Optimizely SDK version.\n *\n * @param toggleId\n */\nexport const isUserInRolloutAudience = (toggleId: ToggleIdType) => {\n // @ts-expect-error we're being naughty here and using internals\n const config = optimizelyClient.projectConfigManager.getConfig()\n // feature in the config object represents an a/b test\n const feature = config.featureKeyMap[toggleId]\n // rollout is a targeted delivery\n const rollout = config.rolloutIdMap[feature.rolloutId]\n // both a/b tests and targeted deliveries can have audiences\n const allRules = [...rollout.experiments]\n\n /**\n * The feature object supplies ids through experimentIds.\n * We find the rules for each experiment and add them to the allRules array.\n */\n if (feature.experimentIds.length > 0) {\n const {experimentIds} = feature\n const experimentRules = experimentIds.map(\n (experimentId) => config.experimentIdMap[experimentId]\n )\n allRules.push(...experimentRules)\n }\n\n const isInAnyAudience = allRules.reduce((acc, rule) => {\n // Reference: https://github.com/optimizely/javascript-sdk/blob/851b06622fa6a0239500b3b65e2d3937334960de/lib/core/decision_service/index.ts#L403\n const decisionIfUserIsInAudience =\n // @ts-expect-error we're being naughty here and using internals\n optimizelyClient.decisionService.checkIfUserIsInAudience(\n config,\n rule,\n 'rule',\n userContext,\n audienceSegmentationAttributes,\n ''\n )\n\n if (decisionIfUserIsInAudience.result && !isPausedBooleanToggle(rule))\n return true\n\n return acc\n }, false)\n\n return isInAnyAudience\n}\n\n/**\n * Determines whether a boolean toggle (feature flag) is fully to one side,\n * which for tracking purposes can be considered to be \"paused\" and therefor shouldn't\n * be tracked as an active experiment.\n *\n * @param rolloutRule Optimizely Rollout Rule\n * @returns\n */\nconst isPausedBooleanToggle = (rolloutRule: {\n trafficAllocation: [{endOfRange: number}]\n}) => {\n const trafficAllocationVariation = rolloutRule.trafficAllocation[0]\n // We consider a toggle paused if traffic is 100% to either side\n return (\n typeof trafficAllocationVariation === 'undefined' ||\n trafficAllocationVariation.endOfRange === 10000\n )\n}\n\nconst getToggle = (toggleId: ToggleIdType): ExperimentToggleValueType => {\n validateUserId(userId)\n\n const DEFAULT = 'a'\n\n if (isForcedOrCached(toggleId, experimentCache)) {\n const value = getForcedOrCached(toggleId, experimentCache)\n return typeof value === 'string' ? value : DEFAULT\n }\n\n userContext = optimizelyClient.createUserContext(\n userId,\n audienceSegmentationAttributes\n )\n\n const variationKey = userContext.decide(toggleId).variationKey\n\n /**\n * This is a temporary support for the initial convention defined during the migration that \"on\" === \"b\" and \"off\" === \"a\"\n * With the latest SDK version, A/B tests and target deliveries return a string key for the variation\n * We will migrate the current experiments to the new convention and remove this temporary support.\n * In the new convention we will always use the variation key as the decision.\n */\n // 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.\n const decision =\n variationKey === 'on' ? 'b' : variationKey === 'off' ? 'a' : variationKey\n\n // Assuming the variation keys follow a, b, c, etc. convention\n // TODO: Enforce ^ ?\n experimentCache[toggleId] = decision || DEFAULT\n return decision || DEFAULT\n}\n\n/**\n * Returns the active value, or executes the active function if an arrow function is passed.\n *\n * Variants can be either values, in which case they both should be of the same type,\n * or arrow functions which will be executed for the winning side, the return type is unknown\n * on purpose since it's expected the return value will likely be implicit.\n *\n * When mixing values and function variants, the return type is unknown and needs to be cast.\n *\n * @param toggleId\n * @param variants\n */\n\nexport function toggle<A extends any[]>(\n toggleId: ToggleIdType,\n ...variants: A\n): ToggleFuncReturnType<A>\nexport function toggle(toggleId: ToggleIdType, ...variants) {\n return baseToggle(getToggle)(toggleId, ...variants)\n}\n\n/**\n * Export imported types\n */\nexport {ToggleIdType, VariantType}\n"],"mappings":"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","names":["OptimizelyLib","NOTIFICATION_TYPES","OptimizelyLib","optimizely","optimizelyClient","userId","audienceSegmentationAttributes","userContext","experimentCache","forcedToggles","registerLibrary","lib","clearExperimentCache","forceToggles","toggleKeyValues","toggleId","value","invalidateCaches","setUserId","id","setAudienceSegmentationAttributes","attributes","newSegmentationAttributes","__spreadValues","resetAudienceSegmentationAttributes","voidActivateHandler","voidEventDispatcher","ExperimentType","initialize","datafile","onExperimentDecision","eventDispatcher","addActivateListener","listener","decisionListener","payload","variationKey","decision","updatedPayload","__spreadProps","isForcedOrCached","cache","getForcedOrCached","validateUserId","isUserInRolloutAudience","config","feature","allRules","experimentIds","experimentRules","experimentId","acc","rule","isPausedBooleanToggle","rolloutRule","trafficAllocationVariation","getToggle","DEFAULT","toggle","variants"]}