hackle-ruby-sdk 0.0.2 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2f680a4a83a54fdb059ce05bc943174d06c254511bd34c1928a723ad8005610
4
- data.tar.gz: 4aae82d21a24eb77f11817f45b86d598e48f950372bef78acddff945d9d60e9c
3
+ metadata.gz: 66d5d7b02005a6256ff31ee8287a5808ad8ed0cbb01cde5ee153b54e587eb5f1
4
+ data.tar.gz: d5703e1a1f9a23a7f03ff44272a4c066ce4fd5b39ac9bb88ad51d873be663453
5
5
  SHA512:
6
- metadata.gz: c07b2e4bd0cd34d4e6499fa9e52c360da6d7319be32753c8908e2384622bbc2b2829156afcbb43d154f71475bfc532e6cb81ac5e79bfac422d8e0321d5c0ea9b
7
- data.tar.gz: af4dd802a0ec793ce0469d0d5c2b2d25e6a2e5211d6231add6887905c58b9d40cd59e11db14decd751b668df9903eae6141eaa31aefd8011b9f4d73e4ad648d4
6
+ metadata.gz: 5856fba092210cd4ae4993b3915de1a357719d54b83a4e3e2c422b42641d1e7cffaa7f510ba282f89cb0278944c9b9697e79fef36c363d091e53ea38f7b2fe53
7
+ data.tar.gz: fdffea3e37dd30685ae8f7cbc50282cd751a07d863795c9874a8b4aaad4b03b3e2c8be71e963681307f6024533ee969ee4c92b7fbc2765d33b134e4270484fca
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Hackle::Ruby::Sdk
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hackle/ruby/sdk`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hackle`.
6
4
 
7
5
  ## Installation
8
6
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require 'hackle-ruby-sdk/version'
5
+ require 'hackle/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = 'hackle-ruby-sdk'
@@ -1,22 +1,3 @@
1
- require 'hackle-ruby-sdk/decision/bucketer'
2
- require 'hackle-ruby-sdk/decision/decider'
1
+ # frozen_string_literal: true
3
2
 
4
- require 'hackle-ruby-sdk/events/event'
5
- require 'hackle-ruby-sdk/events/event_dispatcher'
6
- require 'hackle-ruby-sdk/events/event_processor'
7
-
8
- require 'hackle-ruby-sdk/http/http'
9
-
10
- require 'hackle-ruby-sdk/models/bucket'
11
- require 'hackle-ruby-sdk/models/event_type'
12
- require 'hackle-ruby-sdk/models/experiment'
13
- require 'hackle-ruby-sdk/models/slot'
14
- require 'hackle-ruby-sdk/models/variation'
15
-
16
- require 'hackle-ruby-sdk/workspaces/http_workspace_fetcher'
17
- require 'hackle-ruby-sdk/workspaces/polling_workspace_fetcher'
18
- require 'hackle-ruby-sdk/workspaces/workspace'
19
-
20
- require 'hackle-ruby-sdk/client'
21
- require 'hackle-ruby-sdk/config'
22
- require 'hackle-ruby-sdk/version'
3
+ require 'hackle'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hackle/client'
4
+ require 'hackle/config'
5
+ require 'hackle/version'
6
+
7
+ module Hackle
8
+
9
+ #
10
+ # Instantiates a Hackle client.
11
+ #
12
+ # @see Client#initialize
13
+ #
14
+ # @param sdk_key [String] The SDK key of your Hackle environment
15
+ # @param config [Config] An optional client configuration
16
+ #
17
+ # @return [Client] The Hackle client instance.
18
+ #
19
+ def self.client(sdk_key:, **options)
20
+ config = Config.new(options)
21
+ sdk_info = SdkInfo.new(key: sdk_key)
22
+
23
+ http_workspace_fetcher = HttpWorkspaceFetcher.new(config: config, sdk_info: sdk_info)
24
+ polling_workspace_fetcher = PollingWorkspaceFetcher.new(config: config, http_fetcher: http_workspace_fetcher)
25
+
26
+ event_dispatcher = EventDispatcher.new(config: config, sdk_info: sdk_info)
27
+ event_processor = EventProcessor.new(config: config, event_dispatcher: event_dispatcher)
28
+
29
+ polling_workspace_fetcher.start!
30
+ event_processor.start!
31
+
32
+ Client.new(
33
+ config: config,
34
+ workspace_fetcher: polling_workspace_fetcher,
35
+ event_processor: event_processor,
36
+ decider: Decider.new
37
+ )
38
+ end
39
+ end
@@ -1,12 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'hackle/decision/bucketer'
4
+ require 'hackle/decision/decider'
5
+
6
+ require 'hackle/events/event'
7
+ require 'hackle/events/event_dispatcher'
8
+ require 'hackle/events/event_processor'
9
+
10
+ require 'hackle/http/http'
11
+
12
+ require 'hackle/models/bucket'
13
+ require 'hackle/models/event_type'
14
+ require 'hackle/models/experiment'
15
+ require 'hackle/models/slot'
16
+ require 'hackle/models/variation'
17
+
18
+ require 'hackle/workspaces/http_workspace_fetcher'
19
+ require 'hackle/workspaces/polling_workspace_fetcher'
20
+ require 'hackle/workspaces/workspace'
21
+
3
22
  module Hackle
4
23
 
5
24
  #
6
25
  # A client for Hackle API.
7
26
  #
8
27
  class Client
9
- def initialize(config, workspace_fetcher, event_processor, decider)
28
+
29
+ #
30
+ # Initializes a Hackle client.
31
+ #
32
+ # @param config [Config]
33
+ # @param workspace_fetcher [PollingWorkspaceFetcher]
34
+ # @param event_processor [EventProcessor]
35
+ # @param decider [Decider]
36
+ #
37
+ def initialize(config:, workspace_fetcher:, event_processor:, decider:)
10
38
  @logger = config.logger
11
39
  @workspace_fetcher = workspace_fetcher
12
40
  @event_processor = event_processor
@@ -28,7 +56,7 @@ module Hackle
28
56
  #
29
57
  # @return [String] The decided variation for the user, or default variation
30
58
  #
31
- def variation(experiment_key, user_id, default_variation = 'A')
59
+ def variation(experiment_key:, user_id:, default_variation: 'A')
32
60
 
33
61
  return default_variation if experiment_key.nil?
34
62
  return default_variation if user_id.nil?
@@ -36,17 +64,18 @@ module Hackle
36
64
  workspace = @workspace_fetcher.fetch
37
65
  return default_variation if workspace.nil?
38
66
 
39
- experiment = workspace.get_experiment(experiment_key)
67
+ experiment = workspace.get_experiment(experiment_key: experiment_key)
40
68
  return default_variation if experiment.nil?
41
69
 
42
- decision = @decider.decide(experiment, user_id)
70
+ decision = @decider.decide(experiment: experiment, user_id: user_id)
43
71
  case decision
44
72
  when Decision::NotAllocated
45
73
  default_variation
46
74
  when Decision::ForcedAllocated
47
75
  decision.variation_key
48
76
  when Decision::NaturalAllocated
49
- @event_processor.process(Event::Exposure.new(user_id, experiment, decision.variation))
77
+ exposure_event = Event::Exposure.new(user_id: user_id, experiment: experiment, variation: decision.variation)
78
+ @event_processor.process(event: exposure_event)
50
79
  decision.variation.key
51
80
  else
52
81
  default_variation
@@ -60,7 +89,7 @@ module Hackle
60
89
  # @param user_id [String] The identifier of user that performed the vent.
61
90
  # @param value [Float] Additional numeric value of the events (e.g. purchase_amount, api_latency, etc.)
62
91
  #
63
- def track(event_key, user_id, value = nil)
92
+ def track(event_key:, user_id:, value: nil)
64
93
 
65
94
  return if event_key.nil?
66
95
  return if user_id.nil?
@@ -68,10 +97,11 @@ module Hackle
68
97
  workspace = @workspace_fetcher.fetch
69
98
  return if workspace.nil?
70
99
 
71
- event_type = workspace.get_event_type(event_key)
100
+ event_type = workspace.get_event_type(event_type_key: event_key)
72
101
  return if event_type.nil?
73
102
 
74
- @event_processor.process(Event::Track.new(user_id, event_type, value))
103
+ track_event = Event::Track.new(user_id: user_id, event_type: event_type, value: value)
104
+ @event_processor.process(event: track_event)
75
105
  end
76
106
 
77
107
  #
@@ -81,28 +111,5 @@ module Hackle
81
111
  @workspace_fetcher.stop!
82
112
  @event_processor.stop!
83
113
  end
84
-
85
- #
86
- # Instantiates a Hackle client.
87
- #
88
- # @param sdk_key [String] The SDK key of your Hackle environment
89
- # @param config [Config] An optional client configuration
90
- #
91
- # @return [Client] The Hackle client instance.
92
- #
93
- def self.create(sdk_key, config = Config.new)
94
- sdk_info = SdkInfo.new(sdk_key)
95
-
96
- http_workspace_fetcher = HttpWorkspaceFetcher.new(config, sdk_info)
97
- polling_workspace_fetcher = PollingWorkspaceFetcher.new(config, http_workspace_fetcher)
98
-
99
- event_dispatcher = EventDispatcher.new(config, sdk_info)
100
- event_processor = EventProcessor.new(config, event_dispatcher)
101
-
102
- polling_workspace_fetcher.start!
103
- event_processor.start!
104
-
105
- Client.new(config, polling_workspace_fetcher, event_processor, Decider.new)
106
- end
107
114
  end
108
115
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'murmurhash3'
4
+
5
+ module Hackle
6
+ class Bucketer
7
+ def bucketing(bucket:, user_id:)
8
+ slot_number = calculate_slot_number(
9
+ seed: bucket.seed,
10
+ slot_size: bucket.slot_size,
11
+ user_id: user_id
12
+ )
13
+ bucket.get_slot(slot_number: slot_number)
14
+ end
15
+
16
+ def calculate_slot_number(seed:, slot_size:, user_id:)
17
+ hash_value = hash(data: user_id, seed: seed)
18
+ hash_value.abs % slot_size
19
+ end
20
+
21
+ def hash(data:, seed:)
22
+ unsigned_value = MurmurHash3::V32.str_hash(data, seed)
23
+ if (unsigned_value & 0x80000000).zero?
24
+ unsigned_value
25
+ else
26
+ -((unsigned_value ^ 0xFFFFFFFF) + 1)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hackle
4
+ class Decision
5
+ class NotAllocated < Decision
6
+ end
7
+
8
+ class ForcedAllocated < Decision
9
+ attr_reader :variation_key
10
+
11
+ def initialize(variation_key:)
12
+ @variation_key = variation_key
13
+ end
14
+ end
15
+
16
+ class NaturalAllocated < Decision
17
+ attr_reader :variation
18
+
19
+ def initialize(variation:)
20
+ @variation = variation
21
+ end
22
+ end
23
+ end
24
+
25
+ class Decider
26
+ def initialize
27
+ @bucketer = Bucketer.new
28
+ end
29
+
30
+ def decide(experiment:, user_id:)
31
+ case experiment
32
+ when Experiment::Completed
33
+ Decision::ForcedAllocated.new(variation_key: experiment.winner_variation_key)
34
+ when Experiment::Running
35
+ decide_running(running_experiment: experiment, user_id: user_id)
36
+ end
37
+ end
38
+
39
+ def decide_running(running_experiment:, user_id:)
40
+
41
+ overridden_variation = running_experiment.get_overridden_variation(user_id: user_id)
42
+ return Decision::ForcedAllocated.new(variation_key: overridden_variation.key) unless overridden_variation.nil?
43
+
44
+ allocated_slot = @bucketer.bucketing(bucket: running_experiment.bucket, user_id: user_id)
45
+ return Decision::NotAllocated.new if allocated_slot.nil?
46
+
47
+ allocated_variation = running_experiment.get_variation(variation_id: allocated_slot.variation_id)
48
+ return Decision::NotAllocated.new if allocated_variation.nil?
49
+ return Decision::NotAllocated.new if allocated_variation.dropped
50
+
51
+ Decision::NaturalAllocated.new(variation: allocated_variation)
52
+ end
53
+ end
54
+ end
@@ -7,7 +7,7 @@ module Hackle
7
7
  class Exposure < Event
8
8
  attr_reader :experiment, :variation
9
9
 
10
- def initialize(user_id, experiment, variation)
10
+ def initialize(user_id:, experiment:, variation:)
11
11
  @timestamp = Event.generate_timestamp
12
12
  @user_id = user_id
13
13
  @experiment = experiment
@@ -18,7 +18,7 @@ module Hackle
18
18
  class Track < Event
19
19
  attr_reader :event_type, :value
20
20
 
21
- def initialize(user_id, event_type, value = nil)
21
+ def initialize(user_id:, event_type:, value: nil)
22
22
  @timestamp = Event.generate_timestamp
23
23
  @user_id = user_id
24
24
  @event_type = event_type
@@ -6,10 +6,10 @@ module Hackle
6
6
  DEFAULT_DISPATCH_WORKER_SIZE = 2
7
7
  DEFAULT_DISPATCH_QUEUE_CAPACITY = 50
8
8
 
9
- def initialize(config, sdk_info)
9
+ def initialize(config:, sdk_info:)
10
10
  @logger = config.logger
11
- @client = HTTP.client(config.event_uri)
12
- @headers = HTTP.sdk_headers(sdk_info)
11
+ @client = HTTP.client(base_uri: config.event_uri)
12
+ @headers = HTTP.sdk_headers(sdk_info: sdk_info)
13
13
  @dispatcher_executor = Concurrent::ThreadPoolExecutor.new(
14
14
  min_threads: DEFAULT_DISPATCH_WORKER_SIZE,
15
15
  max_threads: DEFAULT_DISPATCH_WORKER_SIZE,
@@ -17,10 +17,10 @@ module Hackle
17
17
  )
18
18
  end
19
19
 
20
- def dispatch(events)
21
- payload = create_payload(events)
20
+ def dispatch(events:)
21
+ payload = create_payload(events: events)
22
22
  begin
23
- @dispatcher_executor.post { dispatch_payload(payload) }
23
+ @dispatcher_executor.post { dispatch_payload(payload: payload) }
24
24
  rescue Concurrent::RejectedExecutionError
25
25
  @logger.warn { 'Dispatcher executor queue is full. Event dispatch rejected' }
26
26
  end
@@ -35,7 +35,7 @@ module Hackle
35
35
 
36
36
  private
37
37
 
38
- def dispatch_payload(payload)
38
+ def dispatch_payload(payload:)
39
39
  request = Net::HTTP::Post.new('/api/v1/events', @headers)
40
40
  request.content_type = 'application/json'
41
41
  request.body = payload.to_json
@@ -43,12 +43,12 @@ module Hackle
43
43
  response = @client.request(request)
44
44
 
45
45
  status_code = response.code.to_i
46
- HTTP.check_successful(status_code)
46
+ HTTP.check_successful(status_code: status_code)
47
47
  rescue => e
48
48
  @logger.error { "Failed to dispatch events: #{e.inspect}" }
49
49
  end
50
50
 
51
- def create_payload(events)
51
+ def create_payload(events:)
52
52
  exposure_events = []
53
53
  track_events = []
54
54
  events.each do |event|
@@ -6,10 +6,10 @@ module Hackle
6
6
 
7
7
  DEFAULT_FLUSH_INTERVAL = 10
8
8
 
9
- def initialize(config, event_dispatcher)
9
+ def initialize(config:, event_dispatcher:)
10
10
  @logger = config.logger
11
11
  @event_dispatcher = event_dispatcher
12
- @message_processor = MessageProcessor.new(event_dispatcher, config)
12
+ @message_processor = MessageProcessor.new(config: config, event_dispatcher: event_dispatcher)
13
13
  @flush_task = Concurrent::TimerTask.new(execution_interval: DEFAULT_FLUSH_INTERVAL) { flush }
14
14
  @consume_task = nil
15
15
  @running = false
@@ -26,7 +26,7 @@ module Hackle
26
26
  def stop!
27
27
  return unless @running
28
28
 
29
- @message_processor.produce(Message::Shutdown.new, non_block: false)
29
+ @message_processor.produce(message: Message::Shutdown.new, non_block: false)
30
30
  @consume_task.join(10)
31
31
  @flush_task.shutdown
32
32
  @event_dispatcher.shutdown
@@ -34,12 +34,12 @@ module Hackle
34
34
  @running = false
35
35
  end
36
36
 
37
- def process(event)
38
- @message_processor.produce(Message::Event.new(event))
37
+ def process(event:)
38
+ @message_processor.produce(message: Message::Event.new(event))
39
39
  end
40
40
 
41
41
  def flush
42
- @message_processor.produce(Message::Flush.new)
42
+ @message_processor.produce(message: Message::Flush.new)
43
43
  end
44
44
 
45
45
  class Message
@@ -63,7 +63,7 @@ module Hackle
63
63
  DEFAULT_MESSAGE_QUEUE_CAPACITY = 1000
64
64
  DEFAULT_MAX_EVENT_DISPATCH_SIZE = 500
65
65
 
66
- def initialize(event_dispatcher, config)
66
+ def initialize(config:, event_dispatcher:)
67
67
  @logger = config.logger
68
68
  @event_dispatcher = event_dispatcher
69
69
  @message_queue = SizedQueue.new(DEFAULT_MESSAGE_QUEUE_CAPACITY)
@@ -71,7 +71,7 @@ module Hackle
71
71
  @consumed_events = []
72
72
  end
73
73
 
74
- def produce(message, non_block: true)
74
+ def produce(message:, non_block: true)
75
75
  @message_queue.push(message, non_block)
76
76
  rescue ThreadError
77
77
  if @random.rand(1..100) == 1 # log only 1% of the time
@@ -84,7 +84,7 @@ module Hackle
84
84
  message = @message_queue.pop
85
85
  case message
86
86
  when Message::Event
87
- consume_event(message.event)
87
+ consume_event(event: message.event)
88
88
  when Message::Flush
89
89
  dispatch_events
90
90
  when Message::Shutdown
@@ -99,7 +99,7 @@ module Hackle
99
99
 
100
100
  private
101
101
 
102
- def consume_event(event)
102
+ def consume_event(event:)
103
103
  @consumed_events << event
104
104
  dispatch_events if @consumed_events.length >= DEFAULT_MAX_EVENT_DISPATCH_SIZE
105
105
  end
@@ -107,7 +107,7 @@ module Hackle
107
107
  def dispatch_events
108
108
  return if @consumed_events.empty?
109
109
 
110
- @event_dispatcher.dispatch(@consumed_events)
110
+ @event_dispatcher.dispatch(events: @consumed_events)
111
111
  @consumed_events = []
112
112
  end
113
113
  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
@@ -2,14 +2,14 @@ module Hackle
2
2
  class Bucket
3
3
  attr_reader :seed, :slot_size
4
4
 
5
- def initialize(seed, slot_size, slots)
5
+ def initialize(seed:, slot_size:, slots:)
6
6
  @seed = seed
7
7
  @slot_size = slot_size
8
8
  @slots = slots
9
9
  end
10
10
 
11
- def get_slot(slot_number)
12
- @slots.find { |slot| slot.contains?(slot_number) }
11
+ def get_slot(slot_number:)
12
+ @slots.find { |slot| slot.contains?(slot_number: slot_number) }
13
13
  end
14
14
  end
15
15
  end
@@ -2,7 +2,7 @@ module Hackle
2
2
  class EventType
3
3
  attr_reader :id, :key
4
4
 
5
- def initialize(id, key)
5
+ def initialize(id:, key:)
6
6
  @id = id
7
7
  @key = key
8
8
  end
@@ -2,36 +2,33 @@ module Hackle
2
2
  class Experiment
3
3
  attr_reader :id, :key
4
4
 
5
- def initialize(id, key)
6
- @id = id
7
- @key = key
8
- end
9
-
10
5
  class Running < Experiment
11
6
  attr_reader :bucket
12
7
 
13
- def initialize(id, key, bucket, variations, user_overrides)
14
- super(id, key)
8
+ def initialize(id:, key:, bucket:, variations:, user_overrides:)
9
+ @id = id
10
+ @key = key
15
11
  @bucket = bucket
16
12
  @variations = variations
17
13
  @user_overrides = user_overrides
18
14
  end
19
15
 
20
- def get_variation(variation_id)
16
+ def get_variation(variation_id:)
21
17
  @variations[variation_id]
22
18
  end
23
19
 
24
- def get_overridden_variation(user_id)
20
+ def get_overridden_variation(user_id:)
25
21
  variation_id = @user_overrides[user_id]
26
- get_variation(variation_id)
22
+ get_variation(variation_id: variation_id)
27
23
  end
28
24
  end
29
25
 
30
26
  class Completed < Experiment
31
27
  attr_reader :winner_variation_key
32
28
 
33
- def initialize(id, key, winner_variation_key)
34
- super(id, key)
29
+ def initialize(id:, key:, winner_variation_key:)
30
+ @id = id
31
+ @key = key
35
32
  @winner_variation_key = winner_variation_key
36
33
  end
37
34
  end
@@ -2,13 +2,13 @@ module Hackle
2
2
  class Slot
3
3
  attr_reader :variation_id
4
4
 
5
- def initialize(start_inclusive, end_exclusive, variation_id)
5
+ def initialize(start_inclusive:, end_exclusive:, variation_id:)
6
6
  @start_inclusive = start_inclusive
7
7
  @end_exclusive = end_exclusive
8
8
  @variation_id = variation_id
9
9
  end
10
10
 
11
- def contains?(slot_number)
11
+ def contains?(slot_number:)
12
12
  @start_inclusive <= slot_number && slot_number < @end_exclusive
13
13
  end
14
14
  end
@@ -2,7 +2,7 @@ module Hackle
2
2
  class Variation
3
3
  attr_reader :id, :key, :dropped
4
4
 
5
- def initialize(id, key, dropped)
5
+ def initialize(id:, key:, dropped:)
6
6
  @id = id
7
7
  @key = key
8
8
  @dropped = dropped
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hackle
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.3'
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
@@ -7,7 +7,7 @@ module Hackle
7
7
 
8
8
  DEFAULT_POLLING_INTERVAL = 10
9
9
 
10
- def initialize(config, http_fetcher)
10
+ def initialize(config:, http_fetcher:)
11
11
  @logger = config.logger
12
12
  @http_fetcher = http_fetcher
13
13
  @current_workspace = Concurrent::AtomicReference.new
@@ -1,76 +1,79 @@
1
1
  module Hackle
2
2
  class Workspace
3
- def initialize(experiments, event_types)
3
+ def initialize(experiments:, event_types:)
4
4
  @experiments = experiments
5
5
  @event_types = event_types
6
6
  end
7
7
 
8
- def get_experiment(experiment_key)
8
+ def get_experiment(experiment_key:)
9
9
  @experiments[experiment_key]
10
10
  end
11
11
 
12
- def get_event_type(event_type_key)
12
+ def get_event_type(event_type_key:)
13
13
  @event_types[event_type_key]
14
14
  end
15
15
 
16
16
  class << self
17
- def create(data)
17
+ def create(data:)
18
18
  buckets = Hash[data[:buckets].map { |b| [b[:id], bucket(b)] }]
19
19
  running_experiments = Hash[data[:experiments].map { |re| [re[:key], running_experiment(re, buckets)] }]
20
20
  completed_experiment = Hash[data[:completedExperiments].map { |ce| [ce[:experimentKey], completed_experiment(ce)] }]
21
21
  event_types = Hash[data[:events].map { |e| [e[:key], event_type(e)] }]
22
22
  experiments = running_experiments.merge(completed_experiment)
23
- Workspace.new(experiments, event_types)
23
+ Workspace.new(
24
+ experiments: experiments,
25
+ event_types: event_types
26
+ )
24
27
  end
25
28
 
26
29
  private
27
30
 
28
31
  def running_experiment(data, buckets)
29
32
  Experiment::Running.new(
30
- data[:id],
31
- data[:key],
32
- buckets[data[:bucketId]],
33
- Hash[data[:variations].map { |v| [v[:id], variation(v)] }],
34
- Hash[data[:execution][:userOverrides].map { |u| [u[:userId], u[:variationId]] }]
33
+ id: data[:id],
34
+ key: data[:key],
35
+ bucket: buckets[data[:bucketId]],
36
+ variations: Hash[data[:variations].map { |v| [v[:id], variation(v)] }],
37
+ user_overrides: Hash[data[:execution][:userOverrides].map { |u| [u[:userId], u[:variationId]] }]
35
38
  )
36
39
  end
37
40
 
38
41
  def completed_experiment(data)
39
42
  Experiment::Completed.new(
40
- data[:experimentId],
41
- data[:experimentKey],
42
- data[:winnerVariationKey]
43
+ id: data[:experimentId],
44
+ key: data[:experimentKey],
45
+ winner_variation_key: data[:winnerVariationKey]
43
46
  )
44
47
  end
45
48
 
46
49
  def variation(data)
47
50
  Variation.new(
48
- data[:id],
49
- data[:key],
50
- data[:status] == 'DROPPED'
51
+ id: data[:id],
52
+ key: data[:key],
53
+ dropped: data[:status] == 'DROPPED'
51
54
  )
52
55
  end
53
56
 
54
57
  def bucket(data)
55
58
  Bucket.new(
56
- data[:seed],
57
- data[:slotSize],
58
- data[:slots].map { |s| slot(s) }
59
+ seed: data[:seed],
60
+ slot_size: data[:slotSize],
61
+ slots: data[:slots].map { |s| slot(s) }
59
62
  )
60
63
  end
61
64
 
62
65
  def slot(data)
63
66
  Slot.new(
64
- data[:startInclusive],
65
- data[:endExclusive],
66
- data[:variationId]
67
+ start_inclusive: data[:startInclusive],
68
+ end_exclusive: data[:endExclusive],
69
+ variation_id: data[:variationId]
67
70
  )
68
71
  end
69
72
 
70
73
  def event_type(data)
71
74
  EventType.new(
72
- data[:id],
73
- data[:key]
75
+ id: data[:id],
76
+ key: data[:key]
74
77
  )
75
78
  end
76
79
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hackle-ruby-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hackle
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-21 00:00:00.000000000 Z
11
+ date: 2020-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -137,23 +137,24 @@ files:
137
137
  - Rakefile
138
138
  - hackle-ruby-sdk.gemspec
139
139
  - lib/hackle-ruby-sdk.rb
140
- - lib/hackle-ruby-sdk/client.rb
141
- - lib/hackle-ruby-sdk/config.rb
142
- - lib/hackle-ruby-sdk/decision/bucketer.rb
143
- - lib/hackle-ruby-sdk/decision/decider.rb
144
- - lib/hackle-ruby-sdk/events/event.rb
145
- - lib/hackle-ruby-sdk/events/event_dispatcher.rb
146
- - lib/hackle-ruby-sdk/events/event_processor.rb
147
- - lib/hackle-ruby-sdk/http/http.rb
148
- - lib/hackle-ruby-sdk/models/bucket.rb
149
- - lib/hackle-ruby-sdk/models/event_type.rb
150
- - lib/hackle-ruby-sdk/models/experiment.rb
151
- - lib/hackle-ruby-sdk/models/slot.rb
152
- - lib/hackle-ruby-sdk/models/variation.rb
153
- - lib/hackle-ruby-sdk/version.rb
154
- - lib/hackle-ruby-sdk/workspaces/http_workspace_fetcher.rb
155
- - lib/hackle-ruby-sdk/workspaces/polling_workspace_fetcher.rb
156
- - lib/hackle-ruby-sdk/workspaces/workspace.rb
140
+ - lib/hackle.rb
141
+ - lib/hackle/client.rb
142
+ - lib/hackle/config.rb
143
+ - lib/hackle/decision/bucketer.rb
144
+ - lib/hackle/decision/decider.rb
145
+ - lib/hackle/events/event.rb
146
+ - lib/hackle/events/event_dispatcher.rb
147
+ - lib/hackle/events/event_processor.rb
148
+ - lib/hackle/http/http.rb
149
+ - lib/hackle/models/bucket.rb
150
+ - lib/hackle/models/event_type.rb
151
+ - lib/hackle/models/experiment.rb
152
+ - lib/hackle/models/slot.rb
153
+ - lib/hackle/models/variation.rb
154
+ - lib/hackle/version.rb
155
+ - lib/hackle/workspaces/http_workspace_fetcher.rb
156
+ - lib/hackle/workspaces/polling_workspace_fetcher.rb
157
+ - lib/hackle/workspaces/workspace.rb
157
158
  homepage: https://github.com/hackle-io/hackle-ruby-sdk
158
159
  licenses:
159
160
  - Apache-2.0
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'murmurhash3'
4
-
5
- module Hackle
6
- class Bucketer
7
- def bucketing(bucket, user_id)
8
- slot_number = calculate_slot_number(bucket.seed, bucket.slot_size, user_id)
9
- bucket.get_slot(slot_number)
10
- end
11
-
12
- def calculate_slot_number(seed, slot_size, user_id)
13
- hash_value = hash(user_id, seed)
14
- hash_value.abs % slot_size
15
- end
16
-
17
- def hash(data, seed)
18
- unsigned_value = MurmurHash3::V32.str_hash(data, seed)
19
- if (unsigned_value & 0x80000000).zero?
20
- unsigned_value
21
- else
22
- -((unsigned_value ^ 0xFFFFFFFF) + 1)
23
- end
24
- end
25
- end
26
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hackle
4
- class Decision
5
- class NotAllocated < Decision
6
- end
7
-
8
- class ForcedAllocated < Decision
9
- attr_reader :variation_key
10
-
11
- def initialize(variation_key)
12
- @variation_key = variation_key
13
- end
14
- end
15
-
16
- class NaturalAllocated < Decision
17
- attr_reader :variation
18
-
19
- def initialize(variation)
20
- @variation = variation
21
- end
22
- end
23
- end
24
-
25
- class Decider
26
- def initialize
27
- @bucketer = Bucketer.new
28
- end
29
-
30
- def decide(experiment, user_id)
31
- case experiment
32
- when Experiment::Completed
33
- Decision::ForcedAllocated.new(experiment.winner_variation_key)
34
- when Experiment::Running
35
- decide_running(experiment, user_id)
36
- end
37
- end
38
-
39
- def decide_running(experiment, user_id)
40
-
41
- overridden_variation = experiment.get_overridden_variation(user_id)
42
- return Decision::ForcedAllocated.new(overridden_variation.key) unless overridden_variation.nil?
43
-
44
- allocated_slot = @bucketer.bucketing(experiment.bucket, user_id)
45
- return Decision::NotAllocated.new if allocated_slot.nil?
46
-
47
- allocated_variation = experiment.get_variation(allocated_slot.variation_id)
48
- return Decision::NotAllocated.new if allocated_variation.nil?
49
- return Decision::NotAllocated.new if allocated_variation.dropped
50
-
51
- Decision::NaturalAllocated.new(allocated_variation)
52
- end
53
- end
54
- end
@@ -1,24 +0,0 @@
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(config.base_uri)
10
- @headers = HTTP.sdk_headers(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)
19
-
20
- data = JSON.parse(response.body, symbolize_names: true)
21
- Workspace.create(data)
22
- end
23
- end
24
- end