modesty 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +13 -0
- data/Gemfile.lock +18 -0
- data/LICENSE +21 -0
- data/README.md +121 -0
- data/Rakefile +29 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/lib/modesty.rb +26 -0
- data/lib/modesty/api.rb +14 -0
- data/lib/modesty/core_ext.rb +5 -0
- data/lib/modesty/core_ext/array.rb +21 -0
- data/lib/modesty/core_ext/fixnum.rb +5 -0
- data/lib/modesty/core_ext/hash.rb +39 -0
- data/lib/modesty/core_ext/string.rb +9 -0
- data/lib/modesty/core_ext/symbol.rb +33 -0
- data/lib/modesty/datastore.rb +51 -0
- data/lib/modesty/datastore/redis.rb +180 -0
- data/lib/modesty/experiment.rb +87 -0
- data/lib/modesty/experiment/base.rb +47 -0
- data/lib/modesty/experiment/builder.rb +48 -0
- data/lib/modesty/experiment/console.rb +4 -0
- data/lib/modesty/experiment/data.rb +75 -0
- data/lib/modesty/experiment/interface.rb +29 -0
- data/lib/modesty/experiment/significance.rb +376 -0
- data/lib/modesty/experiment/stats.rb +163 -0
- data/lib/modesty/frameworks/rails.rb +27 -0
- data/lib/modesty/identity.rb +32 -0
- data/lib/modesty/load.rb +80 -0
- data/lib/modesty/load/load_experiments.rb +14 -0
- data/lib/modesty/load/load_metrics.rb +17 -0
- data/lib/modesty/metric.rb +56 -0
- data/lib/modesty/metric/base.rb +38 -0
- data/lib/modesty/metric/builder.rb +23 -0
- data/lib/modesty/metric/data.rb +133 -0
- data/modesty.gemspec +192 -0
- data/spec/core_ext_spec.rb +17 -0
- data/spec/experiment_spec.rb +239 -0
- data/spec/identity_spec.rb +161 -0
- data/spec/load_spec.rb +87 -0
- data/spec/metric_spec.rb +176 -0
- data/spec/rails_spec.rb +48 -0
- data/spec/redis_spec.rb +29 -0
- data/spec/significance_spec.rb +147 -0
- data/spec/spec.opts +1 -0
- data/test/myapp/config/modesty.yml +9 -0
- data/test/myapp/modesty/experiments/cookbook.rb +4 -0
- data/test/myapp/modesty/metrics/kitchen_metrics.rb +9 -0
- data/test/myapp/modesty/metrics/stove/burner_metrics.rb +2 -0
- data/vendor/.piston.yml +8 -0
- data/vendor/mock_redis/.gitignore +2 -0
- data/vendor/mock_redis/README +8 -0
- data/vendor/mock_redis/lib/mock_redis.rb +10 -0
- data/vendor/mock_redis/lib/mock_redis/hash.rb +61 -0
- data/vendor/mock_redis/lib/mock_redis/list.rb +6 -0
- data/vendor/mock_redis/lib/mock_redis/misc.rb +69 -0
- data/vendor/mock_redis/lib/mock_redis/set.rb +108 -0
- data/vendor/mock_redis/lib/mock_redis/string.rb +32 -0
- data/vendor/redis-rb/.gitignore +8 -0
- data/vendor/redis-rb/LICENSE +20 -0
- data/vendor/redis-rb/README.markdown +129 -0
- data/vendor/redis-rb/Rakefile +155 -0
- data/vendor/redis-rb/benchmarking/logging.rb +62 -0
- data/vendor/redis-rb/benchmarking/pipeline.rb +51 -0
- data/vendor/redis-rb/benchmarking/speed.rb +21 -0
- data/vendor/redis-rb/benchmarking/suite.rb +24 -0
- data/vendor/redis-rb/benchmarking/thread_safety.rb +38 -0
- data/vendor/redis-rb/benchmarking/worker.rb +71 -0
- data/vendor/redis-rb/examples/basic.rb +15 -0
- data/vendor/redis-rb/examples/dist_redis.rb +43 -0
- data/vendor/redis-rb/examples/incr-decr.rb +17 -0
- data/vendor/redis-rb/examples/list.rb +26 -0
- data/vendor/redis-rb/examples/pubsub.rb +31 -0
- data/vendor/redis-rb/examples/sets.rb +36 -0
- data/vendor/redis-rb/examples/unicorn/config.ru +3 -0
- data/vendor/redis-rb/examples/unicorn/unicorn.rb +20 -0
- data/vendor/redis-rb/lib/redis.rb +676 -0
- data/vendor/redis-rb/lib/redis/client.rb +201 -0
- data/vendor/redis-rb/lib/redis/compat.rb +21 -0
- data/vendor/redis-rb/lib/redis/connection.rb +134 -0
- data/vendor/redis-rb/lib/redis/distributed.rb +526 -0
- data/vendor/redis-rb/lib/redis/hash_ring.rb +131 -0
- data/vendor/redis-rb/lib/redis/pipeline.rb +13 -0
- data/vendor/redis-rb/lib/redis/subscribe.rb +79 -0
- data/vendor/redis-rb/redis.gemspec +29 -0
- data/vendor/redis-rb/test/commands_on_hashes_test.rb +46 -0
- data/vendor/redis-rb/test/commands_on_lists_test.rb +50 -0
- data/vendor/redis-rb/test/commands_on_sets_test.rb +78 -0
- data/vendor/redis-rb/test/commands_on_sorted_sets_test.rb +109 -0
- data/vendor/redis-rb/test/commands_on_strings_test.rb +70 -0
- data/vendor/redis-rb/test/commands_on_value_types_test.rb +88 -0
- data/vendor/redis-rb/test/connection_handling_test.rb +87 -0
- data/vendor/redis-rb/test/db/.gitignore +1 -0
- data/vendor/redis-rb/test/distributd_key_tags_test.rb +53 -0
- data/vendor/redis-rb/test/distributed_blocking_commands_test.rb +54 -0
- data/vendor/redis-rb/test/distributed_commands_on_hashes_test.rb +12 -0
- data/vendor/redis-rb/test/distributed_commands_on_lists_test.rb +18 -0
- data/vendor/redis-rb/test/distributed_commands_on_sets_test.rb +85 -0
- data/vendor/redis-rb/test/distributed_commands_on_strings_test.rb +50 -0
- data/vendor/redis-rb/test/distributed_commands_on_value_types_test.rb +73 -0
- data/vendor/redis-rb/test/distributed_commands_requiring_clustering_test.rb +141 -0
- data/vendor/redis-rb/test/distributed_connection_handling_test.rb +25 -0
- data/vendor/redis-rb/test/distributed_internals_test.rb +18 -0
- data/vendor/redis-rb/test/distributed_persistence_control_commands_test.rb +24 -0
- data/vendor/redis-rb/test/distributed_publish_subscribe_test.rb +90 -0
- data/vendor/redis-rb/test/distributed_remote_server_control_commands_test.rb +31 -0
- data/vendor/redis-rb/test/distributed_sorting_test.rb +21 -0
- data/vendor/redis-rb/test/distributed_test.rb +60 -0
- data/vendor/redis-rb/test/distributed_transactions_test.rb +34 -0
- data/vendor/redis-rb/test/encoding_test.rb +16 -0
- data/vendor/redis-rb/test/helper.rb +86 -0
- data/vendor/redis-rb/test/internals_test.rb +27 -0
- data/vendor/redis-rb/test/lint/hashes.rb +90 -0
- data/vendor/redis-rb/test/lint/internals.rb +53 -0
- data/vendor/redis-rb/test/lint/lists.rb +93 -0
- data/vendor/redis-rb/test/lint/sets.rb +66 -0
- data/vendor/redis-rb/test/lint/sorted_sets.rb +132 -0
- data/vendor/redis-rb/test/lint/strings.rb +98 -0
- data/vendor/redis-rb/test/lint/value_types.rb +84 -0
- data/vendor/redis-rb/test/persistence_control_commands_test.rb +22 -0
- data/vendor/redis-rb/test/pipelining_commands_test.rb +78 -0
- data/vendor/redis-rb/test/publish_subscribe_test.rb +151 -0
- data/vendor/redis-rb/test/redis_mock.rb +64 -0
- data/vendor/redis-rb/test/remote_server_control_commands_test.rb +56 -0
- data/vendor/redis-rb/test/sorting_test.rb +44 -0
- data/vendor/redis-rb/test/test.conf +8 -0
- data/vendor/redis-rb/test/thread_safety_test.rb +34 -0
- data/vendor/redis-rb/test/transactions_test.rb +91 -0
- data/vendor/redis-rb/test/unknown_commands_test.rb +14 -0
- data/vendor/redis-rb/test/url_param_test.rb +52 -0
- metadata +277 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
module Modesty
|
2
|
+
class Experiment
|
3
|
+
|
4
|
+
def stats
|
5
|
+
@stats ||= Hash.new do |hash, key|
|
6
|
+
raise Error, <<-msg.squish
|
7
|
+
Unrecognized stat #{key.inspect}
|
8
|
+
msg
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def reports(*args)
|
13
|
+
self.stats.values.map { |s| s.report(*args) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def aggregates(metric, *args)
|
17
|
+
metric = metric.slug if metric.is_a? Metric
|
18
|
+
context = self.identity_for(metric)
|
19
|
+
self.alternatives.hashmap do |a|
|
20
|
+
agg = self.metrics(a)[metric].aggregate_by(context, *args)
|
21
|
+
agg = agg.sum if agg.is_a?(Array)
|
22
|
+
self.users(a).hashmap { 0 }.merge!(agg)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def distributions(metric, *args)
|
27
|
+
aggregates(metric, *args).map_values! do |agg|
|
28
|
+
agg.values.histogram
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def dist_analysis(metric, *args)
|
33
|
+
Significance.dist_significance(
|
34
|
+
distributions(metric, *args)
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
class Builder
|
39
|
+
def distribution(name, options={}, &blk)
|
40
|
+
@exp.stats[name] = DistributionStat.new(@exp, name, options, &blk)
|
41
|
+
end
|
42
|
+
|
43
|
+
def conversion(name, options={}, &blk)
|
44
|
+
@exp.stats[name] = ConversionStat.new(@exp, name, options, &blk)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class ArgumentProxy
|
49
|
+
def initialize(obj, *args)
|
50
|
+
@obj = obj
|
51
|
+
@args = args
|
52
|
+
end
|
53
|
+
|
54
|
+
def inspect
|
55
|
+
"#<ArgumentProxy[ #{@obj.inspect} ]>"
|
56
|
+
end
|
57
|
+
|
58
|
+
def method_missing(meth, *args)
|
59
|
+
data = @obj.send(meth, *(args + @args))
|
60
|
+
# [Jay] #TODO: Hack alert!
|
61
|
+
# this doesn't take into account Metric#all,
|
62
|
+
# which returns an Array for either a date range
|
63
|
+
# or a single day
|
64
|
+
data = data.sum if data.is_a?(Array)
|
65
|
+
data
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Stat
|
70
|
+
def initialize(exp, name, options={}, &blk)
|
71
|
+
@exp = exp
|
72
|
+
@name = name
|
73
|
+
@get_data = blk || default_get_data(options[:on])
|
74
|
+
end
|
75
|
+
|
76
|
+
def title
|
77
|
+
@name.to_s.split(/_/).map(&:capitalize).join(' ')
|
78
|
+
end
|
79
|
+
|
80
|
+
def report(*args)
|
81
|
+
sig = significance(*args)
|
82
|
+
sig = "not significant" if sig.nil?
|
83
|
+
return <<-report
|
84
|
+
|
85
|
+
=== #{title} ===
|
86
|
+
#{analysis(*args).inspect}
|
87
|
+
Significance: #{sig}
|
88
|
+
report
|
89
|
+
end
|
90
|
+
|
91
|
+
def significant?(tolerance=0.01)
|
92
|
+
sig = self.significance
|
93
|
+
!sig.nil? && sig <= tolerance
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def argument_proxy_hash(hsh, *args)
|
98
|
+
hsh.map_values do |v|
|
99
|
+
ArgumentProxy.new(v, *args)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def data_for(alt, *args)
|
104
|
+
data = @get_data.call(argument_proxy_hash(@exp.metrics(alt), *args))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class DistributionStat < Stat
|
109
|
+
def default_get_data(on_param)
|
110
|
+
lambda do |metrics|
|
111
|
+
metrics[on_param].distribution
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def inspect
|
116
|
+
"#<Modesty::Experiment::DistributionStat[ (on #{@exp.slug}) (of #{@metric_sym.inspect}) ]>"
|
117
|
+
end
|
118
|
+
|
119
|
+
def data(*args)
|
120
|
+
@exp.alternatives.hashmap do |a|
|
121
|
+
data_for(a, *args)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def analysis(*args)
|
126
|
+
Significance.dist_significance(data(*args))
|
127
|
+
end
|
128
|
+
|
129
|
+
def significance(*args)
|
130
|
+
analysis(*args)[:significant]
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
class ConversionStat < Stat
|
136
|
+
def default_get_data(on_param)
|
137
|
+
lambda do |metrics|
|
138
|
+
num_count = metrics[on_param[0]].count
|
139
|
+
denom_count = metrics[on_param[1]].count
|
140
|
+
[num_count, denom_count - num_count]
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def analysis(*args)
|
145
|
+
@exp.alternatives.hashmap do |a|
|
146
|
+
data_for(a, *args)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def data(*args)
|
151
|
+
analysis.values
|
152
|
+
end
|
153
|
+
|
154
|
+
def inspect
|
155
|
+
"#<Modesty::Experiment::ConversionStat[ #{@name} ]>"
|
156
|
+
end
|
157
|
+
|
158
|
+
def significance(*args)
|
159
|
+
Significance.significance(*data(*args))
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
if Rails.version.match('^3')
|
2
|
+
module Modesty
|
3
|
+
class Railtie < Rails::Railtie
|
4
|
+
initializer "modesty.initialize" do |app|
|
5
|
+
Modesty.root = File.join(Rails.root, 'modesty')
|
6
|
+
Modesty.config_path = File.join(Rails.root, 'config', 'modesty.yml')
|
7
|
+
Modesty.environment = Rails.env
|
8
|
+
end
|
9
|
+
|
10
|
+
config.after_initialize do
|
11
|
+
begin
|
12
|
+
Modesty.load_with_redis!(config.redis)
|
13
|
+
rescue NoMethodError
|
14
|
+
Modesty.load!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
else
|
21
|
+
Modesty.root = File.join(Rails.root, 'modesty')
|
22
|
+
Modesty.config_path = File.join(Rails.root, 'config', 'modesty.yml')
|
23
|
+
Modesty.environment = Rails.env
|
24
|
+
Rails.configuration.after_initialize do
|
25
|
+
Modesty.load!
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Modesty
|
2
|
+
#TODO: cookies n stuff
|
3
|
+
|
4
|
+
class IdentityError < RuntimeError; end
|
5
|
+
|
6
|
+
module IdentityMethods
|
7
|
+
attr_reader :identity
|
8
|
+
|
9
|
+
def identify!(id, opts={})
|
10
|
+
unless opts[:ignore]
|
11
|
+
raise(
|
12
|
+
IdentityError,
|
13
|
+
"Identity must be an integer or nil."
|
14
|
+
) unless id.nil? || id.is_a?(Fixnum)
|
15
|
+
|
16
|
+
@identity = id
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_identity(id)
|
21
|
+
old_identity = Modesty.identity
|
22
|
+
Modesty.identify! id
|
23
|
+
ret = yield
|
24
|
+
Modesty.identify! old_identity
|
25
|
+
ret
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class API
|
30
|
+
include IdentityMethods
|
31
|
+
end
|
32
|
+
end
|
data/lib/modesty/load.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module Modesty
|
2
|
+
module LoadMethods
|
3
|
+
attr_writer :root
|
4
|
+
def root
|
5
|
+
@root ||= File.join(
|
6
|
+
File.dirname(__FILE__),
|
7
|
+
'..'
|
8
|
+
)
|
9
|
+
#TODO: is there a better default?
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_writer :config_path
|
13
|
+
def config_path
|
14
|
+
@config_path ||= File.join(
|
15
|
+
Modesty.root,
|
16
|
+
'../config/modesty.yml'
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :environment
|
21
|
+
|
22
|
+
def load_options(quiet = false)
|
23
|
+
options = begin
|
24
|
+
YAML.load(File.read(self.config_path))
|
25
|
+
rescue Errno::ENOENT
|
26
|
+
puts "No Modesty config file found" unless quiet
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
options[self.environment] || options['default'] || options
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_paths(options)
|
33
|
+
if options['paths']
|
34
|
+
options['paths'].each do |data, path|
|
35
|
+
Modesty.send("#{data}_dir=", File.join(Modesty.root, path))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_config!
|
41
|
+
options = load_options
|
42
|
+
load_paths(options)
|
43
|
+
|
44
|
+
if options['datastore'] && options['datastore']['type']
|
45
|
+
type = options['datastore'].delete('type')
|
46
|
+
data_options = Hash[
|
47
|
+
options['datastore'].map { |k,v| [k.to_sym, v] }
|
48
|
+
]
|
49
|
+
self.set_store(type, data_options)
|
50
|
+
else
|
51
|
+
self.set_store :redis, :mock => true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def _load_with_redis(redis)
|
56
|
+
options = load_options(true)
|
57
|
+
load_paths(options)
|
58
|
+
self.set_store(:redis, :redis => redis)
|
59
|
+
end
|
60
|
+
|
61
|
+
def load!
|
62
|
+
load_config!
|
63
|
+
load_all_metrics!
|
64
|
+
load_all_experiments!
|
65
|
+
end
|
66
|
+
|
67
|
+
def load_with_redis!(redis)
|
68
|
+
_load_with_redis(redis)
|
69
|
+
load_all_metrics!
|
70
|
+
load_all_experiments!
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class API
|
75
|
+
include LoadMethods
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
require 'modesty/load/load_experiments.rb'
|
80
|
+
require 'modesty/load/load_metrics.rb'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Modesty
|
2
|
+
module LoadMethods
|
3
|
+
attr_writer :experiments_dir
|
4
|
+
def experiments_dir
|
5
|
+
@experiments_dir ||= File.join(Modesty.root, 'experiments')
|
6
|
+
end
|
7
|
+
|
8
|
+
def load_all_experiments!
|
9
|
+
Dir.glob(
|
10
|
+
File.join(self.experiments_dir, '*.rb')
|
11
|
+
).each { |f| load f }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Modesty
|
2
|
+
module LoadMethods
|
3
|
+
attr_writer :metrics_dir
|
4
|
+
def metrics_dir
|
5
|
+
@metrics_dir ||= File.join(
|
6
|
+
Modesty.root,
|
7
|
+
'metrics'
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_all_metrics!
|
12
|
+
Dir.glob(
|
13
|
+
File.join(self.metrics_dir, '**', '*.rb')
|
14
|
+
).each { |f| load f }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Modesty
|
2
|
+
class Metric
|
3
|
+
class Error < StandardError; end
|
4
|
+
end
|
5
|
+
|
6
|
+
module MetricMethods
|
7
|
+
attr_writer :metrics
|
8
|
+
|
9
|
+
def metrics
|
10
|
+
@metrics ||= Hash.new do |h, k|
|
11
|
+
raise Metric::Error, <<-msg.squish
|
12
|
+
Unrecognized metric #{k.inspect}
|
13
|
+
msg
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def metrics_starting_with(name)
|
18
|
+
self.metrics.select{|k, v| k.to_s.starts_with?(name)}
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_metric(metric)
|
22
|
+
raise Metric::Error, <<-msg if self.metrics.include? metric.slug
|
23
|
+
Metric #{metric.slug.inspect} already defined!
|
24
|
+
msg
|
25
|
+
self.metrics[metric.slug] = metric
|
26
|
+
end
|
27
|
+
|
28
|
+
def new_metric(slug, parent=nil, options={}, &block)
|
29
|
+
if parent.is_a? Hash
|
30
|
+
options=parent
|
31
|
+
else
|
32
|
+
options[:parent] = parent
|
33
|
+
end
|
34
|
+
|
35
|
+
metric = Metric.new(slug, options)
|
36
|
+
yield Metric::Builder.new(metric) if block_given?
|
37
|
+
add_metric(metric)
|
38
|
+
metric
|
39
|
+
end
|
40
|
+
|
41
|
+
# Tracking
|
42
|
+
def track!(name, *args)
|
43
|
+
self.metrics[name.to_sym].track! *args
|
44
|
+
rescue Modesty::Metric::Error
|
45
|
+
# Fail silently in the event that a metric is not found.
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class API
|
50
|
+
include MetricMethods
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
require 'modesty/metric/base'
|
55
|
+
require 'modesty/metric/builder'
|
56
|
+
require 'modesty/metric/data'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Modesty
|
2
|
+
class Metric
|
3
|
+
|
4
|
+
class << self
|
5
|
+
private
|
6
|
+
def data_type(name)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
ATTRIBUTES = [
|
11
|
+
:description
|
12
|
+
]
|
13
|
+
attr_reader *ATTRIBUTES
|
14
|
+
attr_reader :slug
|
15
|
+
attr_reader :parent
|
16
|
+
attr_reader :experiment
|
17
|
+
|
18
|
+
# metrics should know what experiments use them,
|
19
|
+
# to enable smart tracking.
|
20
|
+
def experiments
|
21
|
+
@experiments ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(slug, options={})
|
25
|
+
@slug = slug
|
26
|
+
@parent = options[:parent]
|
27
|
+
@experiment = options[:experiment]
|
28
|
+
end
|
29
|
+
|
30
|
+
def inspect
|
31
|
+
"#<Modesty::Metric[ #{self.slug.inspect} ]>"
|
32
|
+
end
|
33
|
+
|
34
|
+
def /(sym)
|
35
|
+
Modesty.metrics[@slug/sym] || (raise NoMetricError, "Undefined metric :'#{@slug/sym}'")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Modesty
|
2
|
+
class Metric
|
3
|
+
class Builder
|
4
|
+
|
5
|
+
def method_missing(name, *args)
|
6
|
+
if Metric::ATTRIBUTES.include? name
|
7
|
+
@metric.instance_variable_set("@#{name}", args[0])
|
8
|
+
else
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(metric)
|
14
|
+
@metric = metric
|
15
|
+
end
|
16
|
+
|
17
|
+
def submetric(slug, &blk)
|
18
|
+
Modesty.new_metric(@metric.slug/slug, @metric, &blk)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|