summaryrefslogtreecommitdiff
path: root/doc/development/experiment_guide/experimentation.md
diff options
context:
space:
mode:
Diffstat (limited to 'doc/development/experiment_guide/experimentation.md')
-rw-r--r--doc/development/experiment_guide/experimentation.md399
1 files changed, 399 insertions, 0 deletions
diff --git a/doc/development/experiment_guide/experimentation.md b/doc/development/experiment_guide/experimentation.md
new file mode 100644
index 00000000000..7135f8acd9b
--- /dev/null
+++ b/doc/development/experiment_guide/experimentation.md
@@ -0,0 +1,399 @@
+---
+stage: Growth
+group: Activation
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Create an A/B test with `Experimentation Module`
+
+## Implement the experiment
+
+1. Add the experiment to the `Gitlab::Experimentation::EXPERIMENTS` hash in
+ [`experimentation.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib%2Fgitlab%2Fexperimentation.rb):
+
+ ```ruby
+ EXPERIMENTS = {
+ other_experiment: {
+ #...
+ },
+ # Add your experiment here:
+ signup_flow: {
+ tracking_category: 'Growth::Activation::Experiment::SignUpFlow' # Used for providing the category when setting up tracking data
+ }
+ }.freeze
+ ```
+
+1. Use the experiment in the code.
+
+ Experiments can be performed on a `subject`. The provided `subject` should
+ respond to `to_global_id` or `to_s`.
+ The resulting string is bucketed and assigned to either the control or the
+ experimental group, so you must always provide the same `subject`
+ for an experiment to have the same experience.
+
+ 1. Use this standard for the experiment in a controller:
+
+ - Experiment run for a user:
+
+ ```ruby
+ class ProjectController < ApplicationController
+ def show
+ # experiment_enabled?(:experiment_key) is also available in views and helpers
+ if experiment_enabled?(:signup_flow, subject: current_user)
+ # render the experiment
+ else
+ # render the original version
+ end
+ end
+ end
+ ```
+
+ - Experiment run for a namespace:
+
+ ```ruby
+ if experiment_enabled?(:signup_flow, subject: namespace)
+ # experiment code
+ else
+ # control code
+ end
+ ```
+
+ When no subject is given, it falls back to a cookie that gets set and is consistent until
+ the cookie gets deleted.
+
+ ```ruby
+ class RegistrationController < ApplicationController
+ def show
+ # falls back to a cookie
+ if experiment_enabled?(:signup_flow)
+ # render the experiment
+ else
+ # render the original version
+ end
+ end
+ end
+ ```
+
+ 1. Make the experiment available to the frontend in a controller. This example
+ checks whether the experiment is enabled and pushes the result to the frontend:
+
+ ```ruby
+ before_action do
+ push_frontend_experiment(:signup_flow, subject: current_user)
+ end
+ ```
+
+ You can check the state of the feature flag in JavaScript:
+
+ ```javascript
+ import { isExperimentEnabled } from '~/experimentation';
+
+ if ( isExperimentEnabled('signupFlow') ) {
+ // ...
+ }
+ ```
+
+You can also run an experiment outside of the controller scope, such as in a worker:
+
+```ruby
+class SomeWorker
+ def perform
+ # Check if the experiment is active at all (the percentage_of_time_value > 0)
+ return unless Gitlab::Experimentation.active?(:experiment_key)
+
+ # Since we cannot access cookies in a worker, we need to bucket models
+ # based on a unique, unchanging attribute instead.
+ # It is therefore necessery to always provide the same subject.
+ if Gitlab::Experimentation.in_experiment_group?(:experiment_key, subject: user)
+ # execute experimental code
+ else
+ # execute control code
+ end
+ end
+end
+```
+
+## Implement tracking events
+
+To determine whether the experiment is a success or not, we must implement tracking events
+to acquire data for analyzing. We can send events to Snowplow via either the backend or frontend.
+Read the [product intelligence guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/) for more details.
+
+### Track backend events
+
+The framework provides a helper method that is available in controllers:
+
+```ruby
+before_action do
+ track_experiment_event(:signup_flow, 'action', 'value', subject: current_user)
+end
+```
+
+To test it:
+
+```ruby
+context 'when the experiment is active and the user is in the experimental group' do
+ before do
+ stub_experiment(signup_flow: true)
+ stub_experiment_for_subject(signup_flow: true)
+ end
+
+ it 'tracks an event', :snowplow do
+ subject
+
+ expect_snowplow_event(
+ category: 'Growth::Activation::Experiment::SignUpFlow',
+ action: 'action',
+ value: 'value',
+ label: 'experimentation_subject_id',
+ property: 'experimental_group'
+ )
+ end
+end
+```
+
+### Track frontend events
+
+The framework provides a helper method that is available in controllers:
+
+```ruby
+before_action do
+ push_frontend_experiment(:signup_flow, subject: current_user)
+ frontend_experimentation_tracking_data(:signup_flow, 'action', 'value', subject: current_user)
+end
+```
+
+This pushes tracking data to `gon.experiments` and `gon.tracking_data`.
+
+```ruby
+expect(Gon.experiments['signupFlow']).to eq(true)
+
+expect(Gon.tracking_data).to eq(
+ {
+ category: 'Growth::Activation::Experiment::SignUpFlow',
+ action: 'action',
+ value: 'value',
+ label: 'experimentation_subject_id',
+ property: 'experimental_group'
+ }
+)
+```
+
+To track it:
+
+```javascript
+import { isExperimentEnabled } from '~/lib/utils/experimentation';
+import Tracking from '~/tracking';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const signupFlowExperimentEnabled = isExperimentEnabled('signupFlow');
+
+ if (signupFlowExperimentEnabled && gon.tracking_data) {
+ const { category, action, ...data } = gon.tracking_data;
+
+ Tracking.event(category, action, data);
+ }
+}
+```
+
+To test it in Jest:
+
+```javascript
+import { withGonExperiment } from 'helpers/experimentation_helper';
+import Tracking from '~/tracking';
+
+describe('event tracking', () => {
+ describe('with tracking data', () => {
+ withGonExperiment('signupFlow');
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event').mockImplementation(() => {});
+
+ gon.tracking_data = {
+ category: 'Growth::Activation::Experiment::SignUpFlow',
+ action: 'action',
+ value: 'value',
+ label: 'experimentation_subject_id',
+ property: 'experimental_group'
+ };
+ });
+
+ it('should track data', () => {
+ performAction()
+
+ expect(Tracking.event).toHaveBeenCalledWith(
+ 'Growth::Activation::Experiment::SignUpFlow',
+ 'action',
+ {
+ value: 'value',
+ label: 'experimentation_subject_id',
+ property: 'experimental_group'
+ },
+ );
+ });
+ });
+});
+```
+
+## Record experiment user
+
+In addition to the anonymous tracking of events, we can also record which users
+have participated in which experiments, and whether they were given the control
+experience or the experimental experience.
+
+The `record_experiment_user` helper method is available to all controllers, and it
+enables you to record these experiment participants (the current user) and which
+experience they were given:
+
+```ruby
+before_action do
+ record_experiment_user(:signup_flow)
+end
+```
+
+Subsequent calls to this method for the same experiment and the same user have no
+effect unless the user is then enrolled into a different experience. This happens
+when we roll out the experimental experience to a greater percentage of users.
+
+This data is completely separate from the [events tracking data](#implement-tracking-events).
+They are not linked together in any way.
+
+### Add context
+
+You can add arbitrary context data in a hash which gets stored as part of the experiment
+user record. New calls to the `record_experiment_user` with newer contexts are merged
+deeply into the existing context.
+
+This data can then be used by data analytics dashboards.
+
+```ruby
+before_action do
+ record_experiment_user(:signup_flow, foo: 42, bar: { a: 22})
+ # context is { "foo" => 42, "bar" => { "a" => 22 }}
+end
+
+# Additional contexts for newer record calls are merged deeply
+record_experiment_user(:signup_flow, foo: 40, bar: { b: 2 }, thor: 3)
+# context becomes { "foo" => 40, "bar" => { "a" => 22, "b" => 2 }, "thor" => 3}
+```
+
+## Record experiment conversion event
+
+Along with the tracking of backend and frontend events and the
+[recording of experiment participants](#record-experiment-user), we can also record
+when a user performs the desired conversion event action. For example:
+
+- **Experimental experience:** Show an in-product nudge to test if the change causes more
+ people to sign up for trials.
+- **Conversion event:** The user starts a trial.
+
+The `record_experiment_conversion_event` helper method is available to all controllers.
+Use it to record the conversion event for the current user, regardless of whether
+the user is in the control or experimental group:
+
+```ruby
+before_action do
+ record_experiment_conversion_event(:signup_flow)
+end
+```
+
+Note that the use of this method requires that we have first
+[recorded the user](#record-experiment-user) as being part of the experiment.
+
+## Enable the experiment
+
+After all merge requests have been merged, use [ChatOps](../../ci/chatops/index.md) in the
+[appropriate channel](../feature_flags/controls.md#communicate-the-change) to start the experiment for 10% of the users.
+The feature flag should have the name of the experiment with the `_experiment_percentage` suffix appended.
+For visibility, share any commands run against production in the `#s_growth` channel:
+
+ ```shell
+ /chatops run feature set signup_flow_experiment_percentage 10
+ ```
+
+ If you notice issues with the experiment, you can disable the experiment by removing the feature flag:
+
+ ```shell
+ /chatops run feature delete signup_flow_experiment_percentage
+ ```
+
+## Add user to experiment group manually
+
+To force the application to add your current user into the experiment group,
+add a query string parameter to the path where the experiment runs. If you add the
+query string parameter, the experiment works only for this request, and doesn't work
+after following links or submitting forms.
+
+For example, to forcibly enable the `EXPERIMENT_KEY` experiment, add `force_experiment=EXPERIMENT_KEY`
+to the URL:
+
+```shell
+https://gitlab.com/<EXPERIMENT_ENTRY_URL>?force_experiment=<EXPERIMENT_KEY>
+```
+
+## Add user to experiment group with a cookie
+
+You can force the current user into the experiment group for `<EXPERIMENT_KEY>`
+during the browser session by using your browser's developer tools:
+
+```javascript
+document.cookie = "force_experiment=<EXPERIMENT_KEY>; path=/";
+```
+
+Use a comma to list more than one experiment to be forced:
+
+```javascript
+document.cookie = "force_experiment=<EXPERIMENT_KEY>,<ANOTHER_EXPERIMENT_KEY>; path=/";
+```
+
+To clear the experiments, unset the `force_experiment` cookie:
+
+```javascript
+document.cookie = "force_experiment=; path=/";
+```
+
+## Testing and test helpers
+
+### RSpec
+
+Use the following in RSpec to mock the experiment:
+
+```ruby
+context 'when the experiment is active' do
+ before do
+ stub_experiment(signup_flow: true)
+ end
+
+ context 'when the user is in the experimental group' do
+ before do
+ stub_experiment_for_subject(signup_flow: true)
+ end
+
+ it { is_expected.to do_experimental_thing }
+ end
+
+ context 'when the user is in the control group' do
+ before do
+ stub_experiment_for_subject(signup_flow: false)
+ end
+
+ it { is_expected.to do_control_thing }
+ end
+end
+```
+
+### Jest
+
+Use the following in Jest to mock the experiment:
+
+```javascript
+import { withGonExperiment } from 'helpers/experimentation_helper';
+
+describe('given experiment is enabled', () => {
+ withGonExperiment('signupFlow');
+
+ it('should do the experimental thing', () => {
+ expect(wrapper.find('.js-some-experiment-triggered-element')).toEqual(expect.any(Element));
+ });
+});
+```