Commit abf27874 authored by Nicolas Dular's avatar Nicolas Dular

Add experiment vue component

This adds a Vue component that can be used for experiments that are
conducted with `gitlab-experiment`.
parent 2714854b
<script>
import { getExperimentVariant } from '../utils';
export default {
props: {
name: {
type: String,
required: true,
},
},
render() {
return this.$slots?.[getExperimentVariant(this.name)];
},
};
</script>
export const TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0';
export const DEFAULT_VARIANT = 'control';
// This file only applies to use of experiments through https://gitlab.com/gitlab-org/gitlab-experiment
import { get } from 'lodash';
import { DEFAULT_VARIANT } from './constants';
export function getExperimentData(experimentName) {
return get(window, ['gon', 'experiment', experimentName]);
......@@ -8,3 +9,7 @@ export function getExperimentData(experimentName) {
export function isExperimentVariant(experimentName, variantName) {
return getExperimentData(experimentName)?.variant === variantName;
}
export function getExperimentVariant(experimentName) {
return getExperimentData(experimentName)?.variant || DEFAULT_VARIANT;
}
......@@ -501,6 +501,64 @@ Any experiment that's been run in the request lifecycle surfaces in `window.gon.
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.
### Using experiments in Vue
With the `experiment` component, you can define slots that match the name of the variants pushed to `window.gon.experiment`. For example an experiment with the default variants `control` and `candidate` could be implemented the following:
```ruby
def show
experiment(:button_color) do |e|
e.use { } # control
e.try { } # candidate
end.run
end
```
```vue
<script>
import Experiment from '~/experimentation/components/experiment.vue';
export default {
components: { Experiment }
}
</script>
<template>
<experiment name="button_name">
<template #control>
<button>Click me</button>
</template>
<template #candidate>
<button>You will not believe what happens when you click this button</button>
</template>
</experiment>
</template>
```
When using a multivariate experiment, the names of the variant names can be used, e.g. with the `pill_color` experiment from before, the Vue component would look like this:
```vue
<template>
<experiment name="pill_color">
<template #control>
<button class="bg-default">Click default button</button>
</template>
<template #red>
<button class="bg-red">Click red button</button>
</template>
<template #blue>
<button class="bg-blue">Click blue button</button>
</template>
</experiment>
</template>
```
NOTE:
When there is no experiment defined in the frontend via `experiment(:experiment_name)`, then `control` will be rendered if it exists.
## Notes on feature flags
NOTE:
......
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ExperimentComponent from '~/experimentation/components/experiment.vue';
const defaultProps = { name: 'experiment_name' };
const defaultSlots = {
candidate: `<p>Candidate</p>`,
control: `<p>Control</p>`,
};
describe('ExperimentComponent', () => {
const oldGon = window.gon;
let wrapper;
const createComponent = (propsData = defaultProps, slots = defaultSlots) => {
wrapper = extendedWrapper(shallowMount(ExperimentComponent, { propsData, slots }));
};
const mockVariant = (expectedVariant) => {
window.gon = { experiment: { experiment_name: { variant: expectedVariant } } };
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
window.gon = oldGon;
});
describe('when variant and experiment is set', () => {
it('renders control when it is the active variant', () => {
mockVariant('control');
createComponent();
expect(wrapper.text()).toBe('Control');
});
it('renders candidate when it is the active variant', () => {
mockVariant('candidate');
createComponent();
expect(wrapper.text()).toBe('Candidate');
});
});
describe('when variant or experiment is not set', () => {
it('renders the control slot when no variant is defined', () => {
mockVariant(undefined);
createComponent();
expect(wrapper.text()).toBe('Control');
});
it('renders nothing when behavior is not set for variant', () => {
mockVariant('non-existing-variant');
createComponent(defaultProps, { control: `<p>First</p>`, other: `<p>Other</p>` });
expect(wrapper.text()).toBe('');
});
it('renders nothing when there are no slots', () => {
mockVariant('control');
createComponent(defaultProps, {});
expect(wrapper.text()).toBe('');
});
});
});
import { DEFAULT_VARIANT } from '~/experimentation/constants';
import * as experimentUtils from '~/experimentation/utils';
const TEST_KEY = 'abc';
......@@ -35,4 +36,17 @@ describe('experiment Utilities', () => {
expect(experimentUtils.isExperimentVariant(...input)).toEqual(output);
});
});
describe('getExperimentVariant', () => {
it.each`
gon | input | output
${{ experiment: { [TEST_KEY]: { variant: 'control' } } }} | ${[TEST_KEY]} | ${'control'}
${{ experiment: { [TEST_KEY]: { variant: 'candidate' } } }} | ${[TEST_KEY]} | ${'candidate'}
${{}} | ${[TEST_KEY]} | ${DEFAULT_VARIANT}
`('with input=$input and gon=$gon, returns $output', ({ gon, input, output }) => {
window.gon = gon;
expect(experimentUtils.getExperimentVariant(...input)).toEqual(output);
});
});
});
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