fluidfeatures-rails 0.4.4 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +174 -3
- data/fluidfeatures-rails.gemspec +1 -1
- data/lib/fluidfeatures/rails.rb +14 -18
- data/lib/fluidfeatures/rails/version.rb +1 -1
- data/lib/generators/fluidfeatures/rails/install/install_generator.rb +29 -0
- data/spec/generators/fluidfeatures/rails/install/install_generator_spec.rb +9 -0
- data/spec/initializer_spec.rb +9 -10
- data/spec/support/rails.rb +5 -0
- metadata +6 -4
data/README.md
CHANGED
@@ -1,6 +1,177 @@
|
|
1
|
+
[![Build Status](https://secure.travis-ci.org/FluidFeatures/fluidfeatures-rails.png)](http://travis-ci.org/FluidFeatures/fluidfeatures-rails)
|
1
2
|
|
2
|
-
|
3
|
-
|
3
|
+
Rails graceful feature rollout and simple A/B testing
|
4
|
+
=====================================================
|
5
|
+
|
6
|
+
`gem fluidfeatures-rails` is a Ruby on Rails client for the API of FluidFeatures.com, which provides an elegant way to wrap new code so you have real-time control over rolling out new features to your user-base.
|
7
|
+
|
8
|
+
Rollout new versions of features
|
9
|
+
--------------------------------
|
10
|
+
|
11
|
+
Using `if ff? "foo"` you can easily rollout new code to production and then use the FluidFeatures dashboard to rollout this code to yourself, your team or your users.
|
12
|
+
|
13
|
+
Use `if ff? "foo", "v2"` to wrap the code for a new version of the "foo" feature.
|
14
|
+
|
15
|
+
`if ff? "foo"` defaults to version `"default"`, so anyone seeing "v2" will not also see "v2". This is a great way to test new features in production.
|
16
|
+
|
17
|
+
Once you have moved all your users over to the newer "v2" version you can factor out code from the older version.
|
18
|
+
|
19
|
+
Tracking goals
|
20
|
+
--------------
|
21
|
+
|
22
|
+
Sometimes referred to as "conversions", goals are a simple way of flagging that an condition has been met. You can do this in your Rails controllers with `fluidgoal(<goal-name>)`. A statement such as `fluidgoal "upgraded-to-pro-account"` tells FluidFeatures that your user upgraded their account.
|
23
|
+
|
24
|
+
You can do this in controller, or in your view and you will immediately be able to see statistics within the FluidFeatures dashboard. Statistics show you will versions of which features play the greatest roll in driving your users towards these goals.
|
25
|
+
|
26
|
+
A/B testing
|
27
|
+
-----------
|
28
|
+
|
29
|
+
By combining 2 versions of a feature and one goal (see above) you can start to easily perform A/B testing. FluidFeatures will keep track of which version is winning in terms of statistically hitting the goal more often.
|
30
|
+
|
31
|
+
If you only roll out one of the versions to a small percentage, say 2%, of your user base, then this is taken into account when calculating the version success statistics. This means you can easily trial experimental features or new versions of existing features.
|
32
|
+
|
33
|
+
Multi-variant testing and beyond
|
34
|
+
--------------------------------
|
35
|
+
|
36
|
+
While A/B testing might be enough for some, you have the power to do much more.
|
37
|
+
|
38
|
+
You can define any number of versions for a feature and track them all.
|
39
|
+
|
40
|
+
You can also compare the performance of different features against each other over time.
|
41
|
+
|
42
|
+
On your server
|
43
|
+
--------------
|
44
|
+
|
45
|
+
A/B testing on the server gives you the ability to test things at all levels of your stack. Test alternate SQL statements, different versions of emails sent to users, or different backend 3rd party API services.
|
46
|
+
|
47
|
+
In your templates
|
48
|
+
-----------------
|
49
|
+
|
50
|
+
fluidfeatures-rails exposes `def ff?` (alias to `def fluidfeature`) and `def fluidgoal` to your templates, so can also wrap versioned code there.
|
51
|
+
|
52
|
+
User definition and cohorts
|
53
|
+
---------------------------
|
54
|
+
|
55
|
+
In your `application_controller.rb` you will define a function called `def fluidfeatures_current_user` which can be called by `gem fluidfeatures-rails` to determine who the current user is.
|
56
|
+
|
57
|
+
This important for FluidFeatures to determine which feature versions to enable for the user, but is also the place where you can define any cohorts you wish to use for rolling out features.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
def fluidfeatures_current_user(verbose)
|
61
|
+
if current_user
|
62
|
+
if verbose
|
63
|
+
{
|
64
|
+
:id => @current_user[:id],
|
65
|
+
:name => @current_user[:name],
|
66
|
+
:uniques => {
|
67
|
+
:twitter => @current_user[:twitter_id]
|
68
|
+
},
|
69
|
+
:cohorts => {
|
70
|
+
# Example attributes for the user.
|
71
|
+
# These can be any fields you wish to select users by.
|
72
|
+
:company => {
|
73
|
+
# "display" is used to help you find it in the dashboard.
|
74
|
+
# Highly recommended unless you work with ids.
|
75
|
+
# This display name can change over time without consequence.
|
76
|
+
:display => @current_user[:company_name],
|
77
|
+
# "id" should be unique this this cohort and must be static
|
78
|
+
# over time.
|
79
|
+
:id => @current_user[:company_id]
|
80
|
+
},
|
81
|
+
# For this boolean cohort "true"|"false" is the "display"
|
82
|
+
# and the "id"
|
83
|
+
:admin => @current_user[:admin]
|
84
|
+
}
|
85
|
+
}
|
86
|
+
else
|
87
|
+
{
|
88
|
+
:id => @current_user.id
|
89
|
+
}
|
90
|
+
end
|
91
|
+
else
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
The above is an example of `fluidfeatures_current_user` that you might implement. The `verbose` is a boolean that indicates whether all details are required, or just the minimal identifying attributes (unique `:id`). `verbose` is an optimization that will be used at the discretion of `gem fluidfeatures-rails`.
|
98
|
+
|
99
|
+
`:name` allows you to pass the human-readable string, such as `"John Doe"`, that represents the user. This is then used in the FluidFeatures dashboard for display and search purposes. You can very quickly search for this user and enable a new feature for them.
|
100
|
+
|
101
|
+
`:uniques` are other unique attributes of the user that you can pass to FluidFeatures. For instance, their Twitter handle. These can also be used for search in the dashboard. FluidFeatures may use these for further integrate, such as displaying Twitter profile pictures.
|
102
|
+
|
103
|
+
`:cohorts` are non-unique attributes of the user. You can define any attribute you wish. Common ones are `:admin` (`true` or `false`), but could be anything from the month when they joined (`:joined => "2011-04"`), to whether or not they drink coffee (`"coffee-drinker" => false`). Again, you can search on these attributes and enable specific versions of features based on these attributes.
|
104
|
+
|
105
|
+
Anonymous users
|
106
|
+
---------------
|
107
|
+
|
108
|
+
If your `ApplicationController` method `fluidfeatures_current_user` returns `nil` fluidfeatures-rails assumes that your user is anonymous and it will set a cookie in the user's browser. The cookie enables FluidFeatures to provide a consistent experience for that anonymous user. This is mostly important when you have feature versions enabled on a percentage of random users, when two anonymous user may have different features shown to them, depending on which percentage they random fall into.
|
109
|
+
|
110
|
+
It is possible handle anonymous users yourself by managing the cookies yourself, generating a unique `:id` for each anonymous user and additionally passing `:anonymous => true` in the `Hash` of `fluidfeatures_current_user` for both verbose and non-verbose calls. This basically what `gem fluidfeatures-rails` does under the hood for your convenience, so you do not have to.
|
111
|
+
|
112
|
+
Integration
|
113
|
+
-----------
|
114
|
+
|
115
|
+
Add the gem to your application
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
gem "fluidfeatures-rails"
|
119
|
+
```
|
120
|
+
|
121
|
+
Call the initializer when your application starts
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
module MyRailsApp
|
125
|
+
class Application < Rails::Application
|
126
|
+
FluidFeatures::Rails.initializer
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
Create a `fluidfeature_current_user` method in your `ApplicationController`. See [User definition and cohorts](#user-definition-and-cohorts).
|
132
|
+
|
133
|
+
Start adding your features and goals using `if ff?` (or `if fluidfeature`) and `fluidgoal`.
|
134
|
+
|
135
|
+
In your controllers or your views...
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
# "theme" is simply an example feature name, used to represent
|
139
|
+
# a migration to a styling of your website
|
140
|
+
if ff? "theme", "default"
|
141
|
+
# wrap code related to your default theme, so it is
|
142
|
+
# only executed when the user is allocated this version
|
143
|
+
# of the feature "theme".
|
144
|
+
end
|
145
|
+
# Alternate verison of the "theme" feature.
|
146
|
+
# FluidFeatures will only assign a user to one version
|
147
|
+
# of a feature.
|
148
|
+
if ff? "theme", "tropical"
|
149
|
+
# implement code specifically related to your new theme
|
150
|
+
end
|
151
|
+
|
152
|
+
fluidgoal "bought-bieber-dvd"
|
153
|
+
|
154
|
+
fluidgoal "added-a-comment"
|
155
|
+
|
156
|
+
fluidgoal "upgraded-to-pro-account"
|
157
|
+
|
158
|
+
fluidgoal "general-engagement"
|
159
|
+
```
|
160
|
+
|
161
|
+
Dashboard
|
162
|
+
---------
|
163
|
+
|
164
|
+
If you log into your FluidFeatures account and visit [fluidfeatures.com/dashboard](https://www.fluidfeatures.com/dashboard) you will see your feature and goal dashboard.
|
165
|
+
|
166
|
+
![Example dashboard view](http://commondatastorage.googleapis.com/philwhln/blog/images/ab-test-rails/full-dashboard.png)
|
167
|
+
|
168
|
+
This shows one features `"ab-test"` with two versions simply named `"a"` and `"b"`.
|
169
|
+
|
170
|
+
There are two goals called `"yes"` and `"no"`.
|
171
|
+
|
172
|
+
Version `"a"` is seen by 75% of the user-base, whether they are anonymous or not, and version `"b"` is seen by the remaining 25%.
|
173
|
+
|
174
|
+
You can read more about this example in a blog post here...
|
175
|
+
http://www.bigfastblog.com/ab-testing-in-ruby-on-rails
|
4
176
|
|
5
|
-
Rails client for API of FluidFeatures.com
|
6
177
|
|
data/fluidfeatures-rails.gemspec
CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
|
|
15
15
|
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
16
16
|
s.require_paths = ["lib"]
|
17
17
|
|
18
|
-
s.add_dependency "fluidfeatures", "~>0.
|
18
|
+
s.add_dependency "fluidfeatures", "~> 0.5.0" unless ENV["FF_DEV"]
|
19
19
|
|
20
20
|
s.add_development_dependency('rake', '~> 10.0.2')
|
21
21
|
s.add_development_dependency('rspec', '~> 2.12.0')
|
data/lib/fluidfeatures/rails.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
require "fluidfeatures"
|
3
2
|
|
4
3
|
module FluidFeatures
|
@@ -20,12 +19,12 @@ module FluidFeatures
|
|
20
19
|
# Without these FluidFeatures credentials we cannot talk to
|
21
20
|
# the FluidFeatures service.
|
22
21
|
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
22
|
+
#%w[FLUIDFEATURES_BASEURI FLUIDFEATURES_SECRET FLUIDFEATURES_APPID].each do |key|
|
23
|
+
# unless ENV[key]
|
24
|
+
# $stderr.puts "!! fluidfeatures-rails requires ENV[\"#{key}\"] (fluidfeatures is disabled)"
|
25
|
+
# return
|
26
|
+
# end
|
27
|
+
#end
|
29
28
|
unless defined? ::Rails
|
30
29
|
$stderr.puts "!! fluidfeatures-rails requires rails (fluidfeatures is disabled)"
|
31
30
|
return
|
@@ -34,17 +33,12 @@ module FluidFeatures
|
|
34
33
|
|
35
34
|
::Rails::Application.initializer "fluidfeatures.initializer" do
|
36
35
|
ActiveSupport.on_load(:action_controller) do
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
::FluidFeatures::Rails.
|
42
|
-
|
43
|
-
api_appid,
|
44
|
-
api_secret,
|
45
|
-
#::Rails.logger
|
46
|
-
)
|
47
|
-
|
36
|
+
replacements = {}
|
37
|
+
replacements["baseuri"] = ENV["FLUIDFEATURES_BASEURI"] if ENV["FLUIDFEATURES_BASEURI"]
|
38
|
+
replacements["appid"] = ENV["FLUIDFEATURES_APPID"] if ENV["FLUIDFEATURES_APPID"]
|
39
|
+
replacements["secret"] = ENV["FLUIDFEATURES_SECRET"] if ENV["FLUIDFEATURES_SECRET"]
|
40
|
+
config = ::FluidFeatures::Config.new("#{::Rails.root}/config/stormpath.yml", ::Rails.env, replacements)
|
41
|
+
::FluidFeatures::Rails.ff_app = ::FluidFeatures.app(config)
|
48
42
|
ActionController::Base.append_before_filter :fluidfeatures_request_before
|
49
43
|
ActionController::Base.append_after_filter :fluidfeatures_request_after
|
50
44
|
end
|
@@ -62,7 +56,9 @@ module ActionController
|
|
62
56
|
attr_accessor :ff_transaction
|
63
57
|
|
64
58
|
# allow fluidfeature to be called from templates
|
59
|
+
helper_method :ff?
|
65
60
|
helper_method :fluidfeature
|
61
|
+
helper_method :fluidgoal
|
66
62
|
|
67
63
|
#
|
68
64
|
# Here is how we know what your user_id is for the user
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module FluidFeatures
|
2
|
+
module Rails
|
3
|
+
module Generators
|
4
|
+
class InstallGenerator < ::Rails::Generators::Base
|
5
|
+
def create_initializer_file
|
6
|
+
create_file "config/fluidfeatures.yml", "common:
|
7
|
+
baseuri: https://www.fluidfeatures.com/service
|
8
|
+
cache:
|
9
|
+
enable: false
|
10
|
+
dir: tmp/fluidfeatures
|
11
|
+
limit: 2mb
|
12
|
+
|
13
|
+
development:
|
14
|
+
appid:
|
15
|
+
secret:
|
16
|
+
|
17
|
+
test:
|
18
|
+
appid:
|
19
|
+
secret:
|
20
|
+
|
21
|
+
production:
|
22
|
+
appid:
|
23
|
+
secret:
|
24
|
+
"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "generators/fluidfeatures/rails/install/install_generator"
|
3
|
+
|
4
|
+
describe FluidFeatures::Rails::Generators::InstallGenerator do
|
5
|
+
it "creates a configuration file" do
|
6
|
+
subject.should_receive(:create_file).with("config/fluidfeatures.yml", kind_of(String))
|
7
|
+
subject.create_initializer_file
|
8
|
+
end
|
9
|
+
end
|
data/spec/initializer_spec.rb
CHANGED
@@ -8,15 +8,6 @@ describe "FluidFeatures Rails initialization" do
|
|
8
8
|
CONFIG_KEYS.each {|k| ENV[k] = k}
|
9
9
|
end
|
10
10
|
|
11
|
-
CONFIG_KEYS.each do |key|
|
12
|
-
it "should require #{key} environment variable" do
|
13
|
-
ENV[key] = nil
|
14
|
-
$stderr.should_receive(:puts).with("!! fluidfeatures-rails requires ENV[\"#{key}\"] (fluidfeatures is disabled)")
|
15
|
-
FluidFeatures::Rails.initializer
|
16
|
-
FluidFeatures::Rails.enabled.should be_false
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
11
|
it "should start application if Rails defined" do
|
21
12
|
defined?(Rails).should be_true
|
22
13
|
$stderr.should_receive(:puts).with("=> fluidfeatures-rails initializing as app #{ENV["FLUIDFEATURES_APPID"]} with #{ENV["FLUIDFEATURES_BASEURI"]}")
|
@@ -32,12 +23,20 @@ describe "FluidFeatures Rails initialization" do
|
|
32
23
|
FluidFeatures.stub!(:app)
|
33
24
|
ActionController::Base.stub!(:append_before_filter)
|
34
25
|
ActionController::Base.stub!(:append_after_filter)
|
26
|
+
FluidFeatures::Config.stub(:new).and_return({})
|
27
|
+
Rails.stub!(:root).and_return(".")
|
28
|
+
Rails.stub!(:env).and_return(:development)
|
35
29
|
FluidFeatures::Rails.initializer
|
36
30
|
end
|
37
31
|
|
32
|
+
it "should create config" do
|
33
|
+
FluidFeatures::Config.should_receive(:new).and_return({})
|
34
|
+
instance_eval &on_load
|
35
|
+
end
|
36
|
+
|
38
37
|
it "should create app with passed credentials" do
|
39
38
|
@app = mock("app")
|
40
|
-
FluidFeatures.should_receive(:app).with(
|
39
|
+
FluidFeatures.should_receive(:app).with({}).and_return(@app)
|
41
40
|
instance_eval &on_load
|
42
41
|
FluidFeatures::Rails.ff_app.should == @app
|
43
42
|
end
|
data/spec/support/rails.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fluidfeatures-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-01-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: fluidfeatures
|
@@ -18,7 +18,7 @@ dependencies:
|
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.
|
21
|
+
version: 0.5.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ~>
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 0.
|
29
|
+
version: 0.5.0
|
30
30
|
- !ruby/object:Gem::Dependency
|
31
31
|
name: rake
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -111,9 +111,11 @@ files:
|
|
111
111
|
- lib/fluidfeatures/rails/app/controllers/fluidfeatures_controller.rb
|
112
112
|
- lib/fluidfeatures/rails/config/routes.rb
|
113
113
|
- lib/fluidfeatures/rails/version.rb
|
114
|
+
- lib/generators/fluidfeatures/rails/install/install_generator.rb
|
114
115
|
- lib/pre_ruby192/uri.rb
|
115
116
|
- spec/controller_extentions_spec.rb
|
116
117
|
- spec/controller_spec.rb
|
118
|
+
- spec/generators/fluidfeatures/rails/install/install_generator_spec.rb
|
117
119
|
- spec/initializer_spec.rb
|
118
120
|
- spec/routes_spec.rb
|
119
121
|
- spec/spec_helper.rb
|