Commit a40517e8 authored by Nikola Milojevic's avatar Nikola Milojevic

Merge branch '335331-ensure-experiment-is-in-gon-object' into 'master'

Ensure trial status popover events include gitlab_experiment context

See merge request gitlab-org/gitlab!69327
parents e31a494e 708d7fd9
...@@ -3,7 +3,12 @@ import { get } from 'lodash'; ...@@ -3,7 +3,12 @@ import { get } from 'lodash';
import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants'; import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants';
function getExperimentsData() { function getExperimentsData() {
return get(window, ['gon', 'experiment'], {}); // Pull from deprecated window.gon.experiment
const experimentsFromGon = get(window, ['gon', 'experiment'], {});
// Pull from preferred window.gl.experiments
const experimentsFromGl = get(window, ['gl', 'experiments'], {});
return { ...experimentsFromGon, ...experimentsFromGl };
} }
function convertExperimentDataToExperimentContext(experimentData) { function convertExperimentDataToExperimentContext(experimentData) {
......
= javascript_tag(nonce: content_security_policy_nonce) do
:plain
gl = window.gl || {};
gl.experiments = #{raw ApplicationExperiment.published_experiments.reject { |name, data| data[:excluded] }.to_json};
...@@ -16,4 +16,5 @@ ...@@ -16,4 +16,5 @@
= render 'layouts/img_loader' = render 'layouts/img_loader'
= render 'layouts/published_experiments'
= yield :scripts_body = yield :scripts_body
...@@ -92,7 +92,7 @@ end ...@@ -92,7 +92,7 @@ end
``` ```
When this code executes, the experiment is run, a variant is assigned, and (if within a When this code executes, the experiment is run, a variant is assigned, and (if within a
controller or view) a `window.gon.experiment.pill_color` object will be available in the controller or view) a `window.gl.experiments.pill_color` object will be available in the
client layer, with details like: client layer, with details like:
- The assigned variant. - The assigned variant.
...@@ -522,14 +522,14 @@ shared example: [tracks assignment and records the subject](https://gitlab.com/g ...@@ -522,14 +522,14 @@ shared example: [tracks assignment and records the subject](https://gitlab.com/g
This is in flux as of GitLab 13.10, and can't be documented just yet. This is in flux as of GitLab 13.10, and can't be documented just yet.
Any experiment that's been run in the request lifecycle surfaces in `window.gon.experiment`, Any experiment that's been run in the request lifecycle surfaces in and `window.gl.experiments`,
and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-0) and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-0)
so you can use it when resolving some concepts around experimentation in the client layer. so you can use it when resolving some concepts around experimentation in the client layer.
### Use experiments in Vue ### Use experiments in Vue
With the `gitlab-experiment` component, you can define slots that match the name of the With the `gitlab-experiment` component, you can define slots that match the name of the
variants pushed to `window.gon.experiment`. For example, if we alter the `pill_color` variants pushed to `window.gl.experiments`. For example, if we alter the `pill_color`
experiment to just use the default variants of `control` and `candidate` like so: experiment to just use the default variants of `control` and `candidate` like so:
```ruby ```ruby
...@@ -587,7 +587,51 @@ For example, the Vue component for the previously-defined `pill_color` experimen ...@@ -587,7 +587,51 @@ For example, the Vue component for the previously-defined `pill_color` experimen
``` ```
NOTE: NOTE:
When there is no experiment data in the `window.gon.experiment` object for the given experiment name, the `control` slot will be used, if it exists. When there is no experiment data in the `window.gl.experiments` object for the given experiment name, the `control` slot will be used, if it exists.
## Test with Jest
### Stub Helpers
You can stub experiments using the `stubExperiments` helper defined in `spec/frontend/__helpers__/experimentation_helper.js`.
```javascript
import { stubExperiments } from 'helpers/experimentation_helper';
import { getExperimentData } from '~/experimentation/utils';
describe('when my_experiment is enabled', () => {
beforeEach(() => {
stubExperiments({ my_experiment: 'candidate' });
});
it('sets the correct data', () => {
expect(getExperimentData('my_experiment')).toEqual({ experiment: 'my_experiment', variant: 'candidate' });
});
});
```
NOTE:
This method of stubbing in Jest specs will not automatically un-stub itself at the end of the test. We merge our stubbed experiment in with all the other global data in `window.gl`. If you need to remove the stubbed experiment(s) after your test or ensure a clean global object before your test, you'll need to manage the global object directly yourself:
```javascript
desribe('tests that care about global state', () => {
const originalObjects = [];
beforeEach(() => {
// For backwards compatibility for now, we're using both window.gon & window.gl
originalObjects.push(window.gon, window.gl);
});
afterEach(() => {
[window.gon, window.gl] = originalObjects;
});
it('stubs experiment in fresh global state', () => {
stubExperiment({ my_experiment: 'candidate' });
// ...
});
})
```
## Notes on feature flags ## Notes on feature flags
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MrWidgetEnableFeaturePrompt from 'ee/vue_merge_request_widget/components/states/mr_widget_enable_feature_prompt.vue'; import MrWidgetEnableFeaturePrompt from 'ee/vue_merge_request_widget/components/states/mr_widget_enable_feature_prompt.vue';
import { assignGitlabExperiment } from 'helpers/experimentation_helper'; import { stubExperiments } from 'helpers/experimentation_helper';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
const FEATURE = 'my_feature_name'; const FEATURE = 'my_feature_name';
...@@ -30,19 +30,16 @@ describe('MrWidgetEnableFeaturePrompt', () => { ...@@ -30,19 +30,16 @@ describe('MrWidgetEnableFeaturePrompt', () => {
}); });
describe('when the experiment is not enabled', () => { describe('when the experiment is not enabled', () => {
beforeAll(() => {
assignGitlabExperiment(FEATURE, 'control');
});
it('renders nothing', () => { it('renders nothing', () => {
stubExperiments({ [FEATURE]: 'control' });
expect(wrapper.text()).toBe(''); expect(wrapper.text()).toBe('');
}); });
}); });
describe('when the experiment is enabled', () => { describe('when the experiment is enabled', () => {
beforeAll(() => { beforeAll(() => {
stubExperiments({ [FEATURE]: 'candidate' });
localStorage.removeItem(LOCAL_STORAGE_KEY); localStorage.removeItem(LOCAL_STORAGE_KEY);
assignGitlabExperiment(FEATURE, 'candidate');
}); });
it('shows a neutral icon', () => { it('shows a neutral icon', () => {
......
...@@ -24,7 +24,7 @@ import { ...@@ -24,7 +24,7 @@ import {
coverageFuzzingDiffSuccessMock, coverageFuzzingDiffSuccessMock,
apiFuzzingDiffSuccessMock, apiFuzzingDiffSuccessMock,
} from 'ee_jest/vue_shared/security_reports/mock_data'; } from 'ee_jest/vue_shared/security_reports/mock_data';
import { assignGitlabExperiment } from 'helpers/experimentation_helper'; import { stubExperiments } from 'helpers/experimentation_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
...@@ -221,7 +221,9 @@ describe('ee merge request widget options', () => { ...@@ -221,7 +221,9 @@ describe('ee merge request widget options', () => {
}); });
describe('security_reports_mr_widget_prompt experiment', () => { describe('security_reports_mr_widget_prompt experiment', () => {
assignGitlabExperiment('security_reports_mr_widget_prompt', 'candidate'); beforeEach(() => {
stubExperiments({ security_reports_mr_widget_prompt: 'candidate' });
});
it('prompts to enable the feature', () => { it('prompts to enable the feature', () => {
createComponent({ propsData: { mrData: mockData } }); createComponent({ propsData: { mrData: mockData } });
......
import { merge } from 'lodash'; import { merge } from 'lodash';
// This helper is for specs that use `gitlab/experimentation` module
export function withGonExperiment(experimentKey, value = true) { export function withGonExperiment(experimentKey, value = true) {
let origGon; let origGon;
...@@ -12,16 +13,26 @@ export function withGonExperiment(experimentKey, value = true) { ...@@ -12,16 +13,26 @@ export function withGonExperiment(experimentKey, value = true) {
window.gon = origGon; window.gon = origGon;
}); });
} }
// This helper is for specs that use `gitlab-experiment` utilities, which have a different schema that gets pushed via Gon compared to `Experimentation Module`
export function assignGitlabExperiment(experimentKey, variant) {
let origGon;
beforeEach(() => { // The following helper is for specs that use `gitlab-experiment` utilities,
origGon = window.gon; // which have a different schema that gets pushed to the frontend compared to
window.gon = { experiment: { [experimentKey]: { variant } } }; // the `Experimentation` Module.
}); //
// Usage: stubExperiments({ experiment_feature_flag_name: 'variant_name', ... })
export function stubExperiments(experiments = {}) {
// Deprecated
window.gon = window.gon || {};
window.gon.experiment = window.gon.experiment || {};
// Preferred
window.gl = window.gl || {};
window.gl.experiments = window.gl.experiemnts || {};
afterEach(() => { Object.entries(experiments).forEach(([name, variant]) => {
window.gon = origGon; const experimentData = { experiment: name, variant };
// Deprecated
window.gon.experiment[name] = experimentData;
// Preferred
window.gl.experiments[name] = experimentData;
}); });
} }
...@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; ...@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import NewBoardButton from '~/boards/components/new_board_button.vue'; import NewBoardButton from '~/boards/components/new_board_button.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { assignGitlabExperiment } from 'helpers/experimentation_helper'; import { stubExperiments } from 'helpers/experimentation_helper';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
const FEATURE = 'prominent_create_board_btn'; const FEATURE = 'prominent_create_board_btn';
...@@ -28,7 +28,9 @@ describe('NewBoardButton', () => { ...@@ -28,7 +28,9 @@ describe('NewBoardButton', () => {
}); });
describe('control variant', () => { describe('control variant', () => {
assignGitlabExperiment(FEATURE, 'control'); beforeAll(() => {
stubExperiments({ [FEATURE]: 'control' });
});
it('renders nothing', () => { it('renders nothing', () => {
wrapper = createComponent(); wrapper = createComponent();
...@@ -38,7 +40,9 @@ describe('NewBoardButton', () => { ...@@ -38,7 +40,9 @@ describe('NewBoardButton', () => {
}); });
describe('candidate variant', () => { describe('candidate variant', () => {
assignGitlabExperiment(FEATURE, 'candidate'); beforeAll(() => {
stubExperiments({ [FEATURE]: 'candidate' });
});
it('renders New board button when `candidate` variant', () => { it('renders New board button when `candidate` variant', () => {
wrapper = createComponent(); wrapper = createComponent();
......
This diff is collapsed.
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'layouts/_published_experiments', :experiment do
before do
stub_const('TestControlExperiment', ApplicationExperiment)
stub_const('TestCandidateExperiment', ApplicationExperiment)
stub_const('TestExcludedExperiment', ApplicationExperiment)
TestControlExperiment.new('test_control').tap do |e|
e.variant(:control)
e.publish
end
TestCandidateExperiment.new('test_candidate').tap do |e|
e.variant(:candidate)
e.publish
end
TestExcludedExperiment.new('test_excluded').tap do |e|
e.exclude!
e.publish
end
render
end
it 'renders out data for all non-excluded, published experiments' do
output = rendered
expect(output).to include('gl.experiments = {')
expect(output).to match(/"test_control":\{[^}]*"variant":"control"/)
expect(output).to match(/"test_candidate":\{[^}]*"variant":"candidate"/)
expect(output).not_to include('"test_excluded"')
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment