Skip to content
This repository was archived by the owner on Feb 20, 2021. It is now read-only.

Commit b9de0fd

Browse files
committed
Add class definition style
1 parent c2efc19 commit b9de0fd

5 files changed

Lines changed: 261 additions & 11 deletions

File tree

lib/verdict.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def initialize(handle)
4848
require "verdict/railtie" if defined?(Rails::Railtie)
4949

5050
require "verdict/metadata"
51+
require "verdict/experiment_definition"
5152
require "verdict/experiment"
5253
require "verdict/group"
5354
require "verdict/assignment"
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
module Verdict
2+
class ExperimentDefinition
3+
extend Verdict::Metadata
4+
5+
class << self
6+
attr_reader :handle
7+
8+
def inherited(subclass)
9+
@handle = subclass.to_s.underscore.gsub('/', '_')
10+
end
11+
12+
def event_logger
13+
Verdict::EventLogger.new(Verdict.default_logger)
14+
end
15+
16+
def subject_type
17+
nil
18+
end
19+
20+
def store_unqualified?
21+
!!@store_unqualified
22+
end
23+
24+
def manual_assignment_timestamps?
25+
nil
26+
end
27+
28+
def group(handle)
29+
segmenter.groups[handle.to_s]
30+
end
31+
32+
def groups(segmenter_class = Verdict::Segmenters::FixedPercentageSegmenter, &block)
33+
return segmenter.groups unless block_given?
34+
@segmenter ||= segmenter_class.new(self)
35+
@segmenter.instance_eval(&block)
36+
@segmenter.verify!
37+
return self
38+
end
39+
40+
def rollout_percentage(percentage, rollout_group_name = :enabled)
41+
groups(Verdict::Segmenters::RolloutSegmenter) do
42+
group(rollout_group_name, percentage)
43+
end
44+
end
45+
46+
def qualifiers
47+
@qualifiers ||= []
48+
end
49+
50+
def qualify(method_name = nil, &block)
51+
if block_given?
52+
qualifiers << block
53+
elsif method_name.nil?
54+
raise ArgumentError, "no method nor blocked passed!"
55+
elsif respond_to?(method_name, true)
56+
qualifiers << method(method_name).to_proc
57+
else
58+
raise ArgumentError, "No helper for #{method_name.inspect}"
59+
end
60+
end
61+
62+
def storage(storage = :memory, options = {})
63+
@store_unqualified = options[:store_unqualified] if options.has_key?(:store_unqualified)
64+
case storage
65+
when :memory
66+
Verdict::Storage::MemoryStorage.new
67+
when :none
68+
Verdict::Storage::MockStorage.new
69+
when Class
70+
storage.new
71+
end
72+
end
73+
74+
def segmenter
75+
raise Verdict::Error, "No groups defined for experiment #{@handle.inspect}." if @segmenter.nil?
76+
@segmenter
77+
end
78+
79+
def started_at
80+
@started_at ||= storage.retrieve_start_timestamp(self)
81+
rescue Verdict::StorageError
82+
nil
83+
end
84+
85+
def started?
86+
!@started_at.nil?
87+
end
88+
89+
def group_handles
90+
segmenter.groups.keys
91+
end
92+
93+
def subject_assignment(subject, group, originally_created_at = nil, temporary = false)
94+
Verdict::Assignment.new(self, subject, group, originally_created_at, temporary)
95+
end
96+
97+
def subject_conversion(subject, goal, created_at = Time.now.utc)
98+
Verdict::Conversion.new(self, subject, goal, created_at)
99+
end
100+
101+
def convert(subject, goal)
102+
identifier = retrieve_subject_identifier(subject)
103+
conversion = subject_conversion(subject, goal)
104+
event_logger.log_conversion(conversion)
105+
segmenter.conversion_feedback(identifier, subject, conversion)
106+
conversion
107+
rescue Verdict::EmptySubjectIdentifier
108+
raise unless disqualify_empty_identifier?
109+
end
110+
111+
def assign(subject, context = nil)
112+
previous_assignment = lookup(subject)
113+
114+
subject_identifier = retrieve_subject_identifier(subject)
115+
assignment = if previous_assignment
116+
previous_assignment
117+
elsif subject_qualifies?(subject, context)
118+
group = segmenter.assign(subject_identifier, subject, context)
119+
subject_assignment(subject, group, nil, group.nil?)
120+
else
121+
nil_assignment(subject)
122+
end
123+
124+
store_assignment(assignment)
125+
rescue Verdict::StorageError
126+
nil_assignment(subject)
127+
rescue Verdict::EmptySubjectIdentifier
128+
if disqualify_empty_identifier?
129+
nil_assignment(subject)
130+
else
131+
raise
132+
end
133+
end
134+
135+
def assign_manually(subject, group)
136+
assignment = subject_assignment(subject, group)
137+
if !assignment.qualified? && !store_unqualified?
138+
raise Verdict::Error, "Unqualified subject assignments are not stored for this experiment, so manual disqualification is impossible. Consider setting :store_unqualified to true for this experiment."
139+
end
140+
141+
store_assignment(assignment)
142+
assignment
143+
end
144+
145+
def disqualify_manually(subject)
146+
assign_manually(subject, nil)
147+
end
148+
149+
def store_assignment(assignment)
150+
storage.store_assignment(assignment) if should_store_assignment?(assignment)
151+
event_logger.log_assignment(assignment)
152+
assignment
153+
end
154+
155+
def cleanup(options = {})
156+
storage.cleanup(self, options)
157+
end
158+
159+
def remove_subject_assignment(subject)
160+
storage.remove_assignment(self, subject)
161+
end
162+
163+
def switch(subject, context = nil)
164+
assign(subject, context).to_sym
165+
end
166+
167+
def lookup(subject)
168+
storage.retrieve_assignment(self, subject)
169+
end
170+
171+
def retrieve_subject_identifier(subject)
172+
identifier = subject_identifier(subject).to_s
173+
raise Verdict::EmptySubjectIdentifier, "Subject resolved to an empty identifier!" if identifier.empty?
174+
identifier
175+
end
176+
177+
def has_qualifier?
178+
@qualifiers.any?
179+
end
180+
181+
def everybody_qualifies?
182+
!has_qualifier?
183+
end
184+
185+
def as_json(options = {})
186+
{
187+
handle: handle,
188+
has_qualifier: has_qualifier?,
189+
groups: segmenter.groups.values.map { |group| group.as_json(options) },
190+
metadata: metadata,
191+
started_at: started_at.nil? ? nil : started_at.utc.strftime('%FT%TZ')
192+
}.tap do |data|
193+
data[:subject_type] = subject_type.to_s unless subject_type.nil?
194+
end
195+
end
196+
197+
def to_json(options = {})
198+
as_json(options).to_json
199+
end
200+
201+
def fetch_subject(subject_identifier)
202+
raise NotImplementedError, "Fetching subjects based on identifier is not implemented for experiment #{handle.inspect}."
203+
end
204+
205+
def disqualify_empty_identifier?
206+
@disqualify_empty_identifier
207+
end
208+
209+
def subject_qualifies?(subject, context = nil)
210+
ensure_experiment_has_started
211+
everybody_qualifies? || qualifiers.all? { |qualifier| qualifier.call(subject, context) }
212+
end
213+
214+
protected
215+
216+
def default_options
217+
{}
218+
end
219+
220+
def should_store_assignment?(assignment)
221+
assignment.permanent? && !assignment.returning? && (store_unqualified? || assignment.qualified?)
222+
end
223+
224+
def subject_identifier(subject)
225+
subject.respond_to?(:id) ? subject.id : subject.to_s
226+
end
227+
228+
def set_start_timestamp
229+
storage.store_start_timestamp(self, started_now = Time.now.utc)
230+
started_now
231+
rescue NotImplementedError
232+
nil
233+
end
234+
235+
def ensure_experiment_has_started
236+
@started_at ||= started_at || set_start_timestamp
237+
rescue Verdict::StorageError
238+
@started_at ||= Time.now.utc
239+
end
240+
241+
def nil_assignment(subject)
242+
subject_assignment(subject, nil, nil)
243+
end
244+
end
245+
end
246+
end
247+

lib/verdict/railtie.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
class Verdict::Railtie < Rails::Railtie
22
initializer "experiments.configure_rails_initialization" do |app|
3-
app.config.eager_load_namespaces << Verdict
4-
5-
Verdict.default_logger = Rails.logger
6-
7-
Verdict.directory ||= Rails.root.join('app', 'experiments')
8-
app.config.eager_load_paths -= Dir[Verdict.directory.to_s]
9-
10-
# Re-freeze eager load paths to ensure they blow up if modified at runtime, as Rails does
11-
app.config.eager_load_paths.freeze
3+
#app.config.eager_load_namespaces << Verdict
4+
#
5+
#Verdict.default_logger = Rails.logger
6+
#
7+
#Verdict.directory ||= Rails.root.join('app', 'experiments')
8+
#app.config.eager_load_paths -= Dir[Verdict.directory.to_s]
9+
#
10+
## Re-freeze eager load paths to ensure they blow up if modified at runtime, as Rails does
11+
#app.config.eager_load_paths.freeze
1212
end
1313

1414
config.to_prepare do

lib/verdict/version.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module Verdict
2-
VERSION = "0.12.0"
2+
class Version
3+
VERSION = "0.13.0"
4+
end
35
end

verdict.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ require 'verdict/version'
55

66
Gem::Specification.new do |gem|
77
gem.name = "verdict"
8-
gem.version = Verdict::VERSION
8+
gem.version = Verdict::Version::VERSION
99
gem.authors = ["Shopify"]
1010
gem.email = ["kevin.mcphillips@shopify.com", "willem@shopify.com"]
1111
gem.description = %q{Shopify Experiments classes}

0 commit comments

Comments
 (0)