hackle-ruby-sdk 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -3
- data/hackle-ruby-sdk.gemspec +1 -1
- data/lib/hackle-ruby-sdk.rb +2 -21
- data/lib/hackle.rb +76 -0
- data/lib/hackle/client.rb +127 -0
- data/lib/{hackle-ruby-sdk → hackle}/config.rb +1 -1
- data/lib/hackle/decision/bucketer.rb +44 -0
- data/lib/hackle/decision/decider.rb +69 -0
- data/lib/{hackle-ruby-sdk → hackle}/events/event_dispatcher.rb +31 -24
- data/lib/{hackle-ruby-sdk → hackle}/events/event_processor.rb +22 -11
- data/lib/hackle/events/user_event.rb +61 -0
- data/lib/{hackle-ruby-sdk → hackle}/http/http.rb +7 -9
- data/lib/hackle/models/bucket.rb +26 -0
- data/lib/hackle/models/event.rb +26 -0
- data/lib/hackle/models/event_type.rb +22 -0
- data/lib/hackle/models/experiment.rb +69 -0
- data/lib/hackle/models/slot.rb +22 -0
- data/lib/hackle/models/user.rb +24 -0
- data/lib/hackle/models/variation.rb +21 -0
- data/lib/{hackle-ruby-sdk → hackle}/version.rb +2 -2
- data/lib/hackle/workspaces/http_workspace_fetcher.rb +24 -0
- data/lib/{hackle-ruby-sdk → hackle}/workspaces/polling_workspace_fetcher.rb +4 -1
- data/lib/hackle/workspaces/workspace.rb +100 -0
- metadata +22 -19
- data/lib/hackle-ruby-sdk/client.rb +0 -108
- data/lib/hackle-ruby-sdk/decision/bucketer.rb +0 -26
- data/lib/hackle-ruby-sdk/decision/decider.rb +0 -54
- data/lib/hackle-ruby-sdk/events/event.rb +0 -33
- data/lib/hackle-ruby-sdk/models/bucket.rb +0 -15
- data/lib/hackle-ruby-sdk/models/event_type.rb +0 -10
- data/lib/hackle-ruby-sdk/models/experiment.rb +0 -39
- data/lib/hackle-ruby-sdk/models/slot.rb +0 -15
- data/lib/hackle-ruby-sdk/models/variation.rb +0 -11
- data/lib/hackle-ruby-sdk/workspaces/http_workspace_fetcher.rb +0 -24
- data/lib/hackle-ruby-sdk/workspaces/workspace.rb +0 -78
@@ -6,10 +6,12 @@ module Hackle
|
|
6
6
|
|
7
7
|
DEFAULT_FLUSH_INTERVAL = 10
|
8
8
|
|
9
|
-
|
9
|
+
# @param config [Config]
|
10
|
+
# @param event_dispatcher [EventDispatcher]
|
11
|
+
def initialize(config:, event_dispatcher:)
|
10
12
|
@logger = config.logger
|
11
13
|
@event_dispatcher = event_dispatcher
|
12
|
-
@message_processor = MessageProcessor.new(
|
14
|
+
@message_processor = MessageProcessor.new(config: config, event_dispatcher: event_dispatcher)
|
13
15
|
@flush_task = Concurrent::TimerTask.new(execution_interval: DEFAULT_FLUSH_INTERVAL) { flush }
|
14
16
|
@consume_task = nil
|
15
17
|
@running = false
|
@@ -26,7 +28,9 @@ module Hackle
|
|
26
28
|
def stop!
|
27
29
|
return unless @running
|
28
30
|
|
29
|
-
@
|
31
|
+
@logger.info { 'Shutting down Hackle event_processor' }
|
32
|
+
|
33
|
+
@message_processor.produce(message: Message::Shutdown.new, non_block: false)
|
30
34
|
@consume_task.join(10)
|
31
35
|
@flush_task.shutdown
|
32
36
|
@event_dispatcher.shutdown
|
@@ -34,18 +38,22 @@ module Hackle
|
|
34
38
|
@running = false
|
35
39
|
end
|
36
40
|
|
37
|
-
|
38
|
-
|
41
|
+
# @param event [UserEvent]
|
42
|
+
def process(event:)
|
43
|
+
@message_processor.produce(message: Message::Event.new(event))
|
39
44
|
end
|
40
45
|
|
41
46
|
def flush
|
42
|
-
@message_processor.produce(Message::Flush.new)
|
47
|
+
@message_processor.produce(message: Message::Flush.new)
|
43
48
|
end
|
44
49
|
|
45
50
|
class Message
|
46
51
|
class Event < Message
|
52
|
+
|
53
|
+
# @return [UserEvent]
|
47
54
|
attr_reader :event
|
48
55
|
|
56
|
+
# @param event [UserEvent]
|
49
57
|
def initialize(event)
|
50
58
|
@event = event
|
51
59
|
end
|
@@ -63,7 +71,7 @@ module Hackle
|
|
63
71
|
DEFAULT_MESSAGE_QUEUE_CAPACITY = 1000
|
64
72
|
DEFAULT_MAX_EVENT_DISPATCH_SIZE = 500
|
65
73
|
|
66
|
-
def initialize(event_dispatcher
|
74
|
+
def initialize(config:, event_dispatcher:)
|
67
75
|
@logger = config.logger
|
68
76
|
@event_dispatcher = event_dispatcher
|
69
77
|
@message_queue = SizedQueue.new(DEFAULT_MESSAGE_QUEUE_CAPACITY)
|
@@ -71,7 +79,9 @@ module Hackle
|
|
71
79
|
@consumed_events = []
|
72
80
|
end
|
73
81
|
|
74
|
-
|
82
|
+
# @param message [Message]
|
83
|
+
# @param non_block [boolean]
|
84
|
+
def produce(message:, non_block: true)
|
75
85
|
@message_queue.push(message, non_block)
|
76
86
|
rescue ThreadError
|
77
87
|
if @random.rand(1..100) == 1 # log only 1% of the time
|
@@ -84,7 +94,7 @@ module Hackle
|
|
84
94
|
message = @message_queue.pop
|
85
95
|
case message
|
86
96
|
when Message::Event
|
87
|
-
consume_event(message.event)
|
97
|
+
consume_event(event: message.event)
|
88
98
|
when Message::Flush
|
89
99
|
dispatch_events
|
90
100
|
when Message::Shutdown
|
@@ -99,7 +109,8 @@ module Hackle
|
|
99
109
|
|
100
110
|
private
|
101
111
|
|
102
|
-
|
112
|
+
# @param event [UserEvent]
|
113
|
+
def consume_event(event:)
|
103
114
|
@consumed_events << event
|
104
115
|
dispatch_events if @consumed_events.length >= DEFAULT_MAX_EVENT_DISPATCH_SIZE
|
105
116
|
end
|
@@ -107,7 +118,7 @@ module Hackle
|
|
107
118
|
def dispatch_events
|
108
119
|
return if @consumed_events.empty?
|
109
120
|
|
110
|
-
@event_dispatcher.dispatch(@consumed_events)
|
121
|
+
@event_dispatcher.dispatch(events: @consumed_events)
|
111
122
|
@consumed_events = []
|
112
123
|
end
|
113
124
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hackle
|
4
|
+
|
5
|
+
class UserEvent
|
6
|
+
|
7
|
+
# @!attribute [r] timestamp
|
8
|
+
# @return [Integer]
|
9
|
+
# @!attribute [r] user
|
10
|
+
# @return [User]
|
11
|
+
attr_reader :timestamp, :user
|
12
|
+
|
13
|
+
# @param user [User]
|
14
|
+
def initialize(user:)
|
15
|
+
@timestamp = UserEvent.generate_timestamp
|
16
|
+
@user = user
|
17
|
+
end
|
18
|
+
|
19
|
+
class Exposure < UserEvent
|
20
|
+
|
21
|
+
# @!attribute [r] experiment
|
22
|
+
# @return [Experiment]
|
23
|
+
# @!attribute [r] variation
|
24
|
+
# @return [Variation]
|
25
|
+
attr_reader :experiment, :variation
|
26
|
+
|
27
|
+
# @param user [User]
|
28
|
+
# @param experiment [Experiment]
|
29
|
+
# @param variation [Variation]
|
30
|
+
def initialize(user:, experiment:, variation:)
|
31
|
+
super(user: user)
|
32
|
+
@experiment = experiment
|
33
|
+
@variation = variation
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
class Track < UserEvent
|
39
|
+
|
40
|
+
# @!attribute [r] event_type
|
41
|
+
# @return [EventType]
|
42
|
+
# @!attribute [r] event
|
43
|
+
# @return [Event]
|
44
|
+
attr_reader :event_type, :event
|
45
|
+
|
46
|
+
# @param user [User]
|
47
|
+
# @param event_type [EventType]
|
48
|
+
# @param event [Event]
|
49
|
+
def initialize(user:, event_type:, event:)
|
50
|
+
super(user: user)
|
51
|
+
@event_type = event_type
|
52
|
+
@event = event
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Integer]
|
57
|
+
def self.generate_timestamp
|
58
|
+
(Time.now.to_f * 1000).to_i
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -4,14 +4,10 @@ require 'net/http'
|
|
4
4
|
|
5
5
|
module Hackle
|
6
6
|
class UnexpectedResponseError < StandardError
|
7
|
-
|
8
|
-
def initialize(status_code)
|
9
|
-
super("HTTP status code #{status_code}")
|
10
|
-
end
|
11
7
|
end
|
12
8
|
|
13
9
|
class HTTP
|
14
|
-
def self.client(base_uri)
|
10
|
+
def self.client(base_uri:)
|
15
11
|
uri = URI.parse(base_uri)
|
16
12
|
client = Net::HTTP.new(uri.host, uri.port)
|
17
13
|
client.use_ssl = uri.scheme == 'https'
|
@@ -20,7 +16,7 @@ module Hackle
|
|
20
16
|
client
|
21
17
|
end
|
22
18
|
|
23
|
-
def self.sdk_headers(sdk_info)
|
19
|
+
def self.sdk_headers(sdk_info:)
|
24
20
|
{
|
25
21
|
'X-HACKLE-SDK-KEY' => sdk_info.key,
|
26
22
|
'X-HACKLE-SDK-NAME' => sdk_info.name,
|
@@ -28,12 +24,14 @@ module Hackle
|
|
28
24
|
}
|
29
25
|
end
|
30
26
|
|
31
|
-
def self.successful?(status_code)
|
27
|
+
def self.successful?(status_code:)
|
32
28
|
status_code >= 200 && status_code < 300
|
33
29
|
end
|
34
30
|
|
35
|
-
def self.check_successful(status_code)
|
36
|
-
|
31
|
+
def self.check_successful(status_code:)
|
32
|
+
unless successful?(status_code: status_code)
|
33
|
+
raise UnexpectedResponseError, "HTTP status code #{status_code}"
|
34
|
+
end
|
37
35
|
end
|
38
36
|
end
|
39
37
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Hackle
|
2
|
+
|
3
|
+
class Bucket
|
4
|
+
|
5
|
+
# @!attribute [r] seed
|
6
|
+
# @return [Integer]
|
7
|
+
# @!attribute [r] slot_size
|
8
|
+
# @return [Integer]
|
9
|
+
attr_reader :seed, :slot_size
|
10
|
+
|
11
|
+
# @param seed [Integer]
|
12
|
+
# @param slot_size [Integer]
|
13
|
+
# @param slots [Array]
|
14
|
+
def initialize(seed:, slot_size:, slots:)
|
15
|
+
@seed = seed
|
16
|
+
@slot_size = slot_size
|
17
|
+
@slots = slots
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param slot_number [Integer]
|
21
|
+
# @return [Slot, nil]
|
22
|
+
def get_slot(slot_number:)
|
23
|
+
@slots.find { |slot| slot.contains?(slot_number: slot_number) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Hackle
|
2
|
+
class Event
|
3
|
+
|
4
|
+
# @!attribute [r] key
|
5
|
+
# @return [String]
|
6
|
+
# @!attribute [r] value
|
7
|
+
# @return [Float, nil]
|
8
|
+
# @!attribute [r] properties
|
9
|
+
# @return [Hash]
|
10
|
+
attr_reader :key, :value, :properties
|
11
|
+
|
12
|
+
|
13
|
+
# @param key [String]
|
14
|
+
# @param value [Float, nil]
|
15
|
+
# @param properties [Hash{Symbol => String, Number, boolean}]
|
16
|
+
def initialize(key:, value:, properties:)
|
17
|
+
@key = key
|
18
|
+
@value = value
|
19
|
+
@properties = properties
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
!key.nil? && key.is_a?(String)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Hackle
|
2
|
+
class EventType
|
3
|
+
|
4
|
+
# @!attribute [r] id
|
5
|
+
# @return [Integer]
|
6
|
+
# @!attribute [r] key
|
7
|
+
# @return [String]
|
8
|
+
attr_reader :id, :key
|
9
|
+
|
10
|
+
# @param id [Integer]
|
11
|
+
# @param key [String]
|
12
|
+
def initialize(id:, key:)
|
13
|
+
@id = id
|
14
|
+
@key = key
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param key [String]
|
18
|
+
def self.undefined(key:)
|
19
|
+
EventType.new(id: 0, key: key)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Hackle
|
2
|
+
class Experiment
|
3
|
+
|
4
|
+
# @!attribute [r] id
|
5
|
+
# @return [Integer]
|
6
|
+
# @!attribute [r] key
|
7
|
+
# @return [Integer]
|
8
|
+
attr_reader :id, :key
|
9
|
+
|
10
|
+
# @param id [Integer]
|
11
|
+
# @param key [Integer]
|
12
|
+
def initialize(id:, key:)
|
13
|
+
@id = id
|
14
|
+
@key = key
|
15
|
+
end
|
16
|
+
|
17
|
+
class Running < Experiment
|
18
|
+
|
19
|
+
# @!attribute [r] bucket
|
20
|
+
# @return [Bucket]
|
21
|
+
attr_reader :bucket
|
22
|
+
|
23
|
+
# @param id [Integer]
|
24
|
+
# @param key [Integer]
|
25
|
+
# @param bucket [Bucket]
|
26
|
+
# @param variations [Hash{String => Variation}]
|
27
|
+
# @param overrides [Hash{String => Integer}]
|
28
|
+
def initialize(id:, key:, bucket:, variations:, overrides:)
|
29
|
+
super(id: id, key: key)
|
30
|
+
@bucket = bucket
|
31
|
+
|
32
|
+
# @type [Hash{String => Variation}]
|
33
|
+
@variations = variations
|
34
|
+
|
35
|
+
# @type [Hash{String => Integer}]
|
36
|
+
@overrides = overrides
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param variation_id [Integer]
|
40
|
+
# @return [Variation, nil]
|
41
|
+
def get_variation(variation_id:)
|
42
|
+
@variations[variation_id]
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param user [User]
|
46
|
+
# @return [Variation, nil]
|
47
|
+
def get_overridden_variation(user:)
|
48
|
+
overridden_variation_id = @overrides[user.id]
|
49
|
+
return nil if overridden_variation_id.nil?
|
50
|
+
get_variation(variation_id: overridden_variation_id)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Completed < Experiment
|
55
|
+
|
56
|
+
# @!attribute [r] winner_variation_key
|
57
|
+
# @return [String]
|
58
|
+
attr_reader :winner_variation_key
|
59
|
+
|
60
|
+
# @param id [Integer]
|
61
|
+
# @param key [Integer]
|
62
|
+
# @param winner_variation_key [String]
|
63
|
+
def initialize(id:, key:, winner_variation_key:)
|
64
|
+
super(id: id, key: key)
|
65
|
+
@winner_variation_key = winner_variation_key
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Hackle
|
2
|
+
class Slot
|
3
|
+
# @!attribute variation_id
|
4
|
+
# @return [Integer]
|
5
|
+
attr_reader :variation_id
|
6
|
+
|
7
|
+
# @param start_inclusive [Integer]
|
8
|
+
# @param end_exclusive [Integer]
|
9
|
+
# @param variation_id [Integer]
|
10
|
+
def initialize(start_inclusive:, end_exclusive:, variation_id:)
|
11
|
+
@start_inclusive = start_inclusive
|
12
|
+
@end_exclusive = end_exclusive
|
13
|
+
@variation_id = variation_id
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param slot_number [Integer]
|
17
|
+
# @return [boolean]
|
18
|
+
def contains?(slot_number:)
|
19
|
+
@start_inclusive <= slot_number && slot_number < @end_exclusive
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Hackle
|
2
|
+
|
3
|
+
class User
|
4
|
+
|
5
|
+
# @!attribute [r] id
|
6
|
+
# @return [String]
|
7
|
+
# @!attribute [r] properties
|
8
|
+
# @return [Hash]
|
9
|
+
attr_reader :id, :properties
|
10
|
+
|
11
|
+
#
|
12
|
+
# @param id [String]
|
13
|
+
# @param properties [Hash]
|
14
|
+
#
|
15
|
+
def initialize(id:, properties:)
|
16
|
+
@id = id
|
17
|
+
@properties = properties
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
!id.nil? && id.is_a?(String)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Hackle
|
2
|
+
class Variation
|
3
|
+
|
4
|
+
# @!attribute id
|
5
|
+
# @return [Integer]
|
6
|
+
# @!attribute key
|
7
|
+
# @return [String]
|
8
|
+
# @!attribute dropped
|
9
|
+
# @return [boolean]
|
10
|
+
attr_reader :id, :key, :dropped
|
11
|
+
|
12
|
+
# @param id [Integer]
|
13
|
+
# @param key [String]
|
14
|
+
# @param dropped [boolean]
|
15
|
+
def initialize(id:, key:, dropped:)
|
16
|
+
@id = id
|
17
|
+
@key = key
|
18
|
+
@dropped = dropped
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Hackle
|
4
|
-
VERSION = '0.0
|
4
|
+
VERSION = '1.0.0'
|
5
5
|
SDK_NAME = 'ruby-sdk'
|
6
6
|
|
7
7
|
class SdkInfo
|
8
8
|
attr_reader :key, :name, :version
|
9
|
-
def initialize(key)
|
9
|
+
def initialize(key:)
|
10
10
|
@key = key
|
11
11
|
@name = SDK_NAME
|
12
12
|
@version = VERSION
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Hackle
|
6
|
+
class HttpWorkspaceFetcher
|
7
|
+
|
8
|
+
def initialize(config:, sdk_info:)
|
9
|
+
@client = HTTP.client(base_uri: config.base_uri)
|
10
|
+
@headers = HTTP.sdk_headers(sdk_info: sdk_info)
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch
|
14
|
+
request = Net::HTTP::Get.new('/api/v1/workspaces', @headers)
|
15
|
+
response = @client.request(request)
|
16
|
+
|
17
|
+
status_code = response.code.to_i
|
18
|
+
HTTP.check_successful(status_code: status_code)
|
19
|
+
|
20
|
+
response_body = JSON.parse(response.body, symbolize_names: true)
|
21
|
+
Workspace.create(data: response_body)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|