forked from assaf/vanity
/
playground.rb
179 lines (150 loc) · 5.33 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
module Vanity
# These methods are available from experiment definitions (files located in
# the experiments directory, automatically loaded by Vanity). Use these
# methods to define you experiments, for example:
# ab_test "New Banner" do
# alternatives :red, :green, :blue
# end
module Definition
protected
# Defines a new experiment, given the experiment's name, type and
# definition block.
def define(name, type, options = nil, &block)
options ||= {}
Vanity.playground.define(name, type, options, &block)
end
end
# Playground catalogs all your experiments, holds the Vanity configuration.
# For example:
# Vanity.playground.logger = my_logger
# puts Vanity.playground.map(&:name)
class Playground
# Created new Playground. Unless you need to, use the global Vanity.playground.
def initialize
@experiments = {}
@metrics = {}
@host, @port, @db = "127.0.0.1", 6379, 0
@namespace = "vanity:#{Vanity::Version::MAJOR}"
@load_path = "experiments"
@logger = Logger.new(STDOUT)
@logger.level = Logger::ERROR
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).
def define(name, type, options = {}, &block)
id = name.to_s.downcase.gsub(/\W/, "_")
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 named experiment. You may not have guessed, but this method
# raises an exception if it cannot load the experiment's definition.
#
# Experiment names are always mapped by downcasing them and replacing
# non-word characters with underscores, so "Green call to action" becomes
# "green_call_to_action". You can also use a symbol if you feel like it.
def experiment(name)
id = name.to_s.downcase.gsub(/\W/, "_")
unless @experiments.has_key?(id)
@loading ||= []
fail "Circular dependency detected: #{@loading.join('=>')}=>#{id}" if @loading.include?(id)
begin
@loading.push id
source = File.read(File.expand_path("#{id}.rb", load_path))
context = Object.new
context.instance_eval do
extend Definition
eval source
end
rescue
error = LoadError.exception($!.message)
error.set_backtrace $!.backtrace
raise error
ensure
@loading.pop
end
end
@experiments[id] or fail LoadError, "Expected experiments/#{id}.rb to define experiment #{name}"
end
# Returns list of all loaded experiments.
def experiments
Dir[File.join(load_path, "*.rb")].each do |file|
id = File.basename(file).gsub(/.rb$/, "")
experiment id
end
@experiments.values
end
# Reloads all experiments.
def reload!
@experiments.clear
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
# Returns a metric (creating one if doesn't already exist).
def metric(id)
id = id.to_sym
@metrics[id] ||= Metric.new(self, id)
end
# Returns hash of metrics (key is metric id).
def metrics
@metrics
end
# Tracks an action associated with a metric. For example:
# Vanity.playground.track! :uploaded_video
def track!(id)
metric(id).track! Vanity.context.vanity_identity
end
end
@playground = Playground.new
class << self
# Returns the playground instance.
def playground
@playground
end
# 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. For example:
# puts experiment(:text_size).alternatives
def experiment(name)
Vanity.playground.experiment(name)
end
end