# frozen_string_literal: true module Split class Configuration attr_accessor :ignore_ip_addresses attr_accessor :ignore_filter attr_accessor :db_failover attr_accessor :db_failover_on_db_error attr_accessor :db_failover_allow_parameter_override attr_accessor :allow_multiple_experiments attr_accessor :enabled attr_accessor :persistence attr_accessor :persistence_cookie_length attr_accessor :persistence_cookie_domain attr_accessor :algorithm attr_accessor :store_override attr_accessor :start_manually attr_accessor :reset_manually attr_accessor :on_trial attr_accessor :on_trial_choose attr_accessor :on_trial_complete attr_accessor :on_experiment_reset attr_accessor :on_experiment_delete attr_accessor :on_before_experiment_reset attr_accessor :on_experiment_winner_choose attr_accessor :on_before_experiment_delete attr_accessor :include_rails_helper attr_accessor :beta_probability_simulations attr_accessor :winning_alternative_recalculation_interval attr_accessor :redis attr_accessor :dashboard_pagination_default_per_page attr_accessor :cache attr_reader :experiments attr_writer :bots attr_writer :robot_regex def bots @bots ||= { # Indexers "AdsBot-Google" => "Google Adwords", "Baidu" => "Chinese search engine", "Baiduspider" => "Chinese search engine", "bingbot" => "Microsoft bing bot", "Butterfly" => "Topsy Labs", "Gigabot" => "Gigabot spider", "Googlebot" => "Google spider", "MJ12bot" => "Majestic-12 spider", "msnbot" => "Microsoft bot", "rogerbot" => "SeoMoz spider", "PaperLiBot" => "PaperLi is another content curation service", "Slurp" => "Yahoo spider", "Sogou" => "Chinese search engine", "spider" => "generic web spider", "UnwindFetchor" => "Gnip crawler", "WordPress" => "WordPress spider", "YandexAccessibilityBot" => "Yandex accessibility spider", "YandexBot" => "Yandex spider", "YandexMobileBot" => "Yandex mobile spider", "ZIBB" => "ZIBB spider", # HTTP libraries "Apache-HttpClient" => "Java http library", "AppEngine-Google" => "Google App Engine", "curl" => "curl unix CLI http client", "ColdFusion" => "ColdFusion http library", "EventMachine HttpClient" => "Ruby http library", "Go http package" => "Go http library", "Go-http-client" => "Go http library", "Java" => "Generic Java http library", "libwww-perl" => "Perl client-server library loved by script kids", "lwp-trivial" => "Another Perl library loved by script kids", "Python-urllib" => "Python http library", "PycURL" => "Python http library", "Test Certificate Info" => "C http library?", "Typhoeus" => "Ruby http library", "Wget" => "wget unix CLI http client", # URL expanders / previewers "awe.sm" => "Awe.sm URL expander", "bitlybot" => "bit.ly bot", "bot@linkfluence.net" => "Linkfluence bot", "facebookexternalhit" => "facebook bot", "Facebot" => "Facebook crawler", "Feedfetcher-Google" => "Google Feedfetcher", "https://developers.google.com/+/web/snippet" => "Google+ Snippet Fetcher", "LinkedInBot" => "LinkedIn bot", "LongURL" => "URL expander service", "NING" => "NING - Yet Another Twitter Swarmer", "Pinterestbot" => "Pinterest Bot", "redditbot" => "Reddit Bot", "ShortLinkTranslate" => "Link shortener", "Slackbot" => "Slackbot link expander", "TweetmemeBot" => "TweetMeMe Crawler", "Twitterbot" => "Twitter URL expander", "UnwindFetch" => "Gnip URL expander", "vkShare" => "VKontake Sharer", # Uptime monitoring "check_http" => "Nagios monitor", "GoogleStackdriverMonitoring" => "Google Cloud monitor", "NewRelicPinger" => "NewRelic monitor", "Panopta" => "Monitoring service", "Pingdom" => "Pingdom monitoring", "SiteUptime" => "Site monitoring services", "UptimeRobot" => "Monitoring service", # ??? "DigitalPersona Fingerprint Software" => "HP Fingerprint scanner", "ShowyouBot" => "Showyou iOS app spider", "ZyBorg" => "Zyborg? Hmmm....", "ELB-HealthChecker" => "ELB Health Check" } end def experiments=(experiments) raise InvalidExperimentsFormatError.new("Experiments must be a Hash") unless experiments.respond_to?(:keys) @experiments = experiments end def disabled? !enabled end def experiment_for(name) if normalized_experiments # TODO symbols normalized_experiments[name.to_sym] end end def metrics return @metrics if defined?(@metrics) @metrics = {} if self.experiments self.experiments.each do |key, value| metrics = value_for(value, :metric) rescue nil Array(metrics).each do |metric_name| if metric_name @metrics[metric_name.to_sym] ||= [] @metrics[metric_name.to_sym] << Split::Experiment.new(key) end end end end @metrics end def normalized_experiments return nil if @experiments.nil? experiment_config = {} @experiments.keys.each do |name| experiment_config[name.to_sym] = {} end @experiments.each do |experiment_name, settings| alternatives = if (alts = value_for(settings, :alternatives)) normalize_alternatives(alts) end experiment_data = { alternatives: alternatives, goals: value_for(settings, :goals), metadata: value_for(settings, :metadata), algorithm: value_for(settings, :algorithm), resettable: value_for(settings, :resettable) } experiment_data.each do |name, value| experiment_config[experiment_name.to_sym][name] = value if value != nil end end experiment_config end def normalize_alternatives(alternatives) given_probability, num_with_probability = alternatives.inject([0, 0]) do |a, v| p, n = a if percent = value_for(v, :percent) [p + percent, n + 1] else a end end num_without_probability = alternatives.length - num_with_probability unassigned_probability = ((100.0 - given_probability) / num_without_probability / 100.0) if num_with_probability.nonzero? alternatives = alternatives.map do |v| if (name = value_for(v, :name)) && (percent = value_for(v, :percent)) { name => percent / 100.0 } elsif name = value_for(v, :name) { name => unassigned_probability } else { v => unassigned_probability } end end [alternatives.shift, alternatives] else alternatives = alternatives.dup [alternatives.shift, alternatives] end end def robot_regex @robot_regex ||= /\b(?:#{escaped_bots.join('|')})\b|\A\W*\z/i end def initialize @ignore_ip_addresses = [] @ignore_filter = proc { |request| is_robot? || is_ignored_ip_address? } @db_failover = false @db_failover_on_db_error = proc { |error| } # e.g. use Rails logger here @on_experiment_reset = proc { |experiment| } @on_experiment_delete = proc { |experiment| } @on_before_experiment_reset = proc { |experiment| } @on_before_experiment_delete = proc { |experiment| } @on_experiment_winner_choose = proc { |experiment| } @db_failover_allow_parameter_override = false @allow_multiple_experiments = false @enabled = true @experiments = {} @persistence = Split::Persistence::SessionAdapter @persistence_cookie_length = 31536000 # One year from now @persistence_cookie_domain = nil @algorithm = Split::Algorithms::WeightedSample @include_rails_helper = true @beta_probability_simulations = 10000 @winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day @redis = ENV.fetch(ENV.fetch("REDIS_PROVIDER", "REDIS_URL"), "redis://localhost:6379") @dashboard_pagination_default_per_page = 10 end private def value_for(hash, key) if hash.kind_of?(Hash) hash.has_key?(key.to_s) ? hash[key.to_s] : hash[key.to_sym] end end def escaped_bots bots.map { |key, _| Regexp.escape(key) } end end end