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.
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