Class: Verdict::Experiment

Inherits:
Object
  • Object
show all
Includes:
Metadata
Defined in:
lib/verdict/experiment.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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_loggerObject (readonly)

Returns the value of attribute event_logger.



5
6
7
# File 'lib/verdict/experiment.rb', line 5

def event_logger
  @event_logger
end

#handleObject (readonly)

Returns the value of attribute handle.



5
6
7
# File 'lib/verdict/experiment.rb', line 5

def handle
  @handle
end

#qualifiersObject (readonly)

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



7
8
9
10
11
# File 'lib/verdict/experiment.rb', line 7

def self.define(handle, *args, &block)
  experiment = self.new(handle, *args, &block)
  raise Verdict::ExperimentHandleNotUnique.new(experiment.handle) if Verdict.repository.has_key?(experiment.handle)
  Verdict.repository[experiment.handle] = experiment
end

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: ,
    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

Returns:

  • (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

Returns:

  • (Boolean)


218
219
220
# File 'lib/verdict/experiment.rb', line 218

def everybody_qualifies?
  !has_qualifier?
end

#fetch_subject(subject_identifier) ⇒ Object

Raises:

  • (NotImplementedError)


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_handlesObject



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

Returns:

  • (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

Returns:

  • (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

#segmenterObject

Raises:



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

Returns:

  • (Boolean)


119
120
121
# File 'lib/verdict/experiment.rb', line 119

def started?
  !@started_at.nil?
end

#started_atObject



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

Returns:

  • (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

Returns:

  • (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