sinmetrics 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/sinmetrics/abingo.rb +55 -56
- data/lib/sinmetrics/abingo/alternative.rb +0 -17
- data/lib/sinmetrics/abingo/experiment.rb +5 -40
- data/spec/abingo_spec.rb +19 -33
- data/spec/spec_helper.rb +1 -0
- metadata +20 -3
data/lib/sinmetrics/abingo.rb
CHANGED
@@ -42,40 +42,29 @@ module Sinatra
|
|
42
42
|
#A simple convenience method for doing an A/B test. Returns true or false.
|
43
43
|
#If you pass it a block, it will bind the choice to the variable given to the block.
|
44
44
|
def flip(test_name)
|
45
|
+
choice = test(test_name, [true, false])
|
45
46
|
if block_given?
|
46
|
-
yield
|
47
|
+
yield choice
|
47
48
|
else
|
48
|
-
|
49
|
+
choice
|
49
50
|
end
|
50
51
|
end
|
51
52
|
|
52
53
|
#This is the meat of A/Bingo.
|
53
54
|
#options accepts
|
54
55
|
# :multiple_participation (true or false)
|
55
|
-
# :
|
56
|
+
# :no_particiation (true or false)
|
56
57
|
def test(test_name, alternatives, options = {})
|
57
|
-
|
58
|
-
|
59
|
-
|
58
|
+
experiment = Experiment.get(test_name)
|
59
|
+
experiment ||= Experiment.start_experiment!(self, test_name, parse_alternatives(alternatives))
|
60
|
+
|
61
|
+
# Test has been stopped, pick canonical alternative.
|
62
|
+
unless experiment.short_circuit.nil?
|
63
|
+
return experiment.short_circuit
|
60
64
|
end
|
61
65
|
|
62
|
-
unless Experiment.exists?(self, test_name)
|
63
|
-
conversion_name = options[:conversion] || options[:conversion_name]
|
64
|
-
Experiment.start_experiment!(self, test_name, parse_alternatives(alternatives), conversion_name)
|
65
|
-
end
|
66
|
-
|
67
66
|
choice = find_alternative_for_user(test_name, alternatives)
|
68
|
-
|
69
|
-
|
70
|
-
#Set this user to participate in this experiment, and increment participants count.
|
71
|
-
if options[:multiple_participation] || !(participating_tests.include?(test_name))
|
72
|
-
participating_tests = participating_tests.dup if participating_tests.frozen?
|
73
|
-
unless participating_tests.include?(test_name)
|
74
|
-
participating_tests << test_name
|
75
|
-
cache.write("Abingo::participating_tests::#{identity}", participating_tests)
|
76
|
-
end
|
77
|
-
Alternative.score_participation(self, test_name)
|
78
|
-
end
|
67
|
+
score_participation!(test_name, options) unless options[:no_participation]
|
79
68
|
|
80
69
|
if block_given?
|
81
70
|
yield(choice)
|
@@ -87,9 +76,6 @@ module Sinatra
|
|
87
76
|
#Scores conversions for tests.
|
88
77
|
#test_name_or_array supports three types of input:
|
89
78
|
#
|
90
|
-
#A conversion name: scores a conversion for any test the user is participating in which
|
91
|
-
# is listening to the specified conversion.
|
92
|
-
#
|
93
79
|
#A test name: scores a conversion for the named test if the user is participating in it.
|
94
80
|
#
|
95
81
|
#An array of either of the above: for each element of the array, process as above.
|
@@ -107,23 +93,8 @@ module Sinatra
|
|
107
93
|
participating_tests.each do |participating_test|
|
108
94
|
self.bingo!(participating_test, options)
|
109
95
|
end
|
110
|
-
else
|
111
|
-
|
112
|
-
tests_listening_to_conversion = cache.read("Abingo::tests_listening_to_conversion#{conversion_name}")
|
113
|
-
if tests_listening_to_conversion
|
114
|
-
if tests_listening_to_conversion.size > 1
|
115
|
-
tests_listening_to_conversion.map do |individual_test|
|
116
|
-
self.score_conversion!(individual_test.to_s, options)
|
117
|
-
end
|
118
|
-
elsif tests_listening_to_conversion.size == 1
|
119
|
-
test_name_str = tests_listening_to_conversion.first.to_s
|
120
|
-
self.score_conversion!(test_name_str, options)
|
121
|
-
end
|
122
|
-
else
|
123
|
-
#No tests listening for this conversion. Assume it is just a test name.
|
124
|
-
test_name_str = name.to_s
|
125
|
-
self.score_conversion!(test_name_str, options)
|
126
|
-
end
|
96
|
+
else
|
97
|
+
self.score_conversion!(name.to_s, options)
|
127
98
|
end
|
128
99
|
end
|
129
100
|
end
|
@@ -164,33 +135,40 @@ module Sinatra
|
|
164
135
|
end
|
165
136
|
end
|
166
137
|
|
167
|
-
def
|
138
|
+
def find_alternative_for_user(test_name, alternatives)
|
168
139
|
cache_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_")
|
169
140
|
alternative_array = self.cache.fetch(cache_key) do
|
170
141
|
self.parse_alternatives(alternatives)
|
171
142
|
end
|
172
|
-
alternative_array
|
173
|
-
end
|
174
143
|
|
175
|
-
|
176
|
-
|
177
|
-
|
144
|
+
#Quickly determines what alternative to show a given user. Given a test name
|
145
|
+
#and their identity, we hash them together (which, for MD5, provably introduces
|
146
|
+
#enough entropy that we don't care) otherwise
|
147
|
+
choice = Digest::MD5.hexdigest(salt.to_s + test_name + self.identity.to_s).to_i(16) % alternative_array.size
|
148
|
+
alternative_array[choice]
|
178
149
|
end
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
150
|
+
|
151
|
+
def alternatives_for_test(test_name)
|
152
|
+
cache_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_")
|
153
|
+
self.cache.fetch(cache_key) do
|
154
|
+
Experiment.get(test_name).alternatives.map do |alt|
|
155
|
+
if alt.weight > 1
|
156
|
+
[alt.content] * alt.weight
|
157
|
+
else
|
158
|
+
alt.content
|
159
|
+
end
|
160
|
+
end.flatten
|
161
|
+
end
|
185
162
|
end
|
186
163
|
|
187
164
|
def score_conversion!(test_name, options = {})
|
188
|
-
test_name.gsub!(" ", "_")
|
189
165
|
participating_tests = cache.read("Abingo::participating_tests::#{identity}") || []
|
190
166
|
if options[:assume_participation] || participating_tests.include?(test_name)
|
191
167
|
cache_key = "Abingo::conversions(#{identity},#{test_name}"
|
192
168
|
if options[:multiple_conversions] || !cache.read(cache_key)
|
193
|
-
|
169
|
+
viewed_alternative = find_alternative_for_user(test_name, alternatives_for_test(test_name))
|
170
|
+
lookup = calculate_alternative_lookup(test_name, viewed_alternative)
|
171
|
+
Alternative.all(:lookup => lookup).adjust!(:conversions => 1)
|
194
172
|
if cache.exist?(cache_key)
|
195
173
|
cache.increment(cache_key)
|
196
174
|
else
|
@@ -200,6 +178,23 @@ module Sinatra
|
|
200
178
|
end
|
201
179
|
end
|
202
180
|
|
181
|
+
def score_participation!(test_name, options = {})
|
182
|
+
participating_tests = cache.read("Abingo::participating_tests::#{identity}") || []
|
183
|
+
|
184
|
+
#Set this user to participate in this experiment, and increment participants count.
|
185
|
+
if options[:multiple_participation] || !(participating_tests.include?(test_name))
|
186
|
+
participating_tests = participating_tests.dup if participating_tests.frozen?
|
187
|
+
unless participating_tests.include?(test_name)
|
188
|
+
participating_tests << test_name
|
189
|
+
cache.write("Abingo::participating_tests::#{identity}", participating_tests)
|
190
|
+
end
|
191
|
+
|
192
|
+
choice = find_alternative_for_user(test_name, alternatives_for_test(test_name))
|
193
|
+
lookup = calculate_alternative_lookup(test_name, choice)
|
194
|
+
Alternative.all(:lookup => lookup).adjust!(:participants => 1)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
203
198
|
def end_experiment(alternative_id)
|
204
199
|
alternative = Alternative.get(alternative_id)
|
205
200
|
experiment = alternative.experiment
|
@@ -207,6 +202,10 @@ module Sinatra
|
|
207
202
|
experiment.end_experiment!(self, alternative.content)
|
208
203
|
end
|
209
204
|
end
|
205
|
+
|
206
|
+
def calculate_alternative_lookup(test_name, alternative_name)
|
207
|
+
Digest::MD5.hexdigest(self.salt + test_name + alternative_name.to_s)
|
208
|
+
end
|
210
209
|
end
|
211
210
|
|
212
211
|
module AbingoObject::ConversionRate
|
@@ -10,21 +10,4 @@ class Abingo::Alternative
|
|
10
10
|
property :conversions, Integer, :default => 0
|
11
11
|
|
12
12
|
belongs_to :experiment
|
13
|
-
|
14
|
-
def self.calculate_lookup(abingo, test_name, alternative_name)
|
15
|
-
Digest::MD5.hexdigest(abingo.salt + test_name + alternative_name.to_s)
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.score_conversion(abingo, test_name)
|
19
|
-
viewed_alternative = abingo.find_alternative_for_user(test_name,
|
20
|
-
Abingo::Experiment.alternatives_for_test(abingo, test_name))
|
21
|
-
all(:lookup => self.calculate_lookup(abingo, test_name, viewed_alternative)).adjust!(:conversions => 1)
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.score_participation(abingo, test_name)
|
25
|
-
viewed_alternative = abingo.find_alternative_for_user(test_name,
|
26
|
-
Abingo::Experiment.alternatives_for_test(abingo, test_name))
|
27
|
-
all(:lookup => self.calculate_lookup(abingo, test_name, viewed_alternative)).adjust!(:participants => 1)
|
28
|
-
end
|
29
|
-
|
30
13
|
end
|
@@ -5,6 +5,7 @@ class Abingo::Experiment
|
|
5
5
|
|
6
6
|
property :test_name, String, :key => true
|
7
7
|
property :status, String
|
8
|
+
property :short_circuit, String
|
8
9
|
|
9
10
|
has n, :alternatives, "Alternative"
|
10
11
|
|
@@ -37,33 +38,7 @@ class Abingo::Experiment
|
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
40
|
-
def self.
|
41
|
-
cache_key = "Abingo::Experiment::exists(#{test_name})".gsub(" ", "_")
|
42
|
-
ret = abingo.cache.fetch(cache_key) do
|
43
|
-
count = Abingo::Experiment.count(:conditions => {:test_name => test_name})
|
44
|
-
count > 0 ? count : nil
|
45
|
-
end
|
46
|
-
(!ret.nil?)
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.alternatives_for_test(abingo, test_name)
|
50
|
-
cache_key = "Abingo::#{test_name}::alternatives".gsub(" ","_")
|
51
|
-
abingo.cache.fetch(cache_key) do
|
52
|
-
experiment = Abingo::Experiment.first(:test_name => test_name)
|
53
|
-
alternatives_array = abingo.cache.fetch(cache_key) do
|
54
|
-
tmp_array = experiment.alternatives.map do |alt|
|
55
|
-
[alt.content, alt.weight]
|
56
|
-
end
|
57
|
-
tmp_hash = tmp_array.inject({}) {|hash, couplet| hash[couplet[0]] = couplet[1]; hash}
|
58
|
-
abingo.parse_alternatives(tmp_hash)
|
59
|
-
end
|
60
|
-
alternatives_array
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def self.start_experiment!(abingo, test_name, alternatives_array, conversion_name = nil)
|
65
|
-
conversion_name ||= test_name
|
66
|
-
conversion_name.gsub!(" ", "_")
|
41
|
+
def self.start_experiment!(abingo, test_name, alternatives_array)
|
67
42
|
cloned_alternatives_array = alternatives_array.clone
|
68
43
|
Abingo::Experiment.transaction do |txn|
|
69
44
|
experiment = Abingo::Experiment.first_or_create(:test_name => test_name)
|
@@ -72,32 +47,22 @@ class Abingo::Experiment
|
|
72
47
|
alt = cloned_alternatives_array[0]
|
73
48
|
weight = cloned_alternatives_array.size - (cloned_alternatives_array - [alt]).size
|
74
49
|
experiment.alternatives << Abingo::Alternative.new(:content => alt, :weight => weight,
|
75
|
-
:lookup =>
|
50
|
+
:lookup => abingo.calculate_alternative_lookup(test_name, alt))
|
76
51
|
cloned_alternatives_array -= [alt]
|
77
52
|
end
|
78
53
|
experiment.status = "Live"
|
79
54
|
experiment.save
|
80
|
-
abingo.cache.write("Abingo::Experiment::exists(#{test_name})".gsub(" ", "_"), 1)
|
81
|
-
|
82
|
-
#This might have issues in very, very high concurrency environments...
|
83
|
-
|
84
|
-
tests_listening_to_conversion = abingo.cache.read("Abingo::tests_listening_to_conversion#{conversion_name}") || []
|
85
|
-
tests_listening_to_conversion = tests_listening_to_conversion.dup if tests_listening_to_conversion.frozen?
|
86
|
-
tests_listening_to_conversion << test_name unless tests_listening_to_conversion.include? test_name
|
87
|
-
abingo.cache.write("Abingo::tests_listening_to_conversion#{conversion_name}", tests_listening_to_conversion)
|
88
55
|
experiment
|
89
56
|
end
|
90
57
|
end
|
91
58
|
|
92
|
-
def end_experiment!(abingo, final_alternative
|
93
|
-
conversion_name ||= test_name
|
59
|
+
def end_experiment!(abingo, final_alternative)
|
94
60
|
Abingo::Experiment.transaction do
|
95
61
|
alternatives.each do |alternative|
|
96
62
|
alternative.lookup = "Experiment completed. #{alternative.id}"
|
97
63
|
alternative.save!
|
98
64
|
end
|
99
|
-
update(:status => "Finished")
|
100
|
-
abingo.cache.write("Abingo::Experiment::short_circuit(#{test_name})".gsub(" ", "_"), final_alternative)
|
65
|
+
update(:status => "Finished", :short_circuit => final_alternative)
|
101
66
|
end
|
102
67
|
end
|
103
68
|
|
data/spec/abingo_spec.rb
CHANGED
@@ -36,6 +36,13 @@ describe 'Abingo Specs' do
|
|
36
36
|
@abingo.parse_alternatives(2..5).should_not include(1)
|
37
37
|
end
|
38
38
|
|
39
|
+
it "should parse weighted alternatives" do
|
40
|
+
hash = { 'a' => 2, 'b' => 3, 'c' => 1}
|
41
|
+
array = %w{a a a b b c}
|
42
|
+
@abingo.parse_alternatives(array).should == array
|
43
|
+
end
|
44
|
+
|
45
|
+
|
39
46
|
it "should create experiments" do
|
40
47
|
Abingo::Experiment.count.should == 0
|
41
48
|
Abingo::Alternative.count.should == 0
|
@@ -46,12 +53,6 @@ describe 'Abingo Specs' do
|
|
46
53
|
alternatives.should include(alternative_selected)
|
47
54
|
end
|
48
55
|
|
49
|
-
it "should be able to call exists" do
|
50
|
-
@abingo.test("exist words right", %w{does does_not})
|
51
|
-
Abingo::Experiment.exists?(@abingo, "exist words right").should be_true
|
52
|
-
Abingo::Experiment.exists?(@abingo, "other words right").should_not be_true
|
53
|
-
end
|
54
|
-
|
55
56
|
it "should pick alternatives consistently" do
|
56
57
|
alternative_picked = @abingo.test("consistency_test", 1..100)
|
57
58
|
100.times do
|
@@ -76,8 +77,8 @@ describe 'Abingo Specs' do
|
|
76
77
|
test_name = "participants_counted_test"
|
77
78
|
alternative = @abingo.test(test_name, %w{a b c})
|
78
79
|
|
79
|
-
ex = Abingo::Experiment.
|
80
|
-
lookup =
|
80
|
+
ex = Abingo::Experiment.get(test_name)
|
81
|
+
lookup = @abingo.calculate_alternative_lookup(test_name, alternative)
|
81
82
|
chosen_alt = Abingo::Alternative.first(:lookup => lookup)
|
82
83
|
ex.participants.should == 1
|
83
84
|
chosen_alt.participants.should == 1
|
@@ -87,8 +88,8 @@ describe 'Abingo Specs' do
|
|
87
88
|
test_name = "conversion_test_by_name"
|
88
89
|
alternative = @abingo.test(test_name, %w{a b c})
|
89
90
|
@abingo.bingo!(test_name)
|
90
|
-
ex = Abingo::Experiment.
|
91
|
-
lookup =
|
91
|
+
ex = Abingo::Experiment.get(test_name)
|
92
|
+
lookup = @abingo.calculate_alternative_lookup(test_name, alternative)
|
92
93
|
chosen_alt = Abingo::Alternative.first(:lookup => lookup)
|
93
94
|
ex.conversions.should == 1
|
94
95
|
chosen_alt.conversions.should == 1
|
@@ -97,40 +98,25 @@ describe 'Abingo Specs' do
|
|
97
98
|
|
98
99
|
#Should still only have one because this conversion should not be double counted.
|
99
100
|
#We haven't specified that in the test options.
|
100
|
-
ex = Abingo::Experiment.
|
101
|
+
ex = Abingo::Experiment.get(test_name)
|
101
102
|
ex.conversions.should == 1
|
102
103
|
end
|
103
104
|
|
104
105
|
it "should know the best alternative" do
|
105
106
|
test_name = "conversion_test_by_name"
|
106
|
-
alternative = @abingo.test(test_name,
|
107
|
+
alternative = @abingo.test(test_name, {'a' => 3, 'b' => 2, 'c' => 1})
|
107
108
|
@abingo.bingo!(test_name)
|
108
|
-
ex = Abingo::Experiment.
|
109
|
-
ex.best_alternative
|
109
|
+
ex = Abingo::Experiment.get(test_name)
|
110
|
+
ex.best_alternative.content.should == alternative
|
110
111
|
end
|
111
112
|
|
112
|
-
it "should track conversions by conversion name" do
|
113
|
-
conversion_name = "purchase"
|
114
|
-
tests = %w{conversionTrackingByConversionNameA conversionTrackingByConversionNameB conversionTrackingByConversionNameC}
|
115
|
-
tests.map do |test_name|
|
116
|
-
@abingo.test(test_name, %w{A B}, :conversion => conversion_name)
|
117
|
-
end
|
118
|
-
|
119
|
-
@abingo.bingo!(conversion_name)
|
120
|
-
tests.map do |test_name|
|
121
|
-
ex = Abingo::Experiment.first(:test_name => test_name)
|
122
|
-
ex.conversions.should == 1
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
113
|
it "should be possible to short circuit tests" do
|
127
|
-
conversion_name = "purchase"
|
128
114
|
test_name = "short circuit test"
|
129
|
-
alt_picked = @abingo.test(test_name, %w{A B}
|
130
|
-
ex = Abingo::Experiment.
|
115
|
+
alt_picked = @abingo.test(test_name, %w{A B})
|
116
|
+
ex = Abingo::Experiment.get(test_name)
|
131
117
|
alt_not_picked = (%w{A B} - [alt_picked]).first
|
132
118
|
|
133
|
-
ex.end_experiment!(@abingo, alt_not_picked
|
119
|
+
ex.end_experiment!(@abingo, alt_not_picked)
|
134
120
|
|
135
121
|
ex.reload
|
136
122
|
ex.status.should == "Finished"
|
@@ -140,7 +126,7 @@ describe 'Abingo Specs' do
|
|
140
126
|
|
141
127
|
old_identity = @abingo.identity
|
142
128
|
@abingo.identity = "shortCircuitTestNewIdentity"
|
143
|
-
@abingo.test(test_name, %w{A B}
|
129
|
+
@abingo.test(test_name, %w{A B})
|
144
130
|
@abingo.identity = old_identity
|
145
131
|
ex.reload
|
146
132
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sinmetrics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
4
5
|
prerelease: false
|
5
6
|
segments:
|
6
7
|
- 0
|
7
8
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
9
|
+
- 8
|
10
|
+
version: 0.0.8
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Luke Petre
|
@@ -21,9 +22,11 @@ dependencies:
|
|
21
22
|
name: dm-core
|
22
23
|
prerelease: false
|
23
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
24
26
|
requirements:
|
25
27
|
- - ">="
|
26
28
|
- !ruby/object:Gem::Version
|
29
|
+
hash: 51
|
27
30
|
segments:
|
28
31
|
- 0
|
29
32
|
- 10
|
@@ -35,9 +38,11 @@ dependencies:
|
|
35
38
|
name: dm-aggregates
|
36
39
|
prerelease: false
|
37
40
|
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
38
42
|
requirements:
|
39
43
|
- - ">="
|
40
44
|
- !ruby/object:Gem::Version
|
45
|
+
hash: 51
|
41
46
|
segments:
|
42
47
|
- 0
|
43
48
|
- 10
|
@@ -49,9 +54,11 @@ dependencies:
|
|
49
54
|
name: dm-observer
|
50
55
|
prerelease: false
|
51
56
|
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
52
58
|
requirements:
|
53
59
|
- - ">="
|
54
60
|
- !ruby/object:Gem::Version
|
61
|
+
hash: 51
|
55
62
|
segments:
|
56
63
|
- 0
|
57
64
|
- 10
|
@@ -63,9 +70,11 @@ dependencies:
|
|
63
70
|
name: dm-timestamps
|
64
71
|
prerelease: false
|
65
72
|
requirement: &id004 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
66
74
|
requirements:
|
67
75
|
- - ">="
|
68
76
|
- !ruby/object:Gem::Version
|
77
|
+
hash: 51
|
69
78
|
segments:
|
70
79
|
- 0
|
71
80
|
- 10
|
@@ -77,9 +86,11 @@ dependencies:
|
|
77
86
|
name: dm-adjust
|
78
87
|
prerelease: false
|
79
88
|
requirement: &id005 !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
80
90
|
requirements:
|
81
91
|
- - ">="
|
82
92
|
- !ruby/object:Gem::Version
|
93
|
+
hash: 51
|
83
94
|
segments:
|
84
95
|
- 0
|
85
96
|
- 10
|
@@ -91,9 +102,11 @@ dependencies:
|
|
91
102
|
name: activesupport
|
92
103
|
prerelease: false
|
93
104
|
requirement: &id006 !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
94
106
|
requirements:
|
95
107
|
- - ~>
|
96
108
|
- !ruby/object:Gem::Version
|
109
|
+
hash: 9
|
97
110
|
segments:
|
98
111
|
- 2
|
99
112
|
- 3
|
@@ -134,23 +147,27 @@ rdoc_options: []
|
|
134
147
|
require_paths:
|
135
148
|
- lib
|
136
149
|
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
none: false
|
137
151
|
requirements:
|
138
152
|
- - ">="
|
139
153
|
- !ruby/object:Gem::Version
|
154
|
+
hash: 3
|
140
155
|
segments:
|
141
156
|
- 0
|
142
157
|
version: "0"
|
143
158
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
144
160
|
requirements:
|
145
161
|
- - ">="
|
146
162
|
- !ruby/object:Gem::Version
|
163
|
+
hash: 3
|
147
164
|
segments:
|
148
165
|
- 0
|
149
166
|
version: "0"
|
150
167
|
requirements: []
|
151
168
|
|
152
169
|
rubyforge_project:
|
153
|
-
rubygems_version: 1.3.
|
170
|
+
rubygems_version: 1.3.7
|
154
171
|
signing_key:
|
155
172
|
specification_version: 3
|
156
173
|
summary: simple sinatra metrics extension
|