Packages
launchdarkly_server_sdk
2.1.2
3.11.0
3.10.1
3.9.0
3.8.1
3.8.0
3.7.2
3.7.1
3.7.0
3.6.0
3.5.0
3.4.0
3.3.1
3.3.0
3.2.0
3.1.0
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.1.2
2.1.1
2.1.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.6.0
1.5.0
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.3
1.1.2
1.1.1
retired
1.1.0
retired
1.0.1
retired
1.0.0
retired
1.0.0-beta4
retired
1.0.0-beta3
retired
1.0.0-beta2
retired
LaunchDarkly SDK for Erlang
Current section
Files
Jump to
Current section
Files
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.