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
@@ -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
@@ -15,6 +15,7 @@ module Hackle
15
15
  @running = false
16
16
  end
17
17
 
18
+ # @return [Workspace, nil]
18
19
  def fetch
19
20
  @current_workspace.get
20
21
  end
@@ -30,6 +31,8 @@ module Hackle
30
31
  def stop!
31
32
  return unless @running
32
33
 
34
+ @logger.info { 'Shutting down Hackle workspace_fetcher' }
35
+
33
36
  @task.shutdown
34
37
  @running = false
35
38
  end
@@ -0,0 +1,100 @@
1
+ module Hackle
2
+ class Workspace
3
+
4
+ # @param experiments [Hash{Integer => Experiment}]
5
+ # @param event_types [Hash{String => EventType}]
6
+ def initialize(experiments:, event_types:)
7
+
8
+ # @type [Hash{Integer => Experiment}]
9
+ @experiments = experiments
10
+
11
+ # @type [Hash{String => EventType}]
12
+ @event_types = event_types
13
+ end
14
+
15
+ # @param experiment_key [Integer]
16
+ #
17
+ # @return [Experiment, nil]
18
+ def get_experiment(experiment_key:)
19
+ @experiments[experiment_key]
20
+ end
21
+
22
+ # @param event_type_key [String]
23
+ #
24
+ # @return [EventType]
25
+ def get_event_type(event_type_key:)
26
+ event_type = @event_types[event_type_key]
27
+
28
+ if event_type.nil?
29
+ EventType.undefined(key: event_type_key)
30
+ else
31
+ event_type
32
+ end
33
+ end
34
+
35
+ class << self
36
+ def create(data:)
37
+ buckets = Hash[data[:buckets].map { |b| [b[:id], bucket(b)] }]
38
+ running_experiments = Hash[data[:experiments].map { |re| [re[:key], running_experiment(re, buckets)] }]
39
+ completed_experiment = Hash[data[:completedExperiments].map { |ce| [ce[:experimentKey], completed_experiment(ce)] }]
40
+ event_types = Hash[data[:events].map { |e| [e[:key], event_type(e)] }]
41
+ experiments = running_experiments.merge(completed_experiment)
42
+ Workspace.new(
43
+ experiments: experiments,
44
+ event_types: event_types
45
+ )
46
+ end
47
+
48
+ private
49
+
50
+ def running_experiment(data, buckets)
51
+ Experiment::Running.new(
52
+ id: data[:id],
53
+ key: data[:key],
54
+ bucket: buckets[data[:bucketId]],
55
+ variations: Hash[data[:variations].map { |v| [v[:id], variation(v)] }],
56
+ overrides: Hash[data[:execution][:userOverrides].map { |u| [u[:userId], u[:variationId]] }]
57
+ )
58
+ end
59
+
60
+ def completed_experiment(data)
61
+ Experiment::Completed.new(
62
+ id: data[:experimentId],
63
+ key: data[:experimentKey],
64
+ winner_variation_key: data[:winnerVariationKey]
65
+ )
66
+ end
67
+
68
+ def variation(data)
69
+ Variation.new(
70
+ id: data[:id],
71
+ key: data[:key],
72
+ dropped: data[:status] == 'DROPPED'
73
+ )
74
+ end
75
+
76
+ def bucket(data)
77
+ Bucket.new(
78
+ seed: data[:seed],
79
+ slot_size: data[:slotSize],
80
+ slots: data[:slots].map { |s| slot(s) }
81
+ )
82
+ end
83
+
84
+ def slot(data)
85
+ Slot.new(
86
+ start_inclusive: data[:startInclusive],
87
+ end_exclusive: data[:endExclusive],
88
+ variation_id: data[:variationId]
89
+ )
90
+ end
91
+
92
+ def event_type(data)
93
+ EventType.new(
94
+ id: data[:id],
95
+ key: data[:key]
96
+ )
97
+ end
98
+ end
99
+ end
100
+ 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.1
4
+ version: 1.0.0
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-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -137,23 +137,26 @@ 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_dispatcher.rb
146
+ - lib/hackle/events/event_processor.rb
147
+ - lib/hackle/events/user_event.rb
148
+ - lib/hackle/http/http.rb
149
+ - lib/hackle/models/bucket.rb
150
+ - lib/hackle/models/event.rb
151
+ - lib/hackle/models/event_type.rb
152
+ - lib/hackle/models/experiment.rb
153
+ - lib/hackle/models/slot.rb
154
+ - lib/hackle/models/user.rb
155
+ - lib/hackle/models/variation.rb
156
+ - lib/hackle/version.rb
157
+ - lib/hackle/workspaces/http_workspace_fetcher.rb
158
+ - lib/hackle/workspaces/polling_workspace_fetcher.rb
159
+ - lib/hackle/workspaces/workspace.rb
157
160
  homepage: https://github.com/hackle-io/hackle-ruby-sdk
158
161
  licenses:
159
162
  - Apache-2.0
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hackle
4
-
5
- #
6
- # A client for Hackle API.
7
- #
8
- class Client
9
- def initialize(config, workspace_fetcher, event_processor, decider)
10
- @logger = config.logger
11
- @workspace_fetcher = workspace_fetcher
12
- @event_processor = event_processor
13
- @decider = decider
14
- end
15
-
16
- #
17
- # Decide the variation to expose to the user for experiment.
18
- #
19
- # This method return the control variation 'A' if:
20
- # - The experiment key is invalid
21
- # - The experiment has not started yet
22
- # - The user is not allocated to the experiment
23
- # - The decided variation has been dropped
24
- #
25
- # @param experiment_key [Integer] The unique key of the experiment.
26
- # @param user_id [String] The identifier of your customer. (e.g. user_email, account_id, decide_id, etc.)
27
- # @param default_variation [String] The default variation of the experiment.
28
- #
29
- # @return [String] The decided variation for the user, or default variation
30
- #
31
- def variation(experiment_key, user_id, default_variation = 'A')
32
-
33
- return default_variation if experiment_key.nil?
34
- return default_variation if user_id.nil?
35
-
36
- workspace = @workspace_fetcher.fetch
37
- return default_variation if workspace.nil?
38
-
39
- experiment = workspace.get_experiment(experiment_key)
40
- return default_variation if experiment.nil?
41
-
42
- decision = @decider.decide(experiment, user_id)
43
- case decision
44
- when Decision::NotAllocated
45
- default_variation
46
- when Decision::ForcedAllocated
47
- decision.variation_key
48
- when Decision::NaturalAllocated
49
- @event_processor.process(Event::Exposure.new(user_id, experiment, decision.variation))
50
- decision.variation.key
51
- else
52
- default_variation
53
- end
54
- end
55
-
56
- #
57
- # Records the events performed by the user.
58
- #
59
- # @param event_key [String] The unique key of the events.
60
- # @param user_id [String] The identifier of user that performed the vent.
61
- # @param value [Float] Additional numeric value of the events (e.g. purchase_amount, api_latency, etc.)
62
- #
63
- def track(event_key, user_id, value = nil)
64
-
65
- return if event_key.nil?
66
- return if user_id.nil?
67
-
68
- workspace = @workspace_fetcher.fetch
69
- return if workspace.nil?
70
-
71
- event_type = workspace.get_event_type(event_key)
72
- return if event_type.nil?
73
-
74
- @event_processor.process(Event::Track.new(user_id, event_type, value))
75
- end
76
-
77
- #
78
- # Shutdown the background task and release the resources used for the background task.
79
- #
80
- def close
81
- @workspace_fetcher.stop!
82
- @event_processor.stop!
83
- 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
- end
108
- end
@@ -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,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hackle
4
- class Event
5
- attr_reader :timestamp, :user_id
6
-
7
- class Exposure < Event
8
- attr_reader :experiment, :variation
9
-
10
- def initialize(user_id, experiment, variation)
11
- @timestamp = Event.generate_timestamp
12
- @user_id = user_id
13
- @experiment = experiment
14
- @variation = variation
15
- end
16
- end
17
-
18
- class Track < Event
19
- attr_reader :event_type, :value
20
-
21
- def initialize(user_id, event_type, value = nil)
22
- @timestamp = Event.generate_timestamp
23
- @user_id = user_id
24
- @event_type = event_type
25
- @value = value
26
- end
27
- end
28
-
29
- def self.generate_timestamp
30
- (Time.now.to_f * 1000).to_i
31
- end
32
- end
33
- end
@@ -1,15 +0,0 @@
1
- module Hackle
2
- class Bucket
3
- attr_reader :seed, :slot_size
4
-
5
- def initialize(seed, slot_size, slots)
6
- @seed = seed
7
- @slot_size = slot_size
8
- @slots = slots
9
- end
10
-
11
- def get_slot(slot_number)
12
- @slots.find { |slot| slot.contains?(slot_number) }
13
- end
14
- end
15
- end
@@ -1,10 +0,0 @@
1
- module Hackle
2
- class EventType
3
- attr_reader :id, :key
4
-
5
- def initialize(id, key)
6
- @id = id
7
- @key = key
8
- end
9
- end
10
- end
@@ -1,39 +0,0 @@
1
- module Hackle
2
- class Experiment
3
- attr_reader :id, :key
4
-
5
- def initialize(id, key)
6
- @id = id
7
- @key = key
8
- end
9
-
10
- class Running < Experiment
11
- attr_reader :bucket
12
-
13
- def initialize(id, key, bucket, variations, user_overrides)
14
- super(id, key)
15
- @bucket = bucket
16
- @variations = variations
17
- @user_overrides = user_overrides
18
- end
19
-
20
- def get_variation(variation_id)
21
- @variations[variation_id]
22
- end
23
-
24
- def get_overridden_variation(user_id)
25
- variation_id = @user_overrides[user_id]
26
- get_variation(variation_id)
27
- end
28
- end
29
-
30
- class Completed < Experiment
31
- attr_reader :winner_variation_key
32
-
33
- def initialize(id, key, winner_variation_key)
34
- super(id, key)
35
- @winner_variation_key = winner_variation_key
36
- end
37
- end
38
- end
39
- end