modesty 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|