alephbet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5810939a6190541eae1871206aa006520f035e73db99691bbdb55774621ba46a
4
+ data.tar.gz: 5ccd87046ef852d8bab6916cc93ba6505f2a2a6ca0d97a5ebef14bcc505f8291
5
+ SHA512:
6
+ metadata.gz: 22a3cdff22bb2e8879d3c405beb441dcd561a291f9abc01a7d08cd7949e25aacbaaa99e5b1a979345d7435194ed43b281bd0ed747e1c776c4c85aee5f485f266
7
+ data.tar.gz: c5d782af07facd9fb8505329d3f7f4a695f33149fb6f8701e05cda517a275d8937b248ba81dab39d26c920d93b92464e04fc6e1de64730b0aee34bd89699cb9e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2020
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.md ADDED
@@ -0,0 +1,99 @@
1
+ # Alephbet
2
+ A rails-engine backend for the [Alephbet](https://github.com/alephbet/alephbet) A/B testing framework.
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'alephbet'
9
+ ```
10
+
11
+ And then execute:
12
+ ```bash
13
+ $ bundle
14
+ $ bundle exec rails generate alephbet:install
15
+ $ bundle exec rake db:migrate
16
+ ```
17
+
18
+ Update your routes to mount alephbet:
19
+ ```ruby
20
+ # config/routes.rb
21
+
22
+ mount Alephbet::Engine => "/alephbet"
23
+ ```
24
+
25
+ Add alephbet JS:
26
+ ```bash
27
+ # with webpacker
28
+ $ npm install --save alephbet
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### running experiments
34
+
35
+ Add javascript experiments:
36
+
37
+ ```javascript
38
+ import AlephBet from "alephbet"
39
+
40
+ // for more options, see https://github.com/alephbet/alephbet
41
+
42
+ const adapter = new AlephBet.RailsAdapter(
43
+ "http://your.host/alephbet/event" // URL of the mounted engine tracking action
44
+ )
45
+
46
+ const button_clicked = new AlephBet.Goal("button clicked")
47
+
48
+ const experiment = new AlephBet.Experiment({
49
+ name: "my experiment",
50
+ tracking_adapter: adapter,
51
+ variants: {
52
+ "1-Blue": {
53
+ activate: () => { /* set button color to blue */ }
54
+ },
55
+ "2-Red": {
56
+ activate: () => { /* set button color to red */ }
57
+ }
58
+ }
59
+ })
60
+
61
+ experiment.add_goals([button_clicked])
62
+ ```
63
+
64
+ ### viewing results
65
+
66
+ There's no built-in dashboard at the moment. However, the API is compatible with [Gimel](https://github.com/alephbet/gimel) and [Lamed](https://github.com/alephbet/lamed), the Alephbet backends running on AWS Lamba).
67
+
68
+ To access your dashboard, run:
69
+ ```ruby
70
+ bundle exec rake alephbet:dashboard
71
+ ```
72
+
73
+ The url would include the API url and key (with default namespace), to access your experiment results
74
+
75
+ ### cleaning-up
76
+
77
+ Alephbet stores unique ids for events in the database, as well as the tally of test results. Those take space and can
78
+ also slow down response times. It's therefore advisable to clean-up old experiment data from time to time.
79
+
80
+ For example:
81
+ ```ruby
82
+ # delete all unique ids (use with care)
83
+ > Alephbet::Tracking.delete_all
84
+
85
+ # delete all unique ids older than 30 days
86
+ > Alephbet::Tracking.where("created_at < ?", 30.days.ago).delete_all
87
+
88
+ # delete all unique ids for experiment "buy button" in namespace "dev"
89
+ > Alephbet::Tracking.where(:experiment => "buy button", :namespace => "dev").delete_all
90
+
91
+ # delete experiment results for experiment "buy button" in namespace "dev"
92
+ > Alephbet::Experiment.where(:experiment => "buy button", :namespace => "dev").delete_all
93
+ ```
94
+
95
+ ## Contributing
96
+ Create an issue or submit a pull request.
97
+
98
+ ## License
99
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
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
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Alephbet'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/alephbet .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,5 @@
1
+ module Alephbet
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -0,0 +1,93 @@
1
+ module Alephbet
2
+ class ManagementController < ApplicationController
3
+ PARAMS = %i[experiment variant event namespace].freeze
4
+
5
+ # acts a browser-friendly API endpoint
6
+ skip_before_action :verify_authenticity_token
7
+ before_action :cors_headers
8
+ before_action :check_api_key, :except => :cors_preflight_check
9
+
10
+ def experiments
11
+ namespace = permitted_params[:namespace] || "alephbet"
12
+ @scope = Alephbet::Experiment.where(:namespace => namespace)
13
+ refine_scope if permitted_params[:scope].present?
14
+
15
+ respond_to do |format|
16
+ format.json {
17
+ render :json => results, :status => :ok
18
+ }
19
+ end
20
+ end
21
+
22
+ def cors_preflight_check
23
+ head :ok
24
+ end
25
+
26
+ private
27
+
28
+ def results
29
+ unique_experiments.map { |experiment| experiment_data(experiment) }
30
+ end
31
+
32
+ def experiment_data(experiment)
33
+ {
34
+ :experiment => experiment,
35
+ :goals => unique_goals.map { |goal| goal_data(experiment, goal) }
36
+ }
37
+ end
38
+
39
+ def goal_data(experiment, goal)
40
+ {
41
+ :goal => goal,
42
+ :results => variants.map { |variant|
43
+ {
44
+ :label => variant,
45
+ :trials => counter_for(experiment, "participate", variant),
46
+ :successes => counter_for(experiment, goal, variant)
47
+ }
48
+ }
49
+ }
50
+ end
51
+
52
+ def counter_for(experiment, event, variant)
53
+ @scope.find { |item|
54
+ item.experiment == experiment &&
55
+ item.event == event &&
56
+ item.variant == variant
57
+ }.try(:counter).to_i
58
+ end
59
+
60
+ def unique_experiments
61
+ @scope.map(&:experiment).uniq
62
+ end
63
+
64
+ def variants
65
+ @scope.map(&:variant).uniq
66
+ end
67
+
68
+ def unique_goals
69
+ @scope.reject { |item| item.event == "participate" }.map(&:event).uniq
70
+ end
71
+
72
+ def check_api_key
73
+ api_header = request.headers["X-Api-Key"]
74
+ valid_key = ActiveSupport::SecurityUtils.secure_compare(Alephbet.api_key, api_header)
75
+ head :forbidden unless valid_key
76
+ end
77
+
78
+ def refine_scope
79
+ @scope = @scope.where(:experiment => permitted_params[:scope].split(","))
80
+ end
81
+
82
+ def permitted_params
83
+ params.permit(PARAMS)
84
+ end
85
+
86
+ def cors_headers
87
+ response.headers["Access-Control-Allow-Origin"] = Alephbet.cors_allow_origin
88
+ response.headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS"
89
+ response.headers["Access-Control-Allow-Headers"] = Alephbet.cors_allow_headers
90
+ response.headers["Access-Control-Max-Age"] = "1728000"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,33 @@
1
+ module Alephbet
2
+ class TrackingController < ApplicationController
3
+ PARAMS = %i[experiment variant event namespace uuid].freeze
4
+
5
+ def event
6
+ respond_to do |format|
7
+ format.json {
8
+ begin
9
+ Alephbet::Tracking.create(permitted_tracking_params)
10
+ Alephbet::Experiment.increment_counter(:counter, experiment_id)
11
+ rescue ActiveRecord::RecordNotUnique
12
+ # ignoring duplicate requests
13
+ end
14
+ render :json => {}, :status => :ok
15
+ }
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def experiment_id
22
+ Alephbet::Experiment.find_or_create_by(permitted_experiment_params).id
23
+ end
24
+
25
+ def permitted_tracking_params
26
+ params.permit(PARAMS.without(:variant, :event))
27
+ end
28
+
29
+ def permitted_experiment_params
30
+ params.permit(PARAMS.without(:uuid))
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ module Alephbet
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Alephbet
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Alephbet
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Alephbet
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ # rubocop:disable Layout/LineLength
2
+ # == Schema Information
3
+ #
4
+ # Table name: alephbet_experiments
5
+ #
6
+ # id :bigint not null, primary key
7
+ # counter :integer default(0), not null
8
+ # event :string not null
9
+ # experiment :string not null
10
+ # namespace :string default("alephbet"), not null
11
+ # variant :string not null
12
+ #
13
+ # Indexes
14
+ #
15
+ # by_experiments (namespace,experiment,variant,event) UNIQUE
16
+ #
17
+ # rubocop:enable Layout/LineLength
18
+
19
+ module Alephbet
20
+ class Experiment < ApplicationRecord
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # rubocop:disable Layout/LineLength
2
+ # == Schema Information
3
+ #
4
+ # Table name: alephbet_tracking
5
+ #
6
+ # id :bigint not null, primary key
7
+ # event :string not null
8
+ # experiment :string not null
9
+ # namespace :string default("alephbet"), not null
10
+ # uuid :string not null
11
+ # variant :string not null
12
+ #
13
+ # Indexes
14
+ #
15
+ # index_alephbet_tracking_on_uuid (uuid) UNIQUE
16
+ #
17
+ # rubocop:enable Layout/LineLength
18
+ module Alephbet
19
+ class Tracking < ApplicationRecord
20
+ self.table_name = "alephbet_tracking"
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Alephbet</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "alephbet/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ Alephbet::Engine.routes.draw do
2
+ get "experiments" => "management#experiments", :as => "alephbet_experiments"
3
+ match "experiments" => "management#cors_preflight_check", :via => :options
4
+
5
+ get "event" => "tracking#event"
6
+ end
data/lib/alephbet.rb ADDED
@@ -0,0 +1,15 @@
1
+ require "alephbet/engine"
2
+
3
+ module Alephbet
4
+ class << self
5
+ attr_accessor :api_key, :cors_allow_origin, :cors_allow_headers
6
+
7
+ def setup
8
+ # fallback api_key if none specified
9
+ self.api_key = SecureRandom.uuid
10
+ self.cors_allow_origin = "*"
11
+ self.cors_allow_headers = "*"
12
+ yield(self) if block_given?
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Alephbet
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Alephbet
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Alephbet
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,40 @@
1
+ require "rails/generators/active_record"
2
+
3
+ module Alephbet
4
+ module Generators
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ include ::Rails::Generators::Migration
7
+ desc "Generates (but does not run) a migration for the Alephbet tables"
8
+
9
+ source_paths << File.join(File.dirname(__FILE__), "templates")
10
+
11
+ def install
12
+ route "mount Alephbet::Engine => '/alephbet'"
13
+ template "initializer.rb", "config/initializers/alephbet.rb"
14
+ end
15
+
16
+ def self.next_migration_number(dirname)
17
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
18
+ end
19
+
20
+ def self.migration_version
21
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" if requires_migration_number?
22
+ end
23
+
24
+ def self.requires_migration_number?
25
+ Rails::VERSION::MAJOR.to_i >= 5
26
+ end
27
+
28
+ def create_migration_file
29
+ options = {
30
+ :migration_version => migration_version
31
+ }
32
+ migration_template "migration.erb", "db/migrate/create_alephbet_tables.rb", options
33
+ end
34
+
35
+ def migration_version
36
+ self.class.migration_version
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ Alephbet.setup do |config|
2
+ config.api_key = ENV["ALEPHBET_API_KEY"] || SecureRandom.uuid
3
+ config.cors_allow_origin = "*"
4
+ config.cors_allow_headers = "*"
5
+ end
@@ -0,0 +1,22 @@
1
+ class CreateAlephbetTables < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table "alephbet_tracking" do |t|
4
+ t.string :uuid, :null => false
5
+ t.string :experiment, :null => false
6
+ t.string :namespace, :null => false, :default => "alephbet"
7
+ t.timestamps
8
+ end
9
+
10
+ create_table "alephbet_experiments" do |t|
11
+ t.string :experiment, :null => false
12
+ t.string :variant, :null => false
13
+ t.string :event, :null => false
14
+ t.string :namespace, :null => false, :default => "alephbet"
15
+ t.integer :counter, :null => false, :default => 0
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :alephbet_tracking, :uuid, :unique => true
20
+ add_index :alephbet_experiments, [:namespace, :experiment, :variant, :event], :unique => true, :name => "by_experiments"
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ include Rails.application.routes.url_helpers
2
+
3
+ namespace :alephbet do
4
+ desc "view experiment results"
5
+ task :dashboard => :environment do
6
+ base_url = root_url.chomp("/")
7
+ experiments_path = Alephbet::Engine.routes.url_helpers.alephbet_experiments_path
8
+ experiments_url = "#{base_url}#{experiments_path}"
9
+ puts "Open your browser and go to: " \
10
+ "https://codepen.io/anon/pen/LOGGZj/" \
11
+ "?experiment_url=#{experiments_url}&" \
12
+ "api_key=#{Alephbet.api_key}&" \
13
+ "namespace=alephbet"
14
+
15
+ return if base_url =~ /https/
16
+
17
+ puts "NOTE: #{experiments_url} without SSL. The dashboard works only with https"
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alephbet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yoav Aner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.2
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.0.2.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 6.0.2
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.0.2.2
33
+ - !ruby/object:Gem::Dependency
34
+ name: sqlite3
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description: https://github.com/alephbet/alephbet-rails
48
+ email:
49
+ - ''
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - MIT-LICENSE
55
+ - README.md
56
+ - Rakefile
57
+ - app/assets/config/alephbet_manifest.js
58
+ - app/assets/stylesheets/alephbet/application.css
59
+ - app/controllers/alephbet/application_controller.rb
60
+ - app/controllers/alephbet/management_controller.rb
61
+ - app/controllers/alephbet/tracking_controller.rb
62
+ - app/helpers/alephbet/application_helper.rb
63
+ - app/jobs/alephbet/application_job.rb
64
+ - app/mailers/alephbet/application_mailer.rb
65
+ - app/models/alephbet/application_record.rb
66
+ - app/models/alephbet/experiment.rb
67
+ - app/models/alephbet/tracking.rb
68
+ - app/views/layouts/alephbet/application.html.erb
69
+ - config/routes.rb
70
+ - lib/alephbet.rb
71
+ - lib/alephbet/engine.rb
72
+ - lib/alephbet/version.rb
73
+ - lib/generators/alephbet/install_generator.rb
74
+ - lib/generators/alephbet/templates/initializer.rb
75
+ - lib/generators/alephbet/templates/migration.erb
76
+ - lib/tasks/alephbet_tasks.rake
77
+ homepage: https://github.com/alephbet/alephbet-rails
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.1.2
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Alephbet A/B testing backend for Rails
100
+ test_files: []