Current section

Files

Jump to
launchdarkly_server_sdk src ldclient_eval.erl
Raw

src/ldclient_eval.erl

%%-------------------------------------------------------------------
%% @doc Evaluation
%% @private
%% @end
%%-------------------------------------------------------------------
-module(ldclient_eval).
%% API
-export([flag_key_for_context/4]).
-export([all_flags_eval/2]).
-export([all_flags_state/3]).
%% Types
-type result() :: {
Detail :: detail(),
Events :: [ldclient_event:event()]
}.
-type detail() :: {
VariationIndex :: variation_index(),
Value :: result_value(),
Reason :: reason()
}.
-type result_value() :: null | ldclient_flag:variation_value().
-type variation_index() :: null | non_neg_integer().
-type reason() ::
target_match
| {rule_match, RuleIndex :: non_neg_integer(), RuleUUID :: binary()}
| {rule_match, RuleIndex :: non_neg_integer(), in_experiment}
| {rule_match, RuleIndex :: non_neg_integer(), RuleUUID :: binary(), InExperiment :: true}
| {prerequisite_failed, FlagKeys :: [binary()]}
| {error, error_type()}
| fallthrough
| {fallthrough, in_experiment}
| off.
-type error_type() :: client_not_ready | flag_not_found | malformed_flag
| user_not_specified | wrong_type | exception.
-type feature_flags_state() :: #{
flag_values => #{binary() => any()}
}.
-type all_flags_state_options() :: #{
with_reasons => boolean()
%% client_side_only => boolean(), % TODO: Support.
%% details_only_for_tracked_flags => boolean() % TODO: Support.
}.
-export_type([detail/0]).
-export_type([reason/0]).
-export_type([result_value/0]).
-export_type([feature_flags_state/0]).
-export_type([all_flags_state_options/0]).
%%===================================================================
%% API
%%===================================================================
%% @doc Evaluates feature flag for a given context
%%
%% @end
-spec flag_key_for_context(
Tag :: atom(),
FlagKey :: ldclient_flag:key(),
Context :: ldclient_context:context(),
DefaultValue :: result_value()
) -> result().
flag_key_for_context(Tag, FlagKey, Context, DefaultValue) ->
try
flag_key_for_context(Tag, FlagKey, Context, DefaultValue, get_state(Tag), get_initialization_state(Tag))
catch _:_ ->
Reason = {error, exception},
Events = [ldclient_event:new_for_unknown_flag(FlagKey, Context, DefaultValue, Reason)],
{{null, DefaultValue, Reason}, Events}
end.
-spec flag_key_for_context(
Tag :: atom(),
FlagKey :: ldclient_flag:key(),
Context :: ldclient_context:context(),
DefaultValue :: result_value(),
Offline :: atom(),
Initialized :: atom()
) -> result().
flag_key_for_context(_Tag, _FlagKey, _Context, DefaultValue, offline, _) ->
{{null, DefaultValue, {error, client_not_ready}}, []};
flag_key_for_context(_Tag, _FlagKey, _Context, DefaultValue, _, not_initialized) ->
{{null, DefaultValue, {error, client_not_ready}}, []};
flag_key_for_context(Tag, FlagKey, Context, DefaultValue, online, store_initialized) ->
error_logger:warning_msg("Variation called before LaunchDarkly client initialization completed - using last known values from feature store."),
FeatureStore = ldclient_config:get_value(Tag, feature_store),
FlagRecs = FeatureStore:get(Tag, features, FlagKey),
flag_recs_for_context(FlagKey, FlagRecs, Context, FeatureStore, Tag, DefaultValue);
flag_key_for_context(Tag, FlagKey, Context, DefaultValue, online, initialized) ->
FeatureStore = ldclient_config:get_value(Tag, feature_store),
FlagRecs = FeatureStore:get(Tag, features, FlagKey),
flag_recs_for_context(FlagKey, FlagRecs, Context, FeatureStore, Tag, DefaultValue).
%% @doc Returns an object that encapsulates the state of all feature flags for a given context.
%%
%% This includes the flag values, and also metadata that can be used on the front end.
%% The most common use case for this method is to bootstrap a set of client-side feature flags from a
%% back-end service.
%% @end
-spec all_flags_state(
Context :: ldclient_context:context(),
Options :: all_flags_state_options(),
Tag :: atom()
) -> map().
all_flags_state(Context, Options, Tag) ->
all_flags_state(Context, Options, Tag, get_state(Tag), get_initialization_state(Tag)).
-spec is_not_deleted(Item :: map()) -> boolean().
is_not_deleted(#{deleted := true}) -> false;
is_not_deleted(_) -> true.
-spec all_flags_state(
Context :: ldclient_context:context(),
Options :: all_flags_state_options(),
Tag :: atom(),
Offline :: atom(),
InitializationState :: atom()
) -> map().
all_flags_state(_Context, _Options, _Tag, offline, _) ->
#{<<"$valid">> => false, <<"$flagsState">> => #{}};
all_flags_state(_Context, _Options, _Tag, _, not_initialized) ->
#{<<"$valid">> => false, <<"$flagsState">> => #{}};
all_flags_state(Context, #{with_reasons := WithReason} = _Options, Tag, Offline, store_initialized) ->
error_logger:warning_msg("Called allFlagsState before client initialization; using last known values from data store."),
all_flags_state(Context, #{with_reasons := WithReason} = _Options, Tag, Offline, initialized);
all_flags_state(Context, #{with_reasons := WithReason} = _Options, Tag, _, initialized) ->
FeatureStore = ldclient_config:get_value(Tag, feature_store),
AllFlags = [Flag || Flag = {_, FlagValue} <- FeatureStore:all(Tag, features), is_not_deleted(FlagValue)],
EvalFun = fun({FlagKey, #{version := Version} = Flag}, #{<<"$flagsState">> := FlagsState} = Acc) ->
% Here the state is either initialized, or store_initialized, and we are online. Call directly to that version
% of flag_key_for_context. This will prevent additional warnings for the client initialization not being
% complete in the store_initialized state.
{{VariationIndex, V, Reason}, _Events} = flag_key_for_context(Tag, FlagKey, Context, null, online, initialized),
FlagState = maybe_add_track_events(Flag,
maybe_add_debug_events_until_date(Flag, #{
<<"version">> => Version})),
UpdatedFlagState = case is_integer(VariationIndex) of
true -> FlagState#{
<<"variation">> => VariationIndex
};
false -> FlagState
end,
FlagStateWithReason = maybe_add_reason(Flag, Reason, WithReason, UpdatedFlagState),
UpdatedFlagsState = maps:put(FlagKey, FlagStateWithReason, FlagsState),
Acc#{FlagKey => V, <<"$flagsState">> => UpdatedFlagsState}
end,
lists:foldl(EvalFun, #{
<<"$valid">> => true,
<<"$flagsState">> => #{}
}, AllFlags).
-spec maybe_add_reason(Flag :: ldclient_flag:flag(), Reason :: reason(), WithReason :: boolean(), Map :: map()) -> map().
maybe_add_reason(_Flag, Reason, true = _WithReason, Map) ->
Map#{<<"reason">> => ldclient_eval_reason:format(Reason)};
maybe_add_reason(#{trackEventsFallthrough := true} = _Flag, fallthrough = Reason, _WithReason, Map) ->
Map#{
<<"trackReason">> => true,
<<"trackEvents">> => true,
<<"reason">> => ldclient_eval_reason:format(Reason)
};
maybe_add_reason(_Flag, {fallthrough, in_experiment} = Reason, _WithReason, Map) ->
Map#{
<<"trackReason">> => true,
<<"reason">> => ldclient_eval_reason:format(Reason)
};
maybe_add_reason(_Flag, {rule_match, _, in_experiment} = Reason, _WithReason, Map) ->
Map#{
<<"trackReason">> => true,
<<"reason">> => ldclient_eval_reason:format(Reason)
};
maybe_add_reason(#{rules := Rules} = _Flag, {rule_match, RuleIndex, _RuleId} = Reason, _WithReason, Map) ->
MatchedRule = lists:nth(RuleIndex + 1, Rules),
case MatchedRule of
#{trackEvents := true} ->
Map#{
<<"trackEvents">> => true,
<<"trackReason">> => true,
<<"reason">> => ldclient_eval_reason:format(Reason)
};
_ ->
Map
end;
maybe_add_reason(_Flag, _Reason, _WithReason, Map) ->
Map.
-spec maybe_add_track_events(Flag :: ldclient_flag:flag(), Map :: map()) -> map().
maybe_add_track_events(#{trackEvents := true} = _Flag, Map) ->
Map#{<<"trackEvents">> => true};
maybe_add_track_events(_Flag, Map) -> Map.
maybe_add_debug_events_until_date(#{debugEventsUntilDate := DebugEventsUntilDate} = _Flag, Map) when is_integer(DebugEventsUntilDate) ->
Map#{<<"debugEventsUntilDate">> => DebugEventsUntilDate};
maybe_add_debug_events_until_date(_Flag, Map) -> Map.
%% @doc Returns all flags for a given context
%%
%% @end
-spec all_flags_eval(
Context :: ldclient_context:context(),
Tag :: atom()
) -> feature_flags_state().
all_flags_eval(Context, Tag) ->
all_flags_eval(Context, Tag, get_state(Tag), get_initialization_state(Tag)).
-spec all_flags_eval(
Context :: ldclient_context:context(),
Tag :: atom(),
Offline :: atom(),
Initialized :: atom()
) -> feature_flags_state().
all_flags_eval(_Context, _Tag, offline, _) ->
#{flag_values => #{}};
all_flags_eval(_Context, _Tag, _, not_initialized) ->
#{flag_values => #{}};
all_flags_eval(Context, Tag, Offline, store_initialized) ->
error_logger:warning_msg("Called allFlagsState before client initialization; using last known values from data store."),
all_flags_eval(Context, Tag, Offline, initialized);
all_flags_eval(Context, Tag, online, initialized) ->
FeatureStore = ldclient_config:get_value(Tag, feature_store),
AllFlags = [FlagKey || {FlagKey, Flag} <- FeatureStore:all(Tag, features), is_not_deleted(Flag)],
EvalFun = fun(FlagKey, Acc) ->
% Here the state is either initialized, or store_initialized, and we are online. Call directly to that version
% of flag_key_for_context. This will prevent additional warnings for the client initialization not being
% complete in the store_initialized state.
{{_, V, _}, _Events} = flag_key_for_context(Tag, FlagKey, Context, null, online, initialized),
Acc#{FlagKey => V}
end,
#{flag_values => lists:foldl(EvalFun, #{}, AllFlags)}.
%%===================================================================
%% Internal functions
%%===================================================================
-spec get_state(Tag :: atom()) -> atom().
get_state(Tag) -> get_state(Tag, ldclient:is_offline(Tag)).
-spec get_state(Tag :: atom(), Offline :: boolean()) -> atom().
get_state(_Tag, true) -> offline;
get_state(_Tag, false) -> online.
%% An SDK is initialized when it has received a flag payload from launchdarkly. An SDK using a persistent store may
%% not be initialized, but it may have an initialized persistent store. In the case the store is initialized,
%% then flags should be evaluated from the store.
-spec get_initialization_state(Tag :: atom()) -> initialized | not_initialized | store_initialized.
get_initialization_state(Tag) -> get_initialization_state(Tag, ldclient:initialized(Tag)).
get_initialization_state(_Tag, true) -> initialized;
get_initialization_state(Tag, false) ->
% If not initialized, and redis is in use, then ask the redis store if it is initialized.
% After the redis store has been established to be initialized it will set the storage state.
% This logic is currently only applied to redis, because other stores do not currently
% have a meaningful initialized state.
case ldclient_config:get_value(Tag, feature_store) of
ldclient_storage_redis ->
% First check the state, checking redis is slower, so should only be done if the
% state has not been set.
case ldclient_instance:feature_store_initialized(Tag) of
true -> store_initialized;
false -> case ldclient_storage_redis:get_init(Tag) of
true -> store_initialized;
false -> not_initialized
end
end;
% For non-redis stores treat them as not initialized.
_ -> not_initialized
end.
-spec flag_recs_for_context(
FlagKey :: ldclient_flag:key(),
FlagRecs :: [{ldclient_flag:key(), ldclient_flag:flag()}],
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
DefaultValue :: result_value()
) -> result().
flag_recs_for_context(FlagKey, [], Context, _FeatureStore, _Tag, DefaultValue) ->
% Flag not found
error_logger:warning_msg("Unknown feature flag ~p; returning default value", [FlagKey]),
Reason = {error, flag_not_found},
Events = [ldclient_event:new_for_unknown_flag(FlagKey, Context, DefaultValue, Reason)],
{{null, DefaultValue, Reason}, Events};
flag_recs_for_context(FlagKey, [{FlagKey, #{deleted := true}} | _], Context, _FeatureStore, _Tag, DefaultValue) ->
% Flag found, but it's deleted
error_logger:warning_msg("Unknown feature flag ~p; returning default value", [FlagKey]),
Reason = {error, flag_not_found},
Events = [ldclient_event:new_for_unknown_flag(FlagKey, Context, DefaultValue, Reason)],
{{null, DefaultValue, Reason}, Events};
flag_recs_for_context(FlagKey, [{FlagKey, Flag} | _], Context, FeatureStore, Tag, DefaultValue) ->
% Flag found
flag_for_context_check_valid(Flag, Context, FeatureStore, Tag, DefaultValue).
-spec flag_for_context_check_valid(ldclient_flag:flag(), ldclient_context:context(), atom(), atom(), result_value()) -> result().
flag_for_context_check_valid(Flag, Context, FeatureStore, Tag, DefaultValue) ->
case ldclient_context:is_valid(Context, true) of
true -> flag_for_context(Flag, Context, FeatureStore, Tag, DefaultValue);
false -> flag_for_invalid_context(Flag, Context, DefaultValue)
end.
-spec flag_for_invalid_context(ldclient_flag:flag(), ldclient_context:context(), result_value()) -> result().
flag_for_invalid_context(_Flag, _Context, DefaultValue) ->
Reason = {error, user_not_specified},
{{null, DefaultValue, Reason}, []}.
-spec flag_for_context(ldclient_flag:flag(), ldclient_context:context(), atom(), atom(), result_value()) -> result().
flag_for_context(Flag, Context, FeatureStore, Tag, DefaultValue) ->
{{Variation, VariationValue, Reason}, Events} = flag_for_context_valid(Flag, Context, FeatureStore, Tag, DefaultValue),
FlagEvalEvent = ldclient_event:new_flag_eval(Variation, VariationValue, DefaultValue, Context, Reason, Flag),
{{Variation, VariationValue, Reason}, [FlagEvalEvent | Events]}.
-spec flag_for_context_valid(ldclient_flag:flag(), ldclient_context:context(), atom(), atom(), result_value()) -> result().
flag_for_context_valid(#{on := false, offVariation := OffVariation} = Flag, _Context, _FeatureStore, _Tag, DefaultValue)
when is_integer(OffVariation), OffVariation >= 0 ->
result_for_variation_index(OffVariation, off, Flag, [], DefaultValue);
flag_for_context_valid(#{on := false, offVariation := OffVariation} = _Flag, _Context, _FeatureStore, _Tag, DefaultValue)
% offVariation is negative. The flag is malformed.
when is_integer(OffVariation), OffVariation =< 0 ->
Reason = {error, malformed_flag},
{{null, DefaultValue, Reason}, []};
flag_for_context_valid(#{on := false} = _Flag, _Context, _FeatureStore, _Tag, DefaultValue) ->
% offVariation is null or not set
{{null, DefaultValue, off}, []};
flag_for_context_valid(#{prerequisites := Prerequisites} = Flag, Context, FeatureStore, Tag, DefaultValue) ->
check_prerequisites(Prerequisites, Flag, Context, FeatureStore, Tag, DefaultValue).
-spec check_prerequisites([ldclient_flag:prerequisite()], ldclient_flag:flag(), ldclient_context:context(), atom(), atom(), result_value()) ->
result().
check_prerequisites(Prerequisites, #{key := FlagKey} = Flag, Context, FeatureStore, Tag, DefaultValue) ->
check_prerequisites(Prerequisites, Flag, Context, FeatureStore, Tag, DefaultValue, [], [FlagKey]).
-spec check_prerequisites(
Prerequisites :: [ldclient_flag:prerequisite()],
Flag :: ldclient_flag:flag(),
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
DefaultValue :: result_value(),
Events :: [ldclient_event:event()],
VisitedFlags :: [binary()]
) -> result().
check_prerequisites(
[],
Flag,
Context,
FeatureStore,
Tag,
DefaultValue,
Events,
_VisitedFlags
) ->
flag_for_context_prerequisites(success, Flag, Context, FeatureStore, Tag, DefaultValue, Events);
check_prerequisites(
[#{key := PrerequisiteKey, variation := Variation} | Rest],
#{key := FlagKey} = Flag,
Context,
FeatureStore,
Tag,
DefaultValue,
Events,
VisitedFlags
) ->
case lists:member(PrerequisiteKey, VisitedFlags) of
true ->
error_logger:error_msg("Prerequisite of ~p causing a circular reference."
" This is probably a temporary condition due to an incomplete update.", [FlagKey]),
flag_for_context_prerequisites({malformed_flag, {error, malformed_flag}},
Flag, Context, FeatureStore, Tag, DefaultValue, Events);
false ->
PrerequisiteFlagRecs = FeatureStore:get(Tag, features, PrerequisiteKey),
check_prerequisite_recs(PrerequisiteFlagRecs, PrerequisiteKey, Variation,
Rest, Flag, Context, FeatureStore, Tag, DefaultValue, Events, [PrerequisiteKey | VisitedFlags])
end.
-spec check_prerequisite_recs(
PrerequisiteFlagRecs :: [ldclient_flag:flag()], %% Contains 0 or 1 flags.
PrerequisiteKey :: binary(),
Variation :: non_neg_integer(),
Prerequisites :: [binary()],
Flag :: ldclient_flag:flag(),
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
DefaultValue :: result_value(),
Events :: [ldclient_event:event()],
VisitedFlags :: [binary()]
) -> result().
check_prerequisite_recs([], PrerequisiteKey, _Variation, _Prerequisites, #{key := FlagKey} = Flag,
Context, FeatureStore, Tag, DefaultValue, Events, _VisitedFlags) ->
% Short circuit if prerequisite flag is not found
error_logger:error_msg("Could not retrieve prerequisite flag ~p when evaluating ~p", [PrerequisiteKey, FlagKey]),
flag_for_context_prerequisites({fail, {prerequisite_failed, [PrerequisiteKey]}}, Flag, Context,
FeatureStore, Tag, DefaultValue, Events);
check_prerequisite_recs([{PrerequisiteKey, PrerequisiteFlag} | _], PrerequisiteKey, Variation, Prerequisites, Flag,
Context, FeatureStore, Tag, DefaultValue, Events, VisitedFlags) ->
check_prerequisite_flag(PrerequisiteFlag, Variation, Prerequisites, Flag, Context,
FeatureStore, Tag, DefaultValue, Events, VisitedFlags).
-spec check_prerequisite_flag(
PrerequisiteFlag :: ldclient_flag:flag(),
Variation :: non_neg_integer(),
Prerequisites :: [binary()],
Flag :: ldclient_flag:flag(),
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
DefaultValue :: result_value(),
Events :: [ldclient_event:event()],
VisitedFlags :: [binary()]
) -> result().
check_prerequisite_flag(#{key := PrerequisiteKey, deleted := true}, _, _, Flag, Context, FeatureStore,
Tag, DefaultValue, Events, _VisitedFlags) ->
% Prerequisite flag is deleted, short circuit fail
Result = {fail, {prerequisite_failed, [PrerequisiteKey]}},
flag_for_context_prerequisites(Result, Flag, Context, FeatureStore, Tag, DefaultValue, Events);
check_prerequisite_flag(#{key := PrerequisiteKey, on := false}, _, _, Flag, Context, FeatureStore, Tag,
DefaultValue, Events, _VisitedFlags) ->
% Prerequisite flag is off, short circuit fail
Result = {fail, {prerequisite_failed, [PrerequisiteKey]}},
flag_for_context_prerequisites(Result, Flag, Context, FeatureStore, Tag, DefaultValue, Events);
check_prerequisite_flag(#{prerequisites := SubPrerequisites} = PrerequisiteFlag, Variation, Prerequisites,
#{key := FlagKey} = Flag, Context, FeatureStore, Tag, DefaultValue, Events, VisitedFlags) ->
{{ResultVariation, ResultVariationValue, ResultReason}, ResultEvents} =
check_prerequisites(SubPrerequisites, PrerequisiteFlag, Context, FeatureStore,
Tag, DefaultValue, Events, VisitedFlags),
NewEvents = [
ldclient_event:new_prerequisite_eval(ResultVariation, ResultVariationValue, FlagKey,
Context, ResultReason, PrerequisiteFlag) | ResultEvents
],
case ResultReason of
%% If there was a malformed dependent flag, when we want to short circuit, not treat it as a prerequisite
%% failure.
{error, malformed_flag} -> {{ResultVariation, ResultVariationValue, ResultReason}, ResultEvents};
_ -> check_prerequisite_flag_result(PrerequisiteFlag, Variation =:= ResultVariation, Prerequisites,
Flag, Context, FeatureStore, Tag, DefaultValue, NewEvents, VisitedFlags)
end.
-spec check_prerequisite_flag_result(
PrerequisiteFlag :: ldclient_flag:flag(),
MatchedVariation :: boolean(),
Prerequisites :: [binary()],
Flag :: ldclient_flag:flag(),
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
DefaultValue :: result_value(),
Events :: [ldclient_event:event()],
VisitedFlags :: [binary()]
) -> result().
check_prerequisite_flag_result(#{key := PrerequisiteKey}, false, _Prerequisites,
Flag, Context, FeatureStore, Tag, DefaultValue, Events, _VisitedFlags) ->
% Prerequisite flag variation didn't match: short-circuit fail and move on
Result = {fail, {prerequisite_failed, [PrerequisiteKey]}},
flag_for_context_prerequisites(Result, Flag, Context, FeatureStore, Tag, DefaultValue, Events);
check_prerequisite_flag_result(_PrerequisiteFlag, true, Prerequisites, Flag,
Context, FeatureStore, Tag, DefaultValue, Events, VisitedFlags) ->
%% This moves to the next flag is the list of prerequisites, so we want to remove the head from the VisitedFlags
check_prerequisites(Prerequisites, Flag, Context, FeatureStore, Tag, DefaultValue, Events, tl(VisitedFlags)).
-spec flag_for_context_prerequisites(
Reason :: {fail | malformed_flag, Reason :: reason()} | success,
Flag :: ldclient_flag:flag(),
Context :: ldclient_context:context(),
FeatureStore :: atom(),
Tag :: atom(),
DefaultValue :: result_value(),
Events :: [ldclient_event:event()]
) -> result().
flag_for_context_prerequisites({malformed_flag, Reason}, _Flag, _Context, _FeatureStore, _Tag, DefaultValue, Events) ->
{{null, DefaultValue, Reason}, Events};
flag_for_context_prerequisites({fail, Reason}, #{offVariation := OffVariation} = Flag, _Context, _FeatureStore, _Tag, DefaultValue, Events)
when is_integer(OffVariation), OffVariation >= 0 ->
result_for_variation_index(OffVariation, Reason, Flag, Events, DefaultValue);
flag_for_context_prerequisites({fail, Reason}, _Flag, _Context, _FeatureStore, _Tag, DefaultValue, Events) ->
% prerequisite failed, but offVariation is null or not set
{{null, DefaultValue, Reason}, Events};
flag_for_context_prerequisites(success, Flag, Context, FeatureStore, Tag, DefaultValue, Events) ->
check_targets(Flag, Context, FeatureStore, Tag, DefaultValue, Events).
check_targets(Flag, Context, FeatureStore, Tag, DefaultValue, Events) ->
Result = ldclient_targets:eval_flag_targets(Flag, Context),
flag_for_context_targets(Result, Flag, Context, FeatureStore, Tag, DefaultValue, Events).
flag_for_context_targets({match, Variation}, Flag, _Context, _FeatureStore, _Tag, DefaultValue, Events) ->
Reason = target_match,
result_for_variation_index(Variation, Reason, Flag, Events, DefaultValue);
flag_for_context_targets(no_match, #{rules := Rules} = Flag, Context, FeatureStore, Tag, DefaultValue, Events) ->
check_rules(Rules, Flag, Context, FeatureStore, Tag, DefaultValue, Events, 0).
check_rules([], Flag, Context, _FeatureStore, _Tag, DefaultValue, Events, _) ->
flag_for_context_rules(no_match, Flag, Context, DefaultValue, Events);
check_rules([Rule | Rest], Flag, Context, FeatureStore, Tag, DefaultValue, Events, Index) ->
Result = ldclient_rule:match_context(Rule, Context, FeatureStore, Tag),
check_rule_result({Result, Rule, Index}, Rest, Flag, Context, FeatureStore, Tag, DefaultValue, Events).
check_rule_result({malformed_flag, _Rule, _Index}, _Rest, #{key := FlagKey} = _Flag, _Context, _FeatureStore, _Tag, DefaultValue, _Events) ->
error_logger:warning_msg("Data inconsistency in feature flag ~p: clause was malformed", [FlagKey]),
Reason = {error, malformed_flag},
{{null, DefaultValue, Reason}, []};
check_rule_result({no_match, _Rule, Index}, Rest, Flag, Context, FeatureStore, Tag, DefaultValue, Events) ->
check_rules(Rest, Flag, Context, FeatureStore, Tag, DefaultValue, Events, Index + 1);
check_rule_result({match, Rule, Index}, _Rest, Flag, Context, _FeatureStore, _Tag, DefaultValue, Events) ->
% Rule matched: short-circuit
flag_for_context_rules({match, Rule, Index}, Flag, Context, DefaultValue, Events).
flag_for_context_rules({match, #{id := Id, variationOrRollout := VorR}, Index}, Flag, Context, DefaultValue, Events) ->
Reason = {rule_match, Index, Id},
flag_for_context_variation_or_rollout(VorR, Reason, Flag, Context, DefaultValue, Events);
flag_for_context_rules(no_match, #{fallthrough := Fallthrough} = Flag, Context, DefaultValue, Events) ->
flag_for_context_variation_or_rollout(Fallthrough, fallthrough, Flag, Context, DefaultValue, Events).
flag_for_context_variation_or_rollout(Variation, Reason, Flag, _Context, DefaultValue, Events) when is_integer(Variation) ->
result_for_variation_index(Variation, Reason, Flag, Events, DefaultValue);
flag_for_context_variation_or_rollout(Rollout, Reason, #{key := FlagKey} = Flag, Context, DefaultValue, Events) when is_map(Rollout) ->
case ldclient_rollout:rollout_context(Rollout, Flag, Context) of
malformed_flag ->
error_logger:warning_msg("Data inconsistency in feature flag ~p: rollout was malformed", [FlagKey]),
{{null, DefaultValue, {error, malformed_flag}}, []};
{Result, InExperiment} ->
UpdatedReason = experimentize_reason(InExperiment, Reason),
flag_for_context_rollout_result(Result, UpdatedReason, Flag, DefaultValue, Events)
end;
flag_for_context_variation_or_rollout(null, _Reason, #{key := FlagKey}, _Context, DefaultValue, Events) ->
error_logger:warning_msg("Data inconsistency in feature flag ~p: rule object with no variation or rollout", [FlagKey]),
Reason = {error, malformed_flag},
{{null, DefaultValue, Reason}, Events}.
flag_for_context_rollout_result(null, _Reason, #{key := FlagKey}, DefaultValue, Events) ->
error_logger:warning_msg("Data inconsistency in feature flag ~p: variation/rollout object with no variation or rollout", [FlagKey]),
Reason = {error, malformed_flag},
{{null, DefaultValue, Reason}, Events};
flag_for_context_rollout_result(Variation, Reason, Flag, DefaultValue, Events) ->
result_for_variation_index(Variation, Reason, Flag, Events, DefaultValue).
result_for_variation_index(Variation, Reason, Flag, Events, DefaultValue) ->
VariationValue = ldclient_flag:get_variation(Flag, Variation),
result_for_variation_value(VariationValue, Variation, Reason, Flag, Events, DefaultValue).
result_for_variation_value(null, _Variation, _Reason, _Flag, Events, DefaultValue) ->
Reason = {error, malformed_flag},
{{null, DefaultValue, Reason}, Events};
result_for_variation_value(VariationValue, Variation, Reason, _Flag, Events, _DefaultValue) ->
{{Variation, VariationValue, Reason}, Events}.
-spec experimentize_reason(InExperiment :: boolean(), Reason :: reason()) -> reason().
experimentize_reason(true, fallthrough) ->
{fallthrough, in_experiment};
experimentize_reason(true, {rule_match, RuleIndex, RuleUUID}) ->
{rule_match, RuleIndex, RuleUUID, in_experiment};
experimentize_reason(_InExperiment, Reason) ->
Reason.