hackle-ruby-sdk 0.0.1 → 1.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/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
|