forked from assaf/vanity
/
playground.rb
206 lines (177 loc) · 6.02 KB
/
playground.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
module Vanity
# Playground catalogs all your experiments, holds the Vanity configuration.
#
# @example
# Vanity.playground.logger = my_logger
# puts Vanity.playground.map(&:name)
class Playground
DEFAULTS = { :host=>"127.0.0.1", :port=>6379, :db=>0, :load_path=>"experiments" }
# Created new Playground. Unless you need to, use the global Vanity.playground.
def initialize(options = {})
options = options.merge(DEFAULTS)
adapter = options[:adapter] || options[:redis] || :redis
if adapter.respond_to?(:mget)
@redis = adapter
elsif adapter == :redis
@host, @port, @db = options.values_at(:host, :port, :db)
else
require "vanity/store/#{adapter}"
@redis = Vanity::Store.const_get(adapter.to_s.capitalize).new
end
@load_path = options[:load_path]
@namespace = "vanity:#{Vanity::Version::MAJOR}"
@logger = options[:logger]
unless @logger
@logger = Logger.new(STDOUT)
@logger.level = Logger::ERROR
end
@loading = []
end
# Redis host name. Default is 127.0.0.1
attr_accessor :host
# Redis port number. Default is 6379.
attr_accessor :port
# Redis database number. Default is 0.
attr_accessor :db
# Redis database password.
attr_accessor :password
# Namespace for database keys. Default is vanity:n, where n is the major release number, e.g. vanity:1 for 1.0.3.
attr_accessor :namespace
# Path to load experiment files from.
attr_accessor :load_path
# Logger.
attr_accessor :logger
# Defines a new experiment. Generally, do not call this directly,
# use one of the definition methods (ab_test, measure, etc).
#
# @see Vanity::Experiment
def define(name, type, options = {}, &block)
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
# Returns the experiment. You may not have guessed, but this method raises
# an exception if it cannot load the experiment's definition.
#
# @see Vanity::Experiment
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] ||= Experiment::Base.load(self, @loading, File.expand_path(load_path), id)
end
# Returns hash of experiments (key is experiment id).
#
# @see Vanity::Experiment
def experiments
unless @experiments
@experiments = {}
@logger.info "Vanity: loading experiments from #{load_path}"
Dir[File.join(load_path, "*.rb")].each do |file|
id = File.basename(file).gsub(/.rb$/, "")
experiment id.to_sym
end
end
@experiments
end
# Reloads all metrics and experiments. Rails calls this for each request in
# development mode.
def reload!
@experiments = nil
@metrics = nil
load!
end
# Loads all metrics and experiments. Rails calls this during
# initialization.
def load!
experiments
metrics
end
# Use this instance to access the Redis database.
def redis
@redis ||= Redis.new(:host=>self.host, :port=>self.port, :db=>self.db,
:password=>self.password, :logger=>self.logger)
class << self ; self ; end.send(:define_method, :redis) { @redis }
@redis
end
# Switches playground to use Vanity::Store::Mock instead of a live server.
# Particularly useful for testing, e.g. if you can't access Redis on your CI
# server. This method has no affect after playground accesses live Redis
# server.
#
# @example Put this in config/environments/test.rb
# config.after_initialize { Vanity.playground.mock! }
def mock!
@redis ||= Vanity::Store::Mock.new
end
# Returns a metric (raises NameError if no metric with that identifier).
#
# @see Vanity::Metric
# @since 1.1.0
def metric(id)
metrics[id.to_sym] or raise NameError, "No metric #{id}"
end
# Returns hash of metrics (key is metric id).
#
# @see Vanity::Metric
# @since 1.1.0
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
end
@metrics
end
# Tracks an action associated with a metric.
#
# @example
# Vanity.playground.track! :uploaded_video
#
# @since 1.1.0
def track!(id, count = 1)
metric(id).track! count
end
end
@playground = Playground.new
class << self
# The playground instance.
#
# @see Vanity::Playground
attr_accessor :playground
# Returns the Vanity context. For example, when using Rails this would be
# the current controller, which can be used to get/set the vanity identity.
def context
Thread.current[:vanity_context]
end
# Sets the Vanity context. For example, when using Rails this would be
# set by the set_vanity_context before filter (via Vanity::Rails#use_vanity).
def context=(context)
Thread.current[:vanity_context] = context
end
# Path to template.
def template(name)
path = File.join(File.dirname(__FILE__), "templates/#{name}")
path << ".erb" unless name["."]
path
end
end
end
class Object
# Use this method to access an experiment by name.
#
# @example
# puts experiment(:text_size).alternatives
#
# @see Vanity::Playground#experiment
# @deprecated
def experiment(name)
warn "Deprecated. Please call Vanity.playground.experiment directly."
Vanity.playground.experiment(name)
end
end