hackle-ruby-sdk 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hackle/client.rb +186 -87
- 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/{decision → internal/evaluation/bucketer}/bucketer.rb +17 -15
- 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/{models → internal/model}/event_type.rb +5 -8
- 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 -69
- metadata +123 -53
- 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/decider.rb +0 -69
- data/lib/hackle/events/event_dispatcher.rb +0 -96
- data/lib/hackle/events/event_processor.rb +0 -126
- data/lib/hackle/events/user_event.rb +0 -61
- data/lib/hackle/http/http.rb +0 -37
- data/lib/hackle/models/bucket.rb +0 -26
- data/lib/hackle/models/event.rb +0 -26
- data/lib/hackle/models/experiment.rb +0 -69
- data/lib/hackle/models/slot.rb +0 -22
- data/lib/hackle/models/user.rb +0 -24
- data/lib/hackle/models/variation.rb +0 -21
- data/lib/hackle/workspaces/http_workspace_fetcher.rb +0 -24
- data/lib/hackle/workspaces/polling_workspace_fetcher.rb +0 -47
- data/lib/hackle/workspaces/workspace.rb +0 -100
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 572cfbd5a794b4516be482e888dffdbfbf98485ea566ceef44187b8555f754d8
|
|
4
|
+
data.tar.gz: 80c1c73b16728ee50eab4a613c0810f42883db15154a0b697d796e003a222274
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f68e02af4c482fba3d10f8598ffc832240524791ad66e29db644996d128236e594c79602ec37e040ba800e6a77f3bab8eb226e5f336aa852c0b9e60eedbc723
|
|
7
|
+
data.tar.gz: 11eaa326223fd384bc0c0261c1ea3ef6ab7fbff6885570cf62ce3c124cdd0104d63f6525420c5a976c2e14cac932320f07e7aef80f1e0b751be6d1702e11b26d
|
data/lib/hackle/client.rb
CHANGED
|
@@ -1,127 +1,226 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'hackle/
|
|
4
|
-
require 'hackle/decision
|
|
5
|
-
|
|
6
|
-
require 'hackle/
|
|
7
|
-
require 'hackle/
|
|
8
|
-
require 'hackle/
|
|
9
|
-
|
|
10
|
-
require 'hackle/
|
|
11
|
-
|
|
12
|
-
require 'hackle/
|
|
13
|
-
require 'hackle/
|
|
14
|
-
require 'hackle/
|
|
15
|
-
require 'hackle/
|
|
16
|
-
require 'hackle/
|
|
17
|
-
require 'hackle/
|
|
18
|
-
require 'hackle/
|
|
19
|
-
|
|
20
|
-
require 'hackle/workspaces/http_workspace_fetcher'
|
|
21
|
-
require 'hackle/workspaces/polling_workspace_fetcher'
|
|
22
|
-
require 'hackle/workspaces/workspace'
|
|
3
|
+
require 'hackle/event'
|
|
4
|
+
require 'hackle/decision'
|
|
5
|
+
require 'hackle/config'
|
|
6
|
+
require 'hackle/remote_config'
|
|
7
|
+
require 'hackle/version'
|
|
8
|
+
require 'hackle/internal/concurrent/executors'
|
|
9
|
+
require 'hackle/internal/logger/logger'
|
|
10
|
+
require 'hackle/internal/model/sdk'
|
|
11
|
+
require 'hackle/internal/model/decision_reason'
|
|
12
|
+
require 'hackle/internal/config/parameter_config'
|
|
13
|
+
require 'hackle/internal/workspace/http_workspace_fetcher'
|
|
14
|
+
require 'hackle/internal/workspace/polling_workspace_fetcher'
|
|
15
|
+
require 'hackle/internal/event/user_event_dispatcher'
|
|
16
|
+
require 'hackle/internal/event/user_event_processor'
|
|
17
|
+
require 'hackle/internal/http/http_client'
|
|
18
|
+
require 'hackle/internal/core/hackle_core'
|
|
19
|
+
require 'hackle/internal/user/hackle_user_resolver'
|
|
23
20
|
|
|
24
21
|
module Hackle
|
|
25
|
-
|
|
26
22
|
#
|
|
27
|
-
#
|
|
23
|
+
# The entry point of Hackle SDKs.
|
|
28
24
|
#
|
|
29
25
|
class Client
|
|
26
|
+
# @param core [Hackle::Core]
|
|
27
|
+
# @param user_resolver [Hackle::HackleUserResolver]
|
|
28
|
+
def initialize(core:, user_resolver:)
|
|
29
|
+
# @type [Hackle::Core]
|
|
30
|
+
@core = core
|
|
31
|
+
|
|
32
|
+
# @type [Hackle::HackleUserResolver]
|
|
33
|
+
@user_resolver = user_resolver
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@instance = {}
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Instantiates a Hackle client.
|
|
40
|
+
#
|
|
41
|
+
# @param sdk_key [String]
|
|
42
|
+
# @param config [Hackle::Config]
|
|
43
|
+
# @return [Hackle::Client]
|
|
44
|
+
#
|
|
45
|
+
def self.create(sdk_key:, config: Config.builder.build)
|
|
46
|
+
client = @instance[sdk_key]
|
|
47
|
+
unless client.nil?
|
|
48
|
+
client.__resume
|
|
49
|
+
return client
|
|
50
|
+
end
|
|
30
51
|
|
|
52
|
+
Log.init(config.logger)
|
|
53
|
+
|
|
54
|
+
sdk = Sdk.new(name: 'ruby-sdk', version: Hackle::VERSION, key: sdk_key)
|
|
55
|
+
|
|
56
|
+
http_workspace_fetcher = HttpWorkspaceFetcher.new(
|
|
57
|
+
http_client: HttpClient.create(base_url: config.sdk_url, sdk: sdk),
|
|
58
|
+
sdk: sdk
|
|
59
|
+
)
|
|
60
|
+
workspace_fetcher = PollingWorkspaceFetcher.new(
|
|
61
|
+
http_workspace_fetcher: http_workspace_fetcher,
|
|
62
|
+
scheduler: Executors.scheduler,
|
|
63
|
+
polling_interval_seconds: 10.0
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
event_dispatcher = UserEventDispatcher.create(
|
|
67
|
+
http_client: HttpClient.create(base_url: config.event_url, sdk: sdk),
|
|
68
|
+
executor: Executors.thread_pool(pool_size: 2, queue_capacity: 64)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
event_processor = DefaultUserEventProcessor.new(
|
|
72
|
+
queue: SizedQueue.new(10_000),
|
|
73
|
+
event_dispatcher: event_dispatcher,
|
|
74
|
+
event_dispatch_size: 100,
|
|
75
|
+
flush_scheduler: Executors.scheduler,
|
|
76
|
+
flush_interval_seconds: 10.0,
|
|
77
|
+
shutdown_timeout_seconds: 10.0
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
workspace_fetcher.start
|
|
81
|
+
event_processor.start
|
|
82
|
+
|
|
83
|
+
core = Core.create(
|
|
84
|
+
workspace_fetcher: workspace_fetcher,
|
|
85
|
+
event_processor: event_processor
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
new_client = Client.new(
|
|
89
|
+
core: core,
|
|
90
|
+
user_resolver: HackleUserResolver.new
|
|
91
|
+
)
|
|
92
|
+
@instance[sdk_key] = new_client
|
|
93
|
+
new_client
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#
|
|
97
|
+
# Decide the variation to expose to the user for experiment.
|
|
31
98
|
#
|
|
32
|
-
#
|
|
99
|
+
# @param experiment_key [Integer]
|
|
100
|
+
# @param user [Hackle::User]
|
|
33
101
|
#
|
|
34
|
-
# @
|
|
35
|
-
# @param workspace_fetcher [PollingWorkspaceFetcher]
|
|
36
|
-
# @param event_processor [EventProcessor]
|
|
37
|
-
# @param decider [Decider]
|
|
102
|
+
# @return [String] the decided variation for the user
|
|
38
103
|
#
|
|
39
|
-
def
|
|
40
|
-
|
|
104
|
+
def variation(experiment_key, user)
|
|
105
|
+
variation_detail(experiment_key, user).variation
|
|
106
|
+
end
|
|
41
107
|
|
|
42
|
-
|
|
43
|
-
|
|
108
|
+
#
|
|
109
|
+
# Decide the variation to expose to the user for experiment, and returns an object that
|
|
110
|
+
# describes the way the variation was decided.
|
|
111
|
+
#
|
|
112
|
+
# @param experiment_key [Integer] the unique key of the experiment. MUST NOT be nil.
|
|
113
|
+
# @param user [Hackle::User] the user to participate in the experiment. MUST NOT be nil.
|
|
114
|
+
#
|
|
115
|
+
# @return [Hackle::ExperimentDecision] an object describing the result
|
|
116
|
+
#
|
|
117
|
+
def variation_detail(experiment_key, user)
|
|
118
|
+
unless experiment_key.is_a?(Integer)
|
|
119
|
+
Log.get.warn { "Invalid experiment key: #{experiment_key} (expected: integer)" }
|
|
120
|
+
return ExperimentDecision.new('A', DecisionReason::INVALID_INPUT, ParameterConfig.empty)
|
|
121
|
+
end
|
|
44
122
|
|
|
45
|
-
|
|
46
|
-
|
|
123
|
+
hackle_user = @user_resolver.resolve_or_nil(user)
|
|
124
|
+
if hackle_user.nil?
|
|
125
|
+
Log.get.warn { "Invalid hackle user: #{user}" }
|
|
126
|
+
return ExperimentDecision.new('A', DecisionReason::INVALID_INPUT, ParameterConfig.empty)
|
|
127
|
+
end
|
|
47
128
|
|
|
48
|
-
|
|
49
|
-
|
|
129
|
+
@core.experiment(experiment_key, hackle_user, 'A')
|
|
130
|
+
rescue => e
|
|
131
|
+
Log.get.error { "Unexpected error while deciding variation of experiment[#{experiment_key}]: #{e.inspect}]" }
|
|
132
|
+
ExperimentDecision.new('A', DecisionReason::EXCEPTION, ParameterConfig.empty)
|
|
50
133
|
end
|
|
51
134
|
|
|
52
135
|
#
|
|
53
|
-
# Decide the
|
|
136
|
+
# Decide whether the feature is turned on to the user.
|
|
137
|
+
#
|
|
138
|
+
# @param feature_key [Integer] the unique key of the feature.
|
|
139
|
+
# @param user [Hackle::User] the user requesting the feature.
|
|
140
|
+
#
|
|
141
|
+
# @return [TrueClass] of the feature is on
|
|
142
|
+
# @return [FalseClass] of the feature is off
|
|
143
|
+
#
|
|
144
|
+
def is_feature_on(feature_key, user)
|
|
145
|
+
feature_flag_detail(feature_key, user).is_on
|
|
146
|
+
end
|
|
147
|
+
|
|
54
148
|
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# @
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def variation(experiment_key:, user:, default_variation: 'A')
|
|
68
|
-
|
|
69
|
-
return default_variation if experiment_key.nil? || !experiment_key.is_a?(Integer)
|
|
70
|
-
return default_variation if user.nil? || !user.is_a?(User) || !user.valid?
|
|
71
|
-
|
|
72
|
-
workspace = @workspace_fetcher.fetch
|
|
73
|
-
return default_variation if workspace.nil?
|
|
74
|
-
|
|
75
|
-
experiment = workspace.get_experiment(experiment_key: experiment_key)
|
|
76
|
-
return default_variation if experiment.nil?
|
|
77
|
-
|
|
78
|
-
decision = @decider.decide(experiment: experiment, user: user)
|
|
79
|
-
case decision
|
|
80
|
-
when Decision::NotAllocated
|
|
81
|
-
default_variation
|
|
82
|
-
when Decision::ForcedAllocated
|
|
83
|
-
decision.variation_key
|
|
84
|
-
when Decision::NaturalAllocated
|
|
85
|
-
exposure_event = UserEvent::Exposure.new(user: user, experiment: experiment, variation: decision.variation)
|
|
86
|
-
@event_processor.process(event: exposure_event)
|
|
87
|
-
decision.variation.key
|
|
88
|
-
else
|
|
89
|
-
default_variation
|
|
149
|
+
# Decide whether the feature is turned on to the user, and returns an object that
|
|
150
|
+
# describes the way the value was decided.
|
|
151
|
+
#
|
|
152
|
+
# @param feature_key [Integer] the unique key of the feature.
|
|
153
|
+
# @param user [Hackle::User] the user requesting the feature.
|
|
154
|
+
#
|
|
155
|
+
# @return [Hackle::FeatureFlagDecision] an object describing the result
|
|
156
|
+
#
|
|
157
|
+
def feature_flag_detail(feature_key, user)
|
|
158
|
+
unless feature_key.is_a?(Integer)
|
|
159
|
+
Log.get.warn { "Invalid feature key: #{feature_key} (expected: integer)" }
|
|
160
|
+
return FeatureFlagDecision.new(false, DecisionReason::INVALID_INPUT, ParameterConfig.empty)
|
|
90
161
|
end
|
|
91
162
|
|
|
163
|
+
hackle_user = @user_resolver.resolve_or_nil(user)
|
|
164
|
+
if hackle_user.nil?
|
|
165
|
+
Log.get.warn { "Invalid hackle user: #{user}" }
|
|
166
|
+
return FeatureFlagDecision.new(false, DecisionReason::INVALID_INPUT, ParameterConfig.empty)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
@core.feature_flag(feature_key, hackle_user)
|
|
92
170
|
rescue => e
|
|
93
|
-
|
|
94
|
-
|
|
171
|
+
Log.get.error { "Unexpected error while deciding feature flag[#{feature_key}]: #{e.inspect}]" }
|
|
172
|
+
FeatureFlagDecision.new(false, DecisionReason::EXCEPTION, ParameterConfig.empty)
|
|
95
173
|
end
|
|
96
174
|
|
|
97
175
|
#
|
|
98
|
-
#
|
|
176
|
+
# Returns a instance of Hackle::RemoteConfig.
|
|
99
177
|
#
|
|
100
|
-
# @param
|
|
101
|
-
# @param user [User] the user that occurred the event.
|
|
178
|
+
# @param user [Hackle::User] the user requesting the remote config.
|
|
102
179
|
#
|
|
103
|
-
|
|
180
|
+
# @return [Hackle::RemoteConfig]
|
|
181
|
+
#
|
|
182
|
+
def remote_config(user)
|
|
183
|
+
RemoteConfig.new(user: user, user_resolver: @user_resolver, core: @core)
|
|
184
|
+
end
|
|
104
185
|
|
|
105
|
-
|
|
106
|
-
|
|
186
|
+
#
|
|
187
|
+
# Records the event that occurred by the user.
|
|
188
|
+
#
|
|
189
|
+
# @param event [Hackle::Event] the event that occurred.
|
|
190
|
+
# @param user [Hackle::User] the user that occurred the event.
|
|
191
|
+
#
|
|
192
|
+
def track(event, user)
|
|
193
|
+
unless event.is_a?(Event)
|
|
194
|
+
Log.get.warn { "Invalid event: #{event} (expected: Hackle::Event)" }
|
|
195
|
+
return
|
|
196
|
+
end
|
|
107
197
|
|
|
108
|
-
|
|
109
|
-
|
|
198
|
+
unless event.valid?
|
|
199
|
+
Log.get.error { "Invalid event: #{event.error_or_nil}" }
|
|
200
|
+
return
|
|
201
|
+
end
|
|
110
202
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
203
|
+
hackle_user = @user_resolver.resolve_or_nil(user)
|
|
204
|
+
if hackle_user.nil?
|
|
205
|
+
Log.get.warn { "Invalid hackle user: #{user}" }
|
|
206
|
+
return FeatureFlagDecision.new(false, DecisionReason::INVALID_INPUT, ParameterConfig.empty)
|
|
207
|
+
end
|
|
114
208
|
|
|
209
|
+
@core.track(event, hackle_user)
|
|
115
210
|
rescue => e
|
|
116
|
-
|
|
211
|
+
Log.get.error { "Unexpected error while tracking event: #{e.inspect}]" }
|
|
117
212
|
end
|
|
118
213
|
|
|
119
214
|
#
|
|
120
215
|
# Shutdown the background task and release the resources used for the background task.
|
|
216
|
+
# This should only be called when the application shutdown.
|
|
121
217
|
#
|
|
122
218
|
def close
|
|
123
|
-
@
|
|
124
|
-
|
|
219
|
+
@core.close
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def __resume
|
|
223
|
+
@core.resume
|
|
125
224
|
end
|
|
126
225
|
end
|
|
127
226
|
end
|
data/lib/hackle/config.rb
CHANGED
|
@@ -4,30 +4,72 @@ require 'logger'
|
|
|
4
4
|
|
|
5
5
|
module Hackle
|
|
6
6
|
class Config
|
|
7
|
+
# @return [Logger]
|
|
8
|
+
attr_reader :logger
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@base_uri = options[:base_uri] || Config.default_base_uri
|
|
11
|
-
@event_uri = options[:event_uri] || Config.default_event_uri
|
|
12
|
-
end
|
|
10
|
+
# @return [String]
|
|
11
|
+
attr_reader :sdk_url
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
attr_reader :
|
|
16
|
-
attr_reader :event_uri
|
|
13
|
+
# @return [String]
|
|
14
|
+
attr_reader :event_url
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
# @param logger [Logger]
|
|
17
|
+
# @param sdk_url [String]
|
|
18
|
+
# @param event_url [String]
|
|
19
|
+
def initialize(
|
|
20
|
+
logger:,
|
|
21
|
+
sdk_url:,
|
|
22
|
+
event_url:
|
|
23
|
+
)
|
|
24
|
+
@logger = logger
|
|
25
|
+
@sdk_url = sdk_url
|
|
26
|
+
@event_url = event_url
|
|
20
27
|
end
|
|
21
28
|
|
|
22
|
-
def self.
|
|
23
|
-
|
|
29
|
+
def self.builder
|
|
30
|
+
Builder.new
|
|
24
31
|
end
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
class Builder
|
|
34
|
+
def initialize
|
|
35
|
+
# noinspection RubyResolve
|
|
36
|
+
@logger = if defined?(Rails) && Rails.logger
|
|
37
|
+
Rails.logger
|
|
38
|
+
else
|
|
39
|
+
Logger.new($stdout)
|
|
40
|
+
end
|
|
41
|
+
@sdk_url = 'https://sdk.hackle.io'
|
|
42
|
+
@event_url = 'https://event.hackle.io'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param logger [Logger]
|
|
46
|
+
# @return [Hackle::Config::Builder]
|
|
47
|
+
def logger(logger)
|
|
48
|
+
@logger = logger
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param sdk_url [String]
|
|
53
|
+
# @return [Hackle::Config::Builder]
|
|
54
|
+
def sdk_url(sdk_url)
|
|
55
|
+
@sdk_url = sdk_url
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param event_url [String]
|
|
60
|
+
# @return [Hackle::Config::Builder]
|
|
61
|
+
def event_url(event_url)
|
|
62
|
+
@event_url = event_url
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Hackle::Config]
|
|
67
|
+
def build
|
|
68
|
+
Config.new(
|
|
69
|
+
logger: @logger,
|
|
70
|
+
sdk_url: @sdk_url,
|
|
71
|
+
event_url: @event_url
|
|
72
|
+
)
|
|
31
73
|
end
|
|
32
74
|
end
|
|
33
75
|
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hackle
|
|
4
|
+
class ExperimentDecision
|
|
5
|
+
|
|
6
|
+
# @return [String]
|
|
7
|
+
attr_reader :variation
|
|
8
|
+
|
|
9
|
+
# @return [String]
|
|
10
|
+
attr_reader :reason
|
|
11
|
+
|
|
12
|
+
# @return [ParameterConfig]
|
|
13
|
+
attr_reader :config
|
|
14
|
+
|
|
15
|
+
# @param variation [String]
|
|
16
|
+
# @param reason [String]
|
|
17
|
+
# @param config [ParameterConfig]
|
|
18
|
+
def initialize(variation, reason, config)
|
|
19
|
+
@config = config
|
|
20
|
+
@variation = variation
|
|
21
|
+
@reason = reason
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param key [String]
|
|
25
|
+
# @param default_value [Object, nil]
|
|
26
|
+
# @return [Object, nil]
|
|
27
|
+
def get(key, default_value = nil)
|
|
28
|
+
config.get(key, default_value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ==(other)
|
|
32
|
+
other.is_a?(self.class) &&
|
|
33
|
+
variation == other.variation &&
|
|
34
|
+
reason == other.reason &&
|
|
35
|
+
config == other.config
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_s
|
|
39
|
+
"ExperimentDecision(variation=#{variation}, reason=#{reason}, config=#{config})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class FeatureFlagDecision
|
|
45
|
+
|
|
46
|
+
# @return [boolean]
|
|
47
|
+
attr_accessor :is_on
|
|
48
|
+
|
|
49
|
+
# @return [String]
|
|
50
|
+
attr_accessor :reason
|
|
51
|
+
|
|
52
|
+
# @return [ParameterConfig]
|
|
53
|
+
attr_reader :config
|
|
54
|
+
|
|
55
|
+
# @param is_on [boolean]
|
|
56
|
+
# @param reason [String]
|
|
57
|
+
# @param config [ParameterConfig]
|
|
58
|
+
def initialize(is_on, reason, config)
|
|
59
|
+
@is_on = is_on
|
|
60
|
+
@reason = reason
|
|
61
|
+
@config = config
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [boolean]
|
|
65
|
+
def on?
|
|
66
|
+
@is_on
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @param key [String]
|
|
70
|
+
# @param default_value [Object, nil]
|
|
71
|
+
# @return [Object, nil]
|
|
72
|
+
def get(key, default_value = nil)
|
|
73
|
+
config.get(key, default_value)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ==(other)
|
|
77
|
+
other.is_a?(FeatureFlagDecision) &&
|
|
78
|
+
is_on == other.is_on &&
|
|
79
|
+
reason == other.reason &&
|
|
80
|
+
config == other.config
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def to_s
|
|
84
|
+
"FeatureFlagDecision(is_on=#{is_on}, reason=#{reason}, config=#{config})"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class RemoteConfigDecision
|
|
89
|
+
|
|
90
|
+
# @return [Object, nil]
|
|
91
|
+
attr_reader :value
|
|
92
|
+
|
|
93
|
+
# @return [String]
|
|
94
|
+
attr_reader :reason
|
|
95
|
+
|
|
96
|
+
# @param value [Object, nil]
|
|
97
|
+
# @param reason [String]
|
|
98
|
+
def initialize(value, reason)
|
|
99
|
+
@value = value
|
|
100
|
+
@reason = reason
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def ==(other)
|
|
104
|
+
other.is_a?(RemoteConfigDecision) &&
|
|
105
|
+
value == other.value &&
|
|
106
|
+
reason == other.reason
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def to_s
|
|
110
|
+
"RemoteConfigDecision(value=#{value}, reason=#{reason})"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/hackle/event.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'hackle/internal/properties/properties_builder'
|
|
4
|
+
|
|
5
|
+
module Hackle
|
|
6
|
+
class Event
|
|
7
|
+
# @return [String]
|
|
8
|
+
attr_reader :key
|
|
9
|
+
|
|
10
|
+
# @return [Float, nil]
|
|
11
|
+
attr_reader :value
|
|
12
|
+
|
|
13
|
+
# @return [Hash{String => Object}]
|
|
14
|
+
attr_reader :properties
|
|
15
|
+
|
|
16
|
+
# @param key [String]
|
|
17
|
+
# @param value [Numeric, nil]
|
|
18
|
+
# @param properties [Hash{String => Object}]
|
|
19
|
+
def initialize(key:, value:, properties:)
|
|
20
|
+
@key = key
|
|
21
|
+
@value = value
|
|
22
|
+
@properties = properties
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [boolean]
|
|
26
|
+
def valid?
|
|
27
|
+
error_or_nil.nil?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [String, nil]
|
|
31
|
+
def error_or_nil
|
|
32
|
+
return "Invalid event key: #{key} (expected: not empty string)" unless ValueType.not_empty_string?(key)
|
|
33
|
+
return "Invalid event value: #{value} (expected: number)" if !value.nil? && !ValueType.number?(value)
|
|
34
|
+
return "Invalid event properties: #{properties} (expected: Hash)" if !properties.nil? && !properties.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ==(other)
|
|
40
|
+
other.is_a?(Event) && other.key == key && other.value == value && other.properties == properties
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_s
|
|
44
|
+
"Hackle::Event(key: #{key}, value: #{value}, properties: #{properties})"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param key [String]
|
|
48
|
+
# @return [Hackle::Event::Builder]
|
|
49
|
+
def self.builder(key)
|
|
50
|
+
Builder.new(key)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Builder
|
|
54
|
+
# @param key [String]
|
|
55
|
+
def initialize(key)
|
|
56
|
+
@key = key
|
|
57
|
+
@value = nil
|
|
58
|
+
@properties = PropertiesBuilder.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param value [Float, nil]
|
|
62
|
+
# @return [Hackle::Event::Builder]
|
|
63
|
+
def value(value)
|
|
64
|
+
@value = value
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param key [String]
|
|
69
|
+
# @param value [Object, nil]
|
|
70
|
+
# @return [Hackle::Event::Builder]
|
|
71
|
+
def property(key, value)
|
|
72
|
+
@properties.add(key, value)
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param properties [Hash{String => Object}]
|
|
77
|
+
# @return [Hackle::Event::Builder]
|
|
78
|
+
def properties(properties)
|
|
79
|
+
@properties.add_all(properties)
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Hackle::Event]
|
|
84
|
+
def build
|
|
85
|
+
Event.new(key: @key, value: @value, properties: @properties.build)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hackle
|
|
4
|
+
module Clock
|
|
5
|
+
# @return [Integer]
|
|
6
|
+
def current_millis; end
|
|
7
|
+
|
|
8
|
+
# @return [Integer]
|
|
9
|
+
def tick; end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class SystemClock
|
|
13
|
+
include Clock
|
|
14
|
+
|
|
15
|
+
@instance = new
|
|
16
|
+
|
|
17
|
+
# @return [SystemClock]
|
|
18
|
+
def self.instance
|
|
19
|
+
@instance
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def current_millis
|
|
23
|
+
(Time.now.to_f * 1000).to_i
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def tick
|
|
27
|
+
(Time.now.to_f * 1000 * 1000 * 1000).to_i
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class FixedClock
|
|
32
|
+
include Clock
|
|
33
|
+
|
|
34
|
+
# @param time [Integer]
|
|
35
|
+
def initialize(time)
|
|
36
|
+
@time = time
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def current_millis
|
|
40
|
+
@time
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tick
|
|
44
|
+
@time
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
require 'hackle/internal/concurrent/schedule/timer_scheduler'
|
|
5
|
+
|
|
6
|
+
module Hackle
|
|
7
|
+
class Executors
|
|
8
|
+
# @param pool_size [Integer]
|
|
9
|
+
# @param queue_capacity [Integer]
|
|
10
|
+
# @return [Concurrent::ThreadPoolExecutor]
|
|
11
|
+
def self.thread_pool(pool_size:, queue_capacity:)
|
|
12
|
+
Concurrent::ThreadPoolExecutor.new(min_threads: pool_size, max_threads: pool_size, max_queue: queue_capacity)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [TimerScheduler]
|
|
16
|
+
def self.scheduler
|
|
17
|
+
TimerScheduler.new
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|