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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -3
  3. data/hackle-ruby-sdk.gemspec +1 -1
  4. data/lib/hackle-ruby-sdk.rb +2 -21
  5. data/lib/hackle.rb +76 -0
  6. data/lib/hackle/client.rb +127 -0
  7. data/lib/{hackle-ruby-sdk → hackle}/config.rb +1 -1
  8. data/lib/hackle/decision/bucketer.rb +44 -0
  9. data/lib/hackle/decision/decider.rb +69 -0
  10. data/lib/{hackle-ruby-sdk → hackle}/events/event_dispatcher.rb +31 -24
  11. data/lib/{hackle-ruby-sdk → hackle}/events/event_processor.rb +22 -11
  12. data/lib/hackle/events/user_event.rb +61 -0
  13. data/lib/{hackle-ruby-sdk → hackle}/http/http.rb +7 -9
  14. data/lib/hackle/models/bucket.rb +26 -0
  15. data/lib/hackle/models/event.rb +26 -0
  16. data/lib/hackle/models/event_type.rb +22 -0
  17. data/lib/hackle/models/experiment.rb +69 -0
  18. data/lib/hackle/models/slot.rb +22 -0
  19. data/lib/hackle/models/user.rb +24 -0
  20. data/lib/hackle/models/variation.rb +21 -0
  21. data/lib/{hackle-ruby-sdk → hackle}/version.rb +2 -2
  22. data/lib/hackle/workspaces/http_workspace_fetcher.rb +24 -0
  23. data/lib/{hackle-ruby-sdk → hackle}/workspaces/polling_workspace_fetcher.rb +4 -1
  24. data/lib/hackle/workspaces/workspace.rb +100 -0
  25. metadata +22 -19
  26. data/lib/hackle-ruby-sdk/client.rb +0 -108
  27. data/lib/hackle-ruby-sdk/decision/bucketer.rb +0 -26
  28. data/lib/hackle-ruby-sdk/decision/decider.rb +0 -54
  29. data/lib/hackle-ruby-sdk/events/event.rb +0 -33
  30. data/lib/hackle-ruby-sdk/models/bucket.rb +0 -15
  31. data/lib/hackle-ruby-sdk/models/event_type.rb +0 -10
  32. data/lib/hackle-ruby-sdk/models/experiment.rb +0 -39
  33. data/lib/hackle-ruby-sdk/models/slot.rb +0 -15
  34. data/lib/hackle-ruby-sdk/models/variation.rb +0 -11
  35. data/lib/hackle-ruby-sdk/workspaces/http_workspace_fetcher.rb +0 -24
  36. 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
- def initialize(config, event_dispatcher)
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(event_dispatcher, config)
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
- @message_processor.produce(Message::Shutdown.new, non_block: false)
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
- def process(event)
38
- @message_processor.produce(Message::Event.new(event))
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, config)
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
- def produce(message, non_block: true)
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
- def consume_event(event)
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
- raise UnexpectedResponseError.new(status_code) unless successful?(status_code)
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.1'
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