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.
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