Flag dependency
Overview and implementation details of flag dependency in Optimizely Feature Experimentation.
Using flag dependency, you can control a user's exposure to feature flags based on whether they would be exposed to other "parent" flags. For example, you can ensure users only see a particular feature if they qualify for another related feature.
Feature Experimentation SDKs use a deterministic algorithm to bucket users to variations. This means you can evaluate whether a user would have been exposed to a parent flag, even if Optimizely has not made the decision yet. This approach lets you build dependencies without persisting user state.
NoteThis document does not cover the use case where you show a flag only to users who have already encountered a parent flag. Implementing that behavior requires storing state and is outside the scope of this documentation.
Usage note
Variables prefixed with _ in this document are defined at the feature flag level, not at the variation level. These variables set dependency rules for child flags and should remain unchanged within variations.
This approach complements the rules engine rather than replacing it. While you define dependencies at the flag level, the final user experience is still controlled by the targeted delivery or experiment rules configured in both the parent and child flags.
Traditional dependency
Traditional dependency exposes a user to a flag if that user would be exposed to a parent flag.
For example, you create two flags for a new landing page.
new_landing_page– Flag for the new landing page.new_cta– Flag for a call to action (CTA) on the landing page.
Users should only see the new CTA if they qualify for the new landing page, so the new_cta flag depends on new_landing_page.
The child flag should include a variable called _depends_on_parents of type String. This variable is a comma-delimited list of flags on which the child depends. For example, the _depends_on_parents variable is set to new_landing_page. To create this variable, add it to the child flag in the Optimizely app or REST API. See Create flag variables.
NoteIf multiple parents are specified, add a boolean variable,
_depends_evaluate_all_parents. If set totrue, the code evaluates all parents totruebefore evaluating the dependent. Iffalse, the code evaluates the dependent flag if any parent evaluates totrue.
Example code
const decideWithDependencies = function(userContext, flagKey, options) {
if (_optimizelyClient == null) {
return;
}
const config = _optimizelyClient.getOptimizelyConfig();
const _dependencyDecisions = [];
let dependencyMetadata = "";
// Check dependencies
if (config.featuresMap.hasOwnProperty(flagKey)) {
var flag = config.featuresMap[flagKey];
if (flag.variablesMap.hasOwnProperty("_depends_on_parents")) {
let evaluateAllParents = true;
let parentDecisionsEnabled = 0;
if (flag.variablesMap.hasOwnProperty("_depends_evaluate_all_parents")) {
evaluateAllParents = flag.variablesMap._depends_evaluate_all_parents.value === 'true';
}
let dependencies = flag.variablesMap._depends_on_parents.value.split(/[, ]+/);
// Evaluate each dependency
dependencies.some(function(dependency, index) {
// If the dependency is not enabled, force the "off" variation to be returned
const decision = userContext.decide(dependency, {
DISABLE_DECISION_EVENT: true
});
_dependencyDecisions.push(decision);
if (decision.enabled) {
// Parent decision is enabled.
parentDecisionsEnabled += 1;
if (!evaluateAllParents) {
// If we do not require dependency on all parents, we can exit out of the loop
return true;
}
}
});
if (
(evaluateAllParents && parentDecisionsEnabled != dependencies.length) ||
(!evaluateAllParents && parentDecisionsEnabled == 0)) {
userContext.setForcedDecision({
flagKey: flagKey
}, {
variationKey: "off"
});
}
}
}
// Make the decision for flagKey
// If flagKey has any disabled dependencies, decide() returns "off"
let decision = userContext.decide(flagKey, options);
decision.dependencyDecisions = _dependencyDecisions;
// Remove the forced decision rule
userContext.removeForcedDecision({
flagKey: flagKey,
ruleKey: null
});
return decision;
};The previous code sample does the following:
- Retrieve the Optimizely client configuration.
- Check if the requested flag exists in the configuration.
- Determine if the flag has a
_depends_on_parentsvariable. - Evaluate each parent flag and determine if the child flag should be enabled.
Note
The decision event is disabled because this process only checks if the user would be exposed to the parent flag.
- Make a decision on the child flag based on dependencies.
- Attach parent decisions to the returned decision object so the caller can inspect which parents were evaluated and their results.
Inverse activation
In some cases, you may want to enforce the opposite logic of traditional dependency, where a child flag is disabled if a parent flag is enabled. This is useful when certain features should be mutually exclusive.
For example, create the following flags:
mobile-only-experiencedesktop-only-experiencecombined-mobile-and-desktop-experience
Users assigned to mobile-only-experience should not be assigned to desktop-only-experience, and vice versa. Users in combined-mobile-and-desktop-experience should not be assigned to either individual experience.
To accomplish this, create a flag-level variable, _inverse_activation. By default, _inverse_activation is false, which results in the traditional behavior. When _inverse_activation is set to true, it inverts the dependency logic.
Inverse activation code
The updated code retrieves the _inverse_activation variable and modifies the dependency check accordingly.
let inverseActivation = false;
if (flag.variablesMap.hasOwnProperty("_inverse_activation")) {
inverseActivation = flag.variablesMap._inverse_activation.value == "true";
}
if (
(inverseActivation && !decision.enabled) ||
(!inverseActivation && decision.enabled)) {
// Parent decision is enabled.
parentDecisionsEnabled += 1;
if (!evaluateAllParents) {
// If we do not require dependency on all parents, we can exit out of the loop
return true;
}
}The completed code is included in the following code sample:
const decideWithDependencies = function(userContext, flagKey, options) {
if (_optimizelyClient == null) {
return;
}
const config = _optimizelyClient.getOptimizelyConfig();
const _dependencyDecisions = [];
let dependencyMetadata = "";
// Check dependencies
if (config.featuresMap.hasOwnProperty(flagKey)) {
var flag = config.featuresMap[flagKey];
if (flag.variablesMap.hasOwnProperty("_depends_on_parents")) {
let evaluateAllParents = true;
let parentDecisionsEnabled = 0;
if (flag.variablesMap.hasOwnProperty("_depends_evaluate_all_parents")) {
evaluateAllParents = flag.variablesMap._depends_evaluate_all_parents.value === 'true';
}
let dependencies = flag.variablesMap._depends_on_parents.value.split(/[, ]+/);
let inverseActivation = false;
if (flag.variablesMap.hasOwnProperty("_inverse_activation")) {
inverseActivation = flag.variablesMap._inverse_activation.value == "true";
}
// Evaluate each dependency
dependencies.some(function(dependency, index) {
// If the dependency is not enabled, force the "off" variation to be returned
const decision = userContext.decide(dependency, {
DISABLE_DECISION_EVENT: true
});
_dependencyDecisions.push(decision);
if (
(inverseActivation && !decision.enabled) ||
(!inverseActivation && decision.enabled)) {
// Parent decision is enabled.
parentDecisionsEnabled += 1;
if (!evaluateAllParents) {
// If we do not require dependency on all parents, we can exit out of the loop
return true;
}
}
});
if (
(evaluateAllParents && parentDecisionsEnabled != dependencies.length) ||
(!evaluateAllParents && parentDecisionsEnabled == 0)) {
userContext.setForcedDecision({
flagKey: flagKey
}, {
variationKey: "off"
});
}
}
}
// Make the decision for flagKey
// If flagKey has any disabled dependencies, decide() will return "off"
let decision = userContext.decide(flagKey, options);
decision.dependencyDecisions = _dependencyDecisions;
// Remove the forced decision rule
userContext.removeForcedDecision({
flagKey: flagKey,
ruleKey: null
});
return decision;
};Bind to the SDK
By default, feature flag decisions are made directly on the user context object. To integrate the decideWithDependencies function into the SDK, bind it to the Optimizely client during initialization.
_optimizelyClient.decideWithDependencies = decideWithDependencies;Then, use the following function whenever decide is currently called:
const decision = _optimizelyClient.decideWithDependencies(userContext, flagKey, options);This evaluates dependencies before making the final decision.
Updated 6 days ago
