experimental 0.4.0 → 0.5.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.
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