experimental 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5971835dc6a436120c342849f251fd6833819f07
4
- data.tar.gz: 1a0c5fe0f8465ebf62ad31bb1000e32952420488
3
+ metadata.gz: 4c3ca7d05e6f83ec23e057defc0da8e4d8b94b6f
4
+ data.tar.gz: 19dd91453b0154b0d8cbc4b7c03e02860ba796e4
5
5
  SHA512:
6
- metadata.gz: 5a6fd63702f780be58b0c3582d7b66985fcce57643fec71f95df4800396d6c41f485d73a76aa5dc7cd7dad2f0bff60006cf9857a247999ead024326c78215a24
7
- data.tar.gz: e7b1b7467c78ccc8fb77ac50c3d2f8551e1fce7887ffa2c56993d378db3784f8031f8b323176ed5c8aa3d7f7ca4019fbdb3fba75b1cee66337a84b59d3bca291
6
+ metadata.gz: 8f460e8ef869e751e103a936cf99c921293e700dd15387391ce4b84ed9207ecf49c472df59a02e29d38b471afa77ce708ff07d20722165585665e6a4d3dd6b79
7
+ data.tar.gz: 24eeb5bec5c23fcbb3354804368d21fbad288b278942c82b191ead834b2e61b59bf5116443d8e03d5416a86021c2a38dcad013a6a7b2ab45b8fcc81c71b8833a
data/README.markdown CHANGED
@@ -1,4 +1,5 @@
1
- # Experimental
1
+ # Experimental [![Build Status](https://travis-ci.org/howaboutwe/experimental.png?branch=master)](https://travis-ci.org/howaboutwe/experimental)
2
+
2
3
  Experimental is an Split testing framework for Rails.
3
4
  It was written with a few goals in mind:
4
5
  * Split the users in a non-predictable pattern (i.e. half of the users won't always
@@ -14,136 +15,6 @@ be removed make the site explode
14
15
 
15
16
  `rails g experimental`
16
17
 
17
- ### Routes
18
-
19
- ```ruby
20
- resources :experiments, only: [:index, :new, :create] do
21
- collection do
22
- get :inactive
23
- post :set_winner
24
- end
25
- end
26
-
27
- namespace :singles_admin do
28
- resources :experiments, only: [:index, :new, :create] do
29
- collection do
30
- get :inactive
31
- post :set_winner
32
- end
33
- end
34
- end
35
- end
36
- ```
37
-
38
- ### Admin Frontend
39
-
40
- #### Create your own admin controller:
41
- ```ruby
42
- class Admin::ExperimentsController < ApplicationController
43
- include Experimental::ControllerActions
44
-
45
- alias_method :index, :experiments_index
46
- alias_method :new, :experiments_new
47
- alias_method :set_winner, :experiments_set_winner
48
-
49
- def create
50
- if experiments_create
51
- redirect_to admin_experiments_path
52
- else
53
- render :new
54
- end
55
- end
56
-
57
- def base_resource_name
58
- "singles_admin_experiment"
59
- end
60
- end
61
- ```
62
-
63
- #### Using ActiveAdmin:
64
-
65
- `rails g active_admin:resource Experiment`
66
-
67
- ```ruby
68
- require 'experimental/controller_actions'
69
-
70
- ActiveAdmin.register Experimental::Experiment, as: "Experiment" do
71
- actions :index, :new, :create
72
- filter :name
73
-
74
- controller do
75
- class_eval do
76
- include Experimental::ControllerActions
77
- end
78
-
79
- def base_resource_name
80
- "admin_experiment"
81
- end
82
-
83
- def create
84
- if experiments_create
85
- redirect_to admin_experiments_path
86
- else
87
- render :new
88
- end
89
- end
90
-
91
- def new
92
- experiments_new
93
- end
94
- end
95
-
96
- # collection_actions force active_admin to create a route
97
- collection_action :set_winner, method: :post do
98
- experiments_set_winner
99
- end
100
-
101
- # can do this instead of the ended_or_removed scope below
102
- # you will need to add a link to inactive_admins_experiments_path
103
- # in your view
104
- collection_action :inactive do
105
- experiments_inactive
106
- render template: 'admin/experiments/index'
107
- end
108
-
109
- scope :in_progress, :default => true do |experiments|
110
- experiments.in_progress
111
- end
112
-
113
- scope :ended_or_removed do |experiments|
114
- @include_inactive = true
115
- experiments.ended_or_removed
116
- end
117
-
118
- index do
119
- render template: 'admin/experiments/index'
120
- end
121
-
122
- form partial: 'new'
123
- end
124
- ```
125
-
126
- #### Views
127
-
128
- create an index and new view in appropriate view folder, i.e.
129
-
130
- `app/views/admin/experiments/index.html.erb`
131
-
132
- ```erb
133
- <%= render partial: 'experimental/links' %>
134
- <%= render partial: 'experimental/index' %>
135
- ```
136
-
137
- `app/views/admin/experiments/new.html.erb`
138
-
139
- ```erb
140
- <%= render partial: 'experimental/links' %>
141
- <%= render partial: 'experimental/new' %>
142
- ```
143
-
144
- *Note: ActiveAdmin users will not need to include the links
145
- partials*
146
-
147
18
  ### Subject
148
19
 
149
20
  For the class you'd like to be the subject of experiments, include the
@@ -276,29 +147,15 @@ You will likely want to automate the running of `rake
276
147
  experimental:sync` by adding to your deploy file.
277
148
 
278
149
  ### Capistrano
279
- In `config/deploy.rb`:
280
150
 
281
- Create a namespace to run the task:
282
- ```ruby
283
- namespace :database do
284
- desc "Sync experiments"
285
- task :sync_from_app, roles: :db, only: { primary: true } do
286
- run "cd #{current_path} && RAILS_ENV=#{rails_env} bundle exec rake experimental:sync"
287
- end
288
- end
289
- ```
151
+ When you deploy, simply invoke the experimental:sync Rake task to update the
152
+ experiments in your database from the configuration file:
290
153
 
291
- Include that in the deploy:default task:
292
154
  ```ruby
293
- namespace :deploy do
294
- #...
295
- task :default do
296
- begin
297
- update_code
298
- migrate
299
- database.sync_from_app
300
- restart
301
- #...
155
+ after 'deploy:updated', 'experimental:sync' do
156
+ on primary :worker do
157
+ within release_path do
158
+ rake experimental:sync
302
159
  end
303
160
  end
304
161
  end
data/Rakefile CHANGED
@@ -1,19 +1 @@
1
- begin
2
- require 'bundler/setup'
3
- rescue LoadError
4
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
- end
6
-
7
- Bundler::GemHelper.install_tasks
8
-
9
- require 'rake/testtask'
10
-
11
- Rake::TestTask.new(:test) do |t|
12
- t.libs << 'lib'
13
- t.libs << 'spec'
14
- t.pattern = 'spec/**/*_spec.rb'
15
- t.verbose = false
16
- end
17
-
18
-
19
- task :default => :test
1
+ require 'ritual'
data/lib/experimental.rb CHANGED
@@ -2,12 +2,14 @@ require 'rails'
2
2
  require 'experimental/engine'
3
3
 
4
4
  module Experimental
5
- autoload :VERSION, 'experimental/version'
6
- autoload :ControllerActions, 'experimental/controller_actions'
5
+ autoload :Experiment, 'experimental/experiment'
7
6
  autoload :Loader, 'experimental/loader'
8
7
  autoload :Overrides, 'experimental/overrides'
8
+ autoload :Population, 'experimental/population'
9
9
  autoload :Source, 'experimental/source'
10
+ autoload :Subject, 'experimental/subject'
10
11
  autoload :Test, 'experimental/test'
12
+ autoload :VERSION, 'experimental/version'
11
13
 
12
14
  class << self
13
15
  def configure(configuration)
@@ -2,8 +2,9 @@ module Experimental
2
2
  class Experiment < ActiveRecord::Base
3
3
  extend Population::Filter
4
4
 
5
- attr_accessible :name, :num_buckets, :notes, :population
6
-
5
+ if ActiveRecord::VERSION::MAJOR < 4 || defined?(ProtectedAttributes)
6
+ attr_accessible :name, :num_buckets, :notes, :population
7
+ end
7
8
 
8
9
  validates_presence_of :name, :num_buckets
9
10
  validates_numericality_of :num_buckets, :greater_than_or_equal_to => 1
@@ -11,7 +12,7 @@ module Experimental
11
12
  :greater_than_or_equal_to => 0,
12
13
  :less_than => :num_buckets,
13
14
  :if => :ended?
14
- validate :has_valid_dates
15
+ validate :validate_dates
15
16
 
16
17
  def self.in_code
17
18
  where(:removed_at => nil)
@@ -86,9 +87,7 @@ module Experimental
86
87
  result = false
87
88
 
88
89
  unless removed?
89
- result = update_attributes(
90
- { removed_at: Time.now }, without_protection: true
91
- )
90
+ result = update_attribute(:removed_at, Time.now)
92
91
  end
93
92
 
94
93
  result
@@ -130,15 +129,19 @@ module Experimental
130
129
 
131
130
  private
132
131
 
133
- def has_valid_dates
134
- %w(start_date end_date).each do |attribute|
132
+ def validate_dates
133
+ validate_date 'start_date'
134
+ validate_date 'end_date'
135
+ end
136
+
137
+ def validate_date(attribute)
135
138
  value = read_attribute_before_type_cast(attribute)
139
+ return if value.blank?
136
140
  begin
137
- value.try(:to_time)
141
+ return if value.to_time
138
142
  rescue ArgumentError
139
- errors.add(attribute, "is not a valid date")
140
143
  end
141
- end
144
+ errors.add(attribute, "is not a valid date")
142
145
  end
143
146
 
144
147
  def population_filter
@@ -13,7 +13,7 @@ module Experimental
13
13
 
14
14
  Experimental::Experiment.transaction do
15
15
  active = Experimental.experiment_data.map do |name, attributes|
16
- experiment = Experimental::Experiment.find_or_initialize_by_name(name)
16
+ experiment = Experimental::Experiment.where(name: name).first_or_initialize
17
17
 
18
18
  unstarted = attributes.delete('unstarted')
19
19
  defaults = {'num_buckets' => nil, 'notes' => nil, 'population' => nil}
@@ -0,0 +1,7 @@
1
+ module Experimental
2
+ module Population
3
+ autoload :Default, 'experimental/population/default'
4
+ autoload :Filter, 'experimental/population/filter'
5
+ autoload :NewUsers, 'experimental/population/new_users'
6
+ end
7
+ end
File without changes
@@ -1,5 +1,5 @@
1
1
  module Experimental
2
- VERSION = [0, 4, 0]
2
+ VERSION = [0, 5, 0]
3
3
 
4
4
  class << VERSION
5
5
  include Comparable
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: experimental
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - HowAboutWe.com
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2014-06-02 00:00:00.000000000 Z
15
+ date: 2014-06-18 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: rails
@@ -52,22 +52,16 @@ files:
52
52
  - MIT-LICENSE
53
53
  - README.markdown
54
54
  - Rakefile
55
- - app/models/experimental/experiment.rb
56
- - app/models/experimental/population/default.rb
57
- - app/models/experimental/population/filter.rb
58
- - app/models/experimental/population/new_users.rb
59
- - app/models/experimental/subject.rb
60
- - app/views/experimental/_experiment.html.erb
61
- - app/views/experimental/_index.html.erb
62
- - app/views/experimental/_links.html.erb
63
- - app/views/experimental/_new.html.erb
64
55
  - config/routes.rb
65
56
  - lib/experimental.rb
66
- - lib/experimental/controller_actions.rb
67
57
  - lib/experimental/engine.rb
58
+ - lib/experimental/experiment.rb
68
59
  - lib/experimental/loader.rb
69
- - lib/experimental/middleware.rb
70
60
  - lib/experimental/overrides.rb
61
+ - lib/experimental/population.rb
62
+ - lib/experimental/population/default.rb
63
+ - lib/experimental/population/filter.rb
64
+ - lib/experimental/population/new_users.rb
71
65
  - lib/experimental/railtie.rb
72
66
  - lib/experimental/rspec_helpers.rb
73
67
  - lib/experimental/source.rb
@@ -75,6 +69,7 @@ files:
75
69
  - lib/experimental/source/base.rb
76
70
  - lib/experimental/source/cache.rb
77
71
  - lib/experimental/source/configuration.rb
72
+ - lib/experimental/subject.rb
78
73
  - lib/experimental/test.rb
79
74
  - lib/experimental/test/cucumber.rb
80
75
  - lib/experimental/test/rspec.rb
@@ -110,4 +105,3 @@ signing_key:
110
105
  specification_version: 4
111
106
  summary: Adds support for database-backed AB tests in Rails apps
112
107
  test_files: []
113
- has_rdoc:
@@ -1,16 +0,0 @@
1
- <tr>
2
- <td><%= experiment.name.humanize %></td>
3
- <td><%= "#{experiment.num_buckets} (0-#{experiment.num_buckets-1})" %></td>
4
- <td><%= experiment.population || "default" %></td>
5
- <td><%= experiment.notes.try(:gsub, "\n", '<br>').try(:html_safe) %></td>
6
- <td><%= experiment.start_date.try(:in_time_zone).try(:strftime, "%D %r %Z") %></td>
7
- <% if @include_inactive %>
8
- <td><%= experiment.winning_bucket %></td>
9
- <td><%= experiment.end_date.try(:in_time_zone).try(:strftime, "%D %r %Z") %></td>
10
- <td><%= experiment.removed_at.try(:in_time_zone).try(:strftime, "%D %r %Z") %></td>
11
- <% else %>
12
- <td>
13
- <%= hidden_field_tag "exp-#{experiment.id}", experiment.id %>
14
- <%= select_tag "exp-#{experiment.id}-buckets", options_for_select((0..experiment.num_buckets-1).to_a.insert(0, ['-- select --', -1])), class: 'set_winner' %>
15
- </td>
16
- <% end %>
@@ -1,50 +0,0 @@
1
- <h1><%= @h1 %></h1>
2
-
3
- <table>
4
- <thead>
5
- <tr>
6
- <th scope="col"> Name </th>
7
- <th scope="col"> Buckets </th>
8
- <th scope="col"> Population </th>
9
- <th scope="col"> Notes </th>
10
- <th scope="col"> Start </th>
11
- <th scope="col"> Winner </th>
12
- <% if @include_inactive %>
13
- <th scope="col"> End </th>
14
- <th scope="col"> Removed </th>
15
- <% end %>
16
- </tr>
17
- </thead>
18
- <tbody>
19
- <%= render partial: 'experimental/experiment', collection: @experiments %>
20
- </tbody>
21
- </table>
22
-
23
- <script type="text/javascript">
24
- $(function(){
25
- $(".set_winner").change(function(){
26
- var bucket_id = $(this).val();
27
- var msg = "Are you sure you want to end this experiment and set the winning bucket to " + bucket_id;
28
-
29
- if( bucket_id > -1 && confirm(msg) ){
30
- var exp_id = $(this).siblings().val();
31
-
32
- $.ajax({
33
- type: "POST",
34
- data: {id:exp_id, bucket_id:bucket_id},
35
- url: '<%= @experimental_path_names.set_winner %>',
36
- success: function(resp){
37
- window.location.reload();
38
- },
39
- error: function(){
40
- alert("WE BLEW UP!");
41
- $(this).val(-1);
42
- }
43
- });
44
- }
45
- else{
46
- $(this).val(-1);
47
- }
48
- });
49
- });
50
- </script>
@@ -1,7 +0,0 @@
1
- <div class="experiment_links">
2
- <%= link_to "In-progress", @experimental_path_names.index %>
3
- |
4
- <%= link_to "Ended or Removed", @experimental_path_names.inactive %>
5
- |
6
- <%= link_to "Create New Experiment", @experimental_path_names.new %>
7
- </div>
@@ -1,37 +0,0 @@
1
- <h1> New Experiment</h1>
2
- <br />
3
- <%= form_for(@experiment, url: @experimental_path_names.index) do |f| %>
4
- <% if @experiment.errors.any? %>
5
- <div id="error_explanation">
6
- <h2>
7
- <%= pluralize(@experiment.errors.count, "error") %>
8
- probited this experiment from being saved:
9
- </h2>
10
- <ul>
11
- <% @experiment.errors.full_messages.each do |msg| %>
12
- <li><%= msg %></li>
13
- <% end %>
14
- </ul>
15
- </div>
16
- <% end %>
17
-
18
- <p class="field">
19
- <%= f.label :name %>
20
- <br />
21
- <%= f.text_field :name %>
22
- </p>
23
- <p class="field">
24
- <%= f.label :num_buckets %>
25
- <br />
26
- <%= f.text_field :num_buckets %>
27
- </p>
28
- <p class="field">
29
- <%= f.label :notes %>
30
- <br />
31
- <%= f.text_area :notes, cols: "30", rows: "5" %>
32
- </p>
33
-
34
- <p class="actions" >
35
- <%= f.submit %>
36
- </p>
37
- <% end %>
@@ -1,90 +0,0 @@
1
- require 'ostruct'
2
-
3
- module Experimental
4
- module ControllerActions
5
- extend ActiveSupport::Concern
6
-
7
- included do
8
- class_eval do
9
- attr_writer :base_resource_name
10
-
11
- respond_to :json, :only => [:set_winner]
12
-
13
- before_filter :set_experimental_path_names
14
- end
15
- end
16
-
17
- def base_resource_name
18
- @base_resource_name ||= "experiment"
19
- end
20
-
21
- def set_experimental_path_names
22
- @experimental_path_names = OpenStruct.new
23
- plural_path = "#{base_resource_name.pluralize}_path"
24
-
25
- @experimental_path_names.index = self.send(plural_path.to_sym)
26
- if self.respond_to?("inactive_#{plural_path}".to_sym)
27
- @experimental_path_names.inactive = self.send("inactive_#{plural_path}".to_sym)
28
- end
29
- @experimental_path_names.new = self.send("new_#{base_resource_name}_path".to_sym)
30
- @experimental_path_names.set_winner = self.send("set_winner_#{plural_path}".to_sym)
31
- end
32
-
33
- def experimental_path_names
34
- set_experimental_path_names if @experimental_path_names.nil?
35
- @experimental_path_names
36
- end
37
-
38
- def experiments_index
39
- @h1 = "In-progress Experiments"
40
- @include_inactive = false
41
- @experiments = Experiment.in_progress
42
- end
43
-
44
- def experiments_new
45
- @experiment = Experiment.new
46
- end
47
-
48
- def experiments_create
49
- @experiment = Experiment.new(params[:experimental_experiment])
50
- @experiment.admin = true
51
- @experiment.start_date = Time.now
52
-
53
- if @experiment.save
54
- flash[:notice] = "Experiment was successfully created."
55
- return true
56
- else
57
- flash.now[:error] = "There was an error!"
58
- return false
59
- end
60
- end
61
-
62
- def experiments_inactive
63
- @h1 = "Ended or Removed Experiments"
64
- @include_inactive = true
65
- @experiments = Experiment.ended_or_removed
66
- end
67
-
68
- def create
69
- if experiments_create
70
- redirect_to experimental_path_names.index
71
- else
72
- render :new
73
- end
74
- end
75
-
76
- def inactive
77
- experiments_inactive
78
- render :index
79
- end
80
-
81
- def experiments_set_winner
82
- exp = Experiment.find params[:id]
83
- if exp.end(params[:bucket_id])
84
- render json: nil, status: :ok
85
- else
86
- render json: nil, status: :error
87
- end
88
- end
89
- end
90
- end
@@ -1,11 +0,0 @@
1
- module Experimental
2
- class Middleware
3
- def initialize(app)
4
- @app = app
5
- end
6
-
7
- def call(env)
8
- @app.call(env)
9
- end
10
- end
11
- end