Class: Vanity::Playground

Inherits:
Object show all
Defined in:
lib/vanity/playground.rb

Overview

Playground catalogs all your experiments, holds the Vanity configuration.

Examples:

Vanity.playground.logger = my_logger
puts Vanity.playground.map(&:name)

Constant Summary

DEFAULTS =
{ :collecting => true, :load_path=>"experiments" }
DEFAULT_ADD_PARTICIPANT_PATH =
'/vanity/add_participant'

Instance Attribute Summary (collapse)

Instance Method Summary (collapse)

Constructor Details

- (Playground) initialize(*args)

Created new Playground. Unless you need to, use the global Vanity.playground.

First argument is connection specification (see #redis=), last argument is a set of options, both are optional. Supported options are:

  • connection – Connection specification

  • namespace – Namespace to use

  • load_path – Path to load experiments/metrics from

  • logger – Logger to use

  • redis – A Redis object that will be used for the connection



25
26
27
28
29
30
31
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
57
58
# File 'lib/vanity/playground.rb', line 25

def initialize(*args)
  options = Hash === args.last ? args.pop : {}
  # In the case of Rails, use the Rails logger and collect only for
  # production environment by default.
  defaults = options[:rails] ? DEFAULTS.merge(:collecting => true, :logger => ::Rails.logger) : DEFAULTS
  if config_file_exists?
    env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
    config = load_config_file[env]
    if Hash === config
      config = config.inject({}) { |h,kv| h[kv.first.to_sym] = kv.last ; h }
    else
      config = { :connection=>config }
    end
  else
    config = {}
  end

  @options = defaults.merge(config).merge(options)

  warn "Deprecated: namespace option no longer supported directly" if @options[:namespace]
  @load_path = @options[:load_path] || DEFAULTS[:load_path]
  unless @logger = @options[:logger]
    @logger = Logger.new(STDOUT)
    @logger.level = Logger::ERROR
  end

  autoconnect(@options, args) if Vanity::Autoconnect.playground_should_autoconnect?

  @loading = []
  @use_js = false
  @failover_on_datastore_error = false
  self.add_participant_path = DEFAULT_ADD_PARTICIPANT_PATH
  @collecting = !!@options[:collecting]
end

Instance Attribute Details

- (Object) add_participant_path

Path to the add_participant action.



70
71
72
# File 'lib/vanity/playground.rb', line 70

def add_participant_path
  @add_participant_path
end

- (Object) db

Deprecated. Use redis.server instead.



61
62
63
# File 'lib/vanity/playground.rb', line 61

def db
  @db
end

- (Object) host

Deprecated. Use redis.server instead.



61
62
63
# File 'lib/vanity/playground.rb', line 61

def host
  @host
end

- (Object) load_path

Path to load experiment files from.



64
65
66
# File 'lib/vanity/playground.rb', line 64

def load_path
  @load_path
end

- (Object) logger

Logger.



67
68
69
# File 'lib/vanity/playground.rb', line 67

def logger
  @logger
end

- (Object) namespace

Deprecated. Use redis.server instead.



61
62
63
# File 'lib/vanity/playground.rb', line 61

def namespace
  @namespace
end

- (Object) on_datastore_error

Must return a Proc that accepts as parameters: the thrown error, the calling Class, the calling method, and an array of arguments passed to the calling method. The return value is ignored.

Proc.new do |error, klass, method, arguments|
  ...
end

The default implementation logs this information to Playground#logger.

Set a custom action by calling Vanity.playground.on_datastore_error = Proc.new { … }.

Since:

  • 1.9.0



178
179
180
# File 'lib/vanity/playground.rb', line 178

def on_datastore_error
  @on_datastore_error
end

- (Object) password

Deprecated. Use redis.server instead.



61
62
63
# File 'lib/vanity/playground.rb', line 61

def password
  @password
end

- (Object) port

Deprecated. Use redis.server instead.



61
62
63
# File 'lib/vanity/playground.rb', line 61

def port
  @port
end

- (Object) request_filter

Must return a Proc that accepts as a parameter the request object, if made available by the implement framework. The return value should be a boolean whether to ignore the request. This is called only for the JS callback action.

Proc.new do |request|
  ...
end

The default implementation does a simple test of whether the request's HTTP_USER_AGENT header contains a URI, since well behaved bots typically include a reference URI in their user agent strings. (Original idea: stackoverflow.com/a/9285889.)

Alternatively, one could filter an explicit list of IPs, add additional user agent strings to filter, or any custom test. Set a custom filter by calling Vanity.playground.request_filter = Proc.new { … }.

Since:

  • 1.9.0



216
217
218
# File 'lib/vanity/playground.rb', line 216

def request_filter
  @request_filter
end

Instance Method Details

- (Object) collecting=(enabled)

Turns data collection on and off.

Since:

  • 1.4.0



285
286
287
# File 'lib/vanity/playground.rb', line 285

def collecting=(enabled)
  @collecting = !!enabled
end

- (Boolean) collecting?

True if collection data (metrics and experiments). You only want to collect data in production environment, everywhere else run with collection off.

Returns:

  • (Boolean)

Since:

  • 1.4.0



278
279
280
# File 'lib/vanity/playground.rb', line 278

def collecting?
  @collecting
end

- (Boolean) config_file_exists?(basename = "vanity.yml")

Returns:

  • (Boolean)


384
385
386
# File 'lib/vanity/playground.rb', line 384

def config_file_exists?(basename = "vanity.yml")
  File.exists?(config_file_root + basename)
end

- (Object) config_file_root



380
381
382
# File 'lib/vanity/playground.rb', line 380

def config_file_root
  (defined?(::Rails) ? ::Rails.root : Pathname.new(".")) + "config"
end

- (Boolean) connected?

Returns true if connection is open.

Returns:

  • (Boolean)

Since:

  • 1.4.0



402
403
404
# File 'lib/vanity/playground.rb', line 402

def connected?
  @adapter && @adapter.active?
end

- (Object) connection

Returns the current connection. Establishes new connection is necessary.

Since:

  • 1.4.0



395
396
397
# File 'lib/vanity/playground.rb', line 395

def connection
  @adapter || establish_connection
end

- (Object) define(name, type, options = {}, &block)

Defines a new experiment. Generally, do not call this directly, use one of the definition methods (ab_test, measure, etc).

See Also:



80
81
82
83
84
85
86
87
88
89
# File 'lib/vanity/playground.rb', line 80

def define(name, type, options = {}, &block)
  warn "Deprecated: if you need this functionality let's make a better API"
  id = name.to_s.downcase.gsub(/\W/, "_").to_sym
  raise "Experiment #{id} already defined once" if experiments[id]
  klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase })
  experiment = klass.new(self, id, name, options)
  experiment.instance_eval &block
  experiment.save
  experiments[id] = experiment
end

- (Object) disconnect!

Closes the current connection.

Since:

  • 1.4.0



409
410
411
# File 'lib/vanity/playground.rb', line 409

def disconnect!
  @adapter.disconnect! if @adapter
end

- (Object) establish_connection(spec = nil)

This is the preferred way to programmatically create a new connection (or switch to a new connection). If no connection was established, the playground will create a new one by calling this method with no arguments.

With no argument, uses the connection specified in config/vanity.yml file for the current environment (RACK_ENV, RAILS_ENV or development). If there is no config/vanity.yml file, picks the configuration from config/redis.yml, or defaults to Redis on localhost, port 6379.

If the argument is a symbol, uses the connection specified in config/vanity.yml for that environment. For example:

Vanity.playground.establish_connection :production

If the argument is a string, it is processed as a URL. For example:

Vanity.playground.establish_connection "redis://redis.local/5"

Otherwise, the argument is a hash and specifies the adapter name and any additional options understood by that adapter (as with config/vanity.yml). For example:

Vanity.playground.establish_connection :adapter=>:redis,
                                       :host=>"redis.local"

Since:

  • 1.4.0



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/vanity/playground.rb', line 348

def establish_connection(spec = nil)
  @spec = spec
  disconnect! if @adapter
  case spec
  when nil
    if config_file_exists?
      env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
      spec = load_config_file[env]
      fail "No configuration for #{env}" unless spec
      establish_connection spec
    elsif config_file_exists?("redis.yml")
      env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
      redis = load_config_file("redis.yml")[env]
      fail "No configuration for #{env}" unless redis
      establish_connection "redis://" + redis
    else
      establish_connection :adapter=>"redis"
    end
  when Symbol
    spec = load_config_file[spec.to_s]
    establish_connection spec
  when String
    uri = URI.parse(spec)
    params = CGI.parse(uri.query) if uri.query
    establish_connection :adapter=>uri.scheme, :username=>uri.user, :password=>uri.password,
      :host=>uri.host, :port=>uri.port, :path=>uri.path, :params=>params
  else
    spec = spec.inject({}) { |hash,(k,v)| hash[k.to_sym] = v ; hash }
    @adapter = Adapters.establish_connection(spec)
  end
end

- (Object) experiment(name)

Returns the experiment. You may not have guessed, but this method raises an exception if it cannot load the experiment's definition.

See Also:



95
96
97
98
99
# File 'lib/vanity/playground.rb', line 95

def experiment(name)
  id = name.to_s.downcase.gsub(/\W/, "_").to_sym
  warn "Deprecated: pleae call experiment method with experiment identifier (a Ruby symbol)" unless id == name
  experiments[id.to_sym] or raise NoExperimentError, "No experiment #{id}"
end

- (Object) experiments

Returns hash of experiments (key is experiment id). This create the Experiment and persists it to the datastore.

See Also:



234
235
236
237
238
239
240
241
242
243
244
# File 'lib/vanity/playground.rb', line 234

def experiments
  unless @experiments
    @experiments = {}
    @logger.info "Vanity: loading experiments from #{load_path}"
    Dir[File.join(load_path, "*.rb")].each do |file|
      experiment = Experiment::Base.load(self, @loading, file)
      experiment.save
    end
  end
  @experiments
end

- (Boolean) experiments_persisted?

Returns:

  • (Boolean)


246
247
248
# File 'lib/vanity/playground.rb', line 246

def experiments_persisted?
  experiments.keys.all? { |id| connection.experiment_persisted?(id) }
end

- (Object) failover_on_datastore_error!

Turns on passing of errors to the Proc returned by #on_datastore_error. Call Vanity.playground.failover_on_datastore_error! to turn this on.

Since:

  • 1.9.0



153
154
155
# File 'lib/vanity/playground.rb', line 153

def failover_on_datastore_error!
  @failover_on_datastore_error = true
end

- (Boolean) failover_on_datastore_error?

Returns whether to failover on an error raise by the datastore adapter.

Returns:

  • (Boolean)

Since:

  • 1.9.0



160
161
162
# File 'lib/vanity/playground.rb', line 160

def failover_on_datastore_error?
  @failover_on_datastore_error
end

- (Object) load!

Loads all metrics and experiments. Rails calls this during initialization.



260
261
262
263
# File 'lib/vanity/playground.rb', line 260

def load!
  experiments
  metrics
end

- (Object) load_config_file(basename = "vanity.yml")



388
389
390
# File 'lib/vanity/playground.rb', line 388

def load_config_file(basename = "vanity.yml")
  YAML.load(ERB.new(File.read(config_file_root + basename)).result)
end

- (Object) metric(id)

Returns a metric (raises NameError if no metric with that identifier).

See Also:

Since:

  • 1.1.0



269
270
271
# File 'lib/vanity/playground.rb', line 269

def metric(id)
  metrics[id.to_sym] or raise NameError, "No metric #{id}"
end

- (Object) metrics

Returns hash of metrics (key is metric id).

See Also:

Since:

  • 1.1.0



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/vanity/playground.rb', line 293

def metrics
  unless @metrics
    @metrics = {}
    @logger.info "Vanity: loading metrics from #{load_path}/metrics"
    Dir[File.join(load_path, "metrics/*.rb")].each do |file|
      Metric.load self, @loading, file
    end
    if config_file_exists? && remote = load_config_file["metrics"]
      remote.each do |id, url|
        fail "Metric #{id} already defined in playground" if metrics[id.to_sym]
        metric = Metric.new(self, id)
        metric.remote url
        metrics[id.to_sym] = metric
      end
    end
  end
  @metrics
end

- (Object) participant_info(participant_id)

Returns an array of all experiments this participant is involved in, with their assignment.

This is done as an array of arrays [[<experiment_1>, <assignment_1>], [<experiment_2>, <assignment_2>]], sorted by experiment name, so that it will give a consistent string
when converted to_s (so could be used for caching, for example)


107
108
109
110
111
112
113
114
115
116
# File 'lib/vanity/playground.rb', line 107

def participant_info(participant_id)
  participant_array = []
  experiments.values.sort_by(&:name).each do |e|
    index = connection.ab_assigned(e.id, participant_id)
    if index
      participant_array << [e, e.alternatives[index.to_i]]
    end
  end
  participant_array
end

- (Object) reconnect!

Closes the current connection and establishes a new one.

Since:

  • 1.3.0



416
417
418
# File 'lib/vanity/playground.rb', line 416

def reconnect!
  establish_connection(@spec)
end

- (Object) redis



441
442
443
444
# File 'lib/vanity/playground.rb', line 441

def redis
  warn "Deprecated: use connection method instead"
  connection
end

- (Object) redis=(spec_or_connection)

Deprecated. Use establish_connection or configuration file instead.



427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/vanity/playground.rb', line 427

def redis=(spec_or_connection)
  warn "Deprecated: use establish_connection method instead"
  case spec_or_connection
  when String
    establish_connection "redis://" + spec_or_connection
  when ::Redis
    @connection = Adapters::RedisAdapter.new(spec_or_connection)
  when :mock
    establish_connection :adapter=>:mock
  else
    raise "I don't know what to do with #{spec_or_connection.inspect}"
  end
end

- (Object) reload!

Reloads all metrics and experiments. Rails calls this for each request in development mode.



252
253
254
255
256
# File 'lib/vanity/playground.rb', line 252

def reload!
  @experiments = nil
  @metrics = nil
  load!
end

- (Object) test!

Deprecated. Use Vanity.playground.collecting = true/false instead.



421
422
423
424
# File 'lib/vanity/playground.rb', line 421

def test!
  warn "Deprecated: use collecting = false instead"
  self.collecting = false
end

- (Object) track!(id, count = 1)

Tracks an action associated with a metric.

Examples:

Vanity.playground.track! :uploaded_video

Since:

  • 1.1.0



318
319
320
# File 'lib/vanity/playground.rb', line 318

def track!(id, count = 1)
  metric(id).track! count
end

- (Object) use_js!

Call to indicate that participants should be added via js. This helps keep robots from participating in the A/B test and skewing results.

If you want to use this:

  • Add <%= vanity_js %> to any page that needs uses an ab_test. vanity_js needs to be included after your call to ab_test so that it knows which version of the experiment the participant is a member of. The helper will render nothing if the there are no ab_tests running on the current page, so adding vanity_js to the bottom of your layouts is a good option. Keep in mind that if you call use_js! and don't include vanity_js in your view no participants will be recorded.

Note that a custom JS callback path can be set using:

  • Set Vanity.playground.add_participant_path = '/path/to/vanity/action', this should point to the add_participant path that is added with Vanity::Rails::Dashboard, make sure that this action is available to all users.



138
139
140
# File 'lib/vanity/playground.rb', line 138

def use_js!
  @use_js = true
end

- (Boolean) using_js?

Returns:

  • (Boolean)


142
143
144
# File 'lib/vanity/playground.rb', line 142

def using_js?
  @use_js
end