abingo 1.1.0 → 2.0.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/.gitignore +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +20 -1
- data/README +10 -9
- data/lib/abingo.rb +80 -70
- data/lib/abingo/alternative.rb +3 -6
- data/lib/abingo/controller/dashboard.rb +2 -6
- data/lib/abingo/experiment.rb +1 -5
- data/lib/abingo/version.rb +1 -1
- data/lib/abingo_sugar.rb +8 -9
- data/lib/abingo_view_helper.rb +8 -12
- data/test/abingo_test.rb +80 -47
- data/test/test_helper.rb +55 -30
- metadata +5 -7
- data/strip.rb +0 -11
- data/uninstall.rb +0 -1
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
abingo (
|
4
|
+
abingo (2.0.0)
|
5
5
|
rails (~> 3.0)
|
6
6
|
|
7
7
|
GEM
|
@@ -34,19 +34,25 @@ GEM
|
|
34
34
|
activesupport (3.2.9)
|
35
35
|
i18n (~> 0.6)
|
36
36
|
multi_json (~> 1.0)
|
37
|
+
archive-tar-minitar (0.5.2)
|
37
38
|
arel (3.0.2)
|
38
39
|
builder (3.0.4)
|
40
|
+
columnize (0.3.6)
|
41
|
+
contest (0.1.3)
|
39
42
|
erubis (2.7.0)
|
40
43
|
hike (1.2.1)
|
41
44
|
i18n (0.6.1)
|
42
45
|
journey (1.0.4)
|
43
46
|
json (1.7.5)
|
47
|
+
linecache19 (0.5.12)
|
48
|
+
ruby_core_source (>= 0.1.4)
|
44
49
|
mail (2.4.4)
|
45
50
|
i18n (>= 0.4.0)
|
46
51
|
mime-types (~> 1.16)
|
47
52
|
treetop (~> 1.4.8)
|
48
53
|
mime-types (1.19)
|
49
54
|
multi_json (1.3.7)
|
55
|
+
pg (0.14.1)
|
50
56
|
polyglot (0.3.3)
|
51
57
|
rack (1.4.1)
|
52
58
|
rack-cache (1.2)
|
@@ -73,6 +79,16 @@ GEM
|
|
73
79
|
rake (10.0.2)
|
74
80
|
rdoc (3.12)
|
75
81
|
json (~> 1.4)
|
82
|
+
ruby-debug-base19 (0.11.25)
|
83
|
+
columnize (>= 0.3.1)
|
84
|
+
linecache19 (>= 0.5.11)
|
85
|
+
ruby_core_source (>= 0.1.4)
|
86
|
+
ruby-debug19 (0.11.6)
|
87
|
+
columnize (>= 0.3.1)
|
88
|
+
linecache19 (>= 0.5.11)
|
89
|
+
ruby-debug-base19 (>= 0.11.19)
|
90
|
+
ruby_core_source (0.1.5)
|
91
|
+
archive-tar-minitar (>= 0.5.2)
|
76
92
|
sprockets (2.2.1)
|
77
93
|
hike (~> 1.2)
|
78
94
|
multi_json (~> 1.0)
|
@@ -90,3 +106,6 @@ PLATFORMS
|
|
90
106
|
|
91
107
|
DEPENDENCIES
|
92
108
|
abingo!
|
109
|
+
contest
|
110
|
+
pg
|
111
|
+
ruby-debug19
|
data/README
CHANGED
@@ -19,7 +19,7 @@ Key default features:
|
|
19
19
|
|
20
20
|
Example: View
|
21
21
|
|
22
|
-
<% ab_test("login_button", ["/images/button1.jpg", "/images/button2.jpg"]) do |button_file| %>
|
22
|
+
<% ab_test(@abingo_identity, "login_button", ["/images/button1.jpg", "/images/button2.jpg"]) do |button_file| %>
|
23
23
|
<%= img_tag(button_file, :alt => "Login!") %>
|
24
24
|
<% end %>
|
25
25
|
|
@@ -27,13 +27,13 @@ Example: Controller
|
|
27
27
|
|
28
28
|
def register_new_user
|
29
29
|
#See what level of free points maximizes users' decision to buy replacement points.
|
30
|
-
@starter_points = ab_test("new_user_free_points", [100, 200, 300])
|
30
|
+
@starter_points = ab_test(@abingo_identity, "new_user_free_points", [100, 200, 300])
|
31
31
|
end
|
32
32
|
|
33
33
|
Example: Controller
|
34
34
|
|
35
35
|
def registration
|
36
|
-
if (ab_test("send_welcome_email"), :conversion => "purchase")
|
36
|
+
if (ab_test(@abingo_identity, "send_welcome_email"), :conversion => "purchase")
|
37
37
|
#send the email, track to see if it later increases conversion to full version
|
38
38
|
end
|
39
39
|
end
|
@@ -42,12 +42,12 @@ Example: Conversion tracking (in a controller!)
|
|
42
42
|
|
43
43
|
def buy_new_points
|
44
44
|
#some business logic
|
45
|
-
bingo!("buy_new_points") #Either a conversion named with :conversion or a test name.
|
45
|
+
@abingo_identity.bingo!("buy_new_points") #Either a conversion named with :conversion or a test name.
|
46
46
|
end
|
47
47
|
|
48
48
|
Example: Conversion tracking (in a view)
|
49
49
|
|
50
|
-
Thanks for signing up, dude! <% bingo!("signup_page_redesign") >
|
50
|
+
Thanks for signing up, dude! <% @abingo_identity.bingo!("signup_page_redesign") >
|
51
51
|
|
52
52
|
Example: Statistical Significance Testing
|
53
53
|
|
@@ -62,7 +62,7 @@ Abingo::Experiment.last.describe_result_in_words
|
|
62
62
|
Installation
|
63
63
|
=======
|
64
64
|
|
65
|
-
1) REQUIRED: You'll need to generate a DB migration to prepare two tables,
|
65
|
+
1) REQUIRED: You'll need to generate a DB migration to prepare two tables,
|
66
66
|
then migrate your database. (Note: slight edits required if you use the table names
|
67
67
|
"experiments" or "alternatives" at present.) Note: if you are upgrading to A/Bingo 1.0.0, you'll
|
68
68
|
want to do this again.
|
@@ -80,9 +80,10 @@ before_filter :set_abingo_identity
|
|
80
80
|
|
81
81
|
def set_abingo_identity
|
82
82
|
if (session[:abingo_identity])
|
83
|
-
|
83
|
+
@abingo_identity = Abingo.identify(session[:abingo_identity])
|
84
84
|
else
|
85
|
-
|
85
|
+
@abingo_identity = Abingo.identify
|
86
|
+
session[:abingo_identity] = @abingo_identity.identity
|
86
87
|
end
|
87
88
|
end
|
88
89
|
|
@@ -106,4 +107,4 @@ A/Bingo defaults to using the same cache store as Rails. If you want to change
|
|
106
107
|
Abingo.cache = ActiveSupport::Cache::MemCacheStore.new("cache.example.com:12345") #best if really memcacheDB
|
107
108
|
|
108
109
|
|
109
|
-
Copyright (c) 2009-2010 Patrick McKenzie, released under the MIT license
|
110
|
+
Copyright (c) 2009-2010 Patrick McKenzie, released under the MIT license
|
data/lib/abingo.rb
CHANGED
@@ -14,25 +14,20 @@ ActionView::Base.send :include, AbingoViewHelper
|
|
14
14
|
#Usage of ABingo, including practical hints, is covered at http://www.bingocardcreator.com/abingo
|
15
15
|
|
16
16
|
class Abingo
|
17
|
-
|
18
|
-
@@VERSION = "1.1.0"
|
19
|
-
@@MAJOR_VERSION = "1.1"
|
20
|
-
cattr_reader :VERSION
|
21
|
-
cattr_reader :MAJOR_VERSION
|
22
|
-
|
23
|
-
#Not strictly necessary, but eh, as long as I'm here.
|
24
17
|
cattr_accessor :salt
|
25
18
|
@@salt = "Not really necessary."
|
26
19
|
|
27
20
|
@@options ||= {}
|
28
21
|
cattr_accessor :options
|
29
22
|
|
23
|
+
attr_accessor :identity
|
24
|
+
|
30
25
|
#Defined options:
|
31
26
|
# :enable_specification => if true, allow params[test_name] to override the calculated value for a test.
|
32
27
|
# :enable_override_in_session => if true, allows session[test_name] to override the calculated value for a test.
|
33
28
|
# :expires_in => if not nil, passes expire_in to creation of per-user cache keys. Useful for Redis, to prevent expired sessions
|
34
29
|
# from running wild and consuming all of your memory.
|
35
|
-
# :count_humans_only => Count only participation and conversions from humans. Humans can be identified by calling
|
30
|
+
# :count_humans_only => Count only participation and conversions from humans. Humans can be identified by calling @abingo.mark_human!
|
36
31
|
# This can be done in e.g. Javascript code, which bots will typically not execute. See FAQ for details.
|
37
32
|
# :expires_in_for_bots => if not nil, passes expire_in to creation of per-user cache keys, but only for bots.
|
38
33
|
# Only matters if :count_humans_only is on.
|
@@ -47,30 +42,40 @@ class Abingo
|
|
47
42
|
#
|
48
43
|
#You can overwrite Abingo's cache instance, if you would like it to not share
|
49
44
|
#your generic Rails cache.
|
50
|
-
cattr_writer :cache
|
51
45
|
|
52
46
|
def self.cache
|
53
|
-
|
47
|
+
@cache || Rails.cache
|
54
48
|
end
|
55
49
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
50
|
+
def self.cache=(cache)
|
51
|
+
@cache = cache
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.identity=(new_identity)
|
55
|
+
raise RuntimeError.new("Setting identity on the class level has been deprecated. Please create an instance via: @abingo = Abingo.identify('user-id')")
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.generate_identity
|
59
|
+
rand(10 ** 10).to_i.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
#This method identifies a user and ensures they consistently see the same alternative.
|
63
|
+
#This means that if you use Abingo.identify on someone at login, they will
|
61
64
|
#always see the same alternative for a particular test which is past the login
|
62
65
|
#screen. For details and usage notes, see the docs.
|
63
|
-
def self.identity=
|
64
|
-
|
66
|
+
def self.identify(identity = nil)
|
67
|
+
identity ||= generate_identity
|
68
|
+
new(identity)
|
65
69
|
end
|
66
70
|
|
67
|
-
def
|
68
|
-
|
71
|
+
def initialize(identity)
|
72
|
+
@identity = identity
|
73
|
+
super
|
69
74
|
end
|
70
75
|
|
71
76
|
#A simple convenience method for doing an A/B test. Returns true or false.
|
72
77
|
#If you pass it a block, it will bind the choice to the variable given to the block.
|
73
|
-
def
|
78
|
+
def flip(test_name)
|
74
79
|
if block_given?
|
75
80
|
yield(self.test(test_name, [true, false]))
|
76
81
|
else
|
@@ -82,7 +87,7 @@ class Abingo
|
|
82
87
|
#options accepts
|
83
88
|
# :multiple_participation (true or false)
|
84
89
|
# :conversion name of conversion to listen for (alias: conversion_name)
|
85
|
-
def
|
90
|
+
def test(test_name, alternatives, options = {})
|
86
91
|
|
87
92
|
short_circuit = Abingo.cache.read("Abingo::Experiment::short_circuit(#{test_name})".gsub(" ", "_"))
|
88
93
|
unless short_circuit.nil?
|
@@ -91,42 +96,39 @@ class Abingo
|
|
91
96
|
|
92
97
|
unless Abingo::Experiment.exists?(test_name)
|
93
98
|
lock_key = "Abingo::lock_for_creation(#{test_name.gsub(" ", "_")})"
|
94
|
-
|
95
|
-
|
99
|
+
lock_id = SecureRandom.hex
|
96
100
|
#this prevents (most) repeated creations of experiments in high concurrency environments.
|
97
101
|
if Abingo.cache.exist?(lock_key)
|
98
|
-
|
99
|
-
|
100
|
-
|
102
|
+
wait_for_lock_release(lock_key)
|
103
|
+
else
|
104
|
+
Abingo.cache.write(lock_key, lock_id, :expires_in => 5.seconds)
|
105
|
+
sleep(0.1)
|
106
|
+
if Abingo.cache.read(lock_key) == lock_id
|
107
|
+
conversion_name = options[:conversion] || options[:conversion_name]
|
108
|
+
Abingo::Experiment.start_experiment!(test_name, Abingo.parse_alternatives(alternatives), conversion_name)
|
109
|
+
else
|
110
|
+
wait_for_lock_release(lock_key)
|
101
111
|
end
|
102
|
-
creation_required = Abingo::Experiment.exists?(test_name)
|
103
|
-
end
|
104
|
-
|
105
|
-
if creation_required
|
106
|
-
Abingo.cache.write(lock_key, 1, :expires_in => 5.seconds)
|
107
|
-
conversion_name = options[:conversion] || options[:conversion_name]
|
108
|
-
Abingo::Experiment.start_experiment!(test_name, self.parse_alternatives(alternatives), conversion_name)
|
109
|
-
Abingo.cache.delete(lock_key)
|
110
112
|
end
|
113
|
+
Abingo.cache.delete(lock_key)
|
111
114
|
end
|
112
115
|
|
113
116
|
choice = self.find_alternative_for_user(test_name, alternatives)
|
114
|
-
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{
|
117
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{self.identity}") || []
|
115
118
|
|
116
119
|
#Set this user to participate in this experiment, and increment participants count.
|
117
120
|
if options[:multiple_participation] || !(participating_tests.include?(test_name))
|
118
121
|
unless participating_tests.include?(test_name)
|
119
122
|
participating_tests = participating_tests + [test_name]
|
120
|
-
|
121
|
-
|
122
|
-
Abingo.cache.write("Abingo::participating_tests::#{Abingo.identity}", participating_tests, {:expires_in => expires_in})
|
123
|
+
if self.expires_in
|
124
|
+
Abingo.cache.write("Abingo::participating_tests::#{self.identity}", participating_tests, {:expires_in => self.expires_in})
|
123
125
|
else
|
124
|
-
Abingo.cache.write("Abingo::participating_tests::#{
|
126
|
+
Abingo.cache.write("Abingo::participating_tests::#{self.identity}", participating_tests)
|
125
127
|
end
|
126
128
|
end
|
127
129
|
#If we're only counting known humans, then postpone scoring participation until after we know the user is human.
|
128
|
-
if (!@@options[:count_humans_only] ||
|
129
|
-
Abingo::Alternative.score_participation(test_name)
|
130
|
+
if (!@@options[:count_humans_only] || self.is_human?)
|
131
|
+
Abingo::Alternative.score_participation(test_name, choice)
|
130
132
|
end
|
131
133
|
end
|
132
134
|
|
@@ -137,6 +139,11 @@ class Abingo
|
|
137
139
|
end
|
138
140
|
end
|
139
141
|
|
142
|
+
def wait_for_lock_release(lock_key)
|
143
|
+
while Abingo.cache.exist?(lock_key)
|
144
|
+
sleep(0.1)
|
145
|
+
end
|
146
|
+
end
|
140
147
|
|
141
148
|
#Scores conversions for tests.
|
142
149
|
#test_name_or_array supports three types of input:
|
@@ -149,7 +156,7 @@ class Abingo
|
|
149
156
|
#An array of either of the above: for each element of the array, process as above.
|
150
157
|
#
|
151
158
|
#nil: score a conversion for every test the u
|
152
|
-
def
|
159
|
+
def bingo!(name = nil, options = {})
|
153
160
|
if name.kind_of? Array
|
154
161
|
name.map do |single_test|
|
155
162
|
self.bingo!(single_test, options)
|
@@ -157,7 +164,7 @@ class Abingo
|
|
157
164
|
else
|
158
165
|
if name.nil?
|
159
166
|
#Score all participating tests
|
160
|
-
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{
|
167
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{self.identity}") || []
|
161
168
|
participating_tests.each do |participating_test|
|
162
169
|
self.bingo!(participating_test, options)
|
163
170
|
end
|
@@ -182,52 +189,53 @@ class Abingo
|
|
182
189
|
end
|
183
190
|
end
|
184
191
|
|
185
|
-
def
|
186
|
-
identity = Abingo.identity
|
192
|
+
def participating_tests(only_current = true)
|
187
193
|
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{identity}") || []
|
188
194
|
tests_and_alternatives = participating_tests.inject({}) do |acc, test_name|
|
189
195
|
alternatives_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_")
|
190
196
|
alternatives = Abingo.cache.read(alternatives_key)
|
191
|
-
acc[test_name] =
|
197
|
+
acc[test_name] = find_alternative_for_user(test_name, alternatives)
|
192
198
|
acc
|
193
199
|
end
|
194
200
|
if (only_current)
|
195
201
|
tests_and_alternatives.reject! do |key, value|
|
196
|
-
|
202
|
+
Abingo.cache.read("Abingo::Experiment::short_circuit(#{key})")
|
197
203
|
end
|
198
204
|
end
|
199
205
|
tests_and_alternatives
|
200
206
|
end
|
201
207
|
|
202
208
|
#Marks that this user is human.
|
203
|
-
def
|
204
|
-
Abingo.cache.fetch("Abingo::is_human(#{
|
209
|
+
def human!
|
210
|
+
Abingo.cache.fetch("Abingo::is_human(#{self.identity})", {:expires_in => self.expires_in(true)}) do
|
205
211
|
#Now that we know the user is human, score participation for all their tests. (Further participation will *not* be lazy evaluated.)
|
206
212
|
|
207
213
|
#Score all tests which have been deferred.
|
208
|
-
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{
|
214
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{self.identity}") || []
|
209
215
|
|
210
216
|
#Refresh cache expiry for this user to match that of known humans.
|
211
217
|
if (@@options[:expires_in_for_bots] && !participating_tests.blank?)
|
212
|
-
Abingo.cache.write("Abingo::participating_tests::#{
|
218
|
+
Abingo.cache.write("Abingo::participating_tests::#{self.identity}", participating_tests, {:expires_in => self.expires_in(true)})
|
213
219
|
end
|
214
220
|
|
215
221
|
participating_tests.each do |test_name|
|
216
|
-
|
217
|
-
|
218
|
-
|
222
|
+
viewed_alternative = find_alternative_for_user(test_name,
|
223
|
+
Abingo::Experiment.alternatives_for_test(test_name))
|
224
|
+
Alternative.score_participation(test_name, viewed_alternative)
|
225
|
+
if conversions = Abingo.cache.read("Abingo::conversions(#{self.identity},#{test_name}")
|
226
|
+
conversions.times { Alternative.score_conversion(test_name, viewed_alternative) }
|
219
227
|
end
|
220
228
|
end
|
221
229
|
true #Marks this user as human in the cache.
|
222
230
|
end
|
223
231
|
end
|
224
232
|
|
225
|
-
|
226
|
-
|
227
|
-
def self.is_human?
|
228
|
-
!!Abingo.cache.read("Abingo::is_human(#{Abingo.identity})")
|
233
|
+
def is_human?
|
234
|
+
!!Abingo.cache.read("Abingo::is_human(#{self.identity})")
|
229
235
|
end
|
230
236
|
|
237
|
+
protected
|
238
|
+
|
231
239
|
#For programmer convenience, we allow you to specify what the alternatives for
|
232
240
|
#an experiment are in a few ways. Thus, we need to actually be able to handle
|
233
241
|
#all of them. We fire this parser very infrequently (once per test, typically)
|
@@ -264,32 +272,34 @@ class Abingo
|
|
264
272
|
|
265
273
|
def self.retrieve_alternatives(test_name, alternatives)
|
266
274
|
cache_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_")
|
267
|
-
alternative_array =
|
268
|
-
|
275
|
+
alternative_array = Abingo.cache.fetch(cache_key) do
|
276
|
+
Abingo.parse_alternatives(alternatives)
|
269
277
|
end
|
270
278
|
alternative_array
|
271
279
|
end
|
272
280
|
|
273
|
-
def
|
274
|
-
alternatives_array = retrieve_alternatives(test_name, alternatives)
|
281
|
+
def find_alternative_for_user(test_name, alternatives)
|
282
|
+
alternatives_array = Abingo.retrieve_alternatives(test_name, alternatives)
|
275
283
|
alternatives_array[self.modulo_choice(test_name, alternatives_array.size)]
|
276
284
|
end
|
277
285
|
|
278
286
|
#Quickly determines what alternative to show a given user. Given a test name
|
279
287
|
#and their identity, we hash them together (which, for MD5, provably introduces
|
280
288
|
#enough entropy that we don't care) otherwise
|
281
|
-
def
|
289
|
+
def modulo_choice(test_name, choices_count)
|
282
290
|
Digest::MD5.hexdigest(Abingo.salt.to_s + test_name + self.identity.to_s).to_i(16) % choices_count
|
283
291
|
end
|
284
292
|
|
285
|
-
def
|
293
|
+
def score_conversion!(test_name)
|
286
294
|
test_name.gsub!(" ", "_")
|
287
|
-
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{
|
295
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{self.identity}") || []
|
288
296
|
if options[:assume_participation] || participating_tests.include?(test_name)
|
289
|
-
cache_key = "Abingo::conversions(#{
|
297
|
+
cache_key = "Abingo::conversions(#{self.identity},#{test_name}"
|
290
298
|
if options[:multiple_conversions] || !Abingo.cache.read(cache_key)
|
291
|
-
if !options[:count_humans_only] ||
|
292
|
-
|
299
|
+
if !options[:count_humans_only] || is_human?
|
300
|
+
viewed_alternative = find_alternative_for_user(test_name,
|
301
|
+
Abingo::Experiment.alternatives_for_test(test_name))
|
302
|
+
Abingo::Alternative.score_conversion(test_name, viewed_alternative)
|
293
303
|
end
|
294
304
|
|
295
305
|
if Abingo.cache.exist?(cache_key)
|
@@ -301,12 +311,12 @@ class Abingo
|
|
301
311
|
end
|
302
312
|
end
|
303
313
|
|
304
|
-
def
|
314
|
+
def expires_in(known_human = false)
|
305
315
|
expires_in = nil
|
306
316
|
if (@@options[:expires_in])
|
307
317
|
expires_in = @@options[:expires_in]
|
308
318
|
end
|
309
|
-
if (@@options[:count_humans_only] && @@options[:expires_in_for_bots] && !(known_human ||
|
319
|
+
if (@@options[:count_humans_only] && @@options[:expires_in_for_bots] && !(known_human || is_human?))
|
310
320
|
expires_in = @@options[:expires_in_for_bots]
|
311
321
|
end
|
312
322
|
expires_in
|
data/lib/abingo/alternative.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "active_record"
|
1
2
|
require "abingo/conversion_rate"
|
2
3
|
class Abingo::Alternative < ActiveRecord::Base
|
3
4
|
include Abingo::ConversionRate
|
@@ -10,15 +11,11 @@ class Abingo::Alternative < ActiveRecord::Base
|
|
10
11
|
Digest::MD5.hexdigest(Abingo.salt + test_name + alternative_name.to_s)
|
11
12
|
end
|
12
13
|
|
13
|
-
def self.score_conversion(test_name)
|
14
|
-
viewed_alternative = Abingo.find_alternative_for_user(test_name,
|
15
|
-
Abingo::Experiment.alternatives_for_test(test_name))
|
14
|
+
def self.score_conversion(test_name, viewed_alternative)
|
16
15
|
self.update_all("conversions = conversions + 1", :lookup => self.calculate_lookup(test_name, viewed_alternative))
|
17
16
|
end
|
18
17
|
|
19
|
-
def self.score_participation(test_name)
|
20
|
-
viewed_alternative = Abingo.find_alternative_for_user(test_name,
|
21
|
-
Abingo::Experiment.alternatives_for_test(test_name))
|
18
|
+
def self.score_participation(test_name, viewed_alternative)
|
22
19
|
self.update_all("participants = participants + 1", :lookup => self.calculate_lookup(test_name, viewed_alternative))
|
23
20
|
end
|
24
21
|
|
@@ -1,12 +1,8 @@
|
|
1
|
+
require 'action_controller'
|
1
2
|
class Abingo
|
2
3
|
module Controller
|
3
4
|
module Dashboard
|
4
|
-
|
5
|
-
if Rails::VERSION::MAJOR <= 2
|
6
|
-
ActionController::Base.view_paths.unshift File.join(File.dirname(__FILE__), "../views")
|
7
|
-
else
|
8
|
-
ActionController::Base.prepend_view_path File.join(File.dirname(__FILE__), "../views")
|
9
|
-
end
|
5
|
+
ActionController::Base.prepend_view_path File.join(File.dirname(__FILE__), "../views")
|
10
6
|
|
11
7
|
def index
|
12
8
|
@experiments = Abingo::Experiment.all
|
data/lib/abingo/experiment.rb
CHANGED
@@ -76,11 +76,7 @@ class Abingo::Experiment < ActiveRecord::Base
|
|
76
76
|
cloned_alternatives_array -= [alt]
|
77
77
|
end
|
78
78
|
experiment.status = "Live"
|
79
|
-
|
80
|
-
experiment.save(false) #Calling the validation here causes problems b/c of transaction.
|
81
|
-
else
|
82
|
-
experiment.save(:validate => false)
|
83
|
-
end
|
79
|
+
experiment.save(:validate => false)
|
84
80
|
Abingo.cache.write("Abingo::Experiment::exists(#{test_name})".gsub(" ", "_"), 1)
|
85
81
|
|
86
82
|
#This might have issues in very, very high concurrency environments...
|
data/lib/abingo/version.rb
CHANGED
data/lib/abingo_sugar.rb
CHANGED
@@ -5,15 +5,15 @@
|
|
5
5
|
|
6
6
|
module AbingoSugar
|
7
7
|
|
8
|
-
def ab_test(test_name, alternatives = nil, options = {})
|
8
|
+
def ab_test(abingo, test_name, alternatives = nil, options = {})
|
9
9
|
if (Abingo.options[:enable_specification] && !params[test_name].nil?)
|
10
10
|
choice = params[test_name]
|
11
11
|
elsif (Abingo.options[:enable_override_in_session] && !session[test_name].nil?)
|
12
12
|
choice = session[test_name]
|
13
13
|
elsif (alternatives.nil?)
|
14
|
-
choice =
|
14
|
+
choice = abingo.flip(test_name)
|
15
15
|
else
|
16
|
-
choice =
|
16
|
+
choice = abingo.test(test_name, alternatives, options)
|
17
17
|
end
|
18
18
|
|
19
19
|
if block_given?
|
@@ -23,19 +23,19 @@ module AbingoSugar
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
def bingo!(test_name, options = {})
|
27
|
-
|
26
|
+
def bingo!(abingo, test_name, options = {})
|
27
|
+
abingo.bingo!(test_name, options)
|
28
28
|
end
|
29
29
|
|
30
30
|
#Mark the user as a human.
|
31
|
-
def abingo_mark_human
|
31
|
+
def abingo_mark_human(abingo)
|
32
32
|
textual_result = "1"
|
33
33
|
begin
|
34
34
|
a = params[:a].to_i
|
35
35
|
b = params[:b].to_i
|
36
36
|
c = params[:c].to_i
|
37
37
|
if (request.method == :post && (a + b == c))
|
38
|
-
|
38
|
+
abingo.human!
|
39
39
|
else
|
40
40
|
textual_result = "0"
|
41
41
|
end
|
@@ -43,7 +43,6 @@ module AbingoSugar
|
|
43
43
|
textual_result = "0"
|
44
44
|
end
|
45
45
|
render :text => textual_result, :layout => false #Not actually used by browser
|
46
|
-
|
47
46
|
end
|
48
47
|
|
49
|
-
end
|
48
|
+
end
|
data/lib/abingo_view_helper.rb
CHANGED
@@ -2,32 +2,28 @@
|
|
2
2
|
|
3
3
|
module AbingoViewHelper
|
4
4
|
|
5
|
-
def ab_test(test_name, alternatives = nil, options = {}, &block)
|
5
|
+
def ab_test(abingo, test_name, alternatives = nil, options = {}, &block)
|
6
6
|
|
7
7
|
if (Abingo.options[:enable_specification] && !params[test_name].nil?)
|
8
8
|
choice = params[test_name]
|
9
9
|
elsif (Abingo.options[:enable_override_in_session] && !session[test_name].nil?)
|
10
10
|
choice = session[test_name]
|
11
11
|
elsif (alternatives.nil?)
|
12
|
-
choice =
|
12
|
+
choice = abingo.flip(test_name)
|
13
13
|
else
|
14
|
-
choice =
|
14
|
+
choice = abingo.test(test_name, alternatives, options)
|
15
15
|
end
|
16
16
|
|
17
17
|
if block
|
18
18
|
content_tag = capture(choice, &block)
|
19
|
-
|
20
|
-
block_called_from_erb?(block) ? concat(content_tag) : content_tag
|
21
|
-
else
|
22
|
-
content_tag
|
23
|
-
end
|
19
|
+
block_called_from_erb?(block) ? concat(content_tag) : content_tag
|
24
20
|
else
|
25
21
|
choice
|
26
22
|
end
|
27
23
|
end
|
28
24
|
|
29
|
-
def bingo!(test_name, options = {})
|
30
|
-
|
25
|
+
def bingo!(abingo, test_name, options = {})
|
26
|
+
abingo.bingo!(test_name, options)
|
31
27
|
end
|
32
28
|
|
33
29
|
#This causes an AJAX post against the URL. That URL should call Abingo.human!
|
@@ -41,5 +37,5 @@ module AbingoViewHelper
|
|
41
37
|
end
|
42
38
|
script.nil? ? "" : %Q|<script type="text/javascript">#{script}</script>|
|
43
39
|
end
|
44
|
-
|
45
|
-
end
|
40
|
+
|
41
|
+
end
|
data/test/abingo_test.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
|
-
|
1
|
+
require_relative 'test_helper'
|
2
2
|
|
3
|
-
class AbingoTest <
|
3
|
+
class AbingoTest < Test::Unit::TestCase
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
setup do
|
6
|
+
Abingo.options = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
teardown do
|
10
|
+
Abingo.cache.clear
|
11
|
+
Abingo::Experiment.delete_all
|
12
|
+
Abingo::Alternative.delete_all
|
13
|
+
end
|
9
14
|
|
10
15
|
test "identity automatically assigned" do
|
11
|
-
|
16
|
+
abingo = Abingo.identify
|
17
|
+
assert abingo.identity != nil
|
12
18
|
end
|
13
19
|
|
14
20
|
test "alternative parsing" do
|
@@ -23,31 +29,35 @@ class AbingoTest < ActiveSupport::TestCase
|
|
23
29
|
assert_equal 0, Abingo::Experiment.count
|
24
30
|
assert_equal 0, Abingo::Alternative.count
|
25
31
|
alternatives = %w{A B}
|
26
|
-
|
32
|
+
abingo = Abingo.identify
|
33
|
+
alternative_selected = abingo.test("unit_test_sample_A", alternatives)
|
27
34
|
assert_equal 1, Abingo::Experiment.count
|
28
35
|
assert_equal 2, Abingo::Alternative.count
|
29
36
|
assert alternatives.include?(alternative_selected)
|
30
37
|
end
|
31
38
|
|
32
39
|
test "exists works right" do
|
33
|
-
Abingo.
|
40
|
+
abingo = Abingo.identify
|
41
|
+
abingo.test("exist works right", %w{does does_not})
|
34
42
|
assert Abingo::Experiment.exists?("exist works right")
|
35
43
|
end
|
36
44
|
|
37
45
|
test "alternatives picked consistently" do
|
38
|
-
|
46
|
+
abingo = Abingo.identify
|
47
|
+
alternative_picked = abingo.test("consistency_test", 1..100)
|
39
48
|
100.times do
|
40
|
-
assert_equal alternative_picked,
|
49
|
+
assert_equal alternative_picked, abingo.test("consistency_test", 1..100)
|
41
50
|
end
|
42
51
|
end
|
43
52
|
|
44
53
|
test "participation works" do
|
45
54
|
new_tests = %w{participationA participationB participationC}
|
55
|
+
abingo = Abingo.identify
|
46
56
|
new_tests.map do |test_name|
|
47
|
-
|
57
|
+
abingo.test(test_name, 1..5)
|
48
58
|
end
|
49
59
|
|
50
|
-
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{
|
60
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{abingo.identity}") || []
|
51
61
|
|
52
62
|
new_tests.map do |test_name|
|
53
63
|
assert participating_tests.include? test_name
|
@@ -56,7 +66,8 @@ class AbingoTest < ActiveSupport::TestCase
|
|
56
66
|
|
57
67
|
test "participants counted" do
|
58
68
|
test_name = "participants_counted_test"
|
59
|
-
|
69
|
+
abingo = Abingo.identify
|
70
|
+
alternative = abingo.test(test_name, %w{a b c})
|
60
71
|
|
61
72
|
ex = Abingo::Experiment.find_by_test_name(test_name)
|
62
73
|
lookup = Abingo::Alternative.calculate_lookup(test_name, alternative)
|
@@ -67,14 +78,15 @@ class AbingoTest < ActiveSupport::TestCase
|
|
67
78
|
|
68
79
|
test "conversion tracking by test name" do
|
69
80
|
test_name = "conversion_test_by_name"
|
70
|
-
|
71
|
-
|
81
|
+
abingo = Abingo.identify
|
82
|
+
alternative = abingo.test(test_name, %w{a b c})
|
83
|
+
abingo.bingo!(test_name)
|
72
84
|
ex = Abingo::Experiment.find_by_test_name(test_name)
|
73
85
|
lookup = Abingo::Alternative.calculate_lookup(test_name, alternative)
|
74
86
|
chosen_alt = Abingo::Alternative.find_by_lookup(lookup)
|
75
87
|
assert_equal 1, ex.conversions
|
76
88
|
assert_equal 1, chosen_alt.conversions
|
77
|
-
|
89
|
+
abingo.bingo!(test_name)
|
78
90
|
|
79
91
|
#Should still only have one because this conversion should not be double counted.
|
80
92
|
#We haven't specified that in the test options.
|
@@ -82,13 +94,14 @@ class AbingoTest < ActiveSupport::TestCase
|
|
82
94
|
end
|
83
95
|
|
84
96
|
test "conversion tracking by conversion name" do
|
97
|
+
abingo = Abingo.identify
|
85
98
|
conversion_name = "purchase"
|
86
99
|
tests = %w{conversionTrackingByConversionNameA conversionTrackingByConversionNameB conversionTrackingByConversionNameC}
|
87
100
|
tests.map do |test_name|
|
88
|
-
|
101
|
+
abingo.test(test_name, %w{A B}, :conversion => conversion_name)
|
89
102
|
end
|
90
103
|
|
91
|
-
|
104
|
+
abingo.bingo!(conversion_name)
|
92
105
|
tests.map do |test_name|
|
93
106
|
assert_equal 1, Abingo::Experiment.find_by_test_name(test_name).conversions
|
94
107
|
end
|
@@ -97,7 +110,8 @@ class AbingoTest < ActiveSupport::TestCase
|
|
97
110
|
test "short circuiting works" do
|
98
111
|
conversion_name = "purchase"
|
99
112
|
test_name = "short circuit test"
|
100
|
-
|
113
|
+
abingo = Abingo.identify
|
114
|
+
alt_picked = abingo.test(test_name, %w{A B}, :conversion => conversion_name)
|
101
115
|
ex = Abingo::Experiment.find_by_test_name(test_name)
|
102
116
|
alt_not_picked = (%w{A B} - [alt_picked]).first
|
103
117
|
|
@@ -105,14 +119,12 @@ class AbingoTest < ActiveSupport::TestCase
|
|
105
119
|
|
106
120
|
ex.reload
|
107
121
|
assert_equal "Finished", ex.status
|
108
|
-
|
109
|
-
|
122
|
+
|
123
|
+
abingo.bingo!(test_name) #Should not be counted, test is over.
|
110
124
|
assert_equal 0, ex.conversions
|
111
125
|
|
112
|
-
|
113
|
-
|
114
|
-
Abingo.test(test_name, %w{A B}, :conversion => conversion_name)
|
115
|
-
Abingo.identity = old_identity
|
126
|
+
new_bingo = Abingo.identify("shortCircuitTestNewIdentity")
|
127
|
+
new_bingo.test(test_name, %w{A B}, :conversion => conversion_name)
|
116
128
|
ex.reload
|
117
129
|
assert_equal 1, ex.participants #Original identity counted, new identity not counted b/c test stopped
|
118
130
|
end
|
@@ -125,35 +137,56 @@ class AbingoTest < ActiveSupport::TestCase
|
|
125
137
|
threads = []
|
126
138
|
5.times do
|
127
139
|
threads << Thread.new do
|
128
|
-
Abingo.
|
129
|
-
|
140
|
+
abingo = Abingo.identify
|
141
|
+
abingo.test(test_name, alternatives, :conversion => conversion_name)
|
142
|
+
ActiveRecord::Base.connection.close
|
130
143
|
end
|
131
144
|
end
|
132
|
-
|
145
|
+
threads.each(&:join)
|
133
146
|
assert_equal 1, Abingo::Experiment.count_by_sql(["select count(id) from experiments where test_name = ?", test_name])
|
134
147
|
end
|
135
148
|
|
149
|
+
test "proper conversions with concurrency" do
|
150
|
+
test_name = "conversion_concurrency_test"
|
151
|
+
alternatives = %w{foo bar}
|
152
|
+
threads = []
|
153
|
+
alternatives.size.times do |i|
|
154
|
+
threads << Thread.new do
|
155
|
+
abingo = Abingo.identify(i)
|
156
|
+
abingo.test(test_name, alternatives)
|
157
|
+
abingo.bingo!(test_name)
|
158
|
+
sleep(0.3) if i == 0
|
159
|
+
ActiveRecord::Base.connection.close
|
160
|
+
end
|
161
|
+
end
|
162
|
+
threads.each(&:join)
|
163
|
+
ex = Abingo::Experiment.find_by_test_name(test_name)
|
164
|
+
ex.alternatives.each do |alternative|
|
165
|
+
assert_equal 1, alternative.conversions
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
136
169
|
test "non-humans are ignored for participation and conversions if not explicitly counted" do
|
137
170
|
Abingo.options[:count_humans_only] = true
|
138
171
|
Abingo.options[:expires_in] = 1.hour
|
139
172
|
Abingo.options[:expires_in_for_bots] = 3.seconds
|
140
|
-
first_identity = Abingo.
|
173
|
+
first_identity = Abingo.identify("unsure_if_human#{Time.now.to_i}")
|
141
174
|
test_name = "are_you_a_human"
|
142
|
-
|
175
|
+
first_identity.test(test_name, %w{does_not matter})
|
143
176
|
|
144
|
-
|
177
|
+
assert !first_identity.is_human?, "Identity not marked as human yet."
|
145
178
|
|
146
179
|
ex = Abingo::Experiment.find_by_test_name(test_name)
|
147
|
-
|
180
|
+
first_identity.bingo!(test_name)
|
148
181
|
assert_equal 0, ex.participants, "Not human yet, so should have no participants."
|
149
182
|
assert_equal 0, ex.conversions, "Not human yet, so should have no conversions."
|
150
183
|
|
151
|
-
|
152
|
-
|
184
|
+
first_identity.human!
|
185
|
+
|
153
186
|
#Setting up second participant who doesn't convert.
|
154
|
-
|
155
|
-
|
156
|
-
|
187
|
+
second_identity = Abingo.identify("unsure_if_human_2_#{Time.now.to_i}")
|
188
|
+
second_identity.test(test_name, %w{does_not matter})
|
189
|
+
second_identity.human!
|
157
190
|
|
158
191
|
ex = Abingo::Experiment.find_by_test_name(test_name)
|
159
192
|
assert_equal 2, ex.participants, "Now that we're human, our participation should matter."
|
@@ -161,27 +194,27 @@ class AbingoTest < ActiveSupport::TestCase
|
|
161
194
|
end
|
162
195
|
|
163
196
|
test "Participating tests for a given identity" do
|
164
|
-
|
197
|
+
abingo = Abingo.identify("test_participant")
|
165
198
|
test_names = (1..3).map {|t| "participating_test_test_name #{t}"}
|
166
199
|
test_alternatives = %w{yes no}
|
167
|
-
test_names.each {|test_name|
|
200
|
+
test_names.each {|test_name| abingo.test(test_name, test_alternatives)}
|
168
201
|
ex = Abingo::Experiment.last
|
169
202
|
ex.end_experiment!("no") #End final of 3 tests, leaving 2 presently running
|
170
203
|
|
171
|
-
assert_equal 2,
|
172
|
-
|
204
|
+
assert_equal 2, abingo.participating_tests.size #Pairs for two tests
|
205
|
+
abingo.participating_tests.each do |key, value|
|
173
206
|
assert test_names.include? key
|
174
207
|
assert test_alternatives.include? value
|
175
208
|
end
|
176
|
-
|
177
|
-
assert_equal 3,
|
178
|
-
|
209
|
+
|
210
|
+
assert_equal 3, abingo.participating_tests(false).size #pairs for three tests
|
211
|
+
abingo.participating_tests(false).each do |key, value|
|
179
212
|
assert test_names.include? key
|
180
213
|
assert test_alternatives.include? value
|
181
214
|
end
|
182
215
|
|
183
|
-
|
184
|
-
assert_equal({},
|
216
|
+
non_participant = Abingo.identify("test_nonparticipant")
|
217
|
+
assert_equal({}, non_participant.participating_tests)
|
185
218
|
end
|
186
219
|
|
187
220
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,34 +1,59 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'rails'
|
1
|
+
#require 'rubygems'
|
2
|
+
#require 'rails'
|
3
|
+
#
|
4
|
+
#require 'rails/all'
|
5
|
+
require 'test/unit'
|
6
|
+
require 'contest'
|
7
|
+
require 'pg'
|
8
|
+
require 'active_support/cache'
|
9
|
+
require 'active_support/cache/memory_store'
|
10
|
+
require_relative '../lib/abingo'
|
11
|
+
ActiveRecord::Base.establish_connection(
|
12
|
+
:adapter => "postgresql",
|
13
|
+
:database => "abingo-test")
|
14
|
+
ActiveRecord::Schema.define do
|
15
|
+
create_table "experiments", :force => true do |t|
|
16
|
+
t.string "test_name"
|
17
|
+
t.string "status"
|
18
|
+
t.timestamps
|
19
|
+
end
|
3
20
|
|
4
|
-
|
5
|
-
|
6
|
-
require 'test/unit'
|
21
|
+
add_index "experiments", "test_name"
|
22
|
+
#add_index "experiments", "created_on"
|
7
23
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
24
|
+
create_table "alternatives", :force => true do |t|
|
25
|
+
t.integer :experiment_id
|
26
|
+
t.string :content
|
27
|
+
t.string :lookup, :limit => 32
|
28
|
+
t.integer :weight, :default => 1
|
29
|
+
t.integer :participants, :default => 0
|
30
|
+
t.integer :conversions, :default => 0
|
31
|
+
end
|
12
32
|
|
13
|
-
|
14
|
-
|
15
|
-
require 'active_record'
|
16
|
-
require 'active_record/base'
|
17
|
-
|
18
|
-
require 'rails'
|
19
|
-
require 'rails/application'
|
20
|
-
|
21
|
-
require 'rails/railtie'
|
22
|
-
|
23
|
-
#We need to load the whole Rails application to properly initialize Rails.cache and other constants. Oh boy.
|
24
|
-
#We're going to parse it out of RAILS_PATH/config.ru using a little metaprogramming magic.
|
25
|
-
require ::File.expand_path('../../../../../config/environment', __FILE__)
|
26
|
-
lines = File.open(::File.expand_path('../../../../../config.ru', __FILE__)).readlines.select {|a| a =~ /::Application/}
|
27
|
-
application_name = lines.first[/[^ ]*::/].gsub(":", "")
|
28
|
-
Kernel.const_get(application_name).const_get("Application").initialize!
|
29
|
-
else
|
30
|
-
#Rails 2 testing
|
31
|
-
require 'active_support'
|
32
|
-
require 'active_support/test_case'
|
33
|
+
add_index "alternatives", "experiment_id"
|
34
|
+
add_index "alternatives", "lookup" #Critical for speed, since we'll primarily be updating by that.
|
33
35
|
end
|
34
|
-
|
36
|
+
Abingo.cache = ActiveSupport::Cache::MemoryStore.new
|
37
|
+
#
|
38
|
+
#require 'active_support'
|
39
|
+
#require 'active_support/railtie'
|
40
|
+
#require 'active_support/core_ext'
|
41
|
+
#require 'active_support/test_case'
|
42
|
+
#
|
43
|
+
#require 'action_controller'
|
44
|
+
#require 'action_controller/caching'
|
45
|
+
#require 'active_record'
|
46
|
+
#require 'active_record/base'
|
47
|
+
#
|
48
|
+
#require 'rails'
|
49
|
+
#require 'rails/application'
|
50
|
+
#
|
51
|
+
#require 'rails/railtie'
|
52
|
+
#
|
53
|
+
##We need to load the whole Rails application to properly initialize Rails.cache and other constants. Oh boy.
|
54
|
+
##We're going to parse it out of RAILS_PATH/config.ru using a little metaprogramming magic.
|
55
|
+
#require ::File.expand_path('../../../../../config/environment', __FILE__)
|
56
|
+
#lines = File.open(::File.expand_path('../../../../../config.ru', __FILE__)).readlines.select {|a| a =~ /::Application/}
|
57
|
+
#application_name = lines.first[/[^ ]*::/].gsub(":", "")
|
58
|
+
#Kernel.const_get(application_name).const_get("Application").initialize!
|
59
|
+
#
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abingo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2012-11-25 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
16
|
-
requirement: &
|
16
|
+
requirement: &70288958337380 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,7 +21,7 @@ dependencies:
|
|
21
21
|
version: '3.0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70288958337380
|
25
25
|
description: A split testing framework for Rails 3.x.x
|
26
26
|
email:
|
27
27
|
- me@glenngillen.com
|
@@ -50,10 +50,8 @@ files:
|
|
50
50
|
- lib/abingo/views/dashboard/index.erb
|
51
51
|
- lib/abingo_sugar.rb
|
52
52
|
- lib/abingo_view_helper.rb
|
53
|
-
- strip.rb
|
54
53
|
- test/abingo_test.rb
|
55
54
|
- test/test_helper.rb
|
56
|
-
- uninstall.rb
|
57
55
|
homepage: https://github.com/glenngillen/abingo
|
58
56
|
licenses: []
|
59
57
|
post_install_message:
|
@@ -68,7 +66,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
66
|
version: '0'
|
69
67
|
segments:
|
70
68
|
- 0
|
71
|
-
hash: -
|
69
|
+
hash: -1837967124242571080
|
72
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
71
|
none: false
|
74
72
|
requirements:
|
@@ -77,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
75
|
version: '0'
|
78
76
|
segments:
|
79
77
|
- 0
|
80
|
-
hash: -
|
78
|
+
hash: -1837967124242571080
|
81
79
|
requirements: []
|
82
80
|
rubyforge_project:
|
83
81
|
rubygems_version: 1.8.15
|
data/strip.rb
DELETED
data/uninstall.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
# Uninstall hook code here
|