hackle-ruby-sdk 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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