Class: Verdict::Experiment
- Inherits:
-
Object
- Object
- Verdict::Experiment
show all
- Includes:
- Metadata
- Defined in:
- lib/verdict/experiment.rb
Instance Attribute Summary collapse
Class Method Summary
collapse
Instance Method Summary
collapse
-
#as_json(options = {}) ⇒ Object
-
#assign(subject, context = nil, dynamic_qualifiers: []) ⇒ Object
-
#assign_manually(subject, group) ⇒ Object
-
#cleanup(options = {}) ⇒ Object
-
#convert(subject, goal) ⇒ Object
-
#disqualify_empty_identifier? ⇒ Boolean
-
#disqualify_manually(subject) ⇒ Object
-
#everybody_qualifies? ⇒ Boolean
-
#fetch_subject(subject_identifier) ⇒ Object
-
#group(handle) ⇒ Object
-
#group_handles ⇒ Object
-
#groups(segmenter_class = Verdict::Segmenters::FixedPercentageSegmenter, &block) ⇒ Object
-
#has_qualifier? ⇒ Boolean
-
#initialize(handle, options = {}, &block) ⇒ Experiment
constructor
A new instance of Experiment.
-
#lookup(subject) ⇒ Object
-
#manual_assignment_timestamps? ⇒ Boolean
-
#qualify(method_name = nil, &block) ⇒ Object
-
#remove_subject_assignment(subject) ⇒ Object
-
#retrieve_subject_identifier(subject) ⇒ Object
-
#rollout_percentage(percentage, rollout_group_name = :enabled) ⇒ Object
-
#schedule_end_timestamp(timestamp) ⇒ Object
-
#schedule_start_timestamp(timestamp) ⇒ Object
Optional: Together with the “end_timestamp” and “stop_new_assignment_timestamp”, limits the experiment run timeline within the given time interval.
-
#schedule_stop_new_assignment_timestamp(timestamp) ⇒ Object
-
#segmenter ⇒ Object
-
#started? ⇒ Boolean
-
#started_at ⇒ Object
-
#storage(storage = nil, options = {}) ⇒ Object
-
#store_assignment(assignment) ⇒ Object
-
#store_unqualified? ⇒ Boolean
-
#subject_assignment(subject, group, originally_created_at = nil, temporary = false) ⇒ Object
-
#subject_conversion(subject, goal, created_at = Time.now.utc) ⇒ Object
-
#subject_qualifies?(subject, context = nil, dynamic_qualifiers: []) ⇒ Boolean
-
#subject_type(type = nil) ⇒ Object
-
#switch(subject, context = nil, qualifiers: []) ⇒ Object
The qualifiers param accepts an array of procs.
-
#to_json(options = {}) ⇒ Object
Methods included from Metadata
#description, included, #name, #owner, #screenshot
Constructor Details
#initialize(handle, options = {}, &block) ⇒ Experiment
Returns a new instance of Experiment.
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
# File 'lib/verdict/experiment.rb', line 13
def initialize(handle, options = {}, &block)
@started_at = nil
@handle = handle.to_s
options = default_options.merge(options)
@qualifiers = Array(options[:qualifier] || options[:qualifiers])
@event_logger = options[:event_logger] || Verdict::EventLogger.new(Verdict.default_logger)
@storage = storage(options[:storage] || :memory)
@store_unqualified = options[:store_unqualified]
@segmenter = options[:segmenter]
@subject_type = options[:subject_type]
@disqualify_empty_identifier = options[:disqualify_empty_identifier]
@manual_assignment_timestamps = options[:manual_assignment_timestamps]
instance_eval(&block) if block_given?
end
|
Instance Attribute Details
#event_logger ⇒ Object
Returns the value of attribute event_logger.
5
6
7
|
# File 'lib/verdict/experiment.rb', line 5
def event_logger
@event_logger
end
|
#handle ⇒ Object
Returns the value of attribute handle.
5
6
7
|
# File 'lib/verdict/experiment.rb', line 5
def handle
@handle
end
|
#qualifiers ⇒ Object
Returns the value of attribute qualifiers.
5
6
7
|
# File 'lib/verdict/experiment.rb', line 5
def qualifiers
@qualifiers
end
|
Class Method Details
.define(handle, *args, &block) ⇒ Object
Instance Method Details
#as_json(options = {}) ⇒ Object
222
223
224
225
226
227
228
229
230
231
232
|
# File 'lib/verdict/experiment.rb', line 222
def as_json(options = {})
{
handle: handle,
has_qualifier: has_qualifier?,
groups: segmenter.groups.values.map { |group| group.as_json(options) },
metadata: metadata,
started_at: started_at.nil? ? nil : started_at.utc.strftime('%FT%TZ')
}.tap do |data|
data[:subject_type] = subject_type.to_s unless subject_type.nil?
end
end
|
#assign(subject, context = nil, dynamic_qualifiers: []) ⇒ Object
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
|
# File 'lib/verdict/experiment.rb', line 145
def assign(subject, context = nil, dynamic_qualifiers: [])
previous_assignment = lookup(subject)
subject_identifier = retrieve_subject_identifier(subject)
assignment = if previous_assignment
previous_assignment
elsif dynamic_subject_qualifies?(subject, dynamic_qualifiers, context) && is_make_new_assignments?
group = segmenter.assign(subject_identifier, subject, context)
subject_assignment(subject, group, nil, group.nil?)
else
nil_assignment(subject)
end
store_assignment(assignment)
rescue Verdict::StorageError
nil_assignment(subject)
rescue Verdict::EmptySubjectIdentifier
if disqualify_empty_identifier?
nil_assignment(subject)
else
raise
end
end
|
#assign_manually(subject, group) ⇒ Object
169
170
171
172
173
174
175
176
177
|
# File 'lib/verdict/experiment.rb', line 169
def assign_manually(subject, group)
assignment = subject_assignment(subject, group)
if !assignment.qualified? && !store_unqualified?
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."
end
store_assignment(assignment)
assignment
end
|
#cleanup(options = {}) ⇒ Object
189
190
191
|
# File 'lib/verdict/experiment.rb', line 189
def cleanup(options = {})
@storage.cleanup(self, options)
end
|
#convert(subject, goal) ⇒ Object
135
136
137
138
139
140
141
142
143
|
# File 'lib/verdict/experiment.rb', line 135
def convert(subject, goal)
identifier = retrieve_subject_identifier(subject)
conversion = subject_conversion(subject, goal)
event_logger.log_conversion(conversion)
segmenter.conversion_feedback(identifier, subject, conversion)
conversion
rescue Verdict::EmptySubjectIdentifier
raise unless disqualify_empty_identifier?
end
|
#disqualify_empty_identifier? ⇒ Boolean
242
243
244
|
# File 'lib/verdict/experiment.rb', line 242
def disqualify_empty_identifier?
@disqualify_empty_identifier
end
|
#disqualify_manually(subject) ⇒ Object
179
180
181
|
# File 'lib/verdict/experiment.rb', line 179
def disqualify_manually(subject)
assign_manually(subject, nil)
end
|
#everybody_qualifies? ⇒ Boolean
218
219
220
|
# File 'lib/verdict/experiment.rb', line 218
def everybody_qualifies?
!has_qualifier?
end
|
#fetch_subject(subject_identifier) ⇒ Object
238
239
240
|
# File 'lib/verdict/experiment.rb', line 238
def fetch_subject(subject_identifier)
raise NotImplementedError, "Fetching subjects based on identifier is not implemented for experiment #{@handle.inspect}."
end
|
#group(handle) ⇒ Object
43
44
45
|
# File 'lib/verdict/experiment.rb', line 43
def group(handle)
segmenter.groups[handle.to_s]
end
|
#group_handles ⇒ Object
123
124
125
|
# File 'lib/verdict/experiment.rb', line 123
def group_handles
segmenter.groups.keys
end
|
#groups(segmenter_class = Verdict::Segmenters::FixedPercentageSegmenter, &block) ⇒ Object
47
48
49
50
51
52
53
|
# File 'lib/verdict/experiment.rb', line 47
def groups(segmenter_class = Verdict::Segmenters::FixedPercentageSegmenter, &block)
return segmenter.groups unless block_given?
@segmenter ||= segmenter_class.new(self)
@segmenter.instance_eval(&block)
@segmenter.verify!
return self
end
|
#has_qualifier? ⇒ Boolean
214
215
216
|
# File 'lib/verdict/experiment.rb', line 214
def has_qualifier?
@qualifiers.any?
end
|
#lookup(subject) ⇒ Object
204
205
206
|
# File 'lib/verdict/experiment.rb', line 204
def lookup(subject)
@storage.retrieve_assignment(self, subject)
end
|
#manual_assignment_timestamps? ⇒ Boolean
39
40
41
|
# File 'lib/verdict/experiment.rb', line 39
def manual_assignment_timestamps?
@manual_assignment_timestamps
end
|
#qualify(method_name = nil, &block) ⇒ Object
84
85
86
87
88
89
90
91
92
93
94
|
# File 'lib/verdict/experiment.rb', line 84
def qualify(method_name = nil, &block)
if block_given?
@qualifiers << block
elsif method_name.nil?
raise ArgumentError, "no method nor blocked passed!"
elsif respond_to?(method_name, true)
@qualifiers << method(method_name).to_proc
else
raise ArgumentError, "No helper for #{method_name.inspect}"
end
end
|
#remove_subject_assignment(subject) ⇒ Object
193
194
195
|
# File 'lib/verdict/experiment.rb', line 193
def remove_subject_assignment(subject)
@storage.remove_assignment(self, subject)
end
|
#retrieve_subject_identifier(subject) ⇒ Object
208
209
210
211
212
|
# File 'lib/verdict/experiment.rb', line 208
def retrieve_subject_identifier(subject)
identifier = subject_identifier(subject).to_s
raise Verdict::EmptySubjectIdentifier, "Subject resolved to an empty identifier!" if identifier.empty?
identifier
end
|
#rollout_percentage(percentage, rollout_group_name = :enabled) ⇒ Object
78
79
80
81
82
|
# File 'lib/verdict/experiment.rb', line 78
def rollout_percentage(percentage, rollout_group_name = :enabled)
groups(Verdict::Segmenters::RolloutSegmenter) do
group rollout_group_name, percentage
end
end
|
#schedule_end_timestamp(timestamp) ⇒ Object
70
71
72
|
# File 'lib/verdict/experiment.rb', line 70
def schedule_end_timestamp(timestamp)
@schedule_end_timestamp = timestamp
end
|
#schedule_start_timestamp(timestamp) ⇒ Object
Optional: Together with the “end_timestamp” and “stop_new_assignment_timestamp”, limits the experiment run timeline within the given time interval.
Timestamps definitions: start_timestamp: Experiment’s start time. No assignments are made i.e. switch will return nil before this timestamp. stop_new_assignment_timestamp: Experiment’s new assignment stop time. No new assignments are made i.e. switch returns nil for new assignments but the existing assignments are preserved. end_timestamp: Experiment’s end time. No assignments are made i.e. switch returns nil after this timestamp.
Experiment run timeline: start_timestamp -> (new assignments occur) -> stop_new_assignment_timestamp -> (no new assignments occur) -> end_timestamp
66
67
68
|
# File 'lib/verdict/experiment.rb', line 66
def schedule_start_timestamp(timestamp)
@schedule_start_timestamp = timestamp
end
|
#schedule_stop_new_assignment_timestamp(timestamp) ⇒ Object
74
75
76
|
# File 'lib/verdict/experiment.rb', line 74
def schedule_stop_new_assignment_timestamp(timestamp)
@schedule_stop_new_assignment_timestamp = timestamp
end
|
#segmenter ⇒ Object
108
109
110
111
|
# File 'lib/verdict/experiment.rb', line 108
def segmenter
raise Verdict::Error, "No groups defined for experiment #{@handle.inspect}." if @segmenter.nil?
@segmenter
end
|
#started? ⇒ Boolean
119
120
121
|
# File 'lib/verdict/experiment.rb', line 119
def started?
!@started_at.nil?
end
|
#started_at ⇒ Object
113
114
115
116
117
|
# File 'lib/verdict/experiment.rb', line 113
def started_at
@started_at ||= @storage.retrieve_start_timestamp(self)
rescue Verdict::StorageError
nil
end
|
#storage(storage = nil, options = {}) ⇒ Object
96
97
98
99
100
101
102
103
104
105
106
|
# File 'lib/verdict/experiment.rb', line 96
def storage(storage = nil, options = {})
return @storage if storage.nil?
@store_unqualified = options[:store_unqualified] if options.has_key?(:store_unqualified)
@storage = case storage
when :memory; Verdict::Storage::MemoryStorage.new
when :none; Verdict::Storage::MockStorage.new
when Class; storage.new
else storage
end
end
|
#store_assignment(assignment) ⇒ Object
183
184
185
186
187
|
# File 'lib/verdict/experiment.rb', line 183
def store_assignment(assignment)
@storage.store_assignment(assignment) if should_store_assignment?(assignment)
event_logger.log_assignment(assignment)
assignment
end
|
#store_unqualified? ⇒ Boolean
35
36
37
|
# File 'lib/verdict/experiment.rb', line 35
def store_unqualified?
@store_unqualified
end
|
#subject_assignment(subject, group, originally_created_at = nil, temporary = false) ⇒ Object
127
128
129
|
# File 'lib/verdict/experiment.rb', line 127
def subject_assignment(subject, group, originally_created_at = nil, temporary = false)
Verdict::Assignment.new(self, subject, group, originally_created_at, temporary)
end
|
#subject_conversion(subject, goal, created_at = Time.now.utc) ⇒ Object
131
132
133
|
# File 'lib/verdict/experiment.rb', line 131
def subject_conversion(subject, goal, created_at = Time.now.utc)
Verdict::Conversion.new(self, subject, goal, created_at)
end
|
#subject_qualifies?(subject, context = nil, dynamic_qualifiers: []) ⇒ Boolean
246
247
248
249
250
|
# File 'lib/verdict/experiment.rb', line 246
def subject_qualifies?(subject, context = nil, dynamic_qualifiers: [])
ensure_experiment_has_started
return false unless dynamic_qualifiers.all? { |qualifier| qualifier.call(subject) }
everybody_qualifies? || @qualifiers.all? { |qualifier| qualifier.call(subject, context) }
end
|
#subject_type(type = nil) ⇒ Object
30
31
32
33
|
# File 'lib/verdict/experiment.rb', line 30
def subject_type(type = nil)
return @subject_type if type.nil?
@subject_type = type
end
|
#switch(subject, context = nil, qualifiers: []) ⇒ Object
The qualifiers param accepts an array of procs. This is intended for qualification logic that cannot be defined in the experiment definition
199
200
201
202
|
# File 'lib/verdict/experiment.rb', line 199
def switch(subject, context = nil, qualifiers: [])
return unless is_scheduled?
assign(subject, context, dynamic_qualifiers: qualifiers).to_sym
end
|
#to_json(options = {}) ⇒ Object
234
235
236
|
# File 'lib/verdict/experiment.rb', line 234
def to_json(options = {})
as_json(options).to_json
end
|