smooch 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in smooch.gemspec
4
+ gemspec
5
+ gem 'rails', '2.3.5'
6
+
7
+ group :test do
8
+ gem 'rspec', '1.3.0'
9
+ gem 'rspec-rails', '1.3.2'
10
+ end
11
+
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Brian Leonard
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.mdown ADDED
@@ -0,0 +1,79 @@
1
+ # Smooch
2
+ KISSmetrics is a great tool and the Javascript API has some niceties that the Ruby API doesn't have, but sometimes it's nice to make decisions in the Rails controller or views. Smooch makes this possible while keeping most of the Javascript features.
3
+
4
+ Smooch allows you to:
5
+
6
+ * Define a user's unique identifier and uses the API to associate it later if not logged in yet
7
+
8
+ * Define A/B tests and makes the decision in your Ruby code
9
+
10
+ * Record events in your Ruby code
11
+
12
+ * Use the same code in your views for development a production but not KISSmetrics about development actions or for other reasons
13
+
14
+ * Still use the Javascript API as before if you like
15
+
16
+ ## Example Usage
17
+
18
+ ### application_controller.rb
19
+ class ApplicationController < ActionController::Base
20
+ kiss :current_user
21
+ end
22
+
23
+ ### users_controller.rb
24
+ def new
25
+ km.record("Signup page viewed")
26
+ if km.ab("Show faces in sidebar", ["yes", "no"]) == "yes"
27
+ @profiles = User.good_looking.limited(5)
28
+ end
29
+ @user = User.new
30
+ end
31
+
32
+ def create
33
+ @user = User.new(params[:user])
34
+ if @user.save
35
+ km.set("User zipcode", @user.zipcode)
36
+ km.record("Signup converted")
37
+ redirect_to @user
38
+ else
39
+ render :new
40
+ end
41
+ end
42
+
43
+ ### new.erb
44
+ <%= render 'faces', :users => @profiles if @profiles %>
45
+ ...
46
+ <%= f.text_field :zipcode
47
+ <%= submit_tag km.ab("Signup Button Text", ["Go!", "Submit"]) %>
48
+
49
+ ### application.html.erb
50
+ <html>
51
+ <head>
52
+ ...
53
+ <%= km.script %>
54
+ </head>
55
+
56
+ ### config/kissmetrics.yml
57
+ production:
58
+ apikey: kiss_metrics_api_key_here
59
+
60
+ ## Reporting
61
+
62
+ `km.script` will report the data to KISSmetrics if there is a line in kissmetrics.yml for your environment. In the above example it would have reported the choices about showing faces in the sidebar and the button text as well as the conversion points noted. This would have all been correlated to the user's new id and have set the zipcode if they were successful in the signup.
63
+
64
+ You can also pass a boolean variable into the call as follows: `km.script(tracking_metrics?)`
65
+
66
+ We use this to opt-out site admins from our metrics for more relevant results.
67
+
68
+ ## Techniques
69
+
70
+ Smooch replicates some of the A/B behaviors of the KISSmetrics library by using (it's own) cookies to remember decisions and be consistent.
71
+
72
+ For any given set of decisions made or events recorded/set, they are simply given to the Javascript API the next time the application layout is rendered. This will use either flash or in-memory depending on when the calls were made.
73
+
74
+ Several tactics used from other reporting libraries such as [vanity](https://github.com/assaf/vanity).
75
+
76
+
77
+
78
+
79
+ Copyright (c) 2011 Brian Leonard, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'spec/rake/spectask'
5
+
6
+ task :default => :spec
7
+
8
+ desc "Run all specs in spec directory (excluding plugin specs)"
9
+ Spec::Rake::SpecTask.new do |t|
10
+ t.spec_files = FileList['spec/**/*_spec.rb']
11
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/lib/smooch'
data/lib/smooch.rb ADDED
@@ -0,0 +1,20 @@
1
+ module Smooch
2
+ API_KEY = ''
3
+ end
4
+
5
+ begin
6
+ config = YAML.load_file("#{RAILS_ROOT}/config/kissmetrics.yml")
7
+ Smooch::API_KEY = config[RAILS_ENV]['apikey'] if config[RAILS_ENV]
8
+ rescue
9
+ puts "Error opening KISSmetrics configuration file."
10
+ end
11
+
12
+ require 'smooch/base'
13
+ require 'smooch/helpers'
14
+ require 'smooch/controller'
15
+
16
+
17
+
18
+ ActionController::Base.class_eval do
19
+ extend Smooch::Controller
20
+ end
@@ -0,0 +1,191 @@
1
+ module Smooch
2
+ class Base
3
+ attr_accessor :controller
4
+ attr_accessor :view
5
+ def initialize(controller=nil)
6
+ self.controller = controller
7
+ self.view = view
8
+ @choices = {}
9
+ @records = {}
10
+ @sets = {}
11
+
12
+ init_flash
13
+ end
14
+
15
+ def record(property, hash={})
16
+ flash[:kiss_metrics] = property.to_s
17
+ @records[property.to_s] = hash
18
+ end
19
+ def init_flash
20
+ property = flash[:kiss_metrics]
21
+ return unless property
22
+ record(property)
23
+ end
24
+ def clear_flash
25
+ flash[:kiss_metrics] = nil
26
+ end
27
+
28
+ def set(property, value)
29
+ @sets[property.to_s] = value
30
+ end
31
+
32
+ def ab(name, choices=nil)
33
+ val = nil
34
+
35
+ # get from parameter passed in
36
+ val = get_ab_param(name) unless val
37
+ val = nil if val and choices and not choices.include?(val)
38
+
39
+ # get from database
40
+ val = get_ab_database(name) unless val
41
+ val = nil if val and choices and not choices.include?(val)
42
+
43
+ # get from local storage
44
+ val = get_ab_cached(name) unless val
45
+ val = nil if val and choices and not choices.include?(val)
46
+
47
+ # get from cookie
48
+ val = get_ab_cookie(name) unless val
49
+ val = nil if val and choices and not choices.include?(val)
50
+
51
+ # pick a random one
52
+ val = get_ab_random_choice(choices) unless val
53
+
54
+ set_ab_value(name, val)
55
+ end
56
+
57
+ def key(name)
58
+ # TODO: by identity?
59
+ md5 = Digest::MD5.hexdigest("#{name}")[-10,10]
60
+ "ab_#{md5}"
61
+ end
62
+ def get_ab_param(name)
63
+ return params["_ab"]
64
+ end
65
+ def get_ab_random_choice(choices)
66
+ return nil unless choices
67
+ choices[rand(choices.size)]
68
+ end
69
+ def get_ab_cached(name)
70
+ @choices[name]
71
+ end
72
+ def get_ab_cookie(name)
73
+ get_cookie(key(name))
74
+ end
75
+ def set_ab_value(name, val)
76
+ set_ab_database(name, val)
77
+ set_cookie(key(name), val)
78
+ @choices[name] = val
79
+ val
80
+ end
81
+
82
+ # TODO db support
83
+ def get_ab_database(name)
84
+ # get_identity / key(name)
85
+ nil
86
+ end
87
+ def set_ab_database(name, val)
88
+ # get_identity / key(name)
89
+ end
90
+
91
+ # ------ from controller
92
+ def get_smooch_identity
93
+ controller.smooch_identity
94
+ end
95
+ def get_kiss_identity
96
+ controller.kiss_identity
97
+ end
98
+ def set_cookie(key, value)
99
+ cookies[key] = { :value => value, :expires => 2.months.from_now }
100
+ end
101
+ def get_cookie(key)
102
+ cookies[key]
103
+ end
104
+
105
+ # ------ for view
106
+ def js(text)
107
+ text = view.send('h', text)
108
+ text = view.send('escape_javascript', text)
109
+ text
110
+ end
111
+ def push_record(hash)
112
+ out = ""
113
+ hash.each do |key, value|
114
+ out += "_kmq.push(['record', '#{js(key)}'"
115
+ unless value.empty?
116
+ out += ", #{value.as_json}"
117
+ end
118
+ out += "]);\n"
119
+ end
120
+ out.html_safe!
121
+ end
122
+ def push_set(hash)
123
+ out = ""
124
+ hash.each do |key, value|
125
+ out += "_kmq.push(['set', {'#{js(key)}' : '#{js(value)}'}]);\n"
126
+ end
127
+ out.html_safe!
128
+ end
129
+
130
+ def api_key
131
+ Smooch::API_KEY.blank? ? nil : Smooch::API_KEY
132
+ end
133
+
134
+ def script(send=true)
135
+ clear_flash
136
+
137
+ out = <<-JAVASCRIPT
138
+ <script type="text/javascript">
139
+ var _kmq = _kmq || [];
140
+ function _kms(u) {
141
+ setTimeout(function() {
142
+ var s = document.createElement('script');
143
+ var f = document.getElementsByTagName('script')[0];
144
+ s.type = 'text/javascript';
145
+ s.async = true;
146
+ s.src = u;
147
+ f.parentNode.insertBefore(s, f);
148
+ }, 1);
149
+ }
150
+ JAVASCRIPT
151
+ if send and api_key.present?
152
+ out += "_kms('//i.kissmetrics.com/i.js');\n"
153
+ out += "_kms('//doug1izaerwt3.cloudfront.net/#{api_key}.1.js');\n"
154
+ end
155
+
156
+ identity = get_kiss_identity
157
+ out += "_kmq.push(['identify', '#{js(identity)}']);\n" if identity
158
+
159
+ out += push_record(@records)
160
+ out += push_set(@sets)
161
+ out += push_set(@choices)
162
+
163
+ out += "</script>\n"
164
+ out.html_safe!
165
+ end
166
+
167
+
168
+ # ------ for tests
169
+ def has_record?(property)
170
+ !@records[property.to_s].nil?
171
+ end
172
+ def has_set?(property)
173
+ !@sets[property.to_s].nil?
174
+ end
175
+
176
+ def cookies
177
+ return {} unless controller
178
+ controller.send(:cookies)
179
+ end
180
+
181
+ def flash
182
+ return {} unless controller
183
+ controller.send(:flash)
184
+ end
185
+
186
+ def params
187
+ return {} unless controller
188
+ controller.send(:params)
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,46 @@
1
+ require "digest/md5"
2
+
3
+ module Smooch
4
+ COOKIE_ID = "ab_id"
5
+
6
+ module Controller
7
+ def kiss(symbol = nil, &block)
8
+ if block
9
+ define_method(:smooch_calculate_identity) { block.call(self) }
10
+ else
11
+ define_method :smooch_calculate_identity do
12
+ return @smooch_identity if @smooch_identity
13
+ if symbol && object = send(symbol)
14
+ @smooch_identity = object.id
15
+ elsif response # everyday use
16
+ @smooch_generated = true
17
+ @smooch_identity = cookies[COOKIE_ID] || ActiveSupport::SecureRandom.hex(16)
18
+ cookies[COOKIE_ID] = { :value=>@smooch_identity, :expires=>1.month.from_now }
19
+ @smooch_identity
20
+ else
21
+ @smooch_identity = "test"
22
+ end
23
+ end
24
+ end
25
+
26
+ define_method(:smooch_identity) do
27
+ smooch_calculate_identity
28
+ end
29
+ define_method(:kiss_identity) do
30
+ val = smooch_calculate_identity
31
+ return "null" if @smooch_generated
32
+ val
33
+ end
34
+
35
+ define_method(:smooch_object) do
36
+ @smooch_object ||= Smooch::Base.new(self)
37
+ end
38
+ define_method(:km) do
39
+ smooch_object
40
+ end
41
+ helper_method :smooch_object
42
+ helper Smooch::Helpers
43
+ end
44
+ protected :kiss
45
+ end
46
+ end
@@ -0,0 +1,9 @@
1
+ module Smooch
2
+ module Helpers
3
+ def km
4
+ km = smooch_object
5
+ km.view = self
6
+ km
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Smooch
2
+ VERSION = "0.1.0"
3
+ end
data/smooch.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "smooch/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "smooch"
7
+ s.version = Smooch::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Brian Leonard"]
10
+ s.email = ["brian@bleonard.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Smooch interacts with KISS Metrics}
13
+ s.description = %q{Smooch allows you to make A/B decisions and report them to KISS Metrics.
14
+ It combines the power of makings these decisions in Ruby code with the enhcanced reporting of the KISS Metrics Javascript.}
15
+
16
+ s.rubyforge_project = "smooch"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+ end
@@ -0,0 +1,3 @@
1
+ # Create an application controller to satisfy rspec-rails, a dummy controller
2
+ class ApplicationController < ActionController::Base
3
+ end
data/spec/base_spec.rb ADDED
@@ -0,0 +1,51 @@
1
+ require "spec_helper"
2
+
3
+ class TestSmooch < Smooch::Base
4
+ @@cookies = {}
5
+ # override stuff provided by controller
6
+ def get_cookie(key)
7
+ @@cookies[key]
8
+ end
9
+ def set_cookie(key, name)
10
+ @@cookies[key] = name
11
+ end
12
+ end
13
+
14
+ describe TestSmooch do
15
+
16
+ describe "#ab" do
17
+ before(:each) do
18
+ @km = TestSmooch.new
19
+ @val = @km.ab("Signup Button Color", ["red", "green"])
20
+ end
21
+ it "should return one of the variants" do
22
+ (@val=="red" || @val=="green").should == true
23
+ end
24
+ it "should always be the same" do
25
+ 30.times do
26
+ @km.ab("Signup Button Color", ["red", "green"]).should == @val
27
+ end
28
+ end
29
+ it "should persist across sessions" do
30
+ 30.times do
31
+ TestSmooch.new.ab("Signup Button Color", ["red", "green"]).should == @val
32
+ end
33
+ end
34
+ it "should adjust when options change" do
35
+ @val = @km.ab("Signup Button Color", ["blue", "brown", "orange"])
36
+ (@val=="blue" || @val=="brown" || @val=="orange").should == true
37
+ @km.ab("Signup Button Color", ["blue", "brown", "orange"]).should == @val
38
+ end
39
+ it "should return previous value without choices" do
40
+ @km.ab("Signup Button Color").should == @val
41
+ TestSmooch.new.ab("Signup Button Color").should == @val
42
+ end
43
+ end
44
+
45
+ describe "api key" do
46
+ it "should return the api key from the config file" do
47
+ km = TestSmooch.new
48
+ km.api_key.should == "test_key_here"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ production:
2
+ apikey: production_key_here
3
+
4
+ test:
5
+ apikey: test_key_here
@@ -0,0 +1,125 @@
1
+ require "spec_helper"
2
+
3
+ class SmoochUser
4
+ USER_ID = 42
5
+ def id
6
+ USER_ID
7
+ end
8
+ end
9
+ class SmoochController < ActionController::Base
10
+ def get_km
11
+ @results = km
12
+ end
13
+ def get_identity
14
+ @results = smooch_identity
15
+ end
16
+ end
17
+
18
+ ActionController::Routing::Routes.draw do |map|
19
+ map.connect "smooch/:action", :controller => "smooch"
20
+ end
21
+
22
+ describe SmoochController, :type => :controller do
23
+
24
+ context "when called kiss with no inputs" do
25
+ before(:each) do
26
+ SmoochController.class_eval do
27
+ kiss
28
+ end
29
+ end
30
+
31
+ describe "#smooch_identity" do
32
+ it "should set a random cookie" do
33
+ cookies[Smooch::COOKIE_ID].should be_nil
34
+ get :get_identity
35
+ cookies[Smooch::COOKIE_ID].should_not be_blank
36
+ end
37
+ it "should not change the cookie" do
38
+ cookies[Smooch::COOKIE_ID] = "nice!"
39
+ get :get_identity
40
+ cookies[Smooch::COOKIE_ID].should == "nice!"
41
+ end
42
+ end
43
+
44
+ describe "#km" do
45
+ it "should have a reference to the controller" do
46
+ get :get_km
47
+ assigns[:results].controller.should == @controller
48
+ end
49
+
50
+ it "should use controller to get cookie value" do
51
+ cookies["whatever"] = "test!"
52
+ get :get_km
53
+ assigns[:results].get_cookie("whatever").should == "test!"
54
+ end
55
+ end
56
+
57
+ describe "records" do
58
+ it "should transfer flash property" do
59
+ flash[:kiss_metrics] = "whatever"
60
+ get :get_km
61
+ assigns[:results].has_record?("whatever").should == true
62
+ end
63
+ end
64
+ end
65
+
66
+ context "when kiss called with black" do
67
+ before(:each) do
68
+ SmoochController.class_eval do
69
+ def random_stuff
70
+ "my_custom_value"
71
+ end
72
+ kiss { |controller| controller.random_stuff }
73
+ end
74
+ end
75
+ describe "#smooch_identity" do
76
+ it "should return custom value" do
77
+ cookies[Smooch::COOKIE_ID].should be_nil
78
+ get :get_identity
79
+ assigns[:results].should == "my_custom_value"
80
+ cookies[Smooch::COOKIE_ID].should be_nil
81
+ end
82
+ end
83
+ end
84
+
85
+ context "when kiss called with identity method" do
86
+ before(:each) do
87
+ SmoochController.class_eval do
88
+ kiss :current_user
89
+ end
90
+ end
91
+
92
+ describe "#smooch_identity" do
93
+ context "when there is a current user" do
94
+ before(:each) do
95
+ SmoochController.class_eval do
96
+ def current_user
97
+ SmoochUser.new
98
+ end
99
+ end
100
+ end
101
+ it "should return the current user id" do
102
+ cookies[Smooch::COOKIE_ID].should be_nil
103
+ get :get_identity
104
+ assigns[:results].should == SmoochUser::USER_ID
105
+ cookies[Smooch::COOKIE_ID].should be_nil
106
+ end
107
+ end
108
+
109
+ context "when there is no current user" do
110
+ before(:each) do
111
+ SmoochController.class_eval do
112
+ def current_user
113
+ nil
114
+ end
115
+ end
116
+ end
117
+ it "should set a random cookie" do
118
+ cookies[Smooch::COOKIE_ID].should be_nil
119
+ get :get_identity
120
+ cookies[Smooch::COOKIE_ID].should_not be_blank
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.setup
5
+
6
+ RAILS_ENV = 'test'
7
+
8
+ # Load Rails
9
+ require 'active_support'
10
+ require 'action_controller'
11
+ require 'action_mailer'
12
+ require 'rails/version'
13
+
14
+ RAILS_ROOT = File.join(File.dirname(__FILE__))
15
+ $:.unshift(RAILS_ROOT)
16
+
17
+ ActionController::Base.view_paths = RAILS_ROOT
18
+ require File.join(RAILS_ROOT, 'application')
19
+
20
+ ActiveSupport::Dependencies.load_paths << File.join(File.dirname(__FILE__), '..', 'lib')
21
+
22
+ require 'spec/autorun'
23
+ require 'spec/rails'
24
+
25
+ require 'smooch'
26
+
27
+ Spec::Runner.configure do |config|
28
+
29
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: smooch
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Brian Leonard
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-05 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: |-
23
+ Smooch allows you to make A/B decisions and report them to KISS Metrics.
24
+ It combines the power of makings these decisions in Ruby code with the enhcanced reporting of the KISS Metrics Javascript.
25
+ email:
26
+ - brian@bleonard.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - .gitignore
35
+ - Gemfile
36
+ - MIT-LICENSE
37
+ - README.mdown
38
+ - Rakefile
39
+ - init.rb
40
+ - lib/smooch.rb
41
+ - lib/smooch/base.rb
42
+ - lib/smooch/controller.rb
43
+ - lib/smooch/helpers.rb
44
+ - lib/smooch/version.rb
45
+ - smooch.gemspec
46
+ - spec/application.rb
47
+ - spec/base_spec.rb
48
+ - spec/config/kissmetrics.yml
49
+ - spec/controller_spec.rb
50
+ - spec/spec_helper.rb
51
+ has_rdoc: true
52
+ homepage: ""
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 3
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project: smooch
81
+ rubygems_version: 1.5.0
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Smooch interacts with KISS Metrics
85
+ test_files:
86
+ - spec/application.rb
87
+ - spec/base_spec.rb
88
+ - spec/config/kissmetrics.yml
89
+ - spec/controller_spec.rb
90
+ - spec/spec_helper.rb