modesty 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. data/Gemfile +13 -0
  2. data/Gemfile.lock +18 -0
  3. data/LICENSE +21 -0
  4. data/README.md +121 -0
  5. data/Rakefile +29 -0
  6. data/VERSION +1 -0
  7. data/init.rb +1 -0
  8. data/lib/modesty.rb +26 -0
  9. data/lib/modesty/api.rb +14 -0
  10. data/lib/modesty/core_ext.rb +5 -0
  11. data/lib/modesty/core_ext/array.rb +21 -0
  12. data/lib/modesty/core_ext/fixnum.rb +5 -0
  13. data/lib/modesty/core_ext/hash.rb +39 -0
  14. data/lib/modesty/core_ext/string.rb +9 -0
  15. data/lib/modesty/core_ext/symbol.rb +33 -0
  16. data/lib/modesty/datastore.rb +51 -0
  17. data/lib/modesty/datastore/redis.rb +180 -0
  18. data/lib/modesty/experiment.rb +87 -0
  19. data/lib/modesty/experiment/base.rb +47 -0
  20. data/lib/modesty/experiment/builder.rb +48 -0
  21. data/lib/modesty/experiment/console.rb +4 -0
  22. data/lib/modesty/experiment/data.rb +75 -0
  23. data/lib/modesty/experiment/interface.rb +29 -0
  24. data/lib/modesty/experiment/significance.rb +376 -0
  25. data/lib/modesty/experiment/stats.rb +163 -0
  26. data/lib/modesty/frameworks/rails.rb +27 -0
  27. data/lib/modesty/identity.rb +32 -0
  28. data/lib/modesty/load.rb +80 -0
  29. data/lib/modesty/load/load_experiments.rb +14 -0
  30. data/lib/modesty/load/load_metrics.rb +17 -0
  31. data/lib/modesty/metric.rb +56 -0
  32. data/lib/modesty/metric/base.rb +38 -0
  33. data/lib/modesty/metric/builder.rb +23 -0
  34. data/lib/modesty/metric/data.rb +133 -0
  35. data/modesty.gemspec +192 -0
  36. data/spec/core_ext_spec.rb +17 -0
  37. data/spec/experiment_spec.rb +239 -0
  38. data/spec/identity_spec.rb +161 -0
  39. data/spec/load_spec.rb +87 -0
  40. data/spec/metric_spec.rb +176 -0
  41. data/spec/rails_spec.rb +48 -0
  42. data/spec/redis_spec.rb +29 -0
  43. data/spec/significance_spec.rb +147 -0
  44. data/spec/spec.opts +1 -0
  45. data/test/myapp/config/modesty.yml +9 -0
  46. data/test/myapp/modesty/experiments/cookbook.rb +4 -0
  47. data/test/myapp/modesty/metrics/kitchen_metrics.rb +9 -0
  48. data/test/myapp/modesty/metrics/stove/burner_metrics.rb +2 -0
  49. data/vendor/.piston.yml +8 -0
  50. data/vendor/mock_redis/.gitignore +2 -0
  51. data/vendor/mock_redis/README +8 -0
  52. data/vendor/mock_redis/lib/mock_redis.rb +10 -0
  53. data/vendor/mock_redis/lib/mock_redis/hash.rb +61 -0
  54. data/vendor/mock_redis/lib/mock_redis/list.rb +6 -0
  55. data/vendor/mock_redis/lib/mock_redis/misc.rb +69 -0
  56. data/vendor/mock_redis/lib/mock_redis/set.rb +108 -0
  57. data/vendor/mock_redis/lib/mock_redis/string.rb +32 -0
  58. data/vendor/redis-rb/.gitignore +8 -0
  59. data/vendor/redis-rb/LICENSE +20 -0
  60. data/vendor/redis-rb/README.markdown +129 -0
  61. data/vendor/redis-rb/Rakefile +155 -0
  62. data/vendor/redis-rb/benchmarking/logging.rb +62 -0
  63. data/vendor/redis-rb/benchmarking/pipeline.rb +51 -0
  64. data/vendor/redis-rb/benchmarking/speed.rb +21 -0
  65. data/vendor/redis-rb/benchmarking/suite.rb +24 -0
  66. data/vendor/redis-rb/benchmarking/thread_safety.rb +38 -0
  67. data/vendor/redis-rb/benchmarking/worker.rb +71 -0
  68. data/vendor/redis-rb/examples/basic.rb +15 -0
  69. data/vendor/redis-rb/examples/dist_redis.rb +43 -0
  70. data/vendor/redis-rb/examples/incr-decr.rb +17 -0
  71. data/vendor/redis-rb/examples/list.rb +26 -0
  72. data/vendor/redis-rb/examples/pubsub.rb +31 -0
  73. data/vendor/redis-rb/examples/sets.rb +36 -0
  74. data/vendor/redis-rb/examples/unicorn/config.ru +3 -0
  75. data/vendor/redis-rb/examples/unicorn/unicorn.rb +20 -0
  76. data/vendor/redis-rb/lib/redis.rb +676 -0
  77. data/vendor/redis-rb/lib/redis/client.rb +201 -0
  78. data/vendor/redis-rb/lib/redis/compat.rb +21 -0
  79. data/vendor/redis-rb/lib/redis/connection.rb +134 -0
  80. data/vendor/redis-rb/lib/redis/distributed.rb +526 -0
  81. data/vendor/redis-rb/lib/redis/hash_ring.rb +131 -0
  82. data/vendor/redis-rb/lib/redis/pipeline.rb +13 -0
  83. data/vendor/redis-rb/lib/redis/subscribe.rb +79 -0
  84. data/vendor/redis-rb/redis.gemspec +29 -0
  85. data/vendor/redis-rb/test/commands_on_hashes_test.rb +46 -0
  86. data/vendor/redis-rb/test/commands_on_lists_test.rb +50 -0
  87. data/vendor/redis-rb/test/commands_on_sets_test.rb +78 -0
  88. data/vendor/redis-rb/test/commands_on_sorted_sets_test.rb +109 -0
  89. data/vendor/redis-rb/test/commands_on_strings_test.rb +70 -0
  90. data/vendor/redis-rb/test/commands_on_value_types_test.rb +88 -0
  91. data/vendor/redis-rb/test/connection_handling_test.rb +87 -0
  92. data/vendor/redis-rb/test/db/.gitignore +1 -0
  93. data/vendor/redis-rb/test/distributd_key_tags_test.rb +53 -0
  94. data/vendor/redis-rb/test/distributed_blocking_commands_test.rb +54 -0
  95. data/vendor/redis-rb/test/distributed_commands_on_hashes_test.rb +12 -0
  96. data/vendor/redis-rb/test/distributed_commands_on_lists_test.rb +18 -0
  97. data/vendor/redis-rb/test/distributed_commands_on_sets_test.rb +85 -0
  98. data/vendor/redis-rb/test/distributed_commands_on_strings_test.rb +50 -0
  99. data/vendor/redis-rb/test/distributed_commands_on_value_types_test.rb +73 -0
  100. data/vendor/redis-rb/test/distributed_commands_requiring_clustering_test.rb +141 -0
  101. data/vendor/redis-rb/test/distributed_connection_handling_test.rb +25 -0
  102. data/vendor/redis-rb/test/distributed_internals_test.rb +18 -0
  103. data/vendor/redis-rb/test/distributed_persistence_control_commands_test.rb +24 -0
  104. data/vendor/redis-rb/test/distributed_publish_subscribe_test.rb +90 -0
  105. data/vendor/redis-rb/test/distributed_remote_server_control_commands_test.rb +31 -0
  106. data/vendor/redis-rb/test/distributed_sorting_test.rb +21 -0
  107. data/vendor/redis-rb/test/distributed_test.rb +60 -0
  108. data/vendor/redis-rb/test/distributed_transactions_test.rb +34 -0
  109. data/vendor/redis-rb/test/encoding_test.rb +16 -0
  110. data/vendor/redis-rb/test/helper.rb +86 -0
  111. data/vendor/redis-rb/test/internals_test.rb +27 -0
  112. data/vendor/redis-rb/test/lint/hashes.rb +90 -0
  113. data/vendor/redis-rb/test/lint/internals.rb +53 -0
  114. data/vendor/redis-rb/test/lint/lists.rb +93 -0
  115. data/vendor/redis-rb/test/lint/sets.rb +66 -0
  116. data/vendor/redis-rb/test/lint/sorted_sets.rb +132 -0
  117. data/vendor/redis-rb/test/lint/strings.rb +98 -0
  118. data/vendor/redis-rb/test/lint/value_types.rb +84 -0
  119. data/vendor/redis-rb/test/persistence_control_commands_test.rb +22 -0
  120. data/vendor/redis-rb/test/pipelining_commands_test.rb +78 -0
  121. data/vendor/redis-rb/test/publish_subscribe_test.rb +151 -0
  122. data/vendor/redis-rb/test/redis_mock.rb +64 -0
  123. data/vendor/redis-rb/test/remote_server_control_commands_test.rb +56 -0
  124. data/vendor/redis-rb/test/sorting_test.rb +44 -0
  125. data/vendor/redis-rb/test/test.conf +8 -0
  126. data/vendor/redis-rb/test/thread_safety_test.rb +34 -0
  127. data/vendor/redis-rb/test/transactions_test.rb +91 -0
  128. data/vendor/redis-rb/test/unknown_commands_test.rb +14 -0
  129. data/vendor/redis-rb/test/url_param_test.rb +52 -0
  130. 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
@@ -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