Commit e565d0d9 authored by Fabio Pitino's avatar Fabio Pitino

Merge branch 'initial-ci-template-experiment' into 'master'

Add Learn CI/CD syntax template picker

See merge request gitlab-org/gitlab!48141
parents 264bccd1 61d5e805
......@@ -9,6 +9,7 @@ import { deprecatedCreateFlash as Flash } from '../flash';
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import BlobCiSyntaxYamlSelector from './template_selectors/ci_syntax_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
......@@ -33,6 +34,7 @@ export default class FileTemplateMediator {
this.templateSelectors = [
GitignoreSelector,
BlobCiYamlSelector,
BlobCiSyntaxYamlSelector,
MetricsDashboardSelector,
DockerfileSelector,
LicenseSelector,
......@@ -42,15 +44,20 @@ export default class FileTemplateMediator {
initTemplateTypeSelector() {
this.typeSelector = new FileTemplateTypeSelector({
mediator: this,
dropdownData: this.templateSelectors.map(templateSelector => {
const cfg = templateSelector.config;
return {
name: cfg.name,
key: cfg.key,
id: cfg.key,
};
}),
dropdownData: this.templateSelectors
.map(templateSelector => {
const cfg = templateSelector.config;
return {
name: cfg.name,
key: cfg.key,
id: cfg.key,
};
})
.reduce(
(acc, current) => (acc.find(item => item.id === current.id) ? acc : [...acc, current]),
[],
),
});
}
......
import FileTemplateSelector from '../file_template_selector';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class BlobCiSyntaxYamlSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'gitlab-ci-yaml',
name: '.gitlab-ci.yml',
pattern: /(.gitlab-ci.yml)/,
type: 'gitlab_ci_syntax_ymls',
dropdown: '.js-gitlab-ci-syntax-yml-selector',
wrapper: '.js-gitlab-ci-syntax-yml-selector-wrap',
};
}
initDropdown() {
initDeprecatedJQueryDropdown(this.$dropdown, {
data: this.$dropdown.data('data'),
filterable: true,
selectable: true,
search: {
fields: ['name'],
},
clicked: options => this.reportSelectionName(options),
text: item => item.name,
});
}
}
......@@ -181,6 +181,7 @@
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector,
.gitlab-ci-syntax-yml-selector,
.dockerfile-selector,
.template-type-selector,
.metrics-dashboard-selector {
......
......@@ -31,6 +31,9 @@ class Projects::BlobController < Projects::ApplicationController
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff
before_action :set_last_commit_sha, only: [:edit, :update]
before_action only: :new do
record_experiment_user(:ci_syntax_templates, namespace_id: @project.namespace_id) if params[:file_name] == @project.ci_config_path_or_default
end
track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true
......
......@@ -7,6 +7,7 @@ class TemplateFinder
dockerfiles: ::Gitlab::Template::DockerfileTemplate,
gitignores: ::Gitlab::Template::GitignoreTemplate,
gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate,
gitlab_ci_syntax_ymls: ::Gitlab::Template::GitlabCiSyntaxYmlTemplate,
metrics_dashboard_ymls: ::Gitlab::Template::MetricsDashboardTemplate,
issues: ::Gitlab::Template::IssueTemplate,
merge_requests: ::Gitlab::Template::MergeRequestTemplate
......
......@@ -217,6 +217,10 @@ module BlobHelper
@gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls, project).execute)
end
def gitlab_ci_syntax_ymls(project)
@gitlab_ci_syntax_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_syntax_ymls, project).execute)
end
def metrics_dashboard_ymls(project)
@metrics_dashboard_ymls ||= template_dropdown_names(TemplateFinder.build(:metrics_dashboard_ymls, project).execute)
end
......
......@@ -81,7 +81,10 @@ module Ci
.new(pipeline, command, SEQUENCE)
.build!
schedule_head_pipeline_update if pipeline.persisted?
if pipeline.persisted?
schedule_head_pipeline_update
record_conversion_event
end
# If pipeline is not persisted, try to recover IID
pipeline.reset_project_iid unless pipeline.persisted?
......@@ -116,6 +119,10 @@ module Ci
end
end
def record_conversion_event
Experiments::RecordConversionEventWorker.perform_async(:ci_syntax_templates, current_user.id)
end
def extra_options(content: nil, dry_run: false)
{ content: content, dry_run: dry_run }
end
......
......@@ -11,5 +11,8 @@
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector qa-metrics-dashboard-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project) } } )
#gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project) } } )
- if experiment_enabled?(:ci_syntax_templates, subject: current_user)
.gitlab-ci-syntax-yml-selector.js-gitlab-ci-syntax-yml-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag(_("Learn CI/CD syntax"), options: { toggle_class: 'js-gitlab-ci-syntax-yml-selector qa-gitlab-ci-syntax-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_syntax_ymls(@project) } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } )
......@@ -1577,6 +1577,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: experiments_record_conversion_event
:feature_category: :users
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: expire_build_instance_artifacts
:feature_category: :continuous_integration
:has_external_dependencies:
......
# frozen_string_literal: true
module Experiments
class RecordConversionEventWorker
include ApplicationWorker
feature_category :users
urgency :low
idempotent!
def perform(experiment, user_id)
return unless Gitlab::Experimentation.active?(experiment)
::Experiment.record_conversion_event(experiment, user_id)
end
end
end
......@@ -122,6 +122,8 @@
- 2
- - error_tracking_issue_link
- 1
- - experiments_record_conversion_event
- 1
- - expire_build_instance_artifacts
- 1
- - export_csv
......
......@@ -4,7 +4,7 @@ module API
class ProjectTemplates < ::API::Base
include PaginationParams
TEMPLATE_TYPES = %w[dockerfiles gitignores gitlab_ci_ymls licenses metrics_dashboard_ymls issues merge_requests].freeze
TEMPLATE_TYPES = %w[dockerfiles gitignores gitlab_ci_ymls gitlab_ci_syntax_ymls licenses metrics_dashboard_ymls issues merge_requests].freeze
# The regex is needed to ensure a period (e.g. agpl-3.0)
# isn't confused with a format type. We also need to allow encoded
# values (e.g. C%2B%2B for C++), so allow % and + as well.
......@@ -16,7 +16,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
requires :type, type: String, values: TEMPLATE_TYPES, desc: 'The type (dockerfiles|gitignores|gitlab_ci_ymls|licenses|metrics_dashboard_ymls|issues|merge_requests) of the template'
requires :type, type: String, values: TEMPLATE_TYPES, desc: 'The type (dockerfiles|gitignores|gitlab_ci_ymls|gitlab_ci_syntax_ymls|licenses|metrics_dashboard_ymls|issues|merge_requests) of the template'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of templates available to this project' do
......
......@@ -13,6 +13,9 @@ module API
gitlab_ci_ymls: {
gitlab_version: 8.9
},
gitlab_ci_syntax_ymls: {
gitlab_version: 13.8
},
dockerfiles: {
gitlab_version: 8.15
}
......
#
# You can use artifacts to pass data to jobs in later stages.
# For more information, see https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html
#
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- echo "This job might build an important file, and pass it to later jobs."
- echo "This is the content of the important file" > important-file.txt
artifacts:
paths:
- important-file.txt
test-job-with-artifacts:
stage: test
script:
- echo "This job uses the artifact from the job in the earlier stage."
- cat important-file.txt
- echo "It creates another file, and adds it to the artifacts."
- echo "This is a second important file" > important-file2.txt
artifacts:
paths:
- important-file2.txt
test-job-with-no-artifacts:
stage: test
dependencies: [] # Use to skip downloading any artifacts
script:
- echo "This job does not get the artifacts from other jobs."
- cat important-file.txt || exit 0
deploy-job-with-all-artifacts:
stage: deploy
script:
- echo "By default, jobs download all available artifacts."
- cat important-file.txt
- cat important-file2.txt
deploy-job-with-1-artifact:
stage: deploy
dependencies:
- build-job # Download artifacts from only this job
script:
- echo "You can configure a job to download artifacts from only certain jobs."
- cat important-file.txt
- cat important-file2.txt || exit 0
#
# You can define common tasks and run them before or after the main scripts in jobs.
# For more information, see:
# - https://docs.gitlab.com/ee/ci/yaml/README.html#before_script
# - https://docs.gitlab.com/ee/ci/yaml/README.html#after_script
#
stages:
- test
default:
before_script:
- echo "This script runs before the main script in every job, unless the job overrides it."
- echo "It may set up common dependencies, for example."
after_script:
- echo "This script runs after the main script in every job, unless the job overrides it."
- echo "It may do some common final clean up tasks"
job-standard:
stage: test
script:
- echo "This job uses both of the globally defined before and after scripts."
job-override-before:
stage: test
before_script:
- echo "Use a different before_script in this job."
script:
- echo "This job uses its own before_script, and the global after_script."
job-override-after:
stage: test
after_script:
- echo "Use a different after_script in this job."
script:
- echo "This job uses its own after_script, and the global before_script."
#
# A manual job is a type of job that is not executed automatically and must be explicitly started by a user.
# To make a job manual, add when: manual to its configuration.
# For more information, see https://docs.gitlab.com/ee/ci/yaml/README.html#whenmanual
#
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- echo "This job is not a manual job"
manual-build:
stage: build
script:
- echo "This manual job passes after you trigger it."
when: manual
manual-build-allowed-to-fail:
stage: build
script:
- echo "This manual job fails after you trigger it."
- echo "It is allowed to fail, so the pipeline does not fail.
when: manual
allow_failure: true # Default behavior
test-job:
stage: test
script:
- echo "This is a normal test job"
- echo "It runs when the when the build stage completes."
- echo "It does not need to wait for the manual jobs in the build stage to run."
manual-test-not-allowed-to-fail:
stage: test
script:
- echo "This manual job fails after you trigger it."
- echo "It is NOT allowed to fail, so the pipeline is marked as failed
- echo "when this job completes."
- exit 1
when: manual
allow_failure: false # Optional behavior
deploy-job:
stage: deploy
script:
- echo "This is a normal deploy job"
- echo "If a manual job that isn't allowed to fail ran in an earlier stage and failed,
- echo "this job does not run".
#
# A pipeline is composed of independent jobs that run scripts, grouped into stages.
# Stages run in sequential order, but jobs within stages run in parallel.
# For more information, see: https://docs.gitlab.com/ee/ci/yaml/README.html#stages
#
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- echo "This job runs in the build stage, which runs first."
test-job1:
stage: test
script:
- echo "This job runs in the test stage."
- echo "It only starts when the job in the build stage completes successfully."
test-job2:
stage: test
script:
- echo "This job also runs in the test stage."
- echo "This job can run at the same time as test-job2."
deploy-job:
stage: deploy
script:
- echo "This job runs in the deploy stage."
- echo "It only runs when both jobs in the test stage complete successfully"
#
# Variables can be used to for more dynamic behavior in jobs and scripts.
# For more information, see https://docs.gitlab.com/ee/ci/variables/README.html
#
stages:
- test
variables:
VAR1: "Variable 1 defined globally"
use-a-variable:
stage: test
script:
- echo "You can use variables in jobs."
- echo "The content of 'VAR1' is = $VAR1"
override-a-variable:
stage: test
variables:
VAR1: "Variable 1 was overriden in in the job."
script:
- echo "You can override global variables in jobs."
- echo "The content of 'VAR1' is = $VAR1"
define-a-new-variable:
stage: test
variables:
VAR2: "Variable 2 is new and defined in the job only."
script:
- echo "You can mix global variables with variables defined in jobs."
- echo "The content of 'VAR1' is = $VAR1"
- echo "The content of 'VAR2' is = $VAR2"
incorrect-variable-usage:
stage: test
script:
- echo "You can't use variables only defined in other jobs."
- echo "The content of 'VAR2' is = $VAR2"
predefined-variables:
stage: test
script:
- echo "Some variables are predefined by GitLab CI/CD, for example:"
- echo "The commit author's username is $GITLAB_USER_LOGIN"
- echo "The commit branch is $CI_COMMIT_BRANCH"
- echo "The project path is $CI_PROJECT_PATH"
......@@ -90,6 +90,9 @@ module Gitlab
},
trial_during_signup: {
tracking_category: 'Growth::Conversion::Experiment::TrialDuringSignup'
},
ci_syntax_templates: {
tracking_category: 'Growth::Activation::Experiment::CiSyntaxTemplates'
}
}.freeze
......
......@@ -23,7 +23,12 @@ module Gitlab
end
def content
@finder.read(@path)
blob = @finder.read(@path)
[description, blob].compact.join("\n")
end
def description
# override with a comment to be placed at the top of the blob.
end
# Present for compatibility with license templates, which can replace text
......
......@@ -3,9 +3,8 @@
module Gitlab
module Template
class DockerfileTemplate < BaseTemplate
def content
explanation = "# This file is a template, and might need editing before it works on your project."
[explanation, super].join("\n")
def description
"# This file is a template, and might need editing before it works on your project."
end
class << self
......
# frozen_string_literal: true
module Gitlab
module Template
class GitlabCiSyntaxYmlTemplate < BaseTemplate
class << self
def extension
'.gitlab-ci.yml'
end
def categories
{
'General' => ''
}
end
def base_dir
Rails.root.join('lib/gitlab/ci/syntax_templates')
end
def finder(project = nil)
Gitlab::Template::Finders::GlobalTemplateFinder.new(
self.base_dir, self.extension, self.categories
)
end
end
end
end
end
......@@ -5,9 +5,8 @@ module Gitlab
class GitlabCiYmlTemplate < BaseTemplate
BASE_EXCLUDED_PATTERNS = [%r{\.latest\.}].freeze
def content
explanation = "# This file is a template, and might need editing before it works on your project."
[explanation, super].join("\n")
def description
"# This file is a template, and might need editing before it works on your project."
end
class << self
......
......@@ -3,9 +3,8 @@
module Gitlab
module Template
class MetricsDashboardTemplate < BaseTemplate
def content
explanation = "# This file is a template, and might need editing before it works on your project."
[explanation, super].join("\n")
def description
"# This file is a template, and might need editing before it works on your project."
end
class << self
......
......@@ -16226,6 +16226,9 @@ msgstr ""
msgid "Lead Time"
msgstr ""
msgid "Learn CI/CD syntax"
msgstr ""
msgid "Learn GitLab"
msgstr ""
......
......@@ -7,6 +7,82 @@ RSpec.describe Projects::BlobController do
let(:project) { create(:project, :public, :repository) }
describe "GET new" do
context 'with no jobs' do
let_it_be(:user) { create(:user) }
let_it_be(:file_name) { '.gitlab-ci.yml' }
def request
get(:new, params: { namespace_id: project.namespace, project_id: project, id: 'master', file_name: file_name } )
end
before do
project.add_maintainer(user)
sign_in(user)
stub_experiment(ci_syntax_templates: experiment_active)
stub_experiment_for_subject(ci_syntax_templates: in_experiment_group)
end
context 'when the experiment is not active' do
let(:experiment_active) { false }
let(:in_experiment_group) { false }
it 'does not record the experiment user' do
expect(Experiment).not_to receive(:add_user)
request
end
end
context 'when the experiment is active and the user is in the control group' do
let(:experiment_active) { true }
let(:in_experiment_group) { false }
it 'records the experiment user in the control group' do
expect(Experiment).to receive(:add_user)
.with(:ci_syntax_templates, :control, user, namespace_id: project.namespace_id)
request
end
end
context 'when the experiment is active and the user is in the experimental group' do
let(:experiment_active) { true }
let(:in_experiment_group) { true }
it 'records the experiment user in the experimental group' do
expect(Experiment).to receive(:add_user)
.with(:ci_syntax_templates, :experimental, user, namespace_id: project.namespace_id)
request
end
context 'when requesting a non default config file type' do
let(:file_name) { '.non_default_ci_config' }
let(:project) { create(:project, :public, :repository, ci_config_path: file_name) }
it 'records the experiment user in the experimental group' do
expect(Experiment).to receive(:add_user)
.with(:ci_syntax_templates, :experimental, user, namespace_id: project.namespace_id)
request
end
end
context 'when requesting a different file type' do
let(:file_name) { '.gitignore' }
it 'does not record the experiment user' do
expect(Experiment).not_to receive(:add_user)
request
end
end
end
end
end
describe "GET show" do
def request
get(:show, params: { namespace_id: project.namespace, project_id: project, id: id })
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
before do
project = create(:project, :repository)
sign_in project.owner
stub_experiment(ci_syntax_templates: experiment_active)
stub_experiment_for_subject(ci_syntax_templates: in_experiment_group)
visit project_new_blob_path(project, 'master', file_name: '.gitlab-ci.yml')
end
context 'when experiment is not active' do
let(:experiment_active) { false }
let(:in_experiment_group) { false }
it 'does not show the "Learn CI/CD syntax" template dropdown' do
expect(page).not_to have_css('.gitlab-ci-syntax-yml-selector')
end
end
context 'when experiment is active and the user is in the control group' do
let(:experiment_active) { true }
let(:in_experiment_group) { false }
it 'does not show the "Learn CI/CD syntax" template dropdown' do
expect(page).not_to have_css('.gitlab-ci-syntax-yml-selector')
end
end
context 'when experiment is active and the user is in the experimental group' do
let(:experiment_active) { true }
let(:in_experiment_group) { true }
it 'allows the user to pick a "Learn CI/CD syntax" template from the dropdown', :js do
expect(page).to have_css('.gitlab-ci-syntax-yml-selector')
find('.js-gitlab-ci-syntax-yml-selector').click
wait_for_requests
within '.gitlab-ci-syntax-yml-selector' do
find('.dropdown-input-field').set('Artifacts example')
find('.dropdown-content .is-focused', text: 'Artifacts example').click
end
wait_for_requests
expect(page).to have_css('.gitlab-ci-syntax-yml-selector .dropdown-toggle-text', text: 'Learn CI/CD syntax')
expect(page).to have_content('You can use artifacts to pass data to jobs in later stages.')
end
end
end
......@@ -14,6 +14,7 @@ RSpec.describe TemplateFinder do
:gitlab_ci_ymls | described_class
:licenses | ::LicenseTemplateFinder
:metrics_dashboard_ymls | described_class
:gitlab_ci_syntax_ymls | described_class
end
with_them do
......@@ -30,6 +31,7 @@ RSpec.describe TemplateFinder do
:gitignores | 'Actionscript'
:gitlab_ci_ymls | 'Android'
:metrics_dashboard_ymls | 'Default'
:gitlab_ci_syntax_ymls | 'Artifacts example'
end
with_them do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'ci/syntax_templates' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:lint) { Gitlab::Ci::Lint.new(project: project, current_user: user) }
before do
project.add_developer(user)
end
subject(:lint_result) { lint.validate(content) }
Dir.glob('lib/gitlab/ci/syntax_templates/**/*.yml').each do |template|
describe template do
let(:content) { File.read(template) }
it 'validates the template' do
expect(lint_result).to be_valid, "got errors: #{lint_result.errors.join(', ')}"
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Template::GitlabCiSyntaxYmlTemplate do
subject { described_class }
describe '#content' do
it 'loads the full file' do
template = subject.new(Rails.root.join('lib/gitlab/ci/syntax_templates/Artifacts example.gitlab-ci.yml'))
expect(template.content).to start_with('#')
end
end
it_behaves_like 'file template shared examples', 'Artifacts example', '.gitlab-ci.yml'
end
......@@ -53,6 +53,15 @@ RSpec.describe API::ProjectTemplates do
expect(json_response).to satisfy_one { |template| template['key'] == 'Android' }
end
it 'returns gitlab_ci_syntax_ymls' do
get api("/projects/#{public_project.id}/templates/gitlab_ci_syntax_ymls")
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/template_list')
expect(json_response).to satisfy_one { |template| template['key'] == 'Artifacts example' }
end
it 'returns licenses' do
get api("/projects/#{public_project.id}/templates/licenses")
......@@ -163,6 +172,14 @@ RSpec.describe API::ProjectTemplates do
expect(json_response['name']).to eq('Android')
end
it 'returns a specific gitlab_ci_syntax_yml' do
get api("/projects/#{public_project.id}/templates/gitlab_ci_syntax_ymls/Artifacts%20example")
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/template')
expect(json_response['name']).to eq('Artifacts example')
end
it 'returns a specific metrics_dashboard_yml' do
get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls/Default")
......
......@@ -91,6 +91,14 @@ RSpec.describe Ci::CreatePipelineService do
.with({ source: 'push' }, 5)
end
describe 'recording a conversion event' do
it 'schedules a record conversion event worker' do
expect(Experiments::RecordConversionEventWorker).to receive(:perform_async).with(:ci_syntax_templates, user.id)
pipeline
end
end
context 'when merge requests already exist for this source branch' do
let(:merge_request_1) do
create(:merge_request, source_branch: 'feature', target_branch: "master", source_project: project)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Experiments::RecordConversionEventWorker, '#perform' do
subject(:perform) { described_class.new.perform(:experiment_key, 1234) }
before do
stub_experiment(experiment_key: experiment_active)
end
context 'when the experiment is active' do
let(:experiment_active) { true }
it 'records the event' do
expect(Experiment).to receive(:record_conversion_event).with(:experiment_key, 1234)
perform
end
end
context 'when the experiment is not active' do
let(:experiment_active) { false }
it 'records the event' do
expect(Experiment).not_to receive(:record_conversion_event)
perform
end
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