alephbet 0.1.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 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: []