class TrailGuide::Participant

Attributes

context[R]

Public Class Methods

new(context, adapter: nil) click to toggle source
# File lib/trail_guide/participant.rb, line 6
def initialize(context, adapter: nil)
  @context = context
  @adapter = adapter.new(context) if adapter.present?

  cleanup_inactive_experiments! if TrailGuide.configuration.cleanup_participant_experiments == true
end

Public Instance Methods

active_experiments(include_control=true) click to toggle source
# File lib/trail_guide/participant.rb, line 125
def active_experiments(include_control=true)
  return false if adapter.keys.empty?

  inactive = []
  active = adapter.keys.map { |key| key.to_s.split(":").first.to_sym }.uniq.map do |key|
    experiment = TrailGuide.catalog.find(key)
    next unless experiment
    next unless experiment.configuration.sticky_assignment?

    if !experiment.started? && !experiment.calibrating?
      inactive << key
      next
    else
      next unless !experiment.combined? && experiment.running? && participating?(experiment, include_control)
      [ experiment.experiment_name, adapter[experiment.storage_key] ]
    end
  end.compact.to_h

  if TrailGuide.configuration.cleanup_participant_experiments == :inline && !inactive.empty?
    adapter.keys.select do |key|
      inactive.include?(key.to_s.split(":").first.to_sym)
    end.each { |key| adapter.delete(key) }
  end

  return false if active.empty?
  return active
end
adapter() click to toggle source
# File lib/trail_guide/participant.rb, line 13
def adapter
  # TODO move this case selection to Adapters::Participant (like Algorithms)?
  @adapter ||= begin
    config_adapter = TrailGuide.configuration.adapter
    case config_adapter
    when :cookie
      config_adapter = TrailGuide::Adapters::Participants::Cookie
    when :session
      config_adapter = TrailGuide::Adapters::Participants::Session
    when :redis
      config_adapter = TrailGuide::Adapters::Participants::Redis
    when :anonymous
      config_adapter = TrailGuide::Adapters::Participants::Anonymous
    when :multi
      config_adapter = TrailGuide::Adapters::Participants::Multi
    else
      config_adapter = config_adapter.constantize if config_adapter.is_a?(String)
    end
    config_adapter.new(context)
  rescue => e
    [TrailGuide.configuration.on_adapter_failover].flatten.compact.each do |callback|
      callback.call(config_adapter, e)
    end
    TrailGuide::Adapters::Participants::Anonymous.new(context)
  end
end
calibrating_experiments() click to toggle source
# File lib/trail_guide/participant.rb, line 153
def calibrating_experiments
  return false if adapter.keys.empty?

  calibrating = adapter.keys.map { |key| key.to_s.split(":").first.to_sym }.uniq.map do |key|
    experiment = TrailGuide.catalog.find(key)
    next unless experiment && experiment.calibrating?
    [ experiment.experiment_name, adapter[experiment.storage_key] ]
  end.compact.to_h

  return false if calibrating.empty?
  return calibrating
end
cleanup_inactive_experiments!() click to toggle source
# File lib/trail_guide/participant.rb, line 176
def cleanup_inactive_experiments!
  return false if adapter.keys.empty?

  adapter.keys.each do |key|
    experiment_name = key.to_s.split(":").first.to_sym
    experiment = TrailGuide.catalog.find(experiment_name)
    if !experiment || (!experiment.started? && !experiment.calibrating?)
      adapter.delete(key)
    end
  end

  return true
end
converted!(variant, checkpoint=nil, reset: false) click to toggle source
# File lib/trail_guide/participant.rb, line 94
def converted!(variant, checkpoint=nil, reset: false)
  if checkpoint.nil?
    storage_key = "#{variant.experiment.storage_key}:converted"
  else
    storage_key = variant.experiment.goals.find { |g| g == checkpoint }.storage_key
  end

  if reset
    adapter.delete(variant.experiment.storage_key)
    adapter.delete(variant.storage_key)
    adapter.delete(storage_key)
    variant.experiment.goals.each do |goal|
      adapter.delete(goal.storage_key)
    end
  else
    adapter[storage_key] = Time.now.to_i
  end
end
converted?(experiment, checkpoint=nil) click to toggle source
# File lib/trail_guide/participant.rb, line 60
def converted?(experiment, checkpoint=nil)
  variant = variant(experiment)

  return false unless experiment.started? || (experiment.calibrating? && variant.try(:control?))

  if experiment.goals.empty?
    raise InvalidGoalError, "You provided the checkpoint `#{checkpoint}` but the experiment `#{experiment.experiment_name}` does not have any goals defined." unless checkpoint.nil?
    storage_key = "#{experiment.storage_key}:converted"
    return false unless adapter.key?(storage_key)

    converted_at = Time.at(adapter[storage_key].to_i)
    (experiment.calibrating? && variant.try(:control?)) || converted_at >= experiment.started_at
  elsif !checkpoint.nil?
    goal = experiment.goals.find { |g| g == checkpoint }
    raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for experiment `#{experiment.experiment_name}`." if goal.nil?
    return false unless adapter.key?(goal.storage_key)

    converted_at = Time.at(adapter[goal.storage_key].to_i)
    (experiment.calibrating? && variant.try(:control?)) || converted_at >= experiment.started_at
  else
    experiment.goals.each do |goal|
      next unless adapter.key?(goal.storage_key)
      converted_at = Time.at(adapter[goal.storage_key].to_i)
      return true if (experiment.calibrating? && variant.try(:control?)) || converted_at >= experiment.started_at
    end
    return false
  end
end
exit!(experiment) click to toggle source
# File lib/trail_guide/participant.rb, line 113
def exit!(experiment)
  chosen = variant(experiment)
  return true if chosen.nil?
  adapter.delete(experiment.storage_key)
  adapter.delete(chosen.storage_key)
  adapter.delete("#{experiment.storage_key}:converted")
  experiment.goals.each do |goal|
    adapter.delete(goal.storage_key)
  end
  return true
end
participating!(variant) click to toggle source
# File lib/trail_guide/participant.rb, line 89
def participating!(variant)
  adapter[variant.experiment.storage_key] = variant.name
  adapter[variant.storage_key] = Time.now.to_i
end
participating?(experiment, include_control=true) click to toggle source
# File lib/trail_guide/participant.rb, line 53
def participating?(experiment, include_control=true)
  var = variant(experiment)
  return false if var.nil?
  return false if !include_control && var.control?
  return true
end
participating_in_active_experiments?(include_control=true) click to toggle source
# File lib/trail_guide/participant.rb, line 166
def participating_in_active_experiments?(include_control=true)
  return false if adapter.keys.empty?

  adapter.keys.any? do |key|
    experiment_name = key.to_s.split(":").first.to_sym
    experiment = TrailGuide.catalog.find(experiment_name)
    experiment && experiment.configuration.sticky_assignment? && !experiment.combined? && experiment.running? && participating?(experiment, include_control)
  end
end
variant(experiment) click to toggle source
# File lib/trail_guide/participant.rb, line 40
def variant(experiment)
  return nil unless experiment.calibrating? || experiment.started?
  # TODO more efficient to stop checking if keys exist, and just return if the value is blank??
  return nil unless adapter.key?(experiment.storage_key)
  varname = adapter[experiment.storage_key]
  variant = experiment.variants.find { |var| var == varname }
  return nil unless variant && adapter.key?(variant.storage_key)

  chosen_at = Time.at(adapter[variant.storage_key].to_i)
  started_at = experiment.started_at
  return variant if (variant.control? && experiment.calibrating?) || (started_at && chosen_at >= started_at)
end