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