trailguide 0.2.1 → 0.3.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +191 -293
  3. data/app/views/trail_guide/admin/experiments/_btn_join.html.erb +1 -1
  4. data/app/views/trail_guide/admin/experiments/_header.html.erb +3 -3
  5. data/config/initializers/admin.rb +19 -0
  6. data/config/initializers/experiment.rb +261 -0
  7. data/config/initializers/trailguide.rb +6 -279
  8. data/lib/trail_guide/adapters.rb +2 -0
  9. data/lib/trail_guide/adapters/experiments.rb +8 -0
  10. data/lib/trail_guide/adapters/experiments/redis.rb +48 -0
  11. data/lib/trail_guide/adapters/participants/cookie.rb +1 -0
  12. data/lib/trail_guide/adapters/participants/unity.rb +9 -1
  13. data/lib/trail_guide/adapters/variants.rb +8 -0
  14. data/lib/trail_guide/adapters/variants/redis.rb +52 -0
  15. data/lib/trail_guide/admin/engine.rb +1 -0
  16. data/lib/trail_guide/algorithms.rb +4 -0
  17. data/lib/trail_guide/algorithms/algorithm.rb +29 -0
  18. data/lib/trail_guide/algorithms/bandit.rb +9 -18
  19. data/lib/trail_guide/algorithms/distributed.rb +8 -15
  20. data/lib/trail_guide/algorithms/random.rb +2 -12
  21. data/lib/trail_guide/algorithms/static.rb +34 -0
  22. data/lib/trail_guide/algorithms/weighted.rb +5 -17
  23. data/lib/trail_guide/catalog.rb +79 -35
  24. data/lib/trail_guide/config.rb +2 -4
  25. data/lib/trail_guide/engine.rb +2 -1
  26. data/lib/trail_guide/experiments/base.rb +41 -24
  27. data/lib/trail_guide/experiments/combined_config.rb +4 -0
  28. data/lib/trail_guide/experiments/config.rb +59 -30
  29. data/lib/trail_guide/experiments/participant.rb +4 -2
  30. data/lib/trail_guide/helper.rb +4 -216
  31. data/lib/trail_guide/helper/experiment_proxy.rb +160 -0
  32. data/lib/trail_guide/helper/helper_proxy.rb +62 -0
  33. data/lib/trail_guide/metrics/config.rb +2 -0
  34. data/lib/trail_guide/metrics/goal.rb +17 -15
  35. data/lib/trail_guide/participant.rb +10 -2
  36. data/lib/trail_guide/unity.rb +17 -8
  37. data/lib/trail_guide/variant.rb +15 -11
  38. data/lib/trail_guide/version.rb +2 -2
  39. metadata +13 -3
@@ -32,11 +32,13 @@ module TrailGuide
32
32
 
33
33
  # TODO do we allow a method here? do we call it on the experiment?
34
34
  def allow_conversion(meth=nil, &block)
35
+ raise ArgumentError if meth.nil? && !block_given?
35
36
  self[:allow_conversion] ||= []
36
37
  self[:allow_conversion] << (meth || block)
37
38
  end
38
39
 
39
40
  def on_convert(meth=nil, &block)
41
+ raise ArgumentError if meth.nil? && !block_given?
40
42
  self[:on_convert] ||= []
41
43
  self[:on_convert] << (meth || block)
42
44
  end
@@ -30,11 +30,12 @@ module TrailGuide
30
30
  elsif other.is_a?(String) || other.is_a?(Symbol)
31
31
  other = other.to_s.underscore
32
32
  return name == other.to_sym || to_s == other
33
- elsif other.is_a?(Array)
34
- return to_s == other.flatten.map { |o| o.to_s.underscore }.join('/')
35
- elsif other.is_a?(Hash)
36
- # TODO "flatten" it out and compare it to_s
37
- return false
33
+ # Currently unused placeholder for future functionality
34
+ #elsif other.is_a?(Array)
35
+ # return to_s == other.flatten.map { |o| o.to_s.underscore }.join('/')
36
+ #elsif other.is_a?(Hash)
37
+ # # TODO "flatten" it out and compare it to_s
38
+ # return false
38
39
  end
39
40
  end
40
41
 
@@ -58,16 +59,17 @@ module TrailGuide
58
59
  trial.send(callback, trial, result, self, *args)
59
60
  end
60
61
  end
61
- else
62
- args.unshift(self)
63
- args.unshift(trial)
64
- callbacks[hook].each do |callback|
65
- if callback.respond_to?(:call)
66
- callback.call(*args)
67
- else
68
- trial.send(callback, *args)
69
- end
70
- end
62
+ # Currently unused placeholder for future functionality
63
+ #else
64
+ # args.unshift(self)
65
+ # args.unshift(trial)
66
+ # callbacks[hook].each do |callback|
67
+ # if callback.respond_to?(:call)
68
+ # callback.call(*args)
69
+ # else
70
+ # trial.send(callback, *args)
71
+ # end
72
+ # end
71
73
  end
72
74
  end
73
75
 
@@ -6,10 +6,12 @@ module TrailGuide
6
6
  def initialize(context, adapter: nil)
7
7
  @context = context
8
8
  @adapter = adapter.new(context) if adapter.present?
9
+
9
10
  cleanup_inactive_experiments! if TrailGuide.configuration.cleanup_participant_experiments == true
10
11
  end
11
12
 
12
13
  def adapter
14
+ # TODO move this case selection to Adapters::Participant (like Algorithms)?
13
15
  @adapter ||= begin
14
16
  config_adapter = TrailGuide.configuration.adapter
15
17
  case config_adapter
@@ -37,6 +39,7 @@ module TrailGuide
37
39
 
38
40
  def variant(experiment)
39
41
  return nil unless experiment.calibrating? || experiment.started?
42
+ # TODO more efficient to stop checking if keys exist, and just return if the value is blank??
40
43
  return nil unless adapter.key?(experiment.storage_key)
41
44
  varname = adapter[experiment.storage_key]
42
45
  variant = experiment.variants.find { |var| var == varname }
@@ -126,6 +129,7 @@ module TrailGuide
126
129
  active = adapter.keys.map { |key| key.to_s.split(":").first.to_sym }.uniq.map do |key|
127
130
  experiment = TrailGuide.catalog.find(key)
128
131
  next unless experiment
132
+ next unless experiment.configuration.sticky_assignment?
129
133
 
130
134
  if !experiment.started? && !experiment.calibrating?
131
135
  inactive << key
@@ -142,17 +146,21 @@ module TrailGuide
142
146
  end.each { |key| adapter.delete(key) }
143
147
  end
144
148
 
149
+ return false if active.empty?
145
150
  return active
146
151
  end
147
152
 
148
153
  def calibrating_experiments
149
154
  return false if adapter.keys.empty?
150
155
 
151
- adapter.keys.map { |key| key.to_s.split(":").first.to_sym }.uniq.map do |key|
156
+ calibrating = adapter.keys.map { |key| key.to_s.split(":").first.to_sym }.uniq.map do |key|
152
157
  experiment = TrailGuide.catalog.find(key)
153
158
  next unless experiment && experiment.calibrating?
154
159
  [ experiment.experiment_name, adapter[experiment.storage_key] ]
155
160
  end.compact.to_h
161
+
162
+ return false if calibrating.empty?
163
+ return calibrating
156
164
  end
157
165
 
158
166
  def participating_in_active_experiments?(include_control=true)
@@ -161,7 +169,7 @@ module TrailGuide
161
169
  adapter.keys.any? do |key|
162
170
  experiment_name = key.to_s.split(":").first.to_sym
163
171
  experiment = TrailGuide.catalog.find(experiment_name)
164
- experiment && !experiment.combined? && experiment.running? && participating?(experiment, include_control)
172
+ experiment && experiment.configuration.sticky_assignment? && !experiment.combined? && experiment.running? && participating?(experiment, include_control)
165
173
  end
166
174
  end
167
175
 
@@ -1,13 +1,22 @@
1
1
  module TrailGuide
2
2
  class Unity
3
- NAMESPACE = :unity
3
+ class << self
4
+ def configuration
5
+ @configuration ||= Canfig::Config.new(namespace: :unity)
6
+ end
7
+
8
+ def configure(*args, &block)
9
+ configuration.configure(*args, &block)
10
+ end
4
11
 
5
- def self.clear!
6
- keys = TrailGuide.redis.keys("#{NAMESPACE}:*")
7
- TrailGuide.redis.del *keys unless keys.empty?
12
+ def clear!
13
+ keys = TrailGuide.redis.keys("#{configuration.namespace}:*")
14
+ TrailGuide.redis.del *keys unless keys.empty?
15
+ end
8
16
  end
9
17
 
10
18
  attr_reader :visitor_id, :user_id
19
+ delegate :configuration, to: :class
11
20
 
12
21
  def initialize(user_id: nil, visitor_id: nil)
13
22
  @user_id = user_id.to_s if user_id.present?
@@ -74,19 +83,19 @@ module TrailGuide
74
83
  protected
75
84
 
76
85
  def user_key
77
- "#{NAMESPACE}:uids:#{user_id}"
86
+ "#{configuration.namespace}:uids:#{user_id}"
78
87
  end
79
88
 
80
89
  def visitor_key
81
- "#{NAMESPACE}:vids:#{visitor_id}"
90
+ "#{configuration.namespace}:vids:#{visitor_id}"
82
91
  end
83
92
 
84
93
  def stored_user_key
85
- "#{NAMESPACE}:uids:#{stored_user_id}"
94
+ "#{configuration.namespace}:uids:#{stored_user_id}"
86
95
  end
87
96
 
88
97
  def stored_visitor_key
89
- "#{NAMESPACE}:vids:#{stored_visitor_id}"
98
+ "#{configuration.namespace}:vids:#{stored_visitor_id}"
90
99
  end
91
100
  end
92
101
  end
@@ -14,6 +14,10 @@ module TrailGuide
14
14
  @control = control
15
15
  end
16
16
 
17
+ def adapter
18
+ @adapter ||= TrailGuide::Adapters::Variants::Redis.new(self)
19
+ end
20
+
17
21
  def ==(other)
18
22
  if other.is_a?(self.class)
19
23
  # TODO eventually remove the experiment requirement here once we start
@@ -48,15 +52,15 @@ module TrailGuide
48
52
  end
49
53
 
50
54
  def persisted?
51
- TrailGuide.redis.exists(storage_key)
55
+ adapter.persisted?
52
56
  end
53
57
 
54
58
  def save!
55
- TrailGuide.redis.hsetnx(storage_key, 'name', name)
59
+ adapter.setnx(:name, name)
56
60
  end
57
61
 
58
62
  def delete!
59
- TrailGuide.redis.del(storage_key)
63
+ adapter.destroy
60
64
  end
61
65
 
62
66
  def reset!
@@ -65,20 +69,20 @@ module TrailGuide
65
69
  end
66
70
 
67
71
  def participants
68
- (TrailGuide.redis.hget(storage_key, 'participants') || 0).to_i
72
+ (adapter.get(:participants) || 0).to_i
69
73
  end
70
74
 
71
75
  def converted(checkpoint=nil)
72
76
  if experiment.goals.empty?
73
77
  raise InvalidGoalError, "You provided the checkpoint `#{checkpoint}` but the experiment `#{experiment.experiment_name}` does not have any goals defined." unless checkpoint.nil?
74
- (TrailGuide.redis.hget(storage_key, 'converted') || 0).to_i
78
+ (adapter.get(:converted) || 0).to_i
75
79
  elsif !checkpoint.nil?
76
80
  goal = experiment.goals.find { |g| g == checkpoint }
77
81
  raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for experiment `#{experiment.experiment_name}`." if goal.nil?
78
- (TrailGuide.redis.hget(storage_key, goal.to_s) || 0).to_i
82
+ (adapter.get(goal.name) || 0).to_i
79
83
  else
80
84
  experiment.goals.sum do |goal|
81
- (TrailGuide.redis.hget(storage_key, goal.to_s) || 0).to_i
85
+ (adapter.get(goal.name) || 0).to_i
82
86
  end
83
87
  end
84
88
  end
@@ -95,16 +99,16 @@ module TrailGuide
95
99
  end
96
100
 
97
101
  def increment_participation!
98
- TrailGuide.redis.hincrby(storage_key, 'participants', 1)
102
+ adapter.increment(:participants)
99
103
  end
100
104
 
101
105
  def increment_conversion!(checkpoint=nil)
102
106
  if checkpoint.nil?
103
- checkpoint = 'converted'
107
+ checkpoint = :converted
104
108
  else
105
- checkpoint = experiment.goals.find { |g| g == checkpoint }.to_s
109
+ checkpoint = experiment.goals.find { |g| g == checkpoint }.name
106
110
  end
107
- TrailGuide.redis.hincrby(storage_key, checkpoint, 1)
111
+ adapter.increment(checkpoint)
108
112
  end
109
113
 
110
114
  # export the variant state (not config) as json
@@ -1,8 +1,8 @@
1
1
  module TrailGuide
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 2
5
- PATCH = 1
4
+ MINOR = 3
5
+ PATCH = 0
6
6
  VERSION = "#{MAJOR}.#{MINOR}.#{PATCH}"
7
7
 
8
8
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trailguide
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Rebec
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-31 00:00:00.000000000 Z
11
+ date: 2019-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -228,11 +228,15 @@ files:
228
228
  - app/views/trail_guide/admin/groups/index.html.erb
229
229
  - app/views/trail_guide/admin/groups/show.html.erb
230
230
  - app/views/trail_guide/admin/orphans/_alert.html.erb
231
+ - config/initializers/admin.rb
231
232
  - config/initializers/assets.rb
233
+ - config/initializers/experiment.rb
232
234
  - config/initializers/trailguide.rb
233
235
  - config/routes/admin.rb
234
236
  - config/routes/engine.rb
235
237
  - lib/trail_guide/adapters.rb
238
+ - lib/trail_guide/adapters/experiments.rb
239
+ - lib/trail_guide/adapters/experiments/redis.rb
236
240
  - lib/trail_guide/adapters/participants.rb
237
241
  - lib/trail_guide/adapters/participants/anonymous.rb
238
242
  - lib/trail_guide/adapters/participants/base.rb
@@ -241,12 +245,16 @@ files:
241
245
  - lib/trail_guide/adapters/participants/redis.rb
242
246
  - lib/trail_guide/adapters/participants/session.rb
243
247
  - lib/trail_guide/adapters/participants/unity.rb
248
+ - lib/trail_guide/adapters/variants.rb
249
+ - lib/trail_guide/adapters/variants/redis.rb
244
250
  - lib/trail_guide/admin.rb
245
251
  - lib/trail_guide/admin/engine.rb
246
252
  - lib/trail_guide/algorithms.rb
253
+ - lib/trail_guide/algorithms/algorithm.rb
247
254
  - lib/trail_guide/algorithms/bandit.rb
248
255
  - lib/trail_guide/algorithms/distributed.rb
249
256
  - lib/trail_guide/algorithms/random.rb
257
+ - lib/trail_guide/algorithms/static.rb
250
258
  - lib/trail_guide/algorithms/weighted.rb
251
259
  - lib/trail_guide/calculators.rb
252
260
  - lib/trail_guide/calculators/bayesian.rb
@@ -263,6 +271,8 @@ files:
263
271
  - lib/trail_guide/experiments/config.rb
264
272
  - lib/trail_guide/experiments/participant.rb
265
273
  - lib/trail_guide/helper.rb
274
+ - lib/trail_guide/helper/experiment_proxy.rb
275
+ - lib/trail_guide/helper/helper_proxy.rb
266
276
  - lib/trail_guide/metrics.rb
267
277
  - lib/trail_guide/metrics/checkpoint.rb
268
278
  - lib/trail_guide/metrics/config.rb
@@ -294,7 +304,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
294
304
  version: '0'
295
305
  requirements: []
296
306
  rubyforge_project:
297
- rubygems_version: 2.7.9
307
+ rubygems_version: 2.7.6
298
308
  signing_key:
299
309
  specification_version: 4
300
310
  summary: User experiments for rails