hackle-ruby-sdk 1.0.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 +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
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'hackle/internal/http/http'
|
5
|
+
|
6
|
+
module Hackle
|
7
|
+
class UserEventDispatcher
|
8
|
+
# @param http_client [HttpClient]
|
9
|
+
# @param executor [ThreadPoolExecutor]
|
10
|
+
# @param serializer [UserEventSerializer]
|
11
|
+
def initialize(http_client:, executor:, serializer:)
|
12
|
+
# @type [HttpClient]
|
13
|
+
@http_client = http_client
|
14
|
+
# @type [ThreadPoolExecutor]
|
15
|
+
@executor = executor
|
16
|
+
@serializer = serializer
|
17
|
+
@url = '/api/v2/events'
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param http_client [HttpClient]
|
21
|
+
# @param executor [ThreadPoolExecutor]
|
22
|
+
def self.create(http_client:, executor:)
|
23
|
+
UserEventDispatcher.new(
|
24
|
+
http_client: http_client,
|
25
|
+
executor: executor,
|
26
|
+
serializer: UserEventSerializer.new
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param events [Array<UserEvent>]
|
31
|
+
def dispatch(events)
|
32
|
+
payload = @serializer.serialize(events)
|
33
|
+
begin
|
34
|
+
@executor.post { dispatch_internal(payload) }
|
35
|
+
rescue => e
|
36
|
+
Log.get.error { "Unexpected error while posting events: #{e.inspect}" }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def shutdown
|
41
|
+
@executor.shutdown
|
42
|
+
return if @executor.wait_for_termination(10)
|
43
|
+
|
44
|
+
Log.get.warn { 'Failed to dispatch previously submitted events' }
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# @param payload [Hash]
|
50
|
+
def dispatch_internal(payload)
|
51
|
+
request = create_request(payload)
|
52
|
+
response = @http_client.execute(request)
|
53
|
+
handle_response(response)
|
54
|
+
rescue => e
|
55
|
+
Log.get.error { "Failed to dispatch events: #{e.inspect}" }
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param payload [Hash]
|
59
|
+
# @return [Net::HTTPRequest]
|
60
|
+
def create_request(payload)
|
61
|
+
request = Net::HTTP::Post.new(@url)
|
62
|
+
request.content_type = 'application/json'
|
63
|
+
request.body = payload.to_json
|
64
|
+
request
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param response [Net::HTTPResponse]
|
68
|
+
def handle_response(response)
|
69
|
+
raise "http status code: #{response.code}" unless HTTP.successful? response
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class UserEventSerializer
|
74
|
+
# @param events [Array<UserEvent>]
|
75
|
+
# @return [Hash]
|
76
|
+
def serialize(events)
|
77
|
+
exposure_events = []
|
78
|
+
track_events = []
|
79
|
+
remote_config_events = []
|
80
|
+
|
81
|
+
events.each do |event|
|
82
|
+
exposure_events << exposure_event(event) if event.is_a? ExposureEvent
|
83
|
+
track_events << track_event(event) if event.is_a? TrackEvent
|
84
|
+
remote_config_events << remote_config_event(event) if event.is_a? RemoteConfigEvent
|
85
|
+
end
|
86
|
+
{
|
87
|
+
exposureEvents: exposure_events,
|
88
|
+
trackEvents: track_events,
|
89
|
+
remoteConfigEvents: remote_config_events
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param event [ExposureEvent]
|
94
|
+
# @return [Hash]
|
95
|
+
def exposure_event(event)
|
96
|
+
{
|
97
|
+
insertId: event.insert_id,
|
98
|
+
timestamp: event.timestamp,
|
99
|
+
|
100
|
+
userId: event.user.identifiers['$id'],
|
101
|
+
identifiers: event.user.identifiers,
|
102
|
+
userProperties: event.user.properties,
|
103
|
+
hackleProperties: {},
|
104
|
+
|
105
|
+
experimentId: event.experiment.id,
|
106
|
+
experimentKey: event.experiment.key,
|
107
|
+
experimentType: event.experiment.type.name,
|
108
|
+
experimentVersion: event.experiment.version,
|
109
|
+
variationId: event.variation_id,
|
110
|
+
variationKey: event.variation_key,
|
111
|
+
decisionReason: event.decision_reason,
|
112
|
+
properties: event.properties
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
# @param event [TrackEvent]
|
117
|
+
# @return [Hash]
|
118
|
+
def track_event(event)
|
119
|
+
{
|
120
|
+
insertId: event.insert_id,
|
121
|
+
timestamp: event.timestamp,
|
122
|
+
|
123
|
+
userId: event.user.identifiers['$id'],
|
124
|
+
identifiers: event.user.identifiers,
|
125
|
+
userProperties: event.user.properties,
|
126
|
+
hackleProperties: {},
|
127
|
+
|
128
|
+
eventTypeId: event.event_type.id,
|
129
|
+
eventTypeKey: event.event_type.key,
|
130
|
+
value: event.event.value,
|
131
|
+
properties: event.event.properties
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
# @param event [RemoteConfigEvent]
|
136
|
+
# @return [Hash]
|
137
|
+
def remote_config_event(event)
|
138
|
+
{
|
139
|
+
insertId: event.insert_id,
|
140
|
+
timestamp: event.timestamp,
|
141
|
+
|
142
|
+
userId: event.user.identifiers['$id'],
|
143
|
+
identifiers: event.user.identifiers,
|
144
|
+
userProperties: event.user.properties,
|
145
|
+
hackleProperties: {},
|
146
|
+
|
147
|
+
parameterId: event.parameter.id,
|
148
|
+
parameterKey: event.parameter.key,
|
149
|
+
parameterType: event.parameter.type.name,
|
150
|
+
valueId: event.value_id,
|
151
|
+
decisionReason: event.decision_reason,
|
152
|
+
properties: event.properties
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/evaluation/evaluator/experiment/experiment_evaluator'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
class UserEventFactory
|
7
|
+
# @param clock [Clock]
|
8
|
+
def initialize(clock:)
|
9
|
+
# @type [Clock]
|
10
|
+
@clock = clock
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param request [EvaluatorRequest]
|
14
|
+
# @param evaluation [EvaluatorEvaluation]
|
15
|
+
# @return [Array<UserEvent>]
|
16
|
+
def create(request, evaluation)
|
17
|
+
timestamp = @clock.current_millis
|
18
|
+
events = []
|
19
|
+
|
20
|
+
root_event = create_internal(request, evaluation, timestamp, PropertiesBuilder.new)
|
21
|
+
events << root_event unless root_event.nil?
|
22
|
+
|
23
|
+
evaluation.target_evaluations.each do |target_evaluation|
|
24
|
+
properties_builder = PropertiesBuilder.new
|
25
|
+
properties_builder.add('$targetingRootType', request.key.type)
|
26
|
+
properties_builder.add('$targetingRootId', request.key.id)
|
27
|
+
target_event = create_internal(request, target_evaluation, timestamp, properties_builder)
|
28
|
+
events << target_event unless target_event.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
events
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @param request [EvaluatorRequest]
|
37
|
+
# @param evaluation [EvaluatorEvaluation]
|
38
|
+
# @param timestamp [Integer]
|
39
|
+
# @param properties_builder [PropertiesBuilder]
|
40
|
+
# @return [UserEvent, nil]
|
41
|
+
def create_internal(request, evaluation, timestamp, properties_builder)
|
42
|
+
e = evaluation
|
43
|
+
case e
|
44
|
+
when ExperimentEvaluation
|
45
|
+
properties_builder.add('$parameterConfigurationId', e.config&.id)
|
46
|
+
properties_builder.add('$experiment_version', e.experiment.version)
|
47
|
+
properties_builder.add('$execution_version', e.experiment.execution_version)
|
48
|
+
UserEvent.exposure(e, properties_builder.build, request.user, timestamp)
|
49
|
+
when RemoteConfigEvaluation
|
50
|
+
properties_builder.add_all(e.properties)
|
51
|
+
UserEvent.remote_config(e, properties_builder.build, request.user, timestamp)
|
52
|
+
else
|
53
|
+
Log.get.error { "unsupported evaluator evaluation: #{e.class}" }
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/logger/logger'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
module UserEventProcessor
|
7
|
+
# @param event [UserEvent]
|
8
|
+
def process(event) end
|
9
|
+
|
10
|
+
def start
|
11
|
+
end
|
12
|
+
|
13
|
+
def stop
|
14
|
+
end
|
15
|
+
|
16
|
+
def resume
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# noinspection RubyTooManyInstanceVariablesInspection
|
21
|
+
class DefaultUserEventProcessor
|
22
|
+
include UserEventProcessor
|
23
|
+
# @param queue [SizedQueue]
|
24
|
+
# @param event_dispatcher [UserEventDispatcher]
|
25
|
+
# @param event_dispatch_size [Integer]
|
26
|
+
# @param flush_scheduler [Scheduler]
|
27
|
+
# @param flush_interval_seconds [Float]
|
28
|
+
# @param shutdown_timeout_seconds [Float]
|
29
|
+
def initialize(
|
30
|
+
queue:,
|
31
|
+
event_dispatcher:,
|
32
|
+
event_dispatch_size:,
|
33
|
+
flush_scheduler:,
|
34
|
+
flush_interval_seconds:,
|
35
|
+
shutdown_timeout_seconds:
|
36
|
+
)
|
37
|
+
# @type [SizedQueue]
|
38
|
+
@queue = queue
|
39
|
+
|
40
|
+
# @type [UserEventDispatcher]
|
41
|
+
@event_dispatcher = event_dispatcher
|
42
|
+
|
43
|
+
# @type [Integer]
|
44
|
+
@event_dispatch_size = event_dispatch_size
|
45
|
+
|
46
|
+
# @type [Scheduler]
|
47
|
+
@flush_scheduler = flush_scheduler
|
48
|
+
|
49
|
+
# @type [Float]
|
50
|
+
@flush_interval_seconds = flush_interval_seconds
|
51
|
+
|
52
|
+
# @type [Float]
|
53
|
+
@shutdown_timeout_seconds = shutdown_timeout_seconds
|
54
|
+
|
55
|
+
# @type [ScheduledJob, nil]
|
56
|
+
@flushing_job = nil
|
57
|
+
|
58
|
+
# @type [Thread, nil]
|
59
|
+
@consuming_task = nil
|
60
|
+
|
61
|
+
# @type [boolean]
|
62
|
+
@is_started = false
|
63
|
+
|
64
|
+
# @type [Array<UserEvent>]
|
65
|
+
@current_batch = []
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param event [UserEvent]
|
69
|
+
def process(event)
|
70
|
+
produce(message: Message::Event.new(event))
|
71
|
+
end
|
72
|
+
|
73
|
+
def start
|
74
|
+
if @is_started
|
75
|
+
Log.get.info { "#{UserEventProcessor} is already started." }
|
76
|
+
return
|
77
|
+
end
|
78
|
+
|
79
|
+
@consuming_task = Thread.new { consuming }
|
80
|
+
@flushing_job = @flush_scheduler.schedule_periodically(@flush_interval_seconds, -> { flush })
|
81
|
+
@is_started = true
|
82
|
+
Log.get.info { "#{UserEventProcessor} started. Flush event every #{@flush_interval_seconds} seconds." }
|
83
|
+
end
|
84
|
+
|
85
|
+
def stop
|
86
|
+
return unless @is_started
|
87
|
+
|
88
|
+
Log.get.info { "Shutting down #{UserEventProcessor}" }
|
89
|
+
|
90
|
+
@flushing_job&.cancel
|
91
|
+
|
92
|
+
produce(message: Message::Shutdown.new, non_block: false)
|
93
|
+
@consuming_task&.join(@shutdown_timeout_seconds)
|
94
|
+
|
95
|
+
@event_dispatcher.shutdown
|
96
|
+
|
97
|
+
@is_started = false
|
98
|
+
end
|
99
|
+
|
100
|
+
def resume
|
101
|
+
@consuming_task = Thread.new { consuming } if @consuming_task.nil? || !@consuming_task.alive?
|
102
|
+
|
103
|
+
@flushing_job&.cancel
|
104
|
+
@flushing_job = @flush_scheduler.schedule_periodically(@flush_interval_seconds, -> { flush })
|
105
|
+
|
106
|
+
@is_started = true
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# @param message [Message]
|
112
|
+
# @param non_block [boolean]
|
113
|
+
def produce(message:, non_block: true)
|
114
|
+
@queue.push(message, non_block)
|
115
|
+
rescue ThreadError
|
116
|
+
Log.get.warn { 'Events are produced faster than can be consumed. Some events will be dropped.' }
|
117
|
+
end
|
118
|
+
|
119
|
+
def flush
|
120
|
+
produce(message: Message::Flush.new)
|
121
|
+
end
|
122
|
+
|
123
|
+
def consuming
|
124
|
+
loop do
|
125
|
+
message = @queue.pop
|
126
|
+
case message
|
127
|
+
when Message::Event
|
128
|
+
consume_event(message.event)
|
129
|
+
when Message::Flush
|
130
|
+
dispatch_events
|
131
|
+
when Message::Shutdown
|
132
|
+
break
|
133
|
+
else
|
134
|
+
Log.get.error { "Unsupported message type: #{message.class}" }
|
135
|
+
end
|
136
|
+
rescue => e
|
137
|
+
Log.get.error { "Unexpected error in event processor: #{e.inspect}" }
|
138
|
+
end
|
139
|
+
rescue => e
|
140
|
+
Log.get.error { "Unexpected error in event processor: #{e.inspect}" }
|
141
|
+
ensure
|
142
|
+
dispatch_events
|
143
|
+
end
|
144
|
+
|
145
|
+
# @param event [UserEvent]
|
146
|
+
def consume_event(event)
|
147
|
+
@current_batch << event
|
148
|
+
dispatch_events if @current_batch.size >= @event_dispatch_size
|
149
|
+
end
|
150
|
+
|
151
|
+
def dispatch_events
|
152
|
+
return if @current_batch.empty?
|
153
|
+
|
154
|
+
begin
|
155
|
+
@event_dispatcher.dispatch(@current_batch)
|
156
|
+
@current_batch = []
|
157
|
+
rescue => e
|
158
|
+
Log.get.error { "Failed to dispatch events: #{e.inspect}" }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class Message
|
163
|
+
class Event < Message
|
164
|
+
# @return [UserEvent]
|
165
|
+
attr_reader :event
|
166
|
+
|
167
|
+
# @param event [UserEvent]
|
168
|
+
def initialize(event)
|
169
|
+
super()
|
170
|
+
@event = event
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class Flush < Message
|
175
|
+
end
|
176
|
+
|
177
|
+
class Shutdown < Message
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
module HTTP
|
7
|
+
|
8
|
+
def self.client(base_url:)
|
9
|
+
uri = URI.parse(base_url)
|
10
|
+
# noinspection RubyMismatchedArgumentType
|
11
|
+
client = Net::HTTP.new(uri.host, uri.port)
|
12
|
+
client.use_ssl = uri.scheme == 'https'
|
13
|
+
client.open_timeout = 5
|
14
|
+
client.read_timeout = 10
|
15
|
+
client
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param response [Net::HTTPResponse]
|
19
|
+
def self.successful?(response)
|
20
|
+
response.code.start_with?('2')
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param response [Net::HTTPResponse]
|
24
|
+
def self.not_modified?(response)
|
25
|
+
response.code == '304'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/http/http'
|
4
|
+
require 'hackle/internal/clock/clock'
|
5
|
+
|
6
|
+
module Hackle
|
7
|
+
class HttpClient
|
8
|
+
|
9
|
+
# @param http [Net::HTTP]
|
10
|
+
# @param sdk [Sdk]
|
11
|
+
# @param clock [Clock]
|
12
|
+
def initialize(http:, sdk:, clock:)
|
13
|
+
# @type [Net::HTTP]
|
14
|
+
@http = http
|
15
|
+
@sdk = sdk
|
16
|
+
@clock = clock
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param base_url [String]
|
20
|
+
# @param sdk [Sdk]
|
21
|
+
# @param clock [Clock]
|
22
|
+
# @return [HttpClient]
|
23
|
+
def self.create(base_url:, sdk:, clock: SystemClock.instance)
|
24
|
+
HttpClient.new(
|
25
|
+
http: HTTP.client(base_url: base_url),
|
26
|
+
sdk: sdk,
|
27
|
+
clock: clock
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param request [Net::HTTPRequest]
|
32
|
+
# @return [Net::HTTPResponse]
|
33
|
+
def execute(request)
|
34
|
+
decorate(request)
|
35
|
+
@http.request(request)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# @param request [Net::HTTPRequest]
|
41
|
+
def decorate(request)
|
42
|
+
request['X-HACKLE-SDK-KEY'] = @sdk.key
|
43
|
+
request['X-HACKLE-SDK-NAME'] = @sdk.name
|
44
|
+
request['X-HACKLE-SDK-VERSION'] = @sdk.version
|
45
|
+
request['X-HACKLE-SDK-TIME'] = @clock.current_millis.to_s
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hackle/internal/logger/logger'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
class IdentifiersBuilder
|
7
|
+
def initialize
|
8
|
+
# @type [Hash{String => String}]
|
9
|
+
@identifiers = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# @param identifier_type [String]
|
13
|
+
# @param identifier_value [String, nil]
|
14
|
+
# @return [Hackle::IdentifiersBuilder]
|
15
|
+
def add(identifier_type, identifier_value)
|
16
|
+
value = IdentifiersBuilder.sanitize_value_or_nil(identifier_value)
|
17
|
+
|
18
|
+
if valid_type?(identifier_type) && !value.nil?
|
19
|
+
@identifiers[identifier_type] = value
|
20
|
+
else
|
21
|
+
Log.get.warn { "Invalid user identifier [type=#{identifier_type}] value=#{identifier_value}]" }
|
22
|
+
end
|
23
|
+
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param identifiers [Hash]
|
28
|
+
# @return [Hackle::IdentifiersBuilder]
|
29
|
+
def add_all(identifiers)
|
30
|
+
identifiers.each do |identifier_type, identifier_value|
|
31
|
+
add(identifier_type, identifier_value)
|
32
|
+
end
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Hash{String => String}]
|
37
|
+
def build
|
38
|
+
@identifiers.dup
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.sanitize_value_or_nil(identifier_value)
|
42
|
+
return nil if identifier_value.nil?
|
43
|
+
|
44
|
+
if identifier_value.is_a?(String) && !identifier_value.empty? && identifier_value.length <= MAX_IDENTIFIER_VALUE_LENGTH
|
45
|
+
return identifier_value
|
46
|
+
end
|
47
|
+
|
48
|
+
return identifier_value.to_s if identifier_value.is_a?(Numeric)
|
49
|
+
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
MAX_IDENTIFIER_TYPE_LENGTH = 128
|
56
|
+
MAX_IDENTIFIER_VALUE_LENGTH = 512
|
57
|
+
|
58
|
+
def valid_type?(identifier_type)
|
59
|
+
return false if identifier_type.nil?
|
60
|
+
return false unless identifier_type.is_a?(String)
|
61
|
+
return false if identifier_type.empty?
|
62
|
+
return false if identifier_type.length > MAX_IDENTIFIER_TYPE_LENGTH
|
63
|
+
|
64
|
+
true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
|
7
|
+
class Log
|
8
|
+
|
9
|
+
attr_accessor :logger
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@logger = Logger.new($stdout)
|
13
|
+
end
|
14
|
+
|
15
|
+
@instance = new
|
16
|
+
|
17
|
+
# @return [Log]
|
18
|
+
def self.instance
|
19
|
+
@instance
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.init(logger)
|
23
|
+
instance.logger = logger
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Logger]
|
27
|
+
def self.get
|
28
|
+
Log.instance.logger
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackle
|
4
|
+
class Action
|
5
|
+
|
6
|
+
# @!attribute [r] type
|
7
|
+
# @return [ActionType]
|
8
|
+
# @!attribute [r] variation_id
|
9
|
+
# @return [Integer, nil]
|
10
|
+
# @!attribute [r] bucket_id
|
11
|
+
# @return [Integer, nil]
|
12
|
+
attr_reader :type, :variation_id, :bucket_id
|
13
|
+
|
14
|
+
# @param type [ActionType]
|
15
|
+
# @param variation_id [Integer, nil]
|
16
|
+
# @param bucket_id [Integer, nil]
|
17
|
+
def initialize(type:, variation_id: nil, bucket_id: nil)
|
18
|
+
@type = type
|
19
|
+
@variation_id = variation_id
|
20
|
+
@bucket_id = bucket_id
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class ActionType
|
25
|
+
# @!attribute [r] name
|
26
|
+
# @return [String]
|
27
|
+
attr_reader :name
|
28
|
+
|
29
|
+
# @param name [String]
|
30
|
+
def initialize(name)
|
31
|
+
@name = name
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
name
|
36
|
+
end
|
37
|
+
|
38
|
+
VARIATION = new('VARIATION')
|
39
|
+
BUCKET = new('BUCKET')
|
40
|
+
|
41
|
+
@types = {
|
42
|
+
'VARIATION' => VARIATION,
|
43
|
+
'BUCKET' => BUCKET
|
44
|
+
}.freeze
|
45
|
+
|
46
|
+
# @param name [String]
|
47
|
+
# @return [ActionType, nil]
|
48
|
+
def self.from_or_nil(name)
|
49
|
+
@types[name.upcase]
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Array<ActionType>]
|
53
|
+
def self.values
|
54
|
+
@types.values
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackle
|
4
|
+
|
5
|
+
class Bucket
|
6
|
+
|
7
|
+
# @!attribute [r] id
|
8
|
+
# @return [Integer]
|
9
|
+
# @!attribute [r] seed
|
10
|
+
# @return [Integer]
|
11
|
+
# @!attribute [r] slot_size
|
12
|
+
# @return [Integer]
|
13
|
+
# @!attribute [r] slots
|
14
|
+
# @return [Array<Slot>]
|
15
|
+
attr_accessor :id, :seed, :slot_size, :slots
|
16
|
+
|
17
|
+
# @param id [Integer]
|
18
|
+
# @param seed [Integer]
|
19
|
+
# @param slot_size [Integer]
|
20
|
+
# @param slots [Array<Slot>]
|
21
|
+
def initialize(id:, seed:, slot_size:, slots:)
|
22
|
+
@id = id
|
23
|
+
@seed = seed
|
24
|
+
@slot_size = slot_size
|
25
|
+
@slots = slots
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param slot_number [Integer]
|
29
|
+
# @return [Slot, nil]
|
30
|
+
def get_slot_or_nil(slot_number)
|
31
|
+
slots.each do |slot|
|
32
|
+
return slot if slot.contains?(slot_number)
|
33
|
+
end
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Slot
|
39
|
+
# @!attribute [r] variation_id
|
40
|
+
# @return [Integer]
|
41
|
+
attr_reader :variation_id
|
42
|
+
|
43
|
+
# @param start_inclusive [Integer]
|
44
|
+
# @param end_exclusive [Integer]
|
45
|
+
# @param variation_id [Integer]
|
46
|
+
def initialize(start_inclusive:, end_exclusive:, variation_id:)
|
47
|
+
@start_inclusive = start_inclusive
|
48
|
+
@end_exclusive = end_exclusive
|
49
|
+
@variation_id = variation_id
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param slot_number [Integer]
|
53
|
+
# @return [boolean]
|
54
|
+
def contains?(slot_number)
|
55
|
+
@start_inclusive <= slot_number && slot_number < @end_exclusive
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|