sinmetrics 0.0.7 → 0.0.8
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/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
|