GithubHelp home page GithubHelp logo

field_test's Introduction

Field Test

🍁 A/B testing for Rails

  • Designed for web and email
  • Comes with a dashboard to view results and update variants
  • Uses your database for storage
  • Seamlessly handles the transition from anonymous visitor to logged in user

Uses Bayesian statistics to evaluate results so you don’t need to choose a sample size ahead of time.

Build Status

Installation

Add this line to your application’s Gemfile:

gem "field_test"

Run:

rails generate field_test:install
rails db:migrate

And mount the dashboard in your config/routes.rb:

mount FieldTest::Engine, at: "field_test"

Be sure to secure the dashboard in production.

Getting Started

Add an experiment to config/field_test.yml.

experiments:
  button_color:
    variants:
      - red
      - green
      - blue

Refer to it in controllers, views, and mailers.

button_color = field_test(:button_color)

To make testing easier, you can specify a variant with query parameters

http://localhost:3000/?field_test[button_color]=green

When someone converts, record it with:

field_test_converted(:button_color)

When an experiment is over, specify a winner:

experiments:
  button_color:
    winner: green

All calls to field_test will now return the winner, and metrics will stop being recorded.

You can keep returning the variant for existing participants after a winner is declared:

experiments:
  button_color:
    winner: green
    keep_variant: true

You can also close an experiment to new participants without declaring a winner while still recording metrics for existing participants:

experiments:
  button_color:
    closed: true

Calls to field_test for new participants will return the control, and they won’t be added to the experiment.

You can get the list of experiments and variants for a user with:

field_test_experiments

JavaScript and Native Apps

For JavaScript and native apps, add calls to your normal endpoints.

class CheckoutController < ActionController::API
  def start
    render json: {button_color: field_test(:button_color)}
  end

  def finish
    field_test_converted(:button_color)
    # ...
  end
end

For anonymous visitors in native apps, pass a Field-Test-Visitor header with a unique identifier.

Participants

Any model or string can be a participant in an experiment.

For web requests, it uses current_user (if it exists) and an anonymous visitor id to determine the participant. Set your own with:

class ApplicationController < ActionController::Base
  def field_test_participant
    current_company
  end
end

For mailers, it tries @user then params[:user] to determine the participant. Set your own with:

class ApplicationMailer < ActionMailer::Base
  def field_test_participant
    @company
  end
end

You can also manually pass a participant with:

field_test(:button_color, participant: company)

Jobs

To get variants in jobs, models, and other contexts, use:

experiment = FieldTest::Experiment.find(:button_color)
button_color = experiment.variant(user)

Exclusions

By default, bots are returned the first variant and excluded from metrics. Change this with:

exclude:
  bots: false

Exclude certain IP addresses with:

exclude:
  ips:
    - 127.0.0.1
    - 10.0.0.0/8

You can also use custom logic:

field_test(:button_color, exclude: request.user_agent == "Test")

Config

Keep track of when experiments started and ended. Use any format Time.parse accepts. Variants assigned outside this window are not included in metrics.

experiments:
  button_color:
    started_at: Dec 1, 2016 8 am PST
    ended_at: Dec 8, 2016 2 pm PST

Add a friendlier name and description with:

experiments:
  button_color:
    name: Buttons!
    description: >
      Different button colors
      for the landing page.

By default, variants are given the same probability of being selected. Change this with:

experiments:
  button_color:
    variants:
      - red
      - blue
    weights:
      - 85
      - 15

To help with GDPR compliance, you can switch from cookies to anonymity sets for anonymous visitors. Visitors with the same IP mask and user agent are grouped together.

cookies: false

Dashboard Config

If the dashboard gets slow, you can make it faster with:

cache: true

This will use the Rails cache to speed up winning probability calculations.

If you need more precision, set:

precision: 1

Multiple Goals

You can set multiple goals for an experiment to track conversions at different parts of the funnel. First, run:

rails generate field_test:events
rails db:migrate

And add to your config:

experiments:
  button_color:
    goals:
      - signed_up
      - ordered

Specify a goal during conversion with:

field_test_converted(:button_color, goal: "ordered")

The results for all goals will appear on the dashboard.

Analytics Platforms

You may also want to send experiment data as properties to other analytics platforms like Segment, Amplitude, and Ahoy. Get the list of experiments and variants with:

field_test_experiments

Ahoy

You can configure Field Test to use Ahoy’s visitor token instead of creating its own:

class ApplicationController < ActionController::Base
  def field_test_participant
    [ahoy.user, ahoy.visitor_token]
  end
end

Dashboard Security

Devise

authenticate :user, ->(user) { user.admin? } do
  mount FieldTest::Engine, at: "field_test"
end

Basic Authentication

Set the following variables in your environment or an initializer.

ENV["FIELD_TEST_USERNAME"] = "moonrise"
ENV["FIELD_TEST_PASSWORD"] = "kingdom"

Updating Variants

Assign a specific variant to a user with:

experiment = FieldTest::Experiment.find(:button_color)
experiment.variant(participant, variant: "green")

You can also change a user’s variant from the dashboard.

Associations

To associate models with field test memberships, use:

class User < ApplicationRecord
  has_many :field_test_memberships, class_name: "FieldTest::Membership", as: :participant
end

Now you can do:

user.field_test_memberships

Credits

A huge thanks to Evan Miller for deriving the Bayesian formulas.

History

View the changelog

Contributing

Everyone is encouraged to help improve this project. Here are a few ways you can help:

To get started with development:

git clone https://github.com/ankane/field_test.git
cd field_test
bundle install
bundle exec rake compile
bundle exec rake test

field_test's People

Contributors

ankane avatar falusi94 avatar kyleschmolze avatar npezza93 avatar prsimp avatar shubik22 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

field_test's Issues

BinaryTest supports up to 4 variants

Is this just an oversight or is there a reason to support only 3 variants when BinaryTest appears to support 4?

if variants.size <= 3

If you are open to changing this to support 4 variants, I'm happy to submit a Pull Request.

Do you prefer this being:

  1. a straight magic number replacement (change 3 to 4 and add a comment explaining it)
  2. a reference to a local constant named something like BINARY_TEST_VARIANT_MAX or
  3. a change to add that same constant to the BinaryTest gem exposing its own variant max constant (BinaryTest::VARIANT_MAX)?

Thank you for this awesome gem and all your other awesome gems, they are awesome!

Question: how is "converted" defined?

Hi,

I recently joined a team that used your fine tool to run experiments. I was looking for the definition of converted in order to interpret the data. In the example in the readme, I am guessing it is going to be the click event of the button.

When someone converts, record it with:
field_test_converted(:button_color)

Is that correct? Is it customizable? Thanks in advance!

Support when there is not yet a current_user/participant?

We have a use case for sending out email when we don't have a user to associate yet (think: lead gen). We also can't proactively create a user for each one, both for efficiency, and because the email might be going to someone who already has an account.

What's the best strategy, if any, for using field_test here?

Use FieldTest in Sidekiq Workers/Models

I am using a Sidekiq worker to send text messages to individuals to poll them on whether they might appear for (or flake on) on a job. We want to A/B test the value of the message. I happened upon FieldTest and really like the interface, and would really like to incorporate it into our system.

Your instructions stated:

Refer to it in controllers, views, and mailers.

button_color = field_test(:button_color)

However, it does not seem to work in Sidekiq Workers or models. Any particular reason? Is there something I could do to get this working in a Sidekiq worker?

Also, how might I write an RSpec that tests both sides of a simple boolean variant?

Old experiment data preventing conversions from being tracked in new experiments

We ran our first experiment for a couple of weeks using two conversion events in our funnel. It worked very well and we were all super duper happy!

We updated the old experiment and marked the winning variant. Then we proceeded with the second test. This test used the same name for the conversion events, different names for everything else. (These are the same events we push elsewhere, into analytics, data warehouse, reporting, etc).

However, this time, none of the conversion events seemed to track correctly. I tried to rename the new conversion events (suffixed with a number) and still, no dice.

What ultimately worked was deleting the original experiment from the yaml configuration file.

I would very much like to dig into the code and assist in debugging this at some point in the future, since I have really enjoyed using field_test. Wanted to submit this here in case anyone else sees this issue arise.

Thanks for a great library, @ankane !

Failed to build gem native extension with v0.5.2 on Fedora 36

Hey there!

Just updated from Fedora 35 to 36 and I am unable to install the gem. Using "< 0.5" works for now.

gem install field_test -v '0.5.2'
Building native extensions. This could take a while...
ERROR:  Error installing field_test:
        ERROR: Failed to build gem native extension.

    current directory: /home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/field_test-0.5.2/ext/field_test
/home/macsiri/.asdf/installs/ruby/3.0.2/bin/ruby -I /home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/3.0.0 -r ./siteconf20220512-371204-7eavg.rb extconf.rb
checking for rice/rice.hpp in /home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include... yes
checking for -lstdc++... yes
creating Makefile

current directory: /home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/field_test-0.5.2/ext/field_test
make DESTDIR\= clean

current directory: /home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/field_test-0.5.2/ext/field_test
make DESTDIR\=
compiling ext.cpp
In file included from ext.cpp:1:
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp: In function ‘std::string Rice::detail::makeClassName(const std::type_info&)’:
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:428:42: error: ‘istream_iterator’ is not a member of ‘std’
  428 |     std::vector<std::string> words{ std::istream_iterator<std::string>{stream},
      |                                          ^~~~~~~~~~~~~~~~
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:316:1: note: ‘std::istream_iterator’ is defined in header ‘<iterator>’; did you forget to ‘#include <iterator>’?
  315 | #include <cstring>
  +++ |+#include <iterator>
  316 | #endif
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:428:70: error: expected primary-expression before ‘>’ token
  428 |     std::vector<std::string> words{ std::istream_iterator<std::string>{stream},
      |                                                                      ^
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:428:71: error: expected primary-expression before ‘{’ token
  428 |     std::vector<std::string> words{ std::istream_iterator<std::string>{stream},
      |                                                                       ^
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:428:71: error: expected ‘}’ before ‘{’ token
  428 |     std::vector<std::string> words{ std::istream_iterator<std::string>{stream},
      |                                   ~                                   ^
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:428:71: error: no matching function for call to ‘std::vector<std::__cxx11::basic_string<char> >::vector(<brace-enclosed initializer list>)’
In file included from /usr/include/c++/12/vector:64,
                 from /home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:46:
/usr/include/c++/12/bits/stl_vector.h:702:9: note: candidate: ‘template<class _InputIterator, class> std::vector<_Tp, _Alloc>::vector(_InputIterator, _InputIterator, const allocator_type&) [with <template-parameter-2-2> = _InputIterator; _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >]’
  702 |         vector(_InputIterator __first, _InputIterator __last,
      |         ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:702:9: note:   template argument deduction/substitution failed:
/usr/include/c++/12/bits/stl_vector.h:673:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(std::initializer_list<_Tp>, const allocator_type&) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >; allocator_type = std::allocator<std::__cxx11::basic_string<char> >]’
  673 |       vector(initializer_list<value_type> __l,
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:673:7: note:   conversion of argument 1 would be ill-formed:
/usr/include/c++/12/bits/stl_vector.h:654:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(std::vector<_Tp, _Alloc>&&, std::__type_identity_t<_Alloc>&) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >; std::__type_identity_t<_Alloc> = std::allocator<std::__cxx11::basic_string<char> >]’
  654 |       vector(vector&& __rv, const __type_identity_t<allocator_type>& __m)
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:654:7: note:   candidate expects 2 arguments, 1 provided
/usr/include/c++/12/bits/stl_vector.h:635:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(std::vector<_Tp, _Alloc>&&, const allocator_type&, std::false_type) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >; allocator_type = std::allocator<std::__cxx11::basic_string<char> >; std::false_type = std::integral_constant<bool, false>]’
  635 |       vector(vector&& __rv, const allocator_type& __m, false_type)
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:635:7: note:   candidate expects 3 arguments, 1 provided
/usr/include/c++/12/bits/stl_vector.h:630:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(std::vector<_Tp, _Alloc>&&, const allocator_type&, std::true_type) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >; allocator_type = std::allocator<std::__cxx11::basic_string<char> >; std::true_type = std::integral_constant<bool, true>]’
  630 |       vector(vector&& __rv, const allocator_type& __m, true_type) noexcept
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:630:7: note:   candidate expects 3 arguments, 1 provided
/usr/include/c++/12/bits/stl_vector.h:619:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(const std::vector<_Tp, _Alloc>&, std::__type_identity_t<_Alloc>&) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >; std::__type_identity_t<_Alloc> = std::allocator<std::__cxx11::basic_string<char> >]’
  619 |       vector(const vector& __x, const __type_identity_t<allocator_type>& __a)
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:619:7: note:   candidate expects 2 arguments, 1 provided
/usr/include/c++/12/bits/stl_vector.h:615:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(std::vector<_Tp, _Alloc>&&) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >]’
  615 |       vector(vector&&) noexcept = default;
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:615:7: note:   conversion of argument 1 would be ill-formed:
/usr/include/c++/12/bits/stl_vector.h:596:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(const std::vector<_Tp, _Alloc>&) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >]’
  596 |       vector(const vector& __x)
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:596:7: note:   conversion of argument 1 would be ill-formed:
/usr/include/c++/12/bits/stl_vector.h:564:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(size_type, const value_type&, const allocator_type&) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >; size_type = long unsigned int; value_type = std::__cxx11::basic_string<char>; allocator_type = std::allocator<std::__cxx11::basic_string<char> >]’
  564 |       vector(size_type __n, const value_type& __value,
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:564:7: note:   candidate expects 3 arguments, 1 provided
/usr/include/c++/12/bits/stl_vector.h:551:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(size_type, const allocator_type&) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >; size_type = long unsigned int; allocator_type = std::allocator<std::__cxx11::basic_string<char> >]’
  551 |       vector(size_type __n, const allocator_type& __a = allocator_type())
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:551:7: note:   conversion of argument 1 would be ill-formed:
/usr/include/c++/12/bits/stl_vector.h:537:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector(const allocator_type&) [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >; allocator_type = std::allocator<std::__cxx11::basic_string<char> >]’
  537 |       vector(const allocator_type& __a) _GLIBCXX_NOEXCEPT
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:537:7: note:   conversion of argument 1 would be ill-formed:
/usr/include/c++/12/bits/stl_vector.h:526:7: note: candidate: ‘std::vector<_Tp, _Alloc>::vector() [with _Tp = std::__cxx11::basic_string<char>; _Alloc = std::allocator<std::__cxx11::basic_string<char> >]’
  526 |       vector() = default;
      |       ^~~~~~
/usr/include/c++/12/bits/stl_vector.h:526:7: note:   candidate expects 0 arguments, 1 provided
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:428:71: error: expected ‘,’ or ‘;’ before ‘{’ token
  428 |     std::vector<std::string> words{ std::istream_iterator<std::string>{stream},
      |                                                                       ^
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:428:79: error: expected primary-expression before ‘,’ token
  428 |     std::vector<std::string> words{ std::istream_iterator<std::string>{stream},
      |                                                                               ^
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:429:42: error: ‘istream_iterator’ is not a member of ‘std’
  429 |                                     std::istream_iterator<std::string>{} };
      |                                          ^~~~~~~~~~~~~~~~
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:429:42: note: ‘std::istream_iterator’ is defined in header ‘<iterator>’; did you forget to ‘#include <iterator>’?
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:429:70: error: expected primary-expression before ‘>’ token
  429 |                                     std::istream_iterator<std::string>{} };
      |                                                                      ^
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:429:71: error: expected primary-expression before ‘{’ token
  429 |                                     std::istream_iterator<std::string>{} };
      |                                                                       ^
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:429:74: warning: no return statement in function returning non-void [-Wreturn-type]
  429 |                                     std::istream_iterator<std::string>{} };
      |                                                                          ^
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp: At global scope:
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:431:42: error: ‘words’ was not declared in this scope
  431 |     std::string result = std::accumulate(words.begin(), words.end(), std::string(),
      |                                          ^~~~~
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:431:57: error: ‘words’ was not declared in this scope
  431 |     std::string result = std::accumulate(words.begin(), words.end(), std::string(),
      |                                                         ^~~~~
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:439:5: error: expected unqualified-id before ‘return’
  439 |     return result;
      |     ^~~~~~
/home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/rice-4.0.3/include/rice/rice.hpp:441:1: error: expected declaration before ‘}’ token
  441 | }
      | ^
make: *** [Makefile:237: ext.o] Error 1

make failed, exit code 2

Gem files will remain installed in /home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/gems/field_test-0.5.2 for inspection.
Results logged to /home/macsiri/.asdf/installs/ruby/3.0.2/lib/ruby/gems/3.0.0/extensions/x86_64-linux/3.0.0/field_test-0.5.2/gem_make.out

I'm unfamiliar with Rice or C++ but looks like it's might be related to (gcc 12, glibc 2.35) that came with Fedora 36.

Probability of Winning With More Than 3 Variants

I'm running a test right now with a control and 4 variants. Took a look at the source code and I noticed that if the variants.size is greater than 3 then the prob_winning isn't calculated for the experiment.

Can we add support for calculating prob_winning for more than 3 variants?

CSRF Vulnerability with Non-Session Based Authentication

The Field Test dashboard is vulnerable to cross-site request forgery (CSRF) with non-session based authentication methods. This vulnerability has been assigned the CVE identifier CVE-2020-16252.

Versions Affected: 0.2.0 to 0.3.2
Fixed Versions: 0.4.0

Impact

The Field Test dashboard is vulnerable to CSRF with non-session based authentication methods, like basic authentication. Session-based authentication methods (like Devise's default authentication) are not affected.

A CSRF attack works by getting an authorized user to visit a malicious website and then performing requests on behalf of the user. In this instance, a single endpoint is affected, which allows for changing the variant assigned to a user.

All users running an affected release should upgrade immediately.

Technical Details

Field Test uses the protect_from_forgery method from Rails to prevent CSRF. However, this defaults to :null_session, which has no effect on non-session based authentication methods. This has been changed to protect_from_forgery with: :exception.

Exclude users from experiments

Hi! In our projects we're currently rolling our own A/B tool very similar to FieldTest.
Would love to switch to FieldTest and contribute to this instead.

One feature that we use extensively and FieldTest doesn't support at the moment, is the ability to exclude certain participants from experiments based on conditions. Say you only want to run a test for users that are in a certain country, or exclude paying customers, or only for users signed up after a certain date etc. Participants not part of an experiment, would simply be served the control variant and not count in metrics.

It's obviously not feasible to define this in the YAML-format.

A suggestion could be to allow defining experiments in Ruby instead. This could also
follow Rails-style definition of experiments in eg. app/experiments

Example from the top of my head:

# eg app/experiments/button_color_experiment.rb
class ButtonColorExperiment < FieldTest::Experiment
  description 'Red vs blue'
  variants :red, :blue # or variants red: 85, blue: 15 for weighted

  def enabled?
    participant.zip == '90210'
  end
end

I'd love to give it a go, but would appreciate your thoughts beforehand. I think it should be possible
to implement it in a way that is backwards compatible with the YAML experiment configuration.

What do you think? 🎯

Participants stop showing up on dashboard when I run the field_test_events migration

If I run rails g field_test:events and run the migration, then add some goals and track conversions with them, the dashboard stops reporting any new participants. All numbers show up as 0, even though I can see that the FieldTest::Memberships are being created. Once I rollback the migration, the dashboard shows the proper numbers again. Don't have time to try to debug this right now, but I can later, especially if you have any ideas where to look!

BTW thanks for this great gem!

segfault when used with fasttext-ruby?

I've been using field_test 0.5.4 for some time in a Rails 6.1 app, and just added fasttext 0.2.4 to the same app to do some text classification (I'm a big fan of @ankane gems 😉.)

Everything worked fine in development but when I pushed to production I noticed a segfault when booting the app. Because I just added fasttext I suspected it was the issue and spent a fair amount of time debugging but didn't get anywhere. When digging in the stack traces field_test came up! 🤔

Sure enough, removing the gem causes the app to boot OK again. Weird.

The particular A/B experiment we were doing is over, so I'm going to remove field_test to move ahead, but was wondering if you had any ideas.

Thanks!

Stack Trace
2023-03-03T13:53:57.2626537Z terminate called after throwing an instance of 'std::bad_any_cast'
2023-03-03T13:53:57.2626877Z   what():  bad any_cast
2023-03-03T13:53:57.2627319Z /usr/local/bundle/gems/fasttext-0.2.4/lib/fasttext/ext.so: [BUG] Segmentation fault at 0x0000000000000000
2023-03-03T13:53:57.2627756Z ruby 3.2.0 (2022-12-25 revision a528908271) [x86_64-linux]
2023-03-03T13:53:57.2627916Z 
2023-03-03T13:53:57.2628156Z -- Control frame information -----------------------------------------------
2023-03-03T13:53:57.2628533Z c:0030 p:---- s:0146 e:000145 TOP    [FINISH]
2023-03-03T13:53:57.2628839Z c:0029 p:---- s:0143 e:000142 CFUNC  :require
2023-03-03T13:53:57.2629291Z c:0028 p:0006 s:0138 e:000137 BLOCK  /usr/local/bundle/gems/activesupport-6.1.7/lib/active_support/dependencies.rb:332
2023-03-03T13:53:57.2629847Z c:0027 p:0039 s:0135 e:000134 METHOD /usr/local/bundle/gems/activesupport-6.1.7/lib/active_support/dependencies.rb:299
2023-03-03T13:53:57.2630372Z c:0026 p:0010 s:0128 e:000127 METHOD /usr/local/bundle/gems/activesupport-6.1.7/lib/active_support/dependencies.rb:332
2023-03-03T13:53:57.2630866Z c:0025 p:0005 s:0122 e:000121 TOP    /usr/local/bundle/gems/fasttext-0.2.4/lib/fasttext.rb:2 [FINISH]
2023-03-03T13:53:57.2631225Z c:0024 p:---- s:0119 e:000118 CFUNC  :require
2023-03-03T13:53:57.2631641Z c:0023 p:0025 s:0114 e:000113 BLOCK  /usr/local/bundle/gems/bundler-2.4.7/lib/bundler/runtime.rb:60 [FINISH]
2023-03-03T13:53:57.2632001Z c:0022 p:---- s:0109 e:000108 CFUNC  :each
2023-03-03T13:53:57.2632421Z c:0021 p:0042 s:0105 e:000104 BLOCK  /usr/local/bundle/gems/bundler-2.4.7/lib/bundler/runtime.rb:55 [FINISH]
2023-03-03T13:53:57.2632779Z c:0020 p:---- s:0098 e:000097 CFUNC  :each
2023-03-03T13:53:57.2633171Z c:0019 p:0026 s:0094 e:000093 METHOD /usr/local/bundle/gems/bundler-2.4.7/lib/bundler/runtime.rb:44
2023-03-03T13:53:57.2633629Z c:0018 p:0013 s:0089 e:000088 METHOD /usr/local/bundle/gems/bundler-2.4.7/lib/bundler.rb:195
2023-03-03T13:53:57.2633965Z c:0017 p:0076 s:0084 e:000083 TOP    /app/config/application.rb:19 [FINISH]
2023-03-03T13:53:57.2634321Z c:0016 p:---- s:0081 e:000080 CFUNC  :require_relative
2023-03-03T13:53:57.2634603Z c:0015 p:0005 s:0076 e:000075 TOP    /app/Rakefile:4 [FINISH]
2023-03-03T13:53:57.2634905Z c:0014 p:---- s:0073 e:000072 CFUNC  :load
2023-03-03T13:53:57.2642960Z c:0013 p:0005 s:0068 e:000067 METHOD /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/rake_module.rb:29
2023-03-03T13:53:57.2643476Z c:0012 p:0141 s:0063 e:000062 METHOD /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:710
2023-03-03T13:53:57.2643989Z c:0011 p:0003 s:0057 e:000056 BLOCK  /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:104
2023-03-03T13:53:57.2644474Z c:0010 p:0002 s:0054 e:000053 METHOD /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:186
2023-03-03T13:53:57.2644958Z c:0009 p:0004 s:0049 e:000048 METHOD /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:103
2023-03-03T13:53:57.2645464Z c:0008 p:0019 s:0045 e:000044 BLOCK  /usr/local/bundle/gems/railties-6.1.7/lib/rails/commands/rake/rake_command.rb:20
2023-03-03T13:53:57.2645978Z c:0007 p:0023 s:0041 e:000040 METHOD /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/rake_module.rb:59
2023-03-03T13:53:57.2646473Z c:0006 p:0009 s:0035 e:000034 METHOD /usr/local/bundle/gems/railties-6.1.7/lib/rails/commands/rake/rake_command.rb:18
2023-03-03T13:53:57.2647022Z c:0005 p:0143 s:0028 e:000027 METHOD /usr/local/bundle/gems/railties-6.1.7/lib/rails/command.rb:50
2023-03-03T13:53:57.2647636Z c:0004 p:0038 s:0016 e:000015 TOP    /usr/local/bundle/gems/railties-6.1.7/lib/rails/commands.rb:18 [FINISH]
2023-03-03T13:53:57.2648014Z c:0003 p:---- s:0011 e:000010 CFUNC  :require
2023-03-03T13:53:57.2648282Z c:0002 p:0024 s:0006 e:000005 EVAL   bin/rails:4 [FINISH]
2023-03-03T13:53:57.2648547Z c:0001 p:0000 s:0003 E:000590 DUMMY  [FINISH]
2023-03-03T13:53:57.2648695Z 
2023-03-03T13:53:57.2648931Z -- Ruby level backtrace information ----------------------------------------
2023-03-03T13:53:57.2649207Z bin/rails:4:in `'
2023-03-03T13:53:57.2649421Z bin/rails:4:in `require'
2023-03-03T13:53:57.2649807Z /usr/local/bundle/gems/railties-6.1.7/lib/rails/commands.rb:18:in `'
2023-03-03T13:53:57.2650238Z /usr/local/bundle/gems/railties-6.1.7/lib/rails/command.rb:50:in `invoke'
2023-03-03T13:53:57.2650685Z /usr/local/bundle/gems/railties-6.1.7/lib/rails/commands/rake/rake_command.rb:18:in `perform'
2023-03-03T13:53:57.2651168Z /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/rake_module.rb:59:in `with_application'
2023-03-03T13:53:57.2651667Z /usr/local/bundle/gems/railties-6.1.7/lib/rails/commands/rake/rake_command.rb:20:in `block in perform'
2023-03-03T13:53:57.2652153Z /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:103:in `load_rakefile'
2023-03-03T13:53:57.2652637Z /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:186:in `standard_exception_handling'
2023-03-03T13:53:57.2653134Z /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:104:in `block in load_rakefile'
2023-03-03T13:53:57.2653613Z /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/application.rb:710:in `raw_load_rakefile'
2023-03-03T13:53:57.2654078Z /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/rake_module.rb:29:in `load_rakefile'
2023-03-03T13:53:57.2654516Z /usr/local/lib/ruby/gems/3.2.0/gems/rake-13.0.6/lib/rake/rake_module.rb:29:in `load'
2023-03-03T13:53:57.2654804Z /app/Rakefile:4:in `'
2023-03-03T13:53:57.2655101Z /app/Rakefile:4:in `require_relative'
2023-03-03T13:53:57.2655380Z /app/config/application.rb:19:in `'
2023-03-03T13:53:57.2655766Z /usr/local/bundle/gems/bundler-2.4.7/lib/bundler.rb:195:in `require'
2023-03-03T13:53:57.2656163Z /usr/local/bundle/gems/bundler-2.4.7/lib/bundler/runtime.rb:44:in `require'
2023-03-03T13:53:57.2656571Z /usr/local/bundle/gems/bundler-2.4.7/lib/bundler/runtime.rb:44:in `each'
2023-03-03T13:53:57.2656998Z /usr/local/bundle/gems/bundler-2.4.7/lib/bundler/runtime.rb:55:in `block in require'
2023-03-03T13:53:57.2657421Z /usr/local/bundle/gems/bundler-2.4.7/lib/bundler/runtime.rb:55:in `each'
2023-03-03T13:53:57.2657984Z /usr/local/bundle/gems/bundler-2.4.7/lib/bundler/runtime.rb:60:in `block (2 levels) in require'
2023-03-03T13:53:57.2658500Z /usr/local/bundle/gems/bundler-2.4.7/lib/bundler/runtime.rb:60:in `require'
2023-03-03T13:53:57.2658925Z /usr/local/bundle/gems/fasttext-0.2.4/lib/fasttext.rb:2:in `'
2023-03-03T13:53:57.2659403Z /usr/local/bundle/gems/activesupport-6.1.7/lib/active_support/dependencies.rb:332:in `require'
2023-03-03T13:53:57.2659923Z /usr/local/bundle/gems/activesupport-6.1.7/lib/active_support/dependencies.rb:299:in `load_dependency'
2023-03-03T13:53:57.2660437Z /usr/local/bundle/gems/activesupport-6.1.7/lib/active_support/dependencies.rb:332:in `block in require'
2023-03-03T13:53:57.2660936Z /usr/local/bundle/gems/activesupport-6.1.7/lib/active_support/dependencies.rb:332:in `require'
2023-03-03T13:53:57.2661158Z 
2023-03-03T13:53:57.2661396Z -- Machine register context ------------------------------------------------
2023-03-03T13:53:57.2661743Z  RIP: 0x00007fd1305cd611 RBP: 0x00007fd130766840 RSP: 0x00007ffe7b7a1d90
2023-03-03T13:53:57.2662050Z  RAX: 0x0000000000000000 RBX: 0x00007fd128ef5000 RCX: 0x0000000000000000
2023-03-03T13:53:57.2662332Z  RDX: 0x0000000000000000 RDI: 0x0000000000000002 RSI: 0x00007ffe7b7a1c70
2023-03-03T13:53:57.2662617Z   R8: 0x0000000000000000  R9: 0x00007ffe7b7a1c70 R10: 0x0000000000000008
2023-03-03T13:53:57.2662887Z  R11: 0x0000000000000246 R12: 0x000056287ca8aa30 R13: 0x0000000000000008
2023-03-03T13:53:57.2663203Z  R14: 0x00007fd1303fcd08 R15: 0x00007fd129685748 EFL: 0x0000000000010246
2023-03-03T13:53:57.2663366Z 
2023-03-03T13:53:57.2663589Z -- C level backtrace information -------------------------------------------
2023-03-03T13:53:57.6672095Z /usr/local/lib/libruby.so.3.2(rb_print_backtrace+0x11) [0x7fd130d31280] vm_dump.c:785
2023-03-03T13:53:57.6672488Z /usr/local/lib/libruby.so.3.2(rb_vm_bugreport) vm_dump.c:1080
2023-03-03T13:53:57.6672844Z /usr/local/lib/libruby.so.3.2(rb_bug_for_fatal_signal+0xf0) [0x7fd130b505b0] error.c:813
2023-03-03T13:53:57.6673200Z /usr/local/lib/libruby.so.3.2(sigsegv+0x4b) [0x7fd130c94aab] signal.c:964
2023-03-03T13:53:57.6673817Z /lib/x86_64-linux-gnu/libpthread.so.0(__restore_rt+0x0) [0x7fd13077d730]
2023-03-03T13:53:57.6674205Z /lib/x86_64-linux-gnu/libc.so.6(abort+0x1fd) [0x7fd1305cd611]
2023-03-03T13:53:57.6674600Z /usr/lib/x86_64-linux-gnu/libstdc++.so.6(0x7fd129c31983) [0x7fd129c31983]
2023-03-03T13:53:57.6675004Z /usr/lib/x86_64-linux-gnu/libstdc++.so.6(0x7fd129c378c6) [0x7fd129c378c6]
2023-03-03T13:53:57.6675440Z /usr/lib/x86_64-linux-gnu/libstdc++.so.6(_ZSt9terminatev+0x11) [0x7fd129c37901]
2023-03-03T13:53:57.6675861Z /usr/lib/x86_64-linux-gnu/libstdc++.so.6(__cxa_throw+0x44) [0x7fd129c37b34]
2023-03-03T13:53:57.6684047Z /usr/local/bundle/gems/field_test-0.5.4/lib/field_test/ext.so(_ZSt20__throw_bad_any_castv+0x32) [0x7fd128ed21c2]
2023-03-03T13:53:57.6684863Z /usr/local/bundle/gems/fasttext-0.2.4/lib/fasttext/ext.so(_ZN4Rice6detail12RubyFunctionIPFmPKcEmJS3_EEclEv+0x62) [0x7fd128da0c72]
2023-03-03T13:53:57.6685406Z /usr/local/bundle/gems/fasttext-0.2.4/lib/fasttext/ext.so(Init_ext+0x48) [0x7fd128d98bf8]
2023-03-03T13:53:57.6685771Z /usr/local/lib/libruby.so.3.2(dln_load+0x127) [0x7fd130aba927] dln.c:472
2023-03-03T13:53:57.6686239Z /usr/local/lib/libruby.so.3.2(rb_vm_pop_frame+0x0) [0x7fd130d184e1] vm.c:2679
2023-03-03T13:53:57.6686546Z /usr/local/lib/libruby.so.3.2(rb_vm_call_cfunc) vm.c:2681
2023-03-03T13:53:57.6687037Z /usr/local/lib/libruby.so.3.2(rb_long2num_inline+0x0) [0x7fd130bbde95] load.c:1208
2023-03-03T13:53:57.6687372Z /usr/local/lib/libruby.so.3.2(require_internal) load.c:1210
2023-03-03T13:53:57.6687700Z /usr/local/lib/libruby.so.3.2(rb_require_string+0x3d) [0x7fd130bbeacd] load.c:1293
2023-03-03T13:53:57.6688073Z /usr/local/lib/libruby.so.3.2(vm_call_cfunc_with_frame+0xe2) [0x7fd130d0cf82] vm_insnhelper.c:3252
2023-03-03T13:53:57.6688466Z /usr/local/lib/libruby.so.3.2(vm_call_method_each_type+0xe6) [0x7fd130d27ff6] vm_insnhelper.c:3904
2023-03-03T13:53:57.6688842Z /usr/local/lib/libruby.so.3.2(vm_call_alias+0x69) [0x7fd130d28e29] vm_insnhelper.c:3431
2023-03-03T13:53:57.6689205Z /usr/local/lib/libruby.so.3.2(vm_sendish+0x0) [0x7fd130d1bdb3] vm_insnhelper.c:5069
2023-03-03T13:53:57.6689804Z /usr/local/lib/libruby.so.3.2(vm_exec_core) insns.def:922
2023-03-03T13:53:57.6690111Z /usr/local/lib/libruby.so.3.2(rb_vm_exec+0xbb) [0x7fd130d1f75b] vm.c:2374
2023-03-03T13:53:57.6690453Z /usr/local/lib/libruby.so.3.2(load_iseq_eval+0x21) [0x7fd130bbe022] load.c:697
2023-03-03T13:53:57.6690775Z /usr/local/lib/libruby.so.3.2(require_internal) load.c:1202
2023-03-03T13:53:57.6691107Z /usr/local/lib/libruby.so.3.2(rb_require_string+0x3d) [0x7fd130bbeacd] load.c:1293
2023-03-03T13:53:57.6691473Z /usr/local/lib/libruby.so.3.2(vm_call_cfunc_with_frame+0xe2) [0x7fd130d0cf82] vm_insnhelper.c:3252
2023-03-03T13:53:57.6691868Z /usr/local/lib/libruby.so.3.2(vm_call_method_each_type+0xe6) [0x7fd130d27ff6] vm_insnhelper.c:3904
2023-03-03T13:53:57.6692238Z /usr/local/lib/libruby.so.3.2(vm_call_alias+0x69) [0x7fd130d28e29] vm_insnhelper.c:3431
2023-03-03T13:53:57.6692598Z /usr/local/lib/libruby.so.3.2(vm_sendish+0x0) [0x7fd130d1a21d] vm_insnhelper.c:5064
2023-03-03T13:53:57.6692952Z /usr/local/lib/libruby.so.3.2(vm_exec_core) insns.def:820
2023-03-03T13:53:57.6693251Z /usr/local/lib/libruby.so.3.2(rb_vm_exec+0xbb) [0x7fd130d1f75b] vm.c:2374
2023-03-03T13:53:57.6693582Z /usr/local/lib/libruby.so.3.2(invoke_block+0x60) [0x7fd130d2c70e] vm.c:1398
2023-03-03T13:53:57.6693958Z /usr/local/lib/libruby.so.3.2(rb_yield) vm.c:1454
2023-03-03T13:53:57.6694271Z /usr/local/lib/libruby.so.3.2(RB_FL_TEST_RAW+0x0) [0x7fd130abc72c] array.c:2740
2023-03-03T13:53:57.6694625Z /usr/local/lib/libruby.so.3.2(RB_FL_ANY_RAW) ./include/ruby/internal/fl_type.h:552
2023-03-03T13:53:57.6694980Z /usr/local/lib/libruby.so.3.2(rb_array_len) ./include/ruby/internal/core/rarray.h:321
2023-03-03T13:53:57.6695301Z /usr/local/lib/libruby.so.3.2(rb_ary_each) array.c:2739
2023-03-03T13:53:57.6695642Z /usr/local/lib/libruby.so.3.2(vm_call_cfunc_with_frame+0xe2) [0x7fd130d0cf82] vm_insnhelper.c:3252
2023-03-03T13:53:57.6696010Z /usr/local/lib/libruby.so.3.2(vm_sendish+0x0) [0x7fd130d1a327] vm_insnhelper.c:5064
2023-03-03T13:53:57.6696322Z /usr/local/lib/libruby.so.3.2(vm_exec_core) insns.def:801
2023-03-03T13:53:57.6696628Z /usr/local/lib/libruby.so.3.2(rb_vm_exec+0xbb) [0x7fd130d1f75b] vm.c:2374
2023-03-03T13:53:57.6696948Z /usr/local/lib/libruby.so.3.2(invoke_block+0x60) [0x7fd130d2c70e] vm.c:1398
2023-03-03T13:53:57.6697248Z /usr/local/lib/libruby.so.3.2(rb_yield) vm.c:1454
2023-03-03T13:53:57.6697688Z /usr/local/lib/libruby.so.3.2(RB_FL_TEST_RAW+0x0) [0x7fd130abc72c] array.c:2740
2023-03-03T13:53:57.6698045Z /usr/local/lib/libruby.so.3.2(RB_FL_ANY_RAW) ./include/ruby/internal/fl_type.h:552
2023-03-03T13:53:57.6698408Z /usr/local/lib/libruby.so.3.2(rb_array_len) ./include/ruby/internal/core/rarray.h:321
2023-03-03T13:53:57.6698740Z /usr/local/lib/libruby.so.3.2(rb_ary_each) array.c:2739
2023-03-03T13:53:57.6699211Z /usr/local/lib/libruby.so.3.2(vm_call_cfunc_with_frame+0xe2) [0x7fd130d0cf82] vm_insnhelper.c:3252
2023-03-03T13:53:57.6699583Z /usr/local/lib/libruby.so.3.2(vm_sendish+0x0) [0x7fd130d1a327] vm_insnhelper.c:5064
2023-03-03T13:53:57.6700035Z /usr/local/lib/libruby.so.3.2(vm_exec_core) insns.def:801
2023-03-03T13:53:57.6700352Z /usr/local/lib/libruby.so.3.2(rb_vm_exec+0xbb) [0x7fd130d1f75b] vm.c:2374
2023-03-03T13:53:57.6700681Z /usr/local/lib/libruby.so.3.2(load_iseq_eval+0x21) [0x7fd130bbe022] load.c:697
2023-03-03T13:53:57.6700995Z /usr/local/lib/libruby.so.3.2(require_internal) load.c:1202
2023-03-03T13:53:57.6701328Z /usr/local/lib/libruby.so.3.2(rb_require_string+0x3d) [0x7fd130bbeacd] load.c:1293
2023-03-03T13:53:57.6701701Z /usr/local/lib/libruby.so.3.2(vm_call_cfunc_with_frame+0xe2) [0x7fd130d0cf82] vm_insnhelper.c:3252
2023-03-03T13:53:57.6702072Z /usr/local/lib/libruby.so.3.2(vm_sendish+0x0) [0x7fd130d1a21d] vm_insnhelper.c:5064
2023-03-03T13:53:57.6703082Z /usr/local/lib/libruby.so.3.2(vm_exec_core) insns.def:820
2023-03-03T13:53:57.6703403Z /usr/local/lib/libruby.so.3.2(rb_vm_exec+0xbb) [0x7fd130d1f75b] vm.c:2374
2023-03-03T13:53:57.6703741Z /usr/local/lib/libruby.so.3.2(raise_load_if_failed+0x0) [0x7fd130bbd1d4] load.c:697
2023-03-03T13:53:57.6704197Z /usr/local/lib/libruby.so.3.2(rb_load_internal) load.c:760
2023-03-03T13:53:57.6704486Z /usr/local/lib/libruby.so.3.2(rb_f_load) load.c:833
2023-03-03T13:53:57.6704828Z /usr/local/lib/libruby.so.3.2(vm_call_cfunc_with_frame+0xe2) [0x7fd130d0cf82] vm_insnhelper.c:3252
2023-03-03T13:53:57.6705207Z /usr/local/lib/libruby.so.3.2(vm_call_method_each_type+0xe6) [0x7fd130d27ff6] vm_insnhelper.c:3904
2023-03-03T13:53:57.6705594Z /usr/local/lib/libruby.so.3.2(vm_call_method+0xc4) [0x7fd130d289b4] vm_insnhelper.c:4054
2023-03-03T13:53:57.6705957Z /usr/local/lib/libruby.so.3.2(vm_sendish+0x0) [0x7fd130d1a21d] vm_insnhelper.c:5064
2023-03-03T13:53:57.6706272Z /usr/local/lib/libruby.so.3.2(vm_exec_core) insns.def:820
2023-03-03T13:53:57.6706581Z /usr/local/lib/libruby.so.3.2(rb_vm_exec+0x706) [0x7fd130d1fda6] vm.c:2383
2023-03-03T13:53:57.6706914Z /usr/local/lib/libruby.so.3.2(load_iseq_eval+0x21) [0x7fd130bbe022] load.c:697
2023-03-03T13:53:57.6707224Z /usr/local/lib/libruby.so.3.2(require_internal) load.c:1202
2023-03-03T13:53:57.6707548Z /usr/local/lib/libruby.so.3.2(rb_require_string+0x3d) [0x7fd130bbeacd] load.c:1293
2023-03-03T13:53:57.6707915Z /usr/local/lib/libruby.so.3.2(vm_call_cfunc_with_frame+0xe2) [0x7fd130d0cf82] vm_insnhelper.c:3252
2023-03-03T13:53:57.6708342Z /usr/local/lib/libruby.so.3.2(vm_call_method_each_type+0xe6) [0x7fd130d27ff6] vm_insnhelper.c:3904
2023-03-03T13:53:57.6708712Z /usr/local/lib/libruby.so.3.2(vm_call_alias+0x69) [0x7fd130d28e29] vm_insnhelper.c:3431
2023-03-03T13:53:57.6709082Z /usr/local/lib/libruby.so.3.2(vm_call_method_each_type+0x2be) [0x7fd130d281ce] vm_insnhelper.c:3965
2023-03-03T13:53:57.6709453Z /usr/local/lib/libruby.so.3.2(vm_call_method+0xc4) [0x7fd130d289b4] vm_insnhelper.c:4054
2023-03-03T13:53:57.6728422Z /usr/local/lib/libruby.so.3.2(vm_sendish+0x0) [0x7fd130d1a21d] vm_insnhelper.c:5064
2023-03-03T13:53:57.6729186Z /usr/local/lib/libruby.so.3.2(vm_exec_core) insns.def:820
2023-03-03T13:53:57.6729502Z /usr/local/lib/libruby.so.3.2(rb_vm_exec+0xbb) [0x7fd130d1f75b] vm.c:2374
2023-03-03T13:53:57.6730016Z /usr/local/lib/libruby.so.3.2(rb_ec_exec_node+0xab) [0x7fd130b5548b] eval.c:289
2023-03-03T13:53:57.6730347Z /usr/local/lib/libruby.so.3.2(ruby_run_node+0x8b) [0x7fd130b5b77b] eval.c:330
2023-03-03T13:53:57.6730659Z /usr/local/bin/ruby(rb_main+0x21) [0x5628770d5112] ./main.c:38
2023-03-03T13:53:57.6730915Z /usr/local/bin/ruby(main) ./main.c:57
2023-03-03T13:53:57.6731341Z /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xeb) [0x7fd1305cf09b]
2023-03-03T13:53:57.6731594Z [0x5628770d515a]
2023-03-03T13:53:57.6731710Z

Rails 6 Support

Hi.

Rails 6 is out so would be nice to update this library.

Regards

Rows re-generated after reloading dashboard

First let me say how absolutely wonderful this framework is. It just works. So beautifully simple.

I'm noticing some strange ghostly behavior: if I go in my local database and delete all the rows from field_test_memberships, then refresh the dashboard, the rows are automatically regenerated with the last known values.

How is it doing this, and how can I permanently clear testing data?

Dynamic Variants?

Is it possible to dynamically assign variants to an experiment?

My use case is that our platform has a host of clients that have various landing pages. I would prefer to slug each landing page name under a client. For instance

experiments:
  client_a_landing_pages:
    variants:
      - landing_page_a
      - landing_page_b

But variants would also be dynamic set via an initializer rather than a YAML file.

Experiment showing 0 conversions

Experiment is showing 0 conversions, when I click on details I can see that there are participants which converted.

`experiments:
landing_page:
variants:
- 0
- 1

exclude:
bots: true
`

FieldTest::Membership.last
=> #<FieldTest::Membership id: 27, participant_type: "User", participant_id: "2", experiment: "landing_page", variant: "0", created_at: "2022-12-18 10:24:37.263690000 +0000", converted: true>

experiment = FieldTest::Experiment.find(:landing_page)
experiment.results
=> {0=>{:participated=>0, :converted=>0, :conversion_rate=>nil, :prob_winning=>0.5}, 1=>{:participated=>0, :converted=>0, :conversion_rate=>nil, :prob_winning=>0.5}}

FieldTest::Experiment.all
=> [#<FieldTest::Experiment:0x00007f78d3527210 @id="landing_page", @name="Landing Page", @description=nil, @Variants=[0, 1], @Weights=[1, 1], @winner=nil, @closed=nil, @keep_variant=nil, @GOALS=["conversion"], @goals_defined=false, @use_events=nil>]

Filter by date on primary dashboard

As an admin, I might want to segment by only experiment participants from the past 24 hours, or a specific period.

This could help determine whether users convert in the same experiment better on Mondays vs Saturdays, etc.

Performance improvement for 0.5

Before

Warming up --------------------------------------
      prob_b_beats_a    10.000  i/100ms
prob_c_beats_a_and_b     1.000  i/100ms
Calculating -------------------------------------
      prob_b_beats_a    113.439  (± 2.6%) i/s -    570.000  in   5.027763s
prob_c_beats_a_and_b      4.804  (± 0.0%) i/s -     25.000  in   5.205448s

After (calculations rewritten in C++)

Warming up --------------------------------------
      prob_b_beats_a   873.000  i/100ms
prob_c_beats_a_and_b    14.000  i/100ms
Calculating -------------------------------------
      prob_b_beats_a      8.854k (± 2.5%) i/s -     44.523k in   5.031834s
prob_c_beats_a_and_b    139.537  (± 3.6%) i/s -    700.000  in   5.024210s

~78x improvement for prob_b_beats_a and ~29x improvement for prob_c_beats_a_and_b

AB Test on specific controller?

I noticed in query stack trace, there always FieldTest::Membership Load etc.. everywhere, while I only use fieldtest for 1 controller. Can we specify which controller to do ab test on?

PG::AmbiguousColumn when using /field_tests route

I have the Engine installed in my routes.rb file:
mount FieldTest::Engine, at: "field_test"

When I navigate to http://localhost:3000/field_test, I see . . .
PG::AmbiguousColumn: ERROR: column reference "created_at" is ambiguous
LINE 1: ...E "field_test_memberships"."experiment" = $1 AND (created_at...
                                                             ^

I have Rails 5.1.5 and a completely ordinary Postgres setup.

It seems like this should be a simple fix. Or am I missing something obvious??

screen shot 2018-03-13 at 2 27 23 am

Feature toggle, rollout library

hey @ankane thank you for all these fantastic gems, I've been using field_test and ahoy for a while now.
I'm sure this is not the best place to ask, but bear with me :)
through all of those 280 repos you have, did you happen to build a feature toggle and/or gradual rollout library that would complement field_test?

Unable to install gem due to "Could not find rice/rice.hpp header"

Hello!! am having the following issue when i try bundle install, it's related to rice dependency, for some reason am unable to install the gem, what i saw trying to debug the problem it's related to clang compiler but i really don't understand what i need to update or install in my computer to succeed a installation:

Ty for your help.

rice-4.0.2/lib/mkmf-rice.rb:120:in <top (required)>': Could not find rice/rice.hpp header`

MacBook Pro (Retina, 13-inch, Early 2015)
MacOS Mojave version 10.14.6 (18G9323)

Ruby and Rails versions

ruby "2.6.8"

gem "rails", "~> 6.0.4.1"

Clang Version

clang --version
Apple clang version 11.0.0 (clang-1100.0.33.17)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin


llvm-g++ --version
Apple clang version 11.0.0 (clang-1100.0.33.17)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

Error trace

current directory: /Users/edgarLH/.rbenv/versions/2.6.8/gemsets/rails_6_new/gems/field_test-0.5.1/ext/field_test
/Users/edgarLH/.rbenv/versions/2.6.8/bin/ruby -I /Users/edgarLH/.rbenv/versions/2.6.8/lib/ruby/2.6.0 -r ./siteconf20211004-75138-1l4lrqy.rb extconf.rb
/Users/edgarLH/.rbenv/versions/2.6.8/gemsets/rails_6_new/gems/rice-4.0.2/lib/mkmf-rice.rb:16: warning: already initialized constant
MakeMakefile::CONFTEST_C
/Users/edgarLH/.rbenv/versions/2.6.8/lib/ruby/2.6.0/mkmf.rb:259: warning: previous definition of CONFTEST_C was here
checking for rice/rice.hpp in /Users/edgarLH/.rbenv/versions/2.6.8/gemsets/rails_6_new/gems/rice-4.0.2/include... no
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
	--with-opt-dir
	--without-opt-dir
	--with-opt-include
	--without-opt-include=${opt-dir}/include
	--with-opt-lib
	--without-opt-lib=${opt-dir}/lib
	--with-make-prog
	--without-make-prog
	--srcdir=.
	--curdir
	--ruby=/Users/edgarLH/.rbenv/versions/2.6.8/bin/$(RUBY_BASE_NAME)
/Users/edgarLH/.rbenv/versions/2.6.8/gemsets/rails_6_new/gems/rice-4.0.2/lib/mkmf-rice.rb:120:in `<top (required)>': Could not find rice/rice.hpp header
(RuntimeError)
	from /Users/edgarLH/.rbenv/versions/2.6.8/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:130:in `require'
	from /Users/edgarLH/.rbenv/versions/2.6.8/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:130:in `rescue in require'
	from /Users/edgarLH/.rbenv/versions/2.6.8/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:34:in `require'
	from extconf.rb:1:in `<main>'
/Users/edgarLH/.rbenv/versions/2.6.8/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require': cannot load such file -- mkmf-rice (LoadError)
	from /Users/edgarLH/.rbenv/versions/2.6.8/lib/ruby/2.6.0/rubygems/core_ext/kernel_require.rb:54:in `require'
	from extconf.rb:1:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /Users/edgarLH/.rbenv/versions/2.6.8/gemsets/rails_6_new/extensions/x86_64-darwin-18/2.6.0/field_test-0.5.1/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /Users/edgarLH/.rbenv/versions/2.6.8/gemsets/rails_6_new/gems/field_test-0.5.1 for inspection.
Results logged to /Users/edgarLH/.rbenv/versions/2.6.8/gemsets/rails_6_new/extensions/x86_64-darwin-18/2.6.0/field_test-0.5.1/gem_make.out

An error occurred while installing field_test (0.5.1), and Bundler cannot continue.
Make sure that `gem install field_test -v '0.5.1' --source 'http://rubygems.org/'` succeeds before bundling.

mkmf.log file
mkmf.log

Arbitrary Variants Via Query Parameters in Field Test 0.3.0

Due to unvalidated input, an attacker can pass in arbitrary variants via query parameters. This vulnerability has been assigned the CVE identifier CVE-2019-13146.

Versions Affected: 0.3.0
Fixed Versions: 0.3.1
Versions Unaffected: < 0.3.0

Impact

If an application treats variants as trusted, this can lead to a variety of potential vulnerabilities like SQL injection or cross-site scripting (XSS). For instance:

landing_page = field_test(:landing_page)
Page.where("key = '#{landing_page}'")

All users running an affected release should upgrade immediately.

YAML.load permitted_classes

Issue

When upgrading Ruby from 3.0 to 3.1 for a Rails (7.0.4.3) app, I started having the following error on CI:

     Psych::DisallowedClass:
       Tried to load unspecified class: Date

This is an issue related to a CVE on YAML.load (ref: https://discuss.rubyonrails.org/t/cve-2022-32224-possible-rce-escalation-bug-with-serialized-columns-in-active-record/81017).
The Rails app already have a bypass to this issue, using:

    config.active_record.use_yaml_unsafe_load = true

Here is ActiveRecord changes (for the CVE above): rails/rails@9529dc8#diff-935494b14b54c907fc1d206871d2a4797c490b80f2c86d61a7264131dd492e42R48-R57

Error in field_test gem

The CI is breaking on this line: https://github.com/ankane/field_test/blob/master/lib/field_test.rb#LL32C5-L32C86

Detail: this is line is different from release 0.5.5 in Rubygems, but still I get the CI issue above.

Possible solutions

  1. Incorporate permitted_classes: [Date] (and possible others) to YAML.safe_load(...)
  2. Allow it (aka permitted_classes) to be configured in the hosting (Rails) app (BTW: aliases: true could also be refactored to be configured and perhaps its default to be true, as the current version not released already assume that behavior.)
  3. Allow usage of YAML.unsafe_load as a fallback

IMO: item 2 above is the best call.
Let me know if you have a preference of how to go about it.
I'd be happy to work on it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.