Class: Laboratory::Experiment

Inherits:
Object
  • Object
show all
Defined in:
lib/laboratory/experiment.rb,
lib/laboratory/experiment/event.rb,
lib/laboratory/experiment/variant.rb,
lib/laboratory/experiment/changelog_item.rb,
lib/laboratory/experiment/event/recording.rb,
lib/laboratory/experiment/analysis_summary.rb

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: AnalysisSummary, ChangelogItem, ClashingExperimentIdError, Event, IncorrectPercentageTotalError, InvalidExperimentVariantFormatError, MissingExperimentAlgorithmError, MissingExperimentIdError, UserNotInExperimentError, Variant

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id:, variants:, algorithm: Algorithms::Random, changelog: []) ⇒ Experiment

rubocop:disable Metrics/MethodLength



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/laboratory/experiment.rb', line 32

def initialize(id:, variants:, algorithm: Algorithms::Random, changelog: []) # rubocop:disable Metrics/MethodLength
  @id = id
  @algorithm = algorithm
  @changelog = changelog

  # We want to allow users to input Variant objects, or simple hashes.
  # This also helps when decoding from adapters

  @variants =
    if variants.all? { |variant| variant.instance_of?(Experiment::Variant) }
      variants
    elsif variants.all? { |variant| variant.instance_of?(Hash) }
      variants.map do |variant|
        Variant.new(
          id: variant[:id],
          percentage: variant[:percentage],
          participant_ids: [],
          events: []
        )
      end
    end

  @_original_id = id
  @_original_algorithm = algorithm
end

Instance Attribute Details

#_original_algorithmObject (readonly)

Returns the value of attribute _original_algorithm.



25
26
27
# File 'lib/laboratory/experiment.rb', line 25

def _original_algorithm
  @_original_algorithm
end

#_original_idObject (readonly)

Returns the value of attribute _original_id.



25
26
27
# File 'lib/laboratory/experiment.rb', line 25

def _original_id
  @_original_id
end

#algorithmObject

Returns the value of attribute algorithm.



24
25
26
# File 'lib/laboratory/experiment.rb', line 24

def algorithm
  @algorithm
end

#changelogObject (readonly)

Returns the value of attribute changelog.



25
26
27
# File 'lib/laboratory/experiment.rb', line 25

def changelog
  @changelog
end

#idObject

Returns the value of attribute id.



24
25
26
# File 'lib/laboratory/experiment.rb', line 24

def id
  @id
end

#variantsObject (readonly)

Returns the value of attribute variants.



25
26
27
# File 'lib/laboratory/experiment.rb', line 25

def variants
  @variants
end

Class Method Details

.allObject



58
59
60
# File 'lib/laboratory/experiment.rb', line 58

def self.all
  Laboratory.adapter.read_all
end

.clear_overrides!Object



12
13
14
# File 'lib/laboratory/experiment.rb', line 12

def clear_overrides!
  Thread.current[:experiment_overrides] = {}
end

.create(id:, variants:, algorithm: Algorithms::Random) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/laboratory/experiment.rb', line 62

def self.create(id:, variants:, algorithm: Algorithms::Random)
  raise ClashingExperimentIdError if find(id)

  experiment = Experiment.new(
    id: id,
    variants: variants,
    algorithm: algorithm
  )

  experiment.save
  experiment
end

.find(id) ⇒ Object



75
76
77
# File 'lib/laboratory/experiment.rb', line 75

def self.find(id)
  Laboratory.adapter.read(id)
end

.find_or_create(id:, variants:, algorithm: Algorithms::Random) ⇒ Object



79
80
81
# File 'lib/laboratory/experiment.rb', line 79

def self.find_or_create(id:, variants:, algorithm: Algorithms::Random)
  find(id) || create(id: id, variants: variants, algorithm: algorithm)
end

.override!(overrides) ⇒ Object



8
9
10
# File 'lib/laboratory/experiment.rb', line 8

def override!(overrides)
  Thread.current[:experiment_overrides] = overrides
end

.overridesObject



4
5
6
# File 'lib/laboratory/experiment.rb', line 4

def overrides
  Thread.current[:experiment_overrides] || {}
end

Instance Method Details

#analysis_summary_for(event_id) ⇒ Object



156
157
158
# File 'lib/laboratory/experiment.rb', line 156

def analysis_summary_for(event_id)
  Experiment::AnalysisSummary.new(self, event_id)
end

#assign_to_variant(variant_id, user: Laboratory.config.current_user) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/laboratory/experiment.rb', line 119

def assign_to_variant(variant_id, user: Laboratory.config.current_user)
  variants.each do |variant|
    variant.participant_ids.delete(user.id)
  end

  variant = variants.find { |s| s.id == variant_id }
  variant.add_participant(user)

  Laboratory.config.on_assignment_to_variant&.call(self, variant, user)

  save
  variant
end

#deleteObject



83
84
85
86
# File 'lib/laboratory/experiment.rb', line 83

def delete
  Laboratory.adapter.delete(id)
  nil
end

#record_event!(event_id, user: Laboratory.config.current_user) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/MethodLength



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/laboratory/experiment.rb', line 133

def record_event!(event_id, user: Laboratory.config.current_user) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  variant = variants.find { |s| s.participant_ids.include?(user.id) }
  raise UserNotInExperimentError unless variant

  maybe_event = variant.events.find { |event| event.id == event_id }
  event =
    if !maybe_event.nil?
      maybe_event
    else
      e = Event.new(id: event_id)
      variant.events << e
      e
    end
  event_recording = Event::Recording.new(user_id: user.id)

  event.event_recordings << event_recording

  Laboratory.config.on_event_recorded&.call(self, variant, user, event)

  save
  event_recording
end

#resetObject



88
89
90
91
92
93
94
95
96
97
98
# File 'lib/laboratory/experiment.rb', line 88

def reset
  @variants = variants.map do |variant|
    Variant.new(
      id: variant.id,
      percentage: variant.percentage,
      participant_ids: [],
      events: []
    )
  end
  save
end

#saveObject



160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/laboratory/experiment.rb', line 160

def save
  raise errors.first unless valid?

  unless changeset.empty?
    changelog_item = Laboratory::Experiment::ChangelogItem.new(
      changes: changeset,
      timestamp: Time.now,
      actor: Laboratory.config.actor
    )

    @changelog << changelog_item
  end
  Laboratory.adapter.write(self)
end

#valid?Boolean

rubocop:disable Metrics/AbcSize

Returns:

  • (Boolean)


175
176
177
178
179
180
181
182
183
184
185
# File 'lib/laboratory/experiment.rb', line 175

def valid? # rubocop:disable Metrics/AbcSize
  valid_variants =
    variants.all? do |variant|
      !variant.id.nil? && !variant.percentage.nil?
    end

  valid_percentage_amounts =
    variants.map(&:percentage).sum == 100

  !id.nil? && !algorithm.nil? && valid_variants && valid_percentage_amounts
end

#variant(user: Laboratory.config.current_user) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/MethodLength



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/laboratory/experiment.rb', line 100

def variant(user: Laboratory.config.current_user) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  return variant_overridden_with if overridden?

  selected_variant =
    variants.find do |variant|
      variant.participant_ids.include?(user.id)
    end

  return selected_variant unless selected_variant.nil?

  variant = algorithm.pick!(variants)
  variant.add_participant(user)

  Laboratory.config.on_assignment_to_variant&.call(self, variant, user)

  save
  variant
end