hackle-ruby-sdk 0.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/hackle/client.rb +191 -79
- data/lib/hackle/config.rb +59 -17
- data/lib/hackle/decision.rb +113 -0
- data/lib/hackle/event.rb +89 -0
- data/lib/hackle/internal/clock/clock.rb +47 -0
- data/lib/hackle/internal/concurrent/executors.rb +20 -0
- data/lib/hackle/internal/concurrent/schedule/scheduler.rb +12 -0
- data/lib/hackle/internal/concurrent/schedule/timer_scheduler.rb +30 -0
- data/lib/hackle/internal/config/parameter_config.rb +50 -0
- data/lib/hackle/internal/core/hackle_core.rb +182 -0
- data/lib/hackle/internal/evaluation/bucketer/bucketer.rb +46 -0
- data/lib/hackle/internal/evaluation/evaluator/contextual/contextual_evaluator.rb +29 -0
- data/lib/hackle/internal/evaluation/evaluator/delegating/delegating_evaluator.rb +26 -0
- data/lib/hackle/internal/evaluation/evaluator/evaluator.rb +117 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluation_flow_factory.rb +67 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_evaluator.rb +172 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_flow_evaluator.rb +241 -0
- data/lib/hackle/internal/evaluation/evaluator/experiment/experiment_resolver.rb +166 -0
- data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_determiner.rb +48 -0
- data/lib/hackle/internal/evaluation/evaluator/remoteconfig/remote_config_evaluator.rb +174 -0
- data/lib/hackle/internal/evaluation/flow/evaluation_flow.rb +49 -0
- data/lib/hackle/internal/evaluation/flow/flow_evaluator.rb +11 -0
- data/lib/hackle/internal/evaluation/match/condition/condition_matcher.rb +11 -0
- data/lib/hackle/internal/evaluation/match/condition/condition_matcher_factory.rb +53 -0
- data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_condition_matcher.rb +29 -0
- data/lib/hackle/internal/evaluation/match/condition/experiment/experiment_evaluator_matcher.rb +135 -0
- data/lib/hackle/internal/evaluation/match/condition/segment/segment_condition_matcher.rb +67 -0
- data/lib/hackle/internal/evaluation/match/condition/user/user_condition_matcher.rb +44 -0
- data/lib/hackle/internal/evaluation/match/operator/operator_matcher.rb +185 -0
- data/lib/hackle/internal/evaluation/match/operator/operator_matcher_factory.rb +31 -0
- data/lib/hackle/internal/evaluation/match/target/target_matcher.rb +31 -0
- data/lib/hackle/internal/evaluation/match/value/value_matcher.rb +96 -0
- data/lib/hackle/internal/evaluation/match/value/value_matcher_factory.rb +28 -0
- data/lib/hackle/internal/evaluation/match/value/value_operator_matcher.rb +59 -0
- data/lib/hackle/internal/event/user_event.rb +187 -0
- data/lib/hackle/internal/event/user_event_dispatcher.rb +156 -0
- data/lib/hackle/internal/event/user_event_factory.rb +58 -0
- data/lib/hackle/internal/event/user_event_processor.rb +181 -0
- data/lib/hackle/internal/http/http.rb +28 -0
- data/lib/hackle/internal/http/http_client.rb +48 -0
- data/lib/hackle/internal/identifiers/identifier_builder.rb +67 -0
- data/lib/hackle/internal/logger/logger.rb +31 -0
- data/lib/hackle/internal/model/action.rb +57 -0
- data/lib/hackle/internal/model/bucket.rb +58 -0
- data/lib/hackle/internal/model/container.rb +47 -0
- data/lib/hackle/internal/model/decision_reason.rb +31 -0
- data/lib/hackle/internal/model/event_type.rb +19 -0
- data/lib/hackle/internal/model/experiment.rb +194 -0
- data/lib/hackle/internal/model/parameter_configuration.rb +19 -0
- data/lib/hackle/internal/model/remote_config_parameter.rb +76 -0
- data/lib/hackle/internal/model/sdk.rb +23 -0
- data/lib/hackle/internal/model/segment.rb +61 -0
- data/lib/hackle/internal/model/target.rb +203 -0
- data/lib/hackle/internal/model/target_rule.rb +19 -0
- data/lib/hackle/internal/model/targeting.rb +45 -0
- data/lib/hackle/internal/model/value_type.rb +75 -0
- data/lib/hackle/internal/model/variation.rb +27 -0
- data/lib/hackle/internal/model/version.rb +153 -0
- data/lib/hackle/internal/properties/properties_builder.rb +101 -0
- data/lib/hackle/internal/user/hackle_user.rb +74 -0
- data/lib/hackle/internal/user/hackle_user_resolver.rb +27 -0
- data/lib/hackle/internal/workspace/http_workspace_fetcher.rb +50 -0
- data/lib/hackle/internal/workspace/polling_workspace_fetcher.rb +62 -0
- data/lib/hackle/internal/workspace/workspace.rb +353 -0
- data/lib/hackle/internal/workspace/workspace_fetcher.rb +18 -0
- data/lib/hackle/remote_config.rb +55 -0
- data/lib/hackle/user.rb +124 -0
- data/lib/hackle/version.rb +1 -11
- data/lib/hackle.rb +4 -32
- metadata +123 -51
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.travis.yml +0 -7
- data/Gemfile +0 -6
- data/README.md +0 -33
- data/Rakefile +0 -6
- data/hackle-ruby-sdk.gemspec +0 -29
- data/lib/hackle/decision/bucketer.rb +0 -30
- data/lib/hackle/decision/decider.rb +0 -54
- data/lib/hackle/events/event.rb +0 -33
- data/lib/hackle/events/event_dispatcher.rb +0 -89
- data/lib/hackle/events/event_processor.rb +0 -115
- data/lib/hackle/http/http.rb +0 -37
- data/lib/hackle/models/bucket.rb +0 -15
- data/lib/hackle/models/event_type.rb +0 -14
- data/lib/hackle/models/experiment.rb +0 -36
- data/lib/hackle/models/slot.rb +0 -15
- data/lib/hackle/models/variation.rb +0 -11
- data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
- data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -44
- data/lib/hackle/workspaces/workspace.rb +0 -87
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/model/decision_reason'
|
4
|
+
require 'hackle/internal/evaluation/evaluator/evaluator'
|
5
|
+
require 'hackle/internal/evaluation/evaluator/contextual/contextual_evaluator'
|
6
|
+
|
7
|
+
module Hackle
|
8
|
+
class ExperimentEvaluator
|
9
|
+
include ContextualEvaluator
|
10
|
+
|
11
|
+
# @param flow_factory [ExperimentEvaluationFlowFactory]
|
12
|
+
def initialize(flow_factory:)
|
13
|
+
# @type [ExperimentEvaluationFlowFactory]
|
14
|
+
@flow_factory = flow_factory
|
15
|
+
end
|
16
|
+
|
17
|
+
def supports?(request)
|
18
|
+
request.is_a?(ExperimentRequest)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param request [ExperimentRequest]
|
22
|
+
# @param context [EvaluatorContext]
|
23
|
+
# @return [ExperimentEvaluation]
|
24
|
+
def evaluate_internal(request, context)
|
25
|
+
evaluation_flow = @flow_factory.get(request.experiment.type)
|
26
|
+
evaluation = evaluation_flow.evaluate(request, context)
|
27
|
+
return evaluation unless evaluation.nil?
|
28
|
+
|
29
|
+
ExperimentEvaluation.create_default(request, context, DecisionReason::TRAFFIC_NOT_ALLOCATED)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class ExperimentRequest < EvaluatorRequest
|
34
|
+
# @return [Experiment]
|
35
|
+
attr_reader :experiment
|
36
|
+
|
37
|
+
# @return [String]
|
38
|
+
attr_reader :default_variation_key
|
39
|
+
|
40
|
+
# @param key [EvaluatorKey]
|
41
|
+
# @param workspace [Workspace]
|
42
|
+
# @param user [HackleUser]
|
43
|
+
# @param experiment [Experiment]
|
44
|
+
# @param default_variation_key [String]
|
45
|
+
def initialize(key:, workspace:, user:, experiment:, default_variation_key:)
|
46
|
+
super(key: key, workspace: workspace, user: user)
|
47
|
+
@experiment = experiment
|
48
|
+
@default_variation_key = default_variation_key
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param workspace [Workspace]
|
52
|
+
# @param user [HackleUser]
|
53
|
+
# @param experiment [Experiment]
|
54
|
+
# @param default_variation_key [String]
|
55
|
+
# @return [Hackle::ExperimentRequest]
|
56
|
+
def self.create(workspace, user, experiment, default_variation_key)
|
57
|
+
ExperimentRequest.new(
|
58
|
+
key: EvaluatorKey.new(type: 'EXPERIMENT', id: experiment.id),
|
59
|
+
workspace: workspace,
|
60
|
+
user: user,
|
61
|
+
experiment: experiment,
|
62
|
+
default_variation_key: default_variation_key
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @param request [EvaluatorRequest]
|
67
|
+
# @param experiment [Experiment]
|
68
|
+
# @return [Hackle::ExperimentRequest]
|
69
|
+
def self.create_by(request, experiment)
|
70
|
+
ExperimentRequest.new(
|
71
|
+
key: EvaluatorKey.new(type: 'EXPERIMENT', id: experiment.id),
|
72
|
+
workspace: request.workspace,
|
73
|
+
user: request.user,
|
74
|
+
experiment: experiment,
|
75
|
+
default_variation_key: 'A'
|
76
|
+
)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class ExperimentEvaluation < EvaluatorEvaluation
|
81
|
+
# @return [Experiment]
|
82
|
+
attr_reader :experiment
|
83
|
+
|
84
|
+
# @return [Integer, nil]
|
85
|
+
attr_reader :variation_id
|
86
|
+
|
87
|
+
# @return [String]
|
88
|
+
attr_reader :variation_key
|
89
|
+
|
90
|
+
# @return [ParameterConfiguration, nil]
|
91
|
+
attr_reader :config
|
92
|
+
|
93
|
+
# @param reason [String]
|
94
|
+
# @param target_evaluations [Array<EvaluatorEvaluation]
|
95
|
+
# @param experiment [Experiment]
|
96
|
+
# @param variation_id [Integer, nil]
|
97
|
+
# @param variation_key [String]
|
98
|
+
# @param config [ParameterConfiguration, nil]
|
99
|
+
def initialize(reason:, target_evaluations:, experiment:, variation_id:, variation_key:, config:)
|
100
|
+
super(reason: reason, target_evaluations: target_evaluations)
|
101
|
+
@experiment = experiment
|
102
|
+
@variation_id = variation_id
|
103
|
+
@variation_key = variation_key
|
104
|
+
@config = config
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [ParameterConfig]
|
108
|
+
def parameter_config
|
109
|
+
return ParameterConfig.empty if @config.nil?
|
110
|
+
|
111
|
+
ParameterConfig.new(@config.parameters)
|
112
|
+
end
|
113
|
+
|
114
|
+
# @param reason [String]
|
115
|
+
# @return [Hackle::ExperimentEvaluation]
|
116
|
+
def with(reason)
|
117
|
+
ExperimentEvaluation.new(
|
118
|
+
reason: reason,
|
119
|
+
target_evaluations: target_evaluations,
|
120
|
+
experiment: experiment,
|
121
|
+
variation_id: variation_id,
|
122
|
+
variation_key: variation_key,
|
123
|
+
config: config
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
# @param request [ExperimentRequest]
|
128
|
+
# @param context [EvaluatorContext]
|
129
|
+
# @param variation [Variation]
|
130
|
+
# @param reason [String]
|
131
|
+
# @return [Hackle::ExperimentEvaluation]
|
132
|
+
def self.create(request, context, variation, reason)
|
133
|
+
configuration = configuration_or_nil(request.workspace, variation)
|
134
|
+
ExperimentEvaluation.new(
|
135
|
+
reason: reason,
|
136
|
+
target_evaluations: context.evaluations,
|
137
|
+
experiment: request.experiment,
|
138
|
+
variation_id: variation.id,
|
139
|
+
variation_key: variation.key,
|
140
|
+
config: configuration
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
# @param request [ExperimentRequest]
|
145
|
+
# @param context [EvaluatorContext]
|
146
|
+
# @param reason [String]
|
147
|
+
# @return [Hackle::ExperimentEvaluation]
|
148
|
+
def self.create_default(request, context, reason)
|
149
|
+
variation = request.experiment.get_variation_or_nil_by_key(request.default_variation_key)
|
150
|
+
return create(request, context, variation, reason) unless variation.nil?
|
151
|
+
|
152
|
+
ExperimentEvaluation.new(
|
153
|
+
reason: reason,
|
154
|
+
target_evaluations: context.evaluations,
|
155
|
+
experiment: request.experiment,
|
156
|
+
variation_id: nil,
|
157
|
+
variation_key: request.default_variation_key,
|
158
|
+
config: nil
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
# @param workspace [Workspace]
|
163
|
+
# @param variation [Variation]
|
164
|
+
# @return [Hackle::ParameterConfiguration, nil]
|
165
|
+
def self.configuration_or_nil(workspace, variation)
|
166
|
+
parameter_configuration_id = variation.parameter_configuration_id
|
167
|
+
return nil if parameter_configuration_id.nil?
|
168
|
+
|
169
|
+
workspace.get_parameter_configuration_or_nil(parameter_configuration_id)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluator'
|
4
|
+
require 'hackle/internal/evaluation/flow/evaluation_flow'
|
5
|
+
require 'hackle/internal/evaluation/flow/flow_evaluator'
|
6
|
+
require 'hackle/internal/model/experiment'
|
7
|
+
require 'hackle/internal/model/decision_reason'
|
8
|
+
|
9
|
+
module Hackle
|
10
|
+
module ExperimentFlow
|
11
|
+
include EvaluationFlow
|
12
|
+
|
13
|
+
# @param request [ExperimentRequest]
|
14
|
+
# @param context [EvaluatorContext]
|
15
|
+
# @return [ExperimentEvaluation, nil]
|
16
|
+
def evaluate(request, context) end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ExperimentFlowEvaluator
|
20
|
+
include FlowEvaluator
|
21
|
+
|
22
|
+
# @param request [ExperimentRequest]
|
23
|
+
# @param context [EvaluatorContext]
|
24
|
+
# @param next_flow [ExperimentFlow]
|
25
|
+
# @return [ExperimentEvaluation, nil]
|
26
|
+
def evaluate(request, context, next_flow) end
|
27
|
+
end
|
28
|
+
|
29
|
+
class OverrideExperimentFlowEvaluator
|
30
|
+
include ExperimentFlowEvaluator
|
31
|
+
|
32
|
+
# @param override_resolver [ExperimentOverrideResolver]
|
33
|
+
def initialize(override_resolver:)
|
34
|
+
# @type [ExperimentOverrideResolver]
|
35
|
+
@override_resolver = override_resolver
|
36
|
+
end
|
37
|
+
|
38
|
+
def evaluate(request, context, next_flow)
|
39
|
+
overridden_variation = @override_resolver.resolve_or_nil(request, context)
|
40
|
+
return next_flow.evaluate(request, context) if overridden_variation.nil?
|
41
|
+
|
42
|
+
case request.experiment.type
|
43
|
+
when ExperimentType::AB_TEST
|
44
|
+
ExperimentEvaluation.create(request, context, overridden_variation, DecisionReason::OVERRIDDEN)
|
45
|
+
when ExperimentType::FEATURE_FLAG
|
46
|
+
ExperimentEvaluation.create(request, context, overridden_variation, DecisionReason::INDIVIDUAL_TARGET_MATCH)
|
47
|
+
else
|
48
|
+
raise ArgumentError, "unsupported experiment type [#{request.experiment.type}]"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class DraftExperimentFlowEvaluator
|
54
|
+
include ExperimentFlowEvaluator
|
55
|
+
|
56
|
+
def evaluate(request, context, next_flow)
|
57
|
+
return next_flow.evaluate(request, context) if request.experiment.status != ExperimentStatus::DRAFT
|
58
|
+
|
59
|
+
ExperimentEvaluation.create_default(request, context, DecisionReason::EXPERIMENT_DRAFT)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class PausedExperimentFlowEvaluator
|
64
|
+
include ExperimentFlowEvaluator
|
65
|
+
|
66
|
+
def evaluate(request, context, next_flow)
|
67
|
+
return next_flow.evaluate(request, context) if request.experiment.status != ExperimentStatus::PAUSED
|
68
|
+
|
69
|
+
case request.experiment.type
|
70
|
+
when ExperimentType::AB_TEST
|
71
|
+
ExperimentEvaluation.create_default(request, context, DecisionReason::EXPERIMENT_PAUSED)
|
72
|
+
when ExperimentType::FEATURE_FLAG
|
73
|
+
ExperimentEvaluation.create_default(request, context, DecisionReason::FEATURE_FLAG_INACTIVE)
|
74
|
+
else
|
75
|
+
raise ArgumentError, "unsupported experiment type [#{request.experiment.type}]"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class CompletedExperimentFlowEvaluator
|
81
|
+
include ExperimentFlowEvaluator
|
82
|
+
|
83
|
+
def evaluate(request, context, next_flow)
|
84
|
+
return next_flow.evaluate(request, context) if request.experiment.status != ExperimentStatus::COMPLETED
|
85
|
+
|
86
|
+
winner_variation = request.experiment.winner_variation_or_nil
|
87
|
+
raise ArgumentError, "winner variation [#{request.experiment.id}]" if winner_variation.nil?
|
88
|
+
|
89
|
+
ExperimentEvaluation.create(request, context, winner_variation, DecisionReason::EXPERIMENT_COMPLETED)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class TargetExperimentFlowEvaluator
|
94
|
+
include ExperimentFlowEvaluator
|
95
|
+
|
96
|
+
# @param target_determiner [ExperimentTargetDeterminer]
|
97
|
+
def initialize(target_determiner:)
|
98
|
+
# @type [ExperimentTargetDeterminer]
|
99
|
+
@target_determiner = target_determiner
|
100
|
+
end
|
101
|
+
|
102
|
+
def evaluate(request, context, next_flow)
|
103
|
+
if request.experiment.type != ExperimentType::AB_TEST
|
104
|
+
raise ArgumentError, "experiment type must be AB_TEST [#{request.experiment.id}]"
|
105
|
+
end
|
106
|
+
|
107
|
+
is_user_in_experiment_target = @target_determiner.user_in_experiment_target?(request, context)
|
108
|
+
if is_user_in_experiment_target
|
109
|
+
next_flow.evaluate(request, context)
|
110
|
+
else
|
111
|
+
ExperimentEvaluation.create_default(request, context, DecisionReason::NOT_IN_EXPERIMENT_TARGET)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class TrafficAllocateExperimentFlowEvaluator
|
117
|
+
include ExperimentFlowEvaluator
|
118
|
+
|
119
|
+
# @param action_resolver [ExperimentActionResolver]
|
120
|
+
def initialize(action_resolver:)
|
121
|
+
# @type [ExperimentActionResolver]
|
122
|
+
@action_resolver = action_resolver
|
123
|
+
end
|
124
|
+
|
125
|
+
def evaluate(request, context, _next_flow)
|
126
|
+
if request.experiment.status != ExperimentStatus::RUNNING
|
127
|
+
raise ArgumentError, "experiment status must be RUNNING [#{request.experiment.id}]"
|
128
|
+
end
|
129
|
+
if request.experiment.type != ExperimentType::AB_TEST
|
130
|
+
raise ArgumentError, "experiment type must be AB_TEST [#{request.experiment.id}]"
|
131
|
+
end
|
132
|
+
|
133
|
+
variation = @action_resolver.resolve_or_nil(request, request.experiment.default_rule)
|
134
|
+
if variation.nil?
|
135
|
+
return ExperimentEvaluation.create_default(request, context, DecisionReason::TRAFFIC_NOT_ALLOCATED)
|
136
|
+
end
|
137
|
+
if variation.is_dropped
|
138
|
+
return ExperimentEvaluation.create_default(request, context, DecisionReason::VARIATION_DROPPED)
|
139
|
+
end
|
140
|
+
|
141
|
+
ExperimentEvaluation.create(request, context, variation, DecisionReason::TRAFFIC_ALLOCATED)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class TargetRuleExperimentFlowEvaluator
|
146
|
+
include ExperimentFlowEvaluator
|
147
|
+
|
148
|
+
# @param target_rule_determiner [ExperimentTargetRuleDeterminer]
|
149
|
+
# @param action_resolver [ExperimentActionResolver]
|
150
|
+
def initialize(target_rule_determiner:, action_resolver:)
|
151
|
+
# @type [ExperimentTargetRuleDeterminer]
|
152
|
+
@target_rule_determiner = target_rule_determiner
|
153
|
+
# @type [ExperimentActionResolver]
|
154
|
+
@action_resolver = action_resolver
|
155
|
+
end
|
156
|
+
|
157
|
+
def evaluate(request, context, next_flow)
|
158
|
+
if request.experiment.status != ExperimentStatus::RUNNING
|
159
|
+
raise ArgumentError, "experiment status must be RUNNING [#{request.experiment.id}]"
|
160
|
+
end
|
161
|
+
if request.experiment.type != ExperimentType::FEATURE_FLAG
|
162
|
+
raise ArgumentError, "experiment type must be FEATURE_FLAG [#{request.experiment.id}]"
|
163
|
+
end
|
164
|
+
|
165
|
+
return next_flow.evaluate(request, context) if request.user.identifiers[request.experiment.identifier_type].nil?
|
166
|
+
|
167
|
+
target_rule = @target_rule_determiner.determine_target_rule_or_nil(request, context)
|
168
|
+
return next_flow.evaluate(request, context) if target_rule.nil?
|
169
|
+
|
170
|
+
variation = @action_resolver.resolve_or_nil(request, target_rule.action)
|
171
|
+
raise ArgumentError, "feature flag must decide the variation [#{request.experiment.id}]" if variation.nil?
|
172
|
+
|
173
|
+
ExperimentEvaluation.create(request, context, variation, DecisionReason::TARGET_RULE_MATCH)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class DefaultRuleExperimentFlowEvaluator
|
178
|
+
include ExperimentFlowEvaluator
|
179
|
+
|
180
|
+
# @param action_resolver [ExperimentActionResolver]
|
181
|
+
def initialize(action_resolver:)
|
182
|
+
# @type [ExperimentActionResolver]
|
183
|
+
@action_resolver = action_resolver
|
184
|
+
end
|
185
|
+
|
186
|
+
def evaluate(request, context, _next_flow)
|
187
|
+
if request.experiment.status != ExperimentStatus::RUNNING
|
188
|
+
raise ArgumentError, "experiment status must be RUNNING [#{request.experiment.id}]"
|
189
|
+
end
|
190
|
+
if request.experiment.type != ExperimentType::FEATURE_FLAG
|
191
|
+
raise ArgumentError, "experiment type must be FEATURE_FLAG [#{request.experiment.id}]"
|
192
|
+
end
|
193
|
+
|
194
|
+
if request.user.identifiers[request.experiment.identifier_type].nil?
|
195
|
+
return ExperimentEvaluation.create_default(request, context, DecisionReason::DEFAULT_RULE)
|
196
|
+
end
|
197
|
+
|
198
|
+
variation = @action_resolver.resolve_or_nil(request, request.experiment.default_rule)
|
199
|
+
raise ArgumentError, "feature flag must decide the variation [#{request.experiment.id}]" if variation.nil?
|
200
|
+
|
201
|
+
ExperimentEvaluation.create(request, context, variation, DecisionReason::DEFAULT_RULE)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
class ContainerExperimentFlowEvaluator
|
206
|
+
include ExperimentFlowEvaluator
|
207
|
+
|
208
|
+
# @param container_resolver [ExperimentContainerResolver]
|
209
|
+
def initialize(container_resolver:)
|
210
|
+
# @type [ExperimentContainerResolver]
|
211
|
+
@container_resolver = container_resolver
|
212
|
+
end
|
213
|
+
|
214
|
+
def evaluate(request, context, next_flow)
|
215
|
+
container_id = request.experiment.container_id
|
216
|
+
return next_flow.evaluate(request, context) if container_id.nil?
|
217
|
+
|
218
|
+
container = request.workspace.get_container_or_nil(container_id)
|
219
|
+
raise ArgumentError, "container [#{container_id}]" if container.nil?
|
220
|
+
|
221
|
+
is_user_in_container_group = @container_resolver.user_in_container_group?(request, container)
|
222
|
+
if is_user_in_container_group
|
223
|
+
next_flow.evaluate(request, context)
|
224
|
+
else
|
225
|
+
ExperimentEvaluation.create_default(request, context, DecisionReason::NOT_IN_MUTUAL_EXCLUSION_EXPERIMENT)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
class IdentifierExperimentFlowEvaluator
|
231
|
+
include ExperimentFlowEvaluator
|
232
|
+
|
233
|
+
def evaluate(request, context, next_flow)
|
234
|
+
if request.user.identifiers[request.experiment.identifier_type].nil?
|
235
|
+
ExperimentEvaluation.create_default(request, context, DecisionReason::IDENTIFIER_NOT_FOUND)
|
236
|
+
else
|
237
|
+
next_flow.evaluate(request, context)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/model/action'
|
4
|
+
require 'hackle/internal/evaluation/bucketer/bucketer'
|
5
|
+
|
6
|
+
module Hackle
|
7
|
+
class ExperimentActionResolver
|
8
|
+
# @param bucketer [Bucketer]
|
9
|
+
def initialize(bucketer:)
|
10
|
+
# @type [Bucketer]
|
11
|
+
@bucketer = bucketer
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param request [ExperimentRequest]
|
15
|
+
# @param action [Action]
|
16
|
+
# @return [Hackle::Variation, nil]
|
17
|
+
def resolve_or_nil(request, action)
|
18
|
+
case action.type
|
19
|
+
when ActionType::VARIATION
|
20
|
+
resolve_variation(request, action)
|
21
|
+
when ActionType::BUCKET
|
22
|
+
resolve_bucket(request, action)
|
23
|
+
else
|
24
|
+
raise ArgumentError, "unsupported ActionType [#{action.type}]"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# @param request [ExperimentRequest]
|
31
|
+
# @param action [Action]
|
32
|
+
# @return [Hackle::Variation]
|
33
|
+
def resolve_variation(request, action)
|
34
|
+
variation_id = action.variation_id
|
35
|
+
raise ArgumentError, "action variation [#{request.experiment.id}]" if variation_id.nil?
|
36
|
+
|
37
|
+
variation = request.experiment.get_variation_or_nil_by_id(variation_id)
|
38
|
+
raise ArgumentError, "variation [#{variation_id}]" if variation.nil?
|
39
|
+
|
40
|
+
variation
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param request [ExperimentRequest]
|
44
|
+
# @param action [Action]
|
45
|
+
# @return [Hackle::Variation, nil]
|
46
|
+
def resolve_bucket(request, action)
|
47
|
+
bucket_id = action.bucket_id
|
48
|
+
raise ArgumentError, "action bucket [#{request.experiment.id}]" if bucket_id.nil?
|
49
|
+
|
50
|
+
bucket = request.workspace.get_bucket_or_nil(bucket_id)
|
51
|
+
raise ArgumentError, "bucket [#{bucket_id}]" if bucket.nil?
|
52
|
+
|
53
|
+
identifier = request.user.identifiers[request.experiment.identifier_type]
|
54
|
+
return nil if identifier.nil?
|
55
|
+
|
56
|
+
slot = @bucketer.bucketing(bucket, identifier)
|
57
|
+
return nil if slot.nil?
|
58
|
+
|
59
|
+
request.experiment.get_variation_or_nil_by_id(slot.variation_id)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class ExperimentOverrideResolver
|
64
|
+
# @param target_matcher [TargetMatcher]
|
65
|
+
# @param action_resolver [ExperimentActionResolver]
|
66
|
+
|
67
|
+
def initialize(target_matcher:, action_resolver:)
|
68
|
+
# @type [TargetMatcher]
|
69
|
+
@target_matcher = target_matcher
|
70
|
+
# @type [ExperimentActionResolver]
|
71
|
+
@action_resolver = action_resolver
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param request [ExperimentRequest]
|
75
|
+
# @param context [EvaluatorContext]
|
76
|
+
# @return [Hackle::Variation, nil]
|
77
|
+
def resolve_or_nil(request, context)
|
78
|
+
resolve_user_override(request) || resolve_segment_override(request, context)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# @param request [ExperimentRequest]
|
84
|
+
# @return [Hackle::Variation, nil]
|
85
|
+
def resolve_user_override(request)
|
86
|
+
identifier = request.user.identifiers[request.experiment.identifier_type]
|
87
|
+
return nil if identifier.nil?
|
88
|
+
|
89
|
+
overridden_variation_id = request.experiment.user_overrides[identifier]
|
90
|
+
return nil if overridden_variation_id.nil?
|
91
|
+
|
92
|
+
request.experiment.get_variation_or_nil_by_id(overridden_variation_id)
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param request [ExperimentRequest]
|
96
|
+
# @param context [EvaluatorContext]
|
97
|
+
# @return [Hackle::Variation, nil]
|
98
|
+
def resolve_segment_override(request, context)
|
99
|
+
overridden_rule = request.experiment.segment_overrides.find do |it|
|
100
|
+
@target_matcher.matches(request, context, it.target)
|
101
|
+
end
|
102
|
+
return nil if overridden_rule.nil?
|
103
|
+
|
104
|
+
@action_resolver.resolve_or_nil(request, overridden_rule.action)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class ExperimentContainerResolver
|
109
|
+
# @param bucketer [Bucketer]
|
110
|
+
def initialize(bucketer:)
|
111
|
+
# @type [Bucketer]
|
112
|
+
@bucketer = bucketer
|
113
|
+
end
|
114
|
+
|
115
|
+
# @param request [ExperimentRequest]
|
116
|
+
# @param container [Container]
|
117
|
+
def user_in_container_group?(request, container)
|
118
|
+
|
119
|
+
identifier = request.user.identifiers[request.experiment.identifier_type]
|
120
|
+
return false if identifier.nil?
|
121
|
+
|
122
|
+
bucket = request.workspace.get_bucket_or_nil(container.bucket_id)
|
123
|
+
raise ArgumentError, "bucket [#{container.bucket_id}]" if bucket.nil?
|
124
|
+
|
125
|
+
slot = @bucketer.bucketing(bucket, identifier)
|
126
|
+
return false if slot.nil?
|
127
|
+
|
128
|
+
group = container.get_group_or_nil(slot.variation_id)
|
129
|
+
raise ArgumentError, "container group [#{slot.variation_id}]" if group.nil?
|
130
|
+
|
131
|
+
group.experiments.include?(request.experiment.id)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class ExperimentTargetDeterminer
|
136
|
+
|
137
|
+
# @param target_matcher [TargetMatcher]
|
138
|
+
def initialize(target_matcher:)
|
139
|
+
# @type [TargetMatcher]
|
140
|
+
@target_matcher = target_matcher
|
141
|
+
end
|
142
|
+
|
143
|
+
# @param request [ExperimentRequest]
|
144
|
+
# @param context [EvaluatorContext]
|
145
|
+
def user_in_experiment_target?(request, context)
|
146
|
+
return true if request.experiment.target_audiences.empty?
|
147
|
+
|
148
|
+
request.experiment.target_audiences.any? { |it| @target_matcher.matches(request, context, it) }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
class ExperimentTargetRuleDeterminer
|
153
|
+
# @param target_matcher [TargetMatcher]
|
154
|
+
def initialize(target_matcher:)
|
155
|
+
# @type [TargetMatcher]
|
156
|
+
@target_matcher = target_matcher
|
157
|
+
end
|
158
|
+
|
159
|
+
# @param request [ExperimentRequest]
|
160
|
+
# @param context [EvaluatorContext]
|
161
|
+
# @return [Hackle::TargetRule, nil]
|
162
|
+
def determine_target_rule_or_nil(request, context)
|
163
|
+
request.experiment.target_rules.find { |it| @target_matcher.matches(request, context, it.target) }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackle
|
4
|
+
class RemoteConfigTargetRuleDeterminer
|
5
|
+
|
6
|
+
# @param matcher [RemoteConfigTargetRuleMatcher]
|
7
|
+
def initialize(matcher:)
|
8
|
+
# @type [RemoteConfigTargetRuleMatcher]
|
9
|
+
@matcher = matcher
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param request [RemoteConfigRequest]
|
13
|
+
# @param context [EvaluatorContext]
|
14
|
+
# @return [RemoteConfigTargetRule, nil]
|
15
|
+
def determine_or_nil(request, context)
|
16
|
+
request.parameter.target_rules.find { |it| @matcher.matches(request, context, it) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class RemoteConfigTargetRuleMatcher
|
21
|
+
# @param target_matcher [TargetMatcher]
|
22
|
+
# @param bucketer [Bucketer]
|
23
|
+
def initialize(target_matcher:, bucketer:)
|
24
|
+
# @type [TargetMatcher]
|
25
|
+
@target_matcher = target_matcher
|
26
|
+
# @type [Bucketer]
|
27
|
+
@bucketer = bucketer
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param request [RemoteConfigRequest]
|
31
|
+
# @param context [EvaluatorContext]
|
32
|
+
# @param target_rule [RemoteConfigTargetRule]
|
33
|
+
# @return [boolean]
|
34
|
+
def matches(request, context, target_rule)
|
35
|
+
matches = @target_matcher.matches(request, context, target_rule.target)
|
36
|
+
return false unless matches
|
37
|
+
|
38
|
+
identifier = request.user.identifiers[request.parameter.identifier_type]
|
39
|
+
return false if identifier.nil?
|
40
|
+
|
41
|
+
bucket = request.workspace.get_bucket_or_nil(target_rule.bucket_id)
|
42
|
+
raise ArgumentError, "bucket [#{target_rule.bucket_id}]" if bucket.nil?
|
43
|
+
|
44
|
+
allocated = @bucketer.bucketing(bucket, identifier)
|
45
|
+
!allocated.nil?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|