Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
cd9bbd8a3e
commit
17ef30f3df
|
@ -241,10 +241,6 @@ Style/KeywordParametersOrder:
|
|||
Style/Lambda:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 21
|
||||
Style/MissingRespondToMissing:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 35
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, MinBodyLength.
|
||||
|
|
|
@ -14,34 +14,9 @@ Database/MultipleDatabases:
|
|||
- 'ee/spec/services/ee/merge_requests/update_service_spec.rb'
|
||||
- 'lib/backup/database.rb'
|
||||
- 'lib/backup/manager.rb'
|
||||
- lib/gitlab/background_migration/backfill_integrations_type_new.rb
|
||||
- lib/gitlab/background_migration/backfill_issue_search_data.rb
|
||||
- lib/gitlab/background_migration/backfill_member_namespace_for_group_members.rb
|
||||
- lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb
|
||||
- lib/gitlab/background_migration/backfill_namespace_id_for_project_route.rb
|
||||
- lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb
|
||||
- lib/gitlab/background_migration/backfill_projects_with_coverage.rb
|
||||
- lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb
|
||||
- lib/gitlab/background_migration/backfill_user_namespace.rb
|
||||
- lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans.rb
|
||||
- lib/gitlab/background_migration/delete_orphaned_deployments.rb
|
||||
- lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images.rb
|
||||
- lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb
|
||||
- lib/gitlab/background_migration/fix_projects_without_project_feature.rb
|
||||
- lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb
|
||||
- lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb
|
||||
- lib/gitlab/background_migration/migrate_stage_status.rb
|
||||
- lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature.rb
|
||||
- lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb
|
||||
- lib/gitlab/background_migration/populate_container_repository_migration_plan.rb
|
||||
- lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb
|
||||
- lib/gitlab/background_migration/populate_topics_total_projects_count_cache.rb
|
||||
- lib/gitlab/background_migration/populate_vulnerability_reads.rb
|
||||
- lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb
|
||||
- lib/gitlab/background_migration/remove_vulnerability_finding_links.rb
|
||||
- lib/gitlab/background_migration/update_timelogs_null_spent_at.rb
|
||||
- lib/gitlab/background_migration/update_timelogs_project_id.rb
|
||||
- lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb
|
||||
- 'lib/gitlab/database.rb'
|
||||
- 'lib/gitlab/database/load_balancing/load_balancer.rb'
|
||||
- 'lib/gitlab/database/migrations/observers/query_log.rb'
|
||||
|
|
|
@ -28,7 +28,6 @@ Migration/BackgroundMigrationRecord:
|
|||
- lib/gitlab/background_migration/migrate_merge_request_diff_commit_users.rb
|
||||
- lib/gitlab/background_migration/migrate_null_private_profile_to_false.rb
|
||||
- lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics.rb
|
||||
- lib/gitlab/background_migration/migrate_stage_status.rb
|
||||
- lib/gitlab/background_migration/migrate_u2f_webauthn.rb
|
||||
- lib/gitlab/background_migration/populate_latest_pipeline_ids.rb
|
||||
- lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb
|
||||
|
@ -54,4 +53,4 @@ Migration/BackgroundMigrationRecord:
|
|||
- ee/lib/ee/gitlab/background_migration/populate_resolved_on_default_branch_column.rb
|
||||
- ee/lib/ee/gitlab/background_migration/populate_status_column_of_security_scans.rb
|
||||
- ee/lib/ee/gitlab/background_migration/populate_uuids_for_security_findings.rb
|
||||
- ee/lib/ee/gitlab/background_migration/update_vulnerability_occurrences_location.rb
|
||||
- ee/lib/ee/gitlab/background_migration/update_vulnerability_occurrences_location.rb
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
Style/MissingRespondToMissing:
|
||||
# Offense count: 21
|
||||
# Temporarily disabled due to too many offenses
|
||||
Enabled: false
|
||||
Exclude:
|
||||
- 'app/controllers/projects/application_controller.rb'
|
||||
- 'app/models/network/commit.rb'
|
||||
- 'app/services/notification_service.rb'
|
||||
- 'ee/app/controllers/ee/groups/application_controller.rb'
|
||||
- 'ee/app/models/elastic/migration_record.rb'
|
||||
- 'ee/app/services/ee/audit_event_service.rb'
|
||||
- 'lib/declarative_enum.rb'
|
||||
- 'lib/gitlab/auth/ldap/dn.rb'
|
||||
- 'lib/gitlab/fake_application_settings.rb'
|
||||
- 'lib/gitlab/gitaly_client/storage_settings.rb'
|
||||
- 'lib/gitlab/graphql/batch_key.rb'
|
||||
- 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb'
|
||||
- 'lib/gitlab/legacy_github_import/client.rb'
|
||||
- 'lib/gitlab/metrics/null_metric.rb'
|
||||
- 'lib/gitlab/tracking/event_definition.rb'
|
||||
- 'lib/kramdown/parser/atlassian_document_format.rb'
|
||||
- 'lib/mattermost/session.rb'
|
||||
- 'lib/uploaded_file.rb'
|
||||
- 'qa/qa/runtime/release.rb'
|
||||
- 'qa/qa/runtime/scenario.rb'
|
||||
- 'spec/support/helpers/next_found_instance_of.rb'
|
|
@ -57,6 +57,9 @@ export default {
|
|||
update({ currentLicense }) {
|
||||
return currentLicense?.plan;
|
||||
},
|
||||
error() {
|
||||
this.hasCurrentLicenseFetchError = true;
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
|
@ -99,6 +102,7 @@ export default {
|
|||
autoDevopsEnabledAlertDismissedProjects: [],
|
||||
errorMessage: '',
|
||||
currentLicensePlan: '',
|
||||
hasCurrentLicenseFetchError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -120,7 +124,10 @@ export default {
|
|||
);
|
||||
},
|
||||
shouldShowVulnerabilityManagementTab() {
|
||||
return this.currentLicensePlan === LICENSE_ULTIMATE;
|
||||
// if the query fails (if the plan is `null` also means an error has occurred) we still want to show the feature
|
||||
const hasQueryError = this.hasCurrentLicenseFetchError || this.currentLicensePlan === null;
|
||||
|
||||
return hasQueryError || this.currentLicensePlan === LICENSE_ULTIMATE;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -173,16 +173,12 @@ class Clusters::ClustersController < Clusters::BaseController
|
|||
|
||||
private
|
||||
|
||||
def certificate_based_clusters_enabled?
|
||||
Feature.enabled?(:certificate_based_clusters, clusterable.clusterable_namespace, default_enabled: :yaml, type: :ops)
|
||||
end
|
||||
|
||||
def ensure_feature_enabled!
|
||||
render_404 unless certificate_based_clusters_enabled?
|
||||
render_404 unless clusterable.certificate_based_clusters_enabled?
|
||||
end
|
||||
|
||||
def cluster_list
|
||||
return [] unless certificate_based_clusters_enabled?
|
||||
return [] unless clusterable.certificate_based_clusters_enabled?
|
||||
|
||||
finder = ClusterAncestorsFinder.new(clusterable.__subject__, current_user)
|
||||
clusters = finder.execute
|
||||
|
|
|
@ -24,7 +24,7 @@ module Mutations
|
|||
'https://gitlab.com/groups/gitlab-org/configure/-/epics/8'
|
||||
|
||||
def resolve(id:, **kwargs)
|
||||
return { errors: [REMOVAL_ERR_MSG] } if cert_based_clusters_ff_disabled?
|
||||
return { errors: [REMOVAL_ERR_MSG] } unless certificate_based_clusters_enabled?
|
||||
|
||||
environment = authorized_find!(id: id)
|
||||
|
||||
|
@ -43,8 +43,9 @@ module Mutations
|
|||
|
||||
private
|
||||
|
||||
def cert_based_clusters_ff_disabled?
|
||||
Feature.disabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops)
|
||||
def certificate_based_clusters_enabled?
|
||||
instance_cluster = ::Clusters::Instance.new
|
||||
instance_cluster.certificate_based_clusters_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,6 +24,10 @@ module Resolvers
|
|||
end
|
||||
|
||||
def resolve(ids: nil, filenames: nil, at_version: nil)
|
||||
# TODO: remove the coercion when the compatibility layer is removed
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
|
||||
context.scoped_set!(:at_version_argument, VersionID.coerce_isolated_input(at_version)) if at_version
|
||||
|
||||
::DesignManagement::DesignsFinder.new(
|
||||
issue,
|
||||
current_user,
|
||||
|
|
|
@ -9,8 +9,6 @@ module Resolvers
|
|||
|
||||
VersionID = ::Types::GlobalIDType[::DesignManagement::Version]
|
||||
|
||||
extras [:parent]
|
||||
|
||||
argument :earlier_or_equal_to_sha, GraphQL::Types::String,
|
||||
as: :sha,
|
||||
required: false,
|
||||
|
@ -26,11 +24,11 @@ module Resolvers
|
|||
::Resolvers::DesignManagement::VersionInCollectionResolver
|
||||
end
|
||||
|
||||
def resolve(parent: nil, id: nil, sha: nil)
|
||||
def resolve(id: nil, sha: nil)
|
||||
# TODO: remove this line when the compatibility layer is removed
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
|
||||
id &&= VersionID.coerce_isolated_input(id)
|
||||
version = cutoff(parent, id, sha)
|
||||
version = cutoff(id, sha)
|
||||
|
||||
raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'cutoff not found' unless version.present?
|
||||
|
||||
|
@ -44,11 +42,11 @@ module Resolvers
|
|||
private
|
||||
|
||||
# Find the most recent version that the client will accept
|
||||
def cutoff(parent, id, sha)
|
||||
def cutoff(id, sha)
|
||||
if sha.present? || id.present?
|
||||
specific_version(id, sha)
|
||||
elsif at_version = at_version_arg(parent)
|
||||
by_id(at_version)
|
||||
elsif at_version = context[:at_version_argument]
|
||||
by_id(at_version) # See: DesignsResolver
|
||||
else
|
||||
:unconstrained
|
||||
end
|
||||
|
@ -68,20 +66,6 @@ module Resolvers
|
|||
def by_id(gid)
|
||||
::Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(gid))
|
||||
end
|
||||
|
||||
# Find an `at_version` argument passed to a parent node.
|
||||
#
|
||||
# If one is found, then a design collection further up the AST
|
||||
# has been filtered to reflect designs at that version, and so
|
||||
# for consistency we should only present versions up to the given
|
||||
# version here.
|
||||
def at_version_arg(parent)
|
||||
# TODO: remove coercion when the compatibility layer is removed
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
|
||||
version_id = ::Gitlab::Graphql::FindArgumentInParent.find(parent, :at_version, limit_depth: 4)
|
||||
version_id &&= VersionID.coerce_isolated_input(version_id)
|
||||
version_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,14 +23,13 @@ module Types
|
|||
field :versions,
|
||||
Types::DesignManagement::VersionType.connection_type,
|
||||
resolver: Resolvers::DesignManagement::VersionsResolver,
|
||||
description: "All versions related to this design ordered newest first.",
|
||||
extras: [:parent]
|
||||
description: "All versions related to this design ordered newest first."
|
||||
|
||||
# Returns a `DesignManagement::Version` for this query based on the
|
||||
# `atVersion` argument passed to a parent node if present, or otherwise
|
||||
# the most recent `Version` for the issue.
|
||||
def cached_stateful_version(parent_node)
|
||||
version_gid = Gitlab::Graphql::FindArgumentInParent.find(parent_node, :at_version)
|
||||
version_gid = context[:at_version_argument] # See: DesignsResolver
|
||||
|
||||
# Caching is scoped to an `issue_id` to allow us to cache the
|
||||
# most recent `Version` for an issue
|
||||
|
|
|
@ -462,7 +462,7 @@ module ApplicationSettingsHelper
|
|||
def instance_clusters_enabled?
|
||||
clusterable = Clusters::Instance.new
|
||||
|
||||
Feature.enabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops) &&
|
||||
clusterable.certificate_based_clusters_enabled? &&
|
||||
can?(current_user, :read_cluster, clusterable)
|
||||
end
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ module ClustersHelper
|
|||
can_add_cluster: clusterable.can_add_cluster?.to_s,
|
||||
can_admin_cluster: clusterable.can_admin_cluster?.to_s,
|
||||
display_cluster_agents: display_cluster_agents?(clusterable).to_s,
|
||||
certificate_based_clusters_enabled: Feature.enabled?(:certificate_based_clusters, clusterable.clusterable_namespace, default_enabled: :yaml, type: :ops).to_s,
|
||||
certificate_based_clusters_enabled: clusterable.certificate_based_clusters_enabled?.to_s,
|
||||
default_branch_name: default_branch_name(clusterable),
|
||||
project_path: clusterable_project_path(clusterable),
|
||||
kas_address: Gitlab::Kas.external_url,
|
||||
|
|
|
@ -45,7 +45,7 @@ module Ci
|
|||
dotenv: '.env',
|
||||
cobertura: 'cobertura-coverage.xml',
|
||||
terraform: 'tfplan.json',
|
||||
cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/333441
|
||||
cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094
|
||||
requirements: 'requirements.json',
|
||||
coverage_fuzzing: 'gl-coverage-fuzzing.json',
|
||||
api_fuzzing: 'gl-api-fuzzing-report.json'
|
||||
|
@ -64,7 +64,7 @@ module Ci
|
|||
network_referee: :gzip,
|
||||
dotenv: :gzip,
|
||||
cobertura: :gzip,
|
||||
cluster_applications: :gzip,
|
||||
cluster_applications: :gzip, # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094
|
||||
lsif: :zip,
|
||||
|
||||
# Security reports and license scanning reports are raw artifacts
|
||||
|
|
|
@ -9,5 +9,11 @@ module Clusters
|
|||
def flipper_id
|
||||
self.class.to_s
|
||||
end
|
||||
|
||||
def certificate_based_clusters_enabled?
|
||||
::Gitlab::SafeRequestStore.fetch("certificate_based_clusters:") do
|
||||
Feature.enabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module DeploymentPlatform
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def deployment_platform(environment: nil)
|
||||
return if Feature.disabled?(:certificate_based_clusters, self.namespace, default_enabled: :yaml, type: :ops)
|
||||
return unless self.namespace.certificate_based_clusters_enabled?
|
||||
|
||||
@deployment_platform ||= {}
|
||||
|
||||
|
|
|
@ -70,6 +70,11 @@ class Deployment < ApplicationRecord
|
|||
transition created: :blocked
|
||||
end
|
||||
|
||||
# This transition is possible when we have manual jobs.
|
||||
event :create do
|
||||
transition skipped: :created
|
||||
end
|
||||
|
||||
event :unblock do
|
||||
transition blocked: :created
|
||||
end
|
||||
|
@ -348,7 +353,7 @@ class Deployment < ApplicationRecord
|
|||
|
||||
def sync_status_with(build)
|
||||
return false unless ::Deployment.statuses.include?(build.status)
|
||||
return false if build.created? || build.status == self.status
|
||||
return false if build.status == self.status
|
||||
|
||||
update_status!(build.status)
|
||||
rescue StandardError => e
|
||||
|
@ -403,6 +408,8 @@ class Deployment < ApplicationRecord
|
|||
skip!
|
||||
when 'blocked'
|
||||
block!
|
||||
when 'created'
|
||||
create!
|
||||
else
|
||||
raise ArgumentError, "The status #{status.inspect} is invalid"
|
||||
end
|
||||
|
|
|
@ -161,7 +161,7 @@ class Integration < ApplicationRecord
|
|||
end
|
||||
|
||||
def fields
|
||||
self.class.fields
|
||||
self.class.fields.dup
|
||||
end
|
||||
|
||||
# Provide convenient accessor methods for each serialized property.
|
||||
|
|
|
@ -5,7 +5,26 @@ module Integrations
|
|||
include ReactivelyCached
|
||||
prepend EnableSslVerification
|
||||
|
||||
prop_accessor :bamboo_url, :build_key, :username, :password
|
||||
field :bamboo_url,
|
||||
title: s_('BambooService|Bamboo URL'),
|
||||
placeholder: s_('https://bamboo.example.com'),
|
||||
help: s_('BambooService|Bamboo service root URL.'),
|
||||
required: true
|
||||
|
||||
field :build_key,
|
||||
help: s_('BambooService|Bamboo build plan key.'),
|
||||
non_empty_password_title: s_('BambooService|Enter new build key'),
|
||||
non_empty_password_help: s_('BambooService|Leave blank to use your current build key.'),
|
||||
placeholder: s_('KEY'),
|
||||
required: true
|
||||
|
||||
field :username,
|
||||
help: s_('BambooService|The user with API access to the Bamboo server.')
|
||||
|
||||
field :password,
|
||||
type: 'password',
|
||||
non_empty_password_title: s_('ProjectService|Enter new password'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
|
||||
|
||||
validates :bamboo_url, presence: true, public_url: true, if: :activated?
|
||||
validates :build_key, presence: true, if: :activated?
|
||||
|
@ -43,39 +62,6 @@ module Integrations
|
|||
'bamboo'
|
||||
end
|
||||
|
||||
def fields
|
||||
[
|
||||
{
|
||||
type: 'text',
|
||||
name: 'bamboo_url',
|
||||
title: s_('BambooService|Bamboo URL'),
|
||||
placeholder: s_('https://bamboo.example.com'),
|
||||
help: s_('BambooService|Bamboo service root URL.'),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'build_key',
|
||||
help: s_('BambooService|Bamboo build plan key.'),
|
||||
non_empty_password_title: s_('BambooService|Enter new build key'),
|
||||
non_empty_password_help: s_('BambooService|Leave blank to use your current build key.'),
|
||||
placeholder: s_('KEY'),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
help: s_('BambooService|The user with API access to the Bamboo server.')
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
non_empty_password_title: s_('ProjectService|Enter new password'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def build_page(sha, ref)
|
||||
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
|
||||
end
|
||||
|
|
|
@ -10,7 +10,18 @@ module Integrations
|
|||
|
||||
ENDPOINT = "https://buildkite.com"
|
||||
|
||||
prop_accessor :project_url, :token
|
||||
field :project_url,
|
||||
title: _('Pipeline URL'),
|
||||
placeholder: "#{ENDPOINT}/example-org/test-pipeline",
|
||||
required: true
|
||||
|
||||
field :token,
|
||||
type: 'password',
|
||||
title: _('Token'),
|
||||
help: s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.'),
|
||||
non_empty_password_title: s_('ProjectService|Enter new token'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
|
||||
required: true
|
||||
|
||||
validates :project_url, presence: true, public_url: true, if: :activated?
|
||||
validates :token, presence: true, if: :activated?
|
||||
|
@ -74,24 +85,6 @@ module Integrations
|
|||
s_('ProjectService|Run CI/CD pipelines with Buildkite.')
|
||||
end
|
||||
|
||||
def fields
|
||||
[
|
||||
{ type: 'password',
|
||||
name: 'token',
|
||||
title: _('Token'),
|
||||
help: s_('ProjectService|The token you get after you create a Buildkite pipeline with a GitLab repository.'),
|
||||
non_empty_password_title: s_('ProjectService|Enter new token'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
|
||||
required: true },
|
||||
|
||||
{ type: 'text',
|
||||
name: 'project_url',
|
||||
title: _('Pipeline URL'),
|
||||
placeholder: "#{ENDPOINT}/example-org/test-pipeline",
|
||||
required: true }
|
||||
]
|
||||
end
|
||||
|
||||
def calculate_reactive_cache(sha, ref)
|
||||
response = Gitlab::HTTP.try_get(commit_status_path(sha), request_options)
|
||||
|
||||
|
|
|
@ -10,7 +10,17 @@ module Integrations
|
|||
|
||||
DRONE_SAAS_HOSTNAME = 'cloud.drone.io'
|
||||
|
||||
prop_accessor :drone_url, :token
|
||||
field :drone_url,
|
||||
title: s_('ProjectService|Drone server URL'),
|
||||
placeholder: 'http://drone.example.com',
|
||||
required: true
|
||||
|
||||
field :token,
|
||||
type: 'password',
|
||||
help: s_('ProjectService|Token for the Drone project.'),
|
||||
non_empty_password_title: s_('ProjectService|Enter new token'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
|
||||
required: true
|
||||
|
||||
validates :drone_url, presence: true, public_url: true, if: :activated?
|
||||
validates :token, presence: true, if: :activated?
|
||||
|
@ -94,26 +104,6 @@ module Integrations
|
|||
s_('ProjectService|Run CI/CD pipelines with Drone.')
|
||||
end
|
||||
|
||||
def fields
|
||||
[
|
||||
{
|
||||
type: 'password',
|
||||
name: 'token',
|
||||
help: s_('ProjectService|Token for the Drone project.'),
|
||||
non_empty_password_title: s_('ProjectService|Enter new token'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current token.'),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'drone_url',
|
||||
title: s_('ProjectService|Drone server URL'),
|
||||
placeholder: 'http://drone.example.com',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
override :hook_url
|
||||
def hook_url
|
||||
[drone_url, "/hook", "?owner=#{project.namespace.full_path}", "&name=#{project.path}", "&access_token=#{token}"].join
|
||||
|
|
|
@ -7,7 +7,25 @@ module Integrations
|
|||
prepend EnableSslVerification
|
||||
extend Gitlab::Utils::Override
|
||||
|
||||
prop_accessor :jenkins_url, :project_name, :username, :password
|
||||
field :jenkins_url,
|
||||
title: s_('ProjectService|Jenkins server URL'),
|
||||
required: true,
|
||||
placeholder: 'http://jenkins.example.com',
|
||||
help: s_('The URL of the Jenkins server.')
|
||||
|
||||
field :project_name,
|
||||
required: true,
|
||||
placeholder: 'my_project_name',
|
||||
help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
|
||||
|
||||
field :username,
|
||||
help: s_('The username for the Jenkins server.')
|
||||
|
||||
field :password,
|
||||
type: 'password',
|
||||
help: s_('The password for the Jenkins server.'),
|
||||
non_empty_password_title: s_('ProjectService|Enter new password.'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
|
||||
|
||||
before_validation :reset_password
|
||||
|
||||
|
@ -71,37 +89,5 @@ module Integrations
|
|||
def self.to_param
|
||||
'jenkins'
|
||||
end
|
||||
|
||||
def fields
|
||||
[
|
||||
{
|
||||
type: 'text',
|
||||
name: 'jenkins_url',
|
||||
title: s_('ProjectService|Jenkins server URL'),
|
||||
required: true,
|
||||
placeholder: 'http://jenkins.example.com',
|
||||
help: s_('The URL of the Jenkins server.')
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'project_name',
|
||||
required: true,
|
||||
placeholder: 'my_project_name',
|
||||
help: s_('The name of the Jenkins project. Copy the name from the end of the URL to the project.')
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
help: s_('The username for the Jenkins server.')
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
help: s_('The password for the Jenkins server.'),
|
||||
non_empty_password_title: s_('ProjectService|Enter new password.'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current password.')
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,11 @@ module Integrations
|
|||
|
||||
ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze
|
||||
|
||||
prop_accessor :mock_service_url
|
||||
field :mock_service_url,
|
||||
title: s_('ProjectService|Mock service URL'),
|
||||
placeholder: 'http://localhost:4004',
|
||||
required: true
|
||||
|
||||
validates :mock_service_url, presence: true, public_url: true, if: :activated?
|
||||
|
||||
def title
|
||||
|
@ -22,18 +26,6 @@ module Integrations
|
|||
'mock_ci'
|
||||
end
|
||||
|
||||
def fields
|
||||
[
|
||||
{
|
||||
type: 'text',
|
||||
name: 'mock_service_url',
|
||||
title: s_('ProjectService|Mock service URL'),
|
||||
placeholder: 'http://localhost:4004',
|
||||
required: true
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
# Return complete url to build page
|
||||
#
|
||||
# Ex.
|
||||
|
|
|
@ -8,7 +8,22 @@ module Integrations
|
|||
|
||||
TEAMCITY_SAAS_HOSTNAME = /\A[^\.]+\.teamcity\.com\z/i.freeze
|
||||
|
||||
prop_accessor :teamcity_url, :build_type, :username, :password
|
||||
field :teamcity_url,
|
||||
title: s_('ProjectService|TeamCity server URL'),
|
||||
placeholder: 'https://teamcity.example.com',
|
||||
required: true
|
||||
|
||||
field :build_type,
|
||||
help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
|
||||
required: true
|
||||
|
||||
field :username,
|
||||
help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
|
||||
|
||||
field :password,
|
||||
type: 'password',
|
||||
non_empty_password_title: s_('ProjectService|Enter new password'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
|
||||
|
||||
validates :teamcity_url, presence: true, public_url: true, if: :activated?
|
||||
validates :build_type, presence: true, if: :activated?
|
||||
|
@ -51,35 +66,6 @@ module Integrations
|
|||
s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
|
||||
end
|
||||
|
||||
def fields
|
||||
[
|
||||
{
|
||||
type: 'text',
|
||||
name: 'teamcity_url',
|
||||
title: s_('ProjectService|TeamCity server URL'),
|
||||
placeholder: 'https://teamcity.example.com',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'build_type',
|
||||
help: s_('ProjectService|The build configuration ID of the TeamCity project.'),
|
||||
required: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
help: s_('ProjectService|Must have permission to trigger a manual build in TeamCity.')
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'password',
|
||||
non_empty_password_title: s_('ProjectService|Enter new password'),
|
||||
non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def build_page(sha, ref)
|
||||
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
|
||||
end
|
||||
|
|
|
@ -525,6 +525,12 @@ class Namespace < ApplicationRecord
|
|||
nil
|
||||
end
|
||||
|
||||
def certificate_based_clusters_enabled?
|
||||
::Gitlab::SafeRequestStore.fetch("certificate_based_clusters:ns:#{self.id}") do
|
||||
Feature.enabled?(:certificate_based_clusters, self, default_enabled: :yaml, type: :ops)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expire_child_caches
|
||||
|
|
|
@ -465,7 +465,7 @@ class Project < ApplicationRecord
|
|||
delegate :add_user, :add_users, to: :team
|
||||
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_owner, :add_role, to: :team
|
||||
delegate :group_runners_enabled, :group_runners_enabled=, to: :ci_cd_settings, allow_nil: true
|
||||
delegate :root_ancestor, to: :namespace, allow_nil: true
|
||||
delegate :root_ancestor, :certificate_based_clusters_enabled?, to: :namespace, allow_nil: true
|
||||
delegate :last_pipeline, to: :commit, allow_nil: true
|
||||
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
|
||||
delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true
|
||||
|
|
|
@ -12,11 +12,6 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
|
|||
.fabricate!
|
||||
end
|
||||
|
||||
def clusterable_namespace
|
||||
return clusterable.namespace if clusterable.is_a?(Project)
|
||||
return clusterable if clusterable.is_a?(Group)
|
||||
end
|
||||
|
||||
def can_add_cluster?
|
||||
can?(current_user, :add_cluster, clusterable)
|
||||
end
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354442
|
|||
milestone: '14.9'
|
||||
type: development
|
||||
group: group::product intelligence
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- AlertManagement::AlertAssignee
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists metadata between users and alerts to support alert assignments
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/32609
|
||||
milestone: '13.1'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- AlertManagement::MetricImage
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists metadata for uploads related to alerts
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80339
|
||||
milestone: '14.8'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- AlertManagement::AlertUserMention
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists metadata for system notes related to alerts
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33217
|
||||
milestone: '13.1'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- AlertManagement::Alert
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists incoming alert data including its payload
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29864
|
||||
milestone: '13.0'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- AlertManagement::HttpIntegration
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44098
|
||||
description: Persists settings for alert HTTP integrations
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43634
|
||||
milestone: '13.5'
|
||||
|
|
|
@ -3,7 +3,7 @@ table_name: clusters
|
|||
classes:
|
||||
- Clusters::Cluster
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/d0cff7f5855f91b5479f9fdaa39d8d95ec691a9e
|
||||
- kubernetes_management
|
||||
description: Persists information about GitLab managed clusters
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14879
|
||||
milestone: '10.1'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- Clusters::Integrations::Prometheus
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists information about prometheus cluster integration
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59091
|
||||
milestone: '13.11'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- ErrorTracking::ClientKey
|
||||
feature_categories:
|
||||
- error_tracking
|
||||
description: TODO
|
||||
description: Model to store public keys used by Sentry SDK for Error Tracking
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66466
|
||||
milestone: '14.2'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- ErrorTracking::ErrorEvent
|
||||
feature_categories:
|
||||
- error_tracking
|
||||
description: TODO
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/2d1c197ab0bf10071cb52e579edd3808cb0adc21
|
||||
description: Persists error event data for the Error Tracking's GitLab backend
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64712
|
||||
milestone: '14.1'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- ErrorTracking::Error
|
||||
feature_categories:
|
||||
- error_tracking
|
||||
description: TODO
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/2d1c197ab0bf10071cb52e579edd3808cb0adc21
|
||||
description: Persists error data for the Error Tracking's GitLab backend
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64712
|
||||
milestone: '14.1'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- IncidentManagement::EscalationPolicy
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists information about escalation policies in a project
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60685
|
||||
milestone: '13.12'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- IncidentManagement::EscalationRule
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists information about escalation rules for incident management
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60685
|
||||
milestone: '13.12'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- IncidentManagement::IssuableEscalationStatus
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists escalation status information for incidents
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65206
|
||||
milestone: '14.2'
|
||||
|
|
|
@ -4,6 +4,7 @@ classes:
|
|||
- IncidentManagement::OncallParticipant
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
- on_call_schedule_management
|
||||
description: Persists information about on-call rotation participants
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49058
|
||||
milestone: '13.7'
|
||||
|
|
|
@ -4,6 +4,7 @@ classes:
|
|||
- IncidentManagement::OncallRotation
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
- on_call_schedule_management
|
||||
description: Persists information about on-call rotation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49058
|
||||
milestone: '13.7'
|
||||
|
|
|
@ -4,6 +4,7 @@ classes:
|
|||
- IncidentManagement::OncallSchedule
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
- on_call_schedule_management
|
||||
description: Persists on-call schedules for incident management in a project
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47407
|
||||
milestone: '13.7'
|
||||
|
|
|
@ -4,6 +4,7 @@ classes:
|
|||
- IncidentManagement::OncallShift
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
- on_call_schedule_management
|
||||
description: Tracks past and present on-call shifts
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49423
|
||||
milestone: '13.8'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- IncidentManagement::PendingEscalations::Alert
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/e5cc867503dfbd54f99df90cce6be39bc4fde712
|
||||
description: Persists information about pending alert escalations for incidents
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64274
|
||||
milestone: '14.1'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- IncidentManagement::PendingEscalations::Issue
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Represents when issues should be escalated according to a project's escalation policy
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65209
|
||||
milestone: '14.3'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- IncidentManagement::TimelineEvent
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists timeline events for an incident
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74530
|
||||
milestone: '14.6'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- IssuableSla
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44928
|
||||
description: Persists information about incident SLAs for incidents
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44253
|
||||
milestone: '13.5'
|
||||
|
|
|
@ -3,6 +3,6 @@ table_name: issues_prometheus_alert_events
|
|||
classes: []
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Adds relationship between PrometheusAlertEvent and issues created due to them
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17477
|
||||
milestone: '12.4'
|
||||
|
|
|
@ -3,6 +3,6 @@ table_name: issues_self_managed_prometheus_alert_events
|
|||
classes: []
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Adds associations between Issues table and Prometheus alerts from self-managed Prometheus instances
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18046
|
||||
milestone: '12.4'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- Alerting::ProjectAlertingSetting
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/39aa9458c282c1dabd3623698da5af3b9a6122a9
|
||||
description: Persists project-level tokens for manual Prometheus installations
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9334
|
||||
milestone: '11.8'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- ErrorTracking::ProjectErrorTrackingSetting
|
||||
feature_categories:
|
||||
- error_tracking
|
||||
description: TODO
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/f40b5860d76a8ea5d964260834a6e83516b0f1fd
|
||||
description: Project settings related to Error Tracking
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/24047
|
||||
milestone: '11.7'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- IncidentManagement::ProjectIncidentManagementSetting
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists project settings for incident management
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9744
|
||||
milestone: '11.9'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- PrometheusAlert
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists information about prometheus alerts from an environment
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6590
|
||||
milestone: '11.2'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- SentryIssue
|
||||
feature_categories:
|
||||
- error_tracking
|
||||
description: TODO
|
||||
description: Persists issue data for the Error Tracking's Sentry backend
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20629
|
||||
milestone: '12.6'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- StatusPage::PublishedIncident
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Corresponds to an issue which has been published to the Status Page
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29994
|
||||
milestone: '13.0'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- StatusPage::ProjectSetting
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Project settings related to Status Page
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25863
|
||||
milestone: '12.9'
|
||||
|
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- ZoomMeeting
|
||||
feature_categories:
|
||||
- incident_management
|
||||
description: TODO
|
||||
description: Persists Zoom meetings, its associations and its metadata
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17890
|
||||
milestone: '12.5'
|
||||
|
|
|
@ -100,7 +100,7 @@ EE: true
|
|||
database records created during Cycle Analytics model spec."
|
||||
- _Any_ contribution from a community member, no matter how small, **may** have
|
||||
a changelog entry regardless of these guidelines if the contributor wants one.
|
||||
- Any [GLEX experiment](experiment_guide/gitlab_experiment.md) changes **should not** have a changelog entry.
|
||||
- Any [experiment](experiment_guide/index.md) changes **should not** have a changelog entry.
|
||||
- An MR that includes only documentation changes **should not** have a changelog entry.
|
||||
|
||||
For more information, see
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
stage: Growth
|
||||
group: Adoption
|
||||
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
|
||||
---
|
||||
|
||||
# Experiment code reviews
|
||||
|
||||
Experiments' code quality can fail our standards for several reasons. These
|
||||
reasons can include not being added to the codebase for a long time, or because
|
||||
of fast iteration to retrieve data. However, having the experiment run (or not
|
||||
run) shouldn't impact GitLab availability. To avoid or identify issues,
|
||||
experiments are initially deployed to a small number of users. Regardless,
|
||||
experiments still need tests.
|
||||
|
||||
Experiments must have corresponding [frontend or feature tests](../testing_guide/index.md) to ensure they
|
||||
exist in the application. These tests should help prevent the experiment code from
|
||||
being removed before the [experiment cleanup process](https://about.gitlab.com/handbook/engineering/development/growth/experimentation/#experiment-cleanup-issue) starts.
|
||||
|
||||
If, as a reviewer or maintainer, you find code that would usually fail review
|
||||
but is acceptable for now, mention your concerns with a note that there's no
|
||||
need to change the code. The author can then add a comment to this piece of code
|
||||
and link to the issue that resolves the experiment. The author or reviewer can add a link to this concern in the
|
||||
experiment rollout issue under the `Experiment Successful Cleanup Concerns` section of the description.
|
||||
If the experiment is successful and becomes part of the product, any items that appear under this section will be addressed.
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
stage: Growth
|
||||
group: Adoption
|
||||
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
|
||||
---
|
||||
|
||||
# Experiment rollouts and feature flags
|
||||
|
||||
## Experiment rollout issue
|
||||
|
||||
Each experiment should have an [experiment rollout](https://gitlab.com/groups/gitlab-org/-/boards/1352542) issue to track the experiment from rollout through to cleanup and removal.
|
||||
The rollout issue is similar to a feature flag rollout issue, and is also used to track the status of an experiment.
|
||||
|
||||
When an experiment is deployed, the due date of the issue should be set (this depends on the experiment but can be up to a few weeks in the future).
|
||||
After the deadline, the issue needs to be resolved and either:
|
||||
|
||||
- It was successful and the experiment becomes the new default.
|
||||
- It was not successful and all code related to the experiment is removed.
|
||||
|
||||
In either case, an outcome of the experiment should be posted to the issue with the reasoning for the decision.
|
||||
|
||||
## Turn off all experiments
|
||||
|
||||
When there is a case on GitLab.com (SaaS) that necessitates turning off all experiments, we have this control.
|
||||
|
||||
You can toggle experiments on SaaS on and off using the `gitlab_experiment` [feature flag](../feature_flags).
|
||||
|
||||
This can be done via chatops:
|
||||
|
||||
- [disable](../feature_flags/controls.md#disabling-feature-flags): `/chatops run feature set gitlab_experiment false`
|
||||
- [enable](../feature_flags/controls.md#process): `/chatops run feature delete gitlab_experiment`
|
||||
- This allows the `default_enabled` [value of true in the yml](https://gitlab.com/gitlab-org/gitlab/-/blob/016430f6751b0c34abb24f74608c80a1a8268f20/config/feature_flags/ops/gitlab_experiment.yml#L8) to be honored.
|
||||
|
||||
## Notes on feature flags
|
||||
|
||||
NOTE:
|
||||
We use the terms "enabled" and "disabled" here, even though it's against our
|
||||
[documentation style guide recommendations](../documentation/styleguide/word_list.md#enable)
|
||||
because these are the terms that the feature flag documentation uses.
|
||||
|
||||
You may already be familiar with the concept of feature flags in GitLab, but using
|
||||
feature flags in experiments is a bit different. While in general terms, a feature flag
|
||||
is viewed as being either `on` or `off`, this isn't accurate for experiments.
|
||||
|
||||
Generally, `off` means that when we ask if a feature flag is enabled, it will always
|
||||
return `false`, and `on` means that it will always return `true`. An interim state,
|
||||
considered `conditional`, also exists. We take advantage of this trinary state of
|
||||
feature flags. To understand this `conditional` aspect: consider that either of these
|
||||
settings puts a feature flag into this state:
|
||||
|
||||
- Setting a `percentage_of_actors` of any percent greater than 0%.
|
||||
- Enabling it for a single user or group.
|
||||
|
||||
Conditional means that it returns `true` in some situations, but not all situations.
|
||||
|
||||
When a feature flag is disabled (meaning the state is `off`), the experiment is
|
||||
considered _inactive_. You can visualize this in the [decision tree diagram](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment#how-it-works)
|
||||
as reaching the first `Running?` node, and traversing the negative path.
|
||||
|
||||
When a feature flag is rolled out to a `percentage_of_actors` or similar (meaning the
|
||||
state is `conditional`) the experiment is considered to be _running_
|
||||
where sometimes the control is assigned, and sometimes the candidate is assigned.
|
||||
We don't refer to this as being enabled, because that's a confusing and overloaded
|
||||
term here. In the experiment terms, our experiment is _running_, and the feature flag is
|
||||
`conditional`.
|
||||
|
||||
When a feature flag is enabled (meaning the state is `on`), the candidate will always be
|
||||
assigned.
|
||||
|
||||
We should try to be consistent with our terms, and so for experiments, we have an
|
||||
_inactive_ experiment until we set the feature flag to `conditional`. After which,
|
||||
our experiment is then considered _running_. If you choose to "enable" your feature flag,
|
||||
you should consider the experiment to be _resolved_, because everyone is assigned
|
||||
the candidate unless they've opted out of experimentation.
|
||||
|
||||
As of GitLab 13.10, work is being done to improve this process and how we communicate
|
||||
about it.
|
|
@ -1,586 +1,11 @@
|
|||
---
|
||||
stage: Growth
|
||||
group: Adoption
|
||||
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
|
||||
redirect_to: 'index.md'
|
||||
remove_date: '2022-08-05'
|
||||
---
|
||||
|
||||
# Implementing an A/B/n experiment
|
||||
This document was moved to [another location](index.md).
|
||||
|
||||
## Introduction
|
||||
|
||||
Experiments in GitLab are tightly coupled with the concepts provided by
|
||||
[Feature flags in development of GitLab](../feature_flags/index.md). You're strongly encouraged
|
||||
to read and understand the [Feature flags in development of GitLab](../feature_flags/index.md)
|
||||
portion of the documentation before considering running experiments. Experiments add additional
|
||||
concepts which may seem confusing or advanced without understanding the underpinnings of how GitLab
|
||||
uses feature flags in development. One concept: experiments can be run with multiple variants,
|
||||
which are sometimes referred to as A/B/n tests.
|
||||
|
||||
We use the [`gitlab-experiment` gem](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment),
|
||||
sometimes referred to as GLEX, to run our experiments. The gem exists in a separate repository
|
||||
so it can be shared across any GitLab property that uses Ruby. You should feel comfortable reading
|
||||
the documentation on that project if you want to dig into more advanced topics or open issues. Be
|
||||
aware that the documentation there reflects what's in the main branch and may not be the same as
|
||||
the version being used within GitLab.
|
||||
|
||||
## Glossary of terms
|
||||
|
||||
To ensure a shared language, you should understand these fundamental terms we use
|
||||
when communicating about experiments:
|
||||
|
||||
- `experiment`: Any deviation of code paths we want to run at some times, but not others.
|
||||
- `context`: A consistent experience we provide in an experiment.
|
||||
- `control`: The default, or "original" code path.
|
||||
- `candidate`: Defines an experiment with only one code path.
|
||||
- `variant(s)`: Defines an experiment with multiple code paths.
|
||||
- `behaviors`: Used to reference all possible code paths of an experiment, including the control.
|
||||
|
||||
## Implementing an experiment
|
||||
|
||||
[Examples](https://gitlab.com/gitlab-org/growth/growth/-/wikis/GLEX-Framework-code-examples)
|
||||
|
||||
Start by generating a feature flag using the `bin/feature-flag` command as you
|
||||
normally would for a development feature flag, making sure to use `experiment` for
|
||||
the type. For the sake of documentation let's name our feature flag (and experiment)
|
||||
"pill_color".
|
||||
|
||||
```shell
|
||||
bin/feature-flag pill_color -t experiment
|
||||
```
|
||||
|
||||
After you generate the desired feature flag, you can immediately implement an
|
||||
experiment in code. An experiment implementation can be as simple as:
|
||||
|
||||
```ruby
|
||||
experiment(:pill_color, actor: current_user) do |e|
|
||||
e.control { 'control' }
|
||||
e.variant(:red) { 'red' }
|
||||
e.variant(:blue) { 'blue' }
|
||||
end
|
||||
```
|
||||
|
||||
When this code executes, the experiment is run, a variant is assigned, and (if within a
|
||||
controller or view) a `window.gl.experiments.pill_color` object will be available in the
|
||||
client layer, with details like:
|
||||
|
||||
- The assigned variant.
|
||||
- The context key for client tracking events.
|
||||
|
||||
In addition, when an experiment runs, an event is tracked for
|
||||
the experiment `:assignment`. We cover more about events, tracking, and
|
||||
the client layer later.
|
||||
|
||||
In local development, you can make the experiment active by using the feature flag
|
||||
interface. You can also target specific cases by providing the relevant experiment
|
||||
to the call to enable the feature flag:
|
||||
|
||||
```ruby
|
||||
# Enable for everyone
|
||||
Feature.enable(:pill_color)
|
||||
|
||||
# Get the `experiment` method -- already available in controllers, views, and mailers.
|
||||
include Gitlab::Experiment::Dsl
|
||||
# Enable for only the first user
|
||||
Feature.enable(:pill_color, experiment(:pill_color, actor: User.first))
|
||||
```
|
||||
|
||||
To roll out your experiment feature flag on an environment, run
|
||||
the following command using ChatOps (which is covered in more depth in the
|
||||
[Feature flags in development of GitLab](../feature_flags/index.md) documentation).
|
||||
This command creates a scenario where half of everyone who encounters
|
||||
the experiment would be assigned the _control_, 25% would be assigned the _red_
|
||||
variant, and 25% would be assigned the _blue_ variant:
|
||||
|
||||
```slack
|
||||
/chatops run feature set pill_color 50 --actors
|
||||
```
|
||||
|
||||
For an even distribution in this example, change the command to set it to 66% instead
|
||||
of 50.
|
||||
|
||||
NOTE:
|
||||
To immediately stop running an experiment, use the
|
||||
`/chatops run feature set pill_color false` command.
|
||||
|
||||
WARNING:
|
||||
We strongly recommend using the `--actors` flag when using the ChatOps commands,
|
||||
as anything else may give odd behaviors due to how the caching of variant assignment is
|
||||
handled.
|
||||
|
||||
We can also implement this experiment in a HAML file with HTML wrappings:
|
||||
|
||||
```haml
|
||||
#cta-interface
|
||||
- experiment(:pill_color, actor: current_user) do |e|
|
||||
- e.control do
|
||||
.pill-button control
|
||||
- e.variant(:red) do
|
||||
.pill-button.red red
|
||||
- e.variant(:blue) do
|
||||
.pill-button.blue blue
|
||||
```
|
||||
|
||||
### The importance of context
|
||||
|
||||
In our previous example experiment, our context (this is an important term) is a hash
|
||||
that's set to `{ actor: current_user }`. Context must be unique based on how you
|
||||
want to run your experiment, and should be understood at a lower level.
|
||||
|
||||
It's expected, and recommended, that you use some of these
|
||||
contexts to simplify reporting:
|
||||
|
||||
- `{ actor: current_user }`: Assigns a variant and is "sticky" to each user
|
||||
(or "client" if `current_user` is nil) who enters the experiment.
|
||||
- `{ project: project }`: Assigns a variant and is "sticky" to the project currently
|
||||
being viewed. If running your experiment is more useful when viewing a project,
|
||||
rather than when a specific user is viewing any project, consider this approach.
|
||||
- `{ group: group }`: Similar to the project example, but applies to a wider
|
||||
scope of projects and users.
|
||||
- `{ actor: current_user, project: project }`: Assigns a variant and is "sticky"
|
||||
to the user who is viewing the given project. This creates a different variant
|
||||
assignment possibility for every project that `current_user` views. Understand this
|
||||
can create a large cache size if an experiment like this in a highly trafficked part
|
||||
of the application.
|
||||
- `{ wday: Time.current.wday }`: Assigns a variant based on the current day of the
|
||||
week. In this example, it would consistently assign one variant on Friday, and a
|
||||
potentially different variant on Saturday.
|
||||
|
||||
Context is critical to how you define and report on your experiment. It's usually
|
||||
the most important aspect of how you choose to implement your experiment, so consider
|
||||
it carefully, and discuss it with the wider team if needed. Also, take into account
|
||||
that the context you choose affects our cache size.
|
||||
|
||||
After the above examples, we can state the general case: *given a specific
|
||||
and consistent context, we can provide a consistent experience and track events for
|
||||
that experience.* To dive a bit deeper into the implementation details: a context key
|
||||
is generated from the context that's provided. Use this context key to:
|
||||
|
||||
- Determine the assigned variant.
|
||||
- Identify events tracked against that context key.
|
||||
|
||||
We can think about this as the experience that we've rendered, which is both dictated
|
||||
and tracked by the context key. The context key is used to track the interaction and
|
||||
results of the experience we've rendered to that context key. These concepts are
|
||||
somewhat abstract and hard to understand initially, but this approach enables us to
|
||||
communicate about experiments as something that's wider than just user behavior.
|
||||
|
||||
NOTE:
|
||||
Using `actor:` utilizes cookies if the `current_user` is nil. If you don't need
|
||||
cookies though - meaning that the exposed functionality would only be visible to
|
||||
signed in users - `{ user: current_user }` would be just as effective.
|
||||
|
||||
WARNING:
|
||||
The caching of variant assignment is done by using this context, and so consider
|
||||
your impact on the cache size when defining your experiment. If you use
|
||||
`{ time: Time.current }` you would be inflating the cache size every time the
|
||||
experiment is run. Not only that, your experiment would not be "sticky" and events
|
||||
wouldn't be resolvable.
|
||||
|
||||
### Advanced experimentation
|
||||
|
||||
There are two ways to implement an experiment:
|
||||
|
||||
1. The simple experiment style described previously.
|
||||
1. A more advanced style where an experiment class is provided.
|
||||
|
||||
The advanced style is handled by naming convention, and works similar to what you
|
||||
would expect in Rails.
|
||||
|
||||
To generate a custom experiment class that can override the defaults in
|
||||
`ApplicationExperiment` use the Rails generator:
|
||||
|
||||
```shell
|
||||
rails generate gitlab:experiment pill_color control red blue
|
||||
```
|
||||
|
||||
This generates an experiment class in `app/experiments/pill_color_experiment.rb`
|
||||
with the _behaviors_ we've provided to the generator. Here's an example
|
||||
of how that class would look after migrating our previous example into it:
|
||||
|
||||
```ruby
|
||||
class PillColorExperiment < ApplicationExperiment
|
||||
control { 'control' }
|
||||
variant(:red) { 'red' }
|
||||
variant(:blue) { 'blue' }
|
||||
end
|
||||
```
|
||||
|
||||
We can now simplify where we run our experiment to the following call, instead of
|
||||
providing the block we were initially providing, by explicitly calling `run`:
|
||||
|
||||
```ruby
|
||||
experiment(:pill_color, actor: current_user).run
|
||||
```
|
||||
|
||||
The _behaviors_ we defined in our experiment class represent the default
|
||||
implementation. You can still use the block syntax to override these _behaviors_
|
||||
however, so the following would also be valid:
|
||||
|
||||
```ruby
|
||||
experiment(:pill_color, actor: current_user) do |e|
|
||||
e.control { '<strong>control</strong>' }
|
||||
end
|
||||
```
|
||||
|
||||
NOTE:
|
||||
When passing a block to the `experiment` method, it is implicitly invoked as
|
||||
if `run` has been called.
|
||||
|
||||
#### Segmentation rules
|
||||
|
||||
You can use runtime segmentation rules to, for instance, segment contexts into a specific
|
||||
variant. The `segment` method is a callback (like `before_action`) and so allows providing
|
||||
a block or method name.
|
||||
|
||||
In this example, any user named `'Richard'` would always be assigned the _red_
|
||||
variant, and any account older than 2 weeks old would be assigned the _blue_ variant:
|
||||
|
||||
```ruby
|
||||
class PillColorExperiment < ApplicationExperiment
|
||||
# ...registered behaviors
|
||||
|
||||
segment(variant: :red) { context.actor.first_name == 'Richard' }
|
||||
segment :old_account?, variant: :blue
|
||||
|
||||
private
|
||||
|
||||
def old_account?
|
||||
context.actor.created_at < 2.weeks.ago
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
When an experiment runs, the segmentation rules are executed in the order they're
|
||||
defined. The first segmentation rule to produce a truthy result assigns the variant.
|
||||
|
||||
In our example, any user named `'Richard'`, regardless of account age, will always
|
||||
be assigned the _red_ variant. If you want the opposite logic, flip the order.
|
||||
|
||||
NOTE:
|
||||
Keep in mind when defining segmentation rules: after a truthy result, the remaining
|
||||
segmentation rules are skipped to achieve optimal performance.
|
||||
|
||||
#### Exclusion rules
|
||||
|
||||
Exclusion rules are similar to segmentation rules, but are intended to determine
|
||||
if a context should even be considered as something we should include in the experiment
|
||||
and track events toward. Exclusion means we don't care about the events in relation
|
||||
to the given context.
|
||||
|
||||
These examples exclude all users named `'Richard'`, *and* any account
|
||||
older than 2 weeks old. Not only are they given the control behavior - which could
|
||||
be nothing - but no events are tracked in these cases as well.
|
||||
|
||||
```ruby
|
||||
class PillColorExperiment < ApplicationExperiment
|
||||
# ...registered behaviors
|
||||
|
||||
exclude :old_account?, ->{ context.actor.first_name == 'Richard' }
|
||||
|
||||
private
|
||||
|
||||
def old_account?
|
||||
context.actor.created_at < 2.weeks.ago
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
You may also need to check exclusion in custom tracking logic by calling `should_track?`:
|
||||
|
||||
```ruby
|
||||
class PillColorExperiment < ApplicationExperiment
|
||||
# ...registered behaviors
|
||||
|
||||
def expensive_tracking_logic
|
||||
return unless should_track?
|
||||
|
||||
track(:my_event, value: expensive_method_call)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Tracking events
|
||||
|
||||
One of the most important aspects of experiments is gathering data and reporting on
|
||||
it. You can use the `track` method to track events across an experimental implementation.
|
||||
You can track events consistently to an experiment if you provide the same context between
|
||||
calls to your experiment. If you do not yet understand context, you should read
|
||||
about contexts now.
|
||||
|
||||
We can assume we run the experiment in one or a few places, but
|
||||
track events potentially in many places. The tracking call remains the same, with
|
||||
the arguments you would normally use when
|
||||
[tracking events using snowplow](../snowplow/index.md). The easiest example
|
||||
of tracking an event in Ruby would be:
|
||||
|
||||
```ruby
|
||||
experiment(:pill_color, actor: current_user).track(:clicked)
|
||||
```
|
||||
|
||||
When you run an experiment with any of the examples so far, an `:assignment` event
|
||||
is tracked automatically by default. All events that are tracked from an
|
||||
experiment have a special
|
||||
[experiment context](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-3)
|
||||
added to the event. This can be used - typically by the data team - to create a connection
|
||||
between the events on a given experiment.
|
||||
|
||||
If our current user hasn't encountered the experiment yet (meaning where the experiment
|
||||
is run), and we track an event for them, they are assigned a variant and see
|
||||
that variant if they ever encountered the experiment later, when an `:assignment`
|
||||
event would be tracked at that time for them.
|
||||
|
||||
NOTE:
|
||||
GitLab tries to be sensitive and respectful of our customers regarding tracking,
|
||||
so our experimentation library allows us to implement an experiment without ever tracking identifying
|
||||
IDs. It's not always possible, though, based on experiment reporting requirements.
|
||||
You may be asked from time to time to track a specific record ID in experiments.
|
||||
The approach is largely up to the PM and engineer creating the implementation.
|
||||
No recommendations are provided here at this time.
|
||||
|
||||
## Testing with RSpec
|
||||
|
||||
In the course of working with experiments, you'll probably want to utilize the RSpec
|
||||
tooling that's built in. This happens automatically for files in `spec/experiments`, but
|
||||
for other files and specs you want to include it in, you can specify the `:experiment` type:
|
||||
|
||||
```ruby
|
||||
it "tests experiments nicely", :experiment do
|
||||
end
|
||||
```
|
||||
|
||||
### Stub helpers
|
||||
|
||||
You can stub experiments using `stub_experiments`. Pass it a hash using experiment
|
||||
names as the keys, and the variants you want each to resolve to, as the values:
|
||||
|
||||
```ruby
|
||||
# Ensures the experiments named `:example` & `:example2` are both "enabled" and
|
||||
# that each will resolve to the given variant (`:my_variant` and `:control`
|
||||
# respectively).
|
||||
stub_experiments(example: :my_variant, example2: :control)
|
||||
|
||||
experiment(:example) do |e|
|
||||
e.enabled? # => true
|
||||
e.assigned.name # => 'my_variant'
|
||||
end
|
||||
|
||||
experiment(:example2) do |e|
|
||||
e.enabled? # => true
|
||||
e.assigned.name # => 'control'
|
||||
end
|
||||
```
|
||||
|
||||
### Exclusion, segmentation, and behavior matchers
|
||||
|
||||
You can also test things like the registered behaviors, the exclusions, and
|
||||
segmentations using the matchers.
|
||||
|
||||
```ruby
|
||||
class ExampleExperiment < ApplicationExperiment
|
||||
control { }
|
||||
candidate { '_candidate_' }
|
||||
|
||||
exclude { context.actor.first_name == 'Richard' }
|
||||
segment(variant: :candidate) { context.actor.username == 'jejacks0n' }
|
||||
end
|
||||
|
||||
excluded = double(username: 'rdiggitty', first_name: 'Richard')
|
||||
segmented = double(username: 'jejacks0n', first_name: 'Jeremy')
|
||||
|
||||
# register_behavior matcher
|
||||
expect(experiment(:example)).to register_behavior(:control)
|
||||
expect(experiment(:example)).to register_behavior(:candidate).with('_candidate_')
|
||||
|
||||
# exclude matcher
|
||||
expect(experiment(:example)).to exclude(actor: excluded)
|
||||
expect(experiment(:example)).not_to exclude(actor: segmented)
|
||||
|
||||
# segment matcher
|
||||
expect(experiment(:example)).to segment(actor: segmented).into(:candidate)
|
||||
expect(experiment(:example)).not_to segment(actor: excluded)
|
||||
```
|
||||
|
||||
### Tracking matcher
|
||||
|
||||
Tracking events is a major aspect of experimentation. We try
|
||||
to provide a flexible way to ensure your tracking calls are covered.
|
||||
|
||||
You can do this on the instance level or at an "any instance" level:
|
||||
|
||||
```ruby
|
||||
subject = experiment(:example)
|
||||
|
||||
expect(subject).to track(:my_event)
|
||||
|
||||
subject.track(:my_event)
|
||||
```
|
||||
|
||||
You can use the `on_next_instance` chain method to specify that it will happen
|
||||
on the next instance of the experiment. This helps you if you're calling
|
||||
`experiment(:example).track` downstream:
|
||||
|
||||
```ruby
|
||||
expect(experiment(:example)).to track(:my_event).on_next_instance
|
||||
|
||||
experiment(:example).track(:my_event)
|
||||
```
|
||||
|
||||
A full example of the methods you can chain onto the `track` matcher:
|
||||
|
||||
```ruby
|
||||
expect(experiment(:example)).to track(:my_event, value: 1, property: '_property_')
|
||||
.on_next_instance
|
||||
.with_context(foo: :bar)
|
||||
.for(:variant_name)
|
||||
|
||||
experiment(:example, :variant_name, foo: :bar).track(:my_event, value: 1, property: '_property_')
|
||||
```
|
||||
|
||||
## Experiments in the client layer
|
||||
|
||||
Any experiment that's been run in the request lifecycle surfaces in `window.gl.experiments`,
|
||||
and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-3)
|
||||
so it can be used when resolving experimentation in the client layer.
|
||||
|
||||
Given that we've defined a class for our experiment, and have defined the variants for it, we can publish that experiment in a couple ways.
|
||||
|
||||
The first way is simply by running the experiment. Assuming the experiment has been run, it will surface in the client layer without having to do anything special.
|
||||
|
||||
The second way doesn't run the experiment and is intended to be used if the experiment only needs to surface in the client layer. To accomplish this we can simply `.publish` the experiment. This won't run any logic, but does surface the experiment details in the client layer so they can be utilized there.
|
||||
|
||||
An example might be to publish an experiment in a `before_action` in a controller. Assuming we've defined the `PillColorExperiment` class, like we have above, we can surface it to the client by publishing it instead of running it:
|
||||
|
||||
```ruby
|
||||
before_action -> { experiment(:pill_color).publish }, only: [:show]
|
||||
```
|
||||
|
||||
You can then see this surface in the JavaScript console:
|
||||
|
||||
```javascript
|
||||
window.gl.experiments // => { pill_color: { excluded: false, experiment: "pill_color", key: "ca63ac02", variant: "candidate" } }
|
||||
```
|
||||
|
||||
### Using experiments in Vue
|
||||
|
||||
With the `gitlab-experiment` component, you can define slots that match the name of the
|
||||
variants pushed to `window.gl.experiments`.
|
||||
|
||||
We can make use of the named slots in the Vue component, that match the behaviors defined in :
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
|
||||
|
||||
export default {
|
||||
components: { GitlabExperiment }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gitlab-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>
|
||||
</gitlab-experiment>
|
||||
</template>
|
||||
```
|
||||
|
||||
NOTE:
|
||||
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
|
||||
describe('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
|
||||
|
||||
NOTE:
|
||||
We use the terms "enabled" and "disabled" here, even though it's against our
|
||||
[documentation style guide recommendations](../documentation/styleguide/word_list.md#enable)
|
||||
because these are the terms that the feature flag documentation uses.
|
||||
|
||||
You may already be familiar with the concept of feature flags in GitLab, but using
|
||||
feature flags in experiments is a bit different. While in general terms, a feature flag
|
||||
is viewed as being either `on` or `off`, this isn't accurate for experiments.
|
||||
|
||||
Generally, `off` means that when we ask if a feature flag is enabled, it will always
|
||||
return `false`, and `on` means that it will always return `true`. An interim state,
|
||||
considered `conditional`, also exists. We take advantage of this trinary state of
|
||||
feature flags. To understand this `conditional` aspect: consider that either of these
|
||||
settings puts a feature flag into this state:
|
||||
|
||||
- Setting a `percentage_of_actors` of any percent greater than 0%.
|
||||
- Enabling it for a single user or group.
|
||||
|
||||
Conditional means that it returns `true` in some situations, but not all situations.
|
||||
|
||||
When a feature flag is disabled (meaning the state is `off`), the experiment is
|
||||
considered _inactive_. You can visualize this in the [decision tree diagram](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment#how-it-works)
|
||||
as reaching the first `Running?` node, and traversing the negative path.
|
||||
|
||||
When a feature flag is rolled out to a `percentage_of_actors` or similar (meaning the
|
||||
state is `conditional`) the experiment is considered to be _running_
|
||||
where sometimes the control is assigned, and sometimes the candidate is assigned.
|
||||
We don't refer to this as being enabled, because that's a confusing and overloaded
|
||||
term here. In the experiment terms, our experiment is _running_, and the feature flag is
|
||||
`conditional`.
|
||||
|
||||
When a feature flag is enabled (meaning the state is `on`), the candidate will always be
|
||||
assigned.
|
||||
|
||||
We should try to be consistent with our terms, and so for experiments, we have an
|
||||
_inactive_ experiment until we set the feature flag to `conditional`. After which,
|
||||
our experiment is then considered _running_. If you choose to "enable" your feature flag,
|
||||
you should consider the experiment to be _resolved_, because everyone is assigned
|
||||
the candidate unless they've opted out of experimentation.
|
||||
|
||||
As of GitLab 13.10, work is being done to improve this process and how we communicate
|
||||
about it.
|
||||
<!-- This redirect file can be deleted after 2022-08-05. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -0,0 +1,369 @@
|
|||
---
|
||||
stage: Growth
|
||||
group: Adoption
|
||||
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
|
||||
---
|
||||
|
||||
# Implementing an A/B/n experiment
|
||||
|
||||
## Implementing an experiment
|
||||
|
||||
[Examples](https://gitlab.com/gitlab-org/growth/growth/-/wikis/GLEX-Framework-code-examples)
|
||||
|
||||
Start by generating a feature flag using the `bin/feature-flag` command as you
|
||||
normally would for a development feature flag, making sure to use `experiment` for
|
||||
the type. For the sake of documentation let's name our feature flag (and experiment)
|
||||
"pill_color".
|
||||
|
||||
```shell
|
||||
bin/feature-flag pill_color -t experiment
|
||||
```
|
||||
|
||||
After you generate the desired feature flag, you can immediately implement an
|
||||
experiment in code. An experiment implementation can be as simple as:
|
||||
|
||||
```ruby
|
||||
experiment(:pill_color, actor: current_user) do |e|
|
||||
e.control { 'control' }
|
||||
e.variant(:red) { 'red' }
|
||||
e.variant(:blue) { 'blue' }
|
||||
end
|
||||
```
|
||||
|
||||
When this code executes, the experiment is run, a variant is assigned, and (if within a
|
||||
controller or view) a `window.gl.experiments.pill_color` object will be available in the
|
||||
client layer, with details like:
|
||||
|
||||
- The assigned variant.
|
||||
- The context key for client tracking events.
|
||||
|
||||
In addition, when an experiment runs, an event is tracked for
|
||||
the experiment `:assignment`. We cover more about events, tracking, and
|
||||
the client layer later.
|
||||
|
||||
In local development, you can make the experiment active by using the feature flag
|
||||
interface. You can also target specific cases by providing the relevant experiment
|
||||
to the call to enable the feature flag:
|
||||
|
||||
```ruby
|
||||
# Enable for everyone
|
||||
Feature.enable(:pill_color)
|
||||
|
||||
# Get the `experiment` method -- already available in controllers, views, and mailers.
|
||||
include Gitlab::Experiment::Dsl
|
||||
# Enable for only the first user
|
||||
Feature.enable(:pill_color, experiment(:pill_color, actor: User.first))
|
||||
```
|
||||
|
||||
To roll out your experiment feature flag on an environment, run
|
||||
the following command using ChatOps (which is covered in more depth in the
|
||||
[Feature flags in development of GitLab](../feature_flags/index.md) documentation).
|
||||
This command creates a scenario where half of everyone who encounters
|
||||
the experiment would be assigned the _control_, 25% would be assigned the _red_
|
||||
variant, and 25% would be assigned the _blue_ variant:
|
||||
|
||||
```slack
|
||||
/chatops run feature set pill_color 50 --actors
|
||||
```
|
||||
|
||||
For an even distribution in this example, change the command to set it to 66% instead
|
||||
of 50.
|
||||
|
||||
NOTE:
|
||||
To immediately stop running an experiment, use the
|
||||
`/chatops run feature set pill_color false` command.
|
||||
|
||||
WARNING:
|
||||
We strongly recommend using the `--actors` flag when using the ChatOps commands,
|
||||
as anything else may give odd behaviors due to how the caching of variant assignment is
|
||||
handled.
|
||||
|
||||
We can also implement this experiment in a HAML file with HTML wrappings:
|
||||
|
||||
```haml
|
||||
#cta-interface
|
||||
- experiment(:pill_color, actor: current_user) do |e|
|
||||
- e.control do
|
||||
.pill-button control
|
||||
- e.variant(:red) do
|
||||
.pill-button.red red
|
||||
- e.variant(:blue) do
|
||||
.pill-button.blue blue
|
||||
```
|
||||
|
||||
### The importance of context
|
||||
|
||||
In our previous example experiment, our context (this is an important term) is a hash
|
||||
that's set to `{ actor: current_user }`. Context must be unique based on how you
|
||||
want to run your experiment, and should be understood at a lower level.
|
||||
|
||||
It's expected, and recommended, that you use some of these
|
||||
contexts to simplify reporting:
|
||||
|
||||
- `{ actor: current_user }`: Assigns a variant and is "sticky" to each user
|
||||
(or "client" if `current_user` is nil) who enters the experiment.
|
||||
- `{ project: project }`: Assigns a variant and is "sticky" to the project currently
|
||||
being viewed. If running your experiment is more useful when viewing a project,
|
||||
rather than when a specific user is viewing any project, consider this approach.
|
||||
- `{ group: group }`: Similar to the project example, but applies to a wider
|
||||
scope of projects and users.
|
||||
- `{ actor: current_user, project: project }`: Assigns a variant and is "sticky"
|
||||
to the user who is viewing the given project. This creates a different variant
|
||||
assignment possibility for every project that `current_user` views. Understand this
|
||||
can create a large cache size if an experiment like this in a highly trafficked part
|
||||
of the application.
|
||||
- `{ wday: Time.current.wday }`: Assigns a variant based on the current day of the
|
||||
week. In this example, it would consistently assign one variant on Friday, and a
|
||||
potentially different variant on Saturday.
|
||||
|
||||
Context is critical to how you define and report on your experiment. It's usually
|
||||
the most important aspect of how you choose to implement your experiment, so consider
|
||||
it carefully, and discuss it with the wider team if needed. Also, take into account
|
||||
that the context you choose affects our cache size.
|
||||
|
||||
After the above examples, we can state the general case: *given a specific
|
||||
and consistent context, we can provide a consistent experience and track events for
|
||||
that experience.* To dive a bit deeper into the implementation details: a context key
|
||||
is generated from the context that's provided. Use this context key to:
|
||||
|
||||
- Determine the assigned variant.
|
||||
- Identify events tracked against that context key.
|
||||
|
||||
We can think about this as the experience that we've rendered, which is both dictated
|
||||
and tracked by the context key. The context key is used to track the interaction and
|
||||
results of the experience we've rendered to that context key. These concepts are
|
||||
somewhat abstract and hard to understand initially, but this approach enables us to
|
||||
communicate about experiments as something that's wider than just user behavior.
|
||||
|
||||
NOTE:
|
||||
Using `actor:` utilizes cookies if the `current_user` is nil. If you don't need
|
||||
cookies though - meaning that the exposed functionality would only be visible to
|
||||
signed in users - `{ user: current_user }` would be just as effective.
|
||||
|
||||
WARNING:
|
||||
The caching of variant assignment is done by using this context, and so consider
|
||||
your impact on the cache size when defining your experiment. If you use
|
||||
`{ time: Time.current }` you would be inflating the cache size every time the
|
||||
experiment is run. Not only that, your experiment would not be "sticky" and events
|
||||
wouldn't be resolvable.
|
||||
|
||||
### Advanced experimentation
|
||||
|
||||
There are two ways to implement an experiment:
|
||||
|
||||
1. The simple experiment style described previously.
|
||||
1. A more advanced style where an experiment class is provided.
|
||||
|
||||
The advanced style is handled by naming convention, and works similar to what you
|
||||
would expect in Rails.
|
||||
|
||||
To generate a custom experiment class that can override the defaults in
|
||||
`ApplicationExperiment` use the Rails generator:
|
||||
|
||||
```shell
|
||||
rails generate gitlab:experiment pill_color control red blue
|
||||
```
|
||||
|
||||
This generates an experiment class in `app/experiments/pill_color_experiment.rb`
|
||||
with the _behaviors_ we've provided to the generator. Here's an example
|
||||
of how that class would look after migrating our previous example into it:
|
||||
|
||||
```ruby
|
||||
class PillColorExperiment < ApplicationExperiment
|
||||
control { 'control' }
|
||||
variant(:red) { 'red' }
|
||||
variant(:blue) { 'blue' }
|
||||
end
|
||||
```
|
||||
|
||||
We can now simplify where we run our experiment to the following call, instead of
|
||||
providing the block we were initially providing, by explicitly calling `run`:
|
||||
|
||||
```ruby
|
||||
experiment(:pill_color, actor: current_user).run
|
||||
```
|
||||
|
||||
The _behaviors_ we defined in our experiment class represent the default
|
||||
implementation. You can still use the block syntax to override these _behaviors_
|
||||
however, so the following would also be valid:
|
||||
|
||||
```ruby
|
||||
experiment(:pill_color, actor: current_user) do |e|
|
||||
e.control { '<strong>control</strong>' }
|
||||
end
|
||||
```
|
||||
|
||||
NOTE:
|
||||
When passing a block to the `experiment` method, it is implicitly invoked as
|
||||
if `run` has been called.
|
||||
|
||||
#### Segmentation rules
|
||||
|
||||
You can use runtime segmentation rules to, for instance, segment contexts into a specific
|
||||
variant. The `segment` method is a callback (like `before_action`) and so allows providing
|
||||
a block or method name.
|
||||
|
||||
In this example, any user named `'Richard'` would always be assigned the _red_
|
||||
variant, and any account older than 2 weeks old would be assigned the _blue_ variant:
|
||||
|
||||
```ruby
|
||||
class PillColorExperiment < ApplicationExperiment
|
||||
# ...registered behaviors
|
||||
|
||||
segment(variant: :red) { context.actor.first_name == 'Richard' }
|
||||
segment :old_account?, variant: :blue
|
||||
|
||||
private
|
||||
|
||||
def old_account?
|
||||
context.actor.created_at < 2.weeks.ago
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
When an experiment runs, the segmentation rules are executed in the order they're
|
||||
defined. The first segmentation rule to produce a truthy result assigns the variant.
|
||||
|
||||
In our example, any user named `'Richard'`, regardless of account age, will always
|
||||
be assigned the _red_ variant. If you want the opposite logic, flip the order.
|
||||
|
||||
NOTE:
|
||||
Keep in mind when defining segmentation rules: after a truthy result, the remaining
|
||||
segmentation rules are skipped to achieve optimal performance.
|
||||
|
||||
#### Exclusion rules
|
||||
|
||||
Exclusion rules are similar to segmentation rules, but are intended to determine
|
||||
if a context should even be considered as something we should include in the experiment
|
||||
and track events toward. Exclusion means we don't care about the events in relation
|
||||
to the given context.
|
||||
|
||||
These examples exclude all users named `'Richard'`, *and* any account
|
||||
older than 2 weeks old. Not only are they given the control behavior - which could
|
||||
be nothing - but no events are tracked in these cases as well.
|
||||
|
||||
```ruby
|
||||
class PillColorExperiment < ApplicationExperiment
|
||||
# ...registered behaviors
|
||||
|
||||
exclude :old_account?, ->{ context.actor.first_name == 'Richard' }
|
||||
|
||||
private
|
||||
|
||||
def old_account?
|
||||
context.actor.created_at < 2.weeks.ago
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
You may also need to check exclusion in custom tracking logic by calling `should_track?`:
|
||||
|
||||
```ruby
|
||||
class PillColorExperiment < ApplicationExperiment
|
||||
# ...registered behaviors
|
||||
|
||||
def expensive_tracking_logic
|
||||
return unless should_track?
|
||||
|
||||
track(:my_event, value: expensive_method_call)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Tracking events
|
||||
|
||||
One of the most important aspects of experiments is gathering data and reporting on
|
||||
it. You can use the `track` method to track events across an experimental implementation.
|
||||
You can track events consistently to an experiment if you provide the same context between
|
||||
calls to your experiment. If you do not yet understand context, you should read
|
||||
about contexts now.
|
||||
|
||||
We can assume we run the experiment in one or a few places, but
|
||||
track events potentially in many places. The tracking call remains the same, with
|
||||
the arguments you would normally use when
|
||||
[tracking events using snowplow](../snowplow/index.md). The easiest example
|
||||
of tracking an event in Ruby would be:
|
||||
|
||||
```ruby
|
||||
experiment(:pill_color, actor: current_user).track(:clicked)
|
||||
```
|
||||
|
||||
When you run an experiment with any of the examples so far, an `:assignment` event
|
||||
is tracked automatically by default. All events that are tracked from an
|
||||
experiment have a special
|
||||
[experiment context](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-3)
|
||||
added to the event. This can be used - typically by the data team - to create a connection
|
||||
between the events on a given experiment.
|
||||
|
||||
If our current user hasn't encountered the experiment yet (meaning where the experiment
|
||||
is run), and we track an event for them, they are assigned a variant and see
|
||||
that variant if they ever encountered the experiment later, when an `:assignment`
|
||||
event would be tracked at that time for them.
|
||||
|
||||
NOTE:
|
||||
GitLab tries to be sensitive and respectful of our customers regarding tracking,
|
||||
so our experimentation library allows us to implement an experiment without ever tracking identifying
|
||||
IDs. It's not always possible, though, based on experiment reporting requirements.
|
||||
You may be asked from time to time to track a specific record ID in experiments.
|
||||
The approach is largely up to the PM and engineer creating the implementation.
|
||||
No recommendations are provided here at this time.
|
||||
|
||||
## Experiments in the client layer
|
||||
|
||||
Any experiment that's been run in the request lifecycle surfaces in `window.gl.experiments`,
|
||||
and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-3)
|
||||
so it can be used when resolving experimentation in the client layer.
|
||||
|
||||
Given that we've defined a class for our experiment, and have defined the variants for it, we can publish that experiment in a couple ways.
|
||||
|
||||
The first way is simply by running the experiment. Assuming the experiment has been run, it will surface in the client layer without having to do anything special.
|
||||
|
||||
The second way doesn't run the experiment and is intended to be used if the experiment only needs to surface in the client layer. To accomplish this we can simply `.publish` the experiment. This won't run any logic, but does surface the experiment details in the client layer so they can be utilized there.
|
||||
|
||||
An example might be to publish an experiment in a `before_action` in a controller. Assuming we've defined the `PillColorExperiment` class, like we have above, we can surface it to the client by publishing it instead of running it:
|
||||
|
||||
```ruby
|
||||
before_action -> { experiment(:pill_color).publish }, only: [:show]
|
||||
```
|
||||
|
||||
You can then see this surface in the JavaScript console:
|
||||
|
||||
```javascript
|
||||
window.gl.experiments // => { pill_color: { excluded: false, experiment: "pill_color", key: "ca63ac02", variant: "candidate" } }
|
||||
```
|
||||
|
||||
### Using experiments in Vue
|
||||
|
||||
With the `gitlab-experiment` component, you can define slots that match the name of the
|
||||
variants pushed to `window.gl.experiments`.
|
||||
|
||||
We can make use of the named slots in the Vue component, that match the behaviors defined in :
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
|
||||
|
||||
export default {
|
||||
components: { GitlabExperiment }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gitlab-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>
|
||||
</gitlab-experiment>
|
||||
</template>
|
||||
```
|
||||
|
||||
NOTE:
|
||||
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.
|
|
@ -6,47 +6,46 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Experiment Guide
|
||||
|
||||
Experiments can be conducted by any GitLab team, most often the teams from the [Growth Sub-department](https://about.gitlab.com/handbook/engineering/development/growth/). Experiments are not tied to releases because they primarily target GitLab.com.
|
||||
Experiments can be conducted by any GitLab team, most often the teams from the
|
||||
[Growth Sub-department](https://about.gitlab.com/handbook/engineering/development/growth/).
|
||||
Experiments are not tied to releases because they primarily target GitLab.com.
|
||||
|
||||
Experiments are run as an A/B/n test, and are behind an [experiment feature flag](../feature_flags/#experiment-type) to turn the test on or off. Based on the data the experiment generates, the team decides if the experiment had a positive impact and should be made the new default, or rolled back.
|
||||
Experiments are run as an A/B/n test, and are behind an [experiment feature flag](../feature_flags/#experiment-type)
|
||||
to turn the test on or off. Based on the data the experiment generates, the team decides
|
||||
if the experiment had a positive impact and should be made the new default, or rolled back.
|
||||
|
||||
## Experiment rollout issue
|
||||
Experiments in GitLab are tightly coupled with the concepts provided by
|
||||
[Feature flags in development of GitLab](../feature_flags/index.md). You're strongly encouraged
|
||||
to read and understand the [Feature flags in development of GitLab](../feature_flags/index.md)
|
||||
portion of the documentation before considering running experiments. Experiments add additional
|
||||
concepts which may seem confusing or advanced without understanding the underpinnings of how GitLab
|
||||
uses feature flags in development. One concept: experiments can be run with multiple variants,
|
||||
which are sometimes referred to as A/B/n tests.
|
||||
|
||||
Each experiment should have an [experiment rollout](https://gitlab.com/groups/gitlab-org/-/boards/1352542) issue to track the experiment from rollout through to cleanup and removal.
|
||||
The rollout issue is similar to a feature flag rollout issue, and is also used to track the status of an experiment.
|
||||
When an experiment is deployed, the due date of the issue should be set (this depends on the experiment but can be up to a few weeks in the future).
|
||||
After the deadline, the issue needs to be resolved and either:
|
||||
We use the [`gitlab-experiment` gem](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment),
|
||||
sometimes referred to as GLEX, to run our experiments. The gem exists in a separate repository
|
||||
so it can be shared across any GitLab property that uses Ruby. You should feel comfortable reading
|
||||
the documentation on that project if you want to dig into more advanced topics or open issues. Be
|
||||
aware that the documentation there reflects what's in the main branch and may not be the same as
|
||||
the version being used within GitLab.
|
||||
|
||||
- It was successful and the experiment becomes the new default.
|
||||
- It was not successful and all code related to the experiment is removed.
|
||||
## Glossary of terms
|
||||
|
||||
In either case, an outcome of the experiment should be posted to the issue with the reasoning for the decision.
|
||||
To ensure a shared language, you should understand these fundamental terms we use
|
||||
when communicating about experiments:
|
||||
|
||||
## Code reviews
|
||||
|
||||
Experiments' code quality can fail our standards for several reasons. These
|
||||
reasons can include not being added to the codebase for a long time, or because
|
||||
of fast iteration to retrieve data. However, having the experiment run (or not
|
||||
run) shouldn't impact GitLab availability. To avoid or identify issues,
|
||||
experiments are initially deployed to a small number of users. Regardless,
|
||||
experiments still need tests.
|
||||
|
||||
Experiments must have corresponding [frontend or feature tests](../testing_guide/index.md) to ensure they
|
||||
exist in the application. These tests should help prevent the experiment code from
|
||||
being removed before the [experiment cleanup process](https://about.gitlab.com/handbook/engineering/development/growth/experimentation/#experiment-cleanup-issue) starts.
|
||||
|
||||
If, as a reviewer or maintainer, you find code that would usually fail review
|
||||
but is acceptable for now, mention your concerns with a note that there's no
|
||||
need to change the code. The author can then add a comment to this piece of code
|
||||
and link to the issue that resolves the experiment. The author or reviewer can add a link to this concern in the
|
||||
experiment rollout issue under the `Experiment Successful Cleanup Concerns` section of the description.
|
||||
If the experiment is successful and becomes part of the product, any items that appear under this section will be addressed.
|
||||
- `experiment`: Any deviation of code paths we want to run at some times, but not others.
|
||||
- `context`: A consistent experience we provide in an experiment.
|
||||
- `control`: The default, or "original" code path.
|
||||
- `candidate`: Defines an experiment with only one code path.
|
||||
- `variant(s)`: Defines an experiment with multiple code paths.
|
||||
- `behaviors`: Used to reference all possible code paths of an experiment, including the control.
|
||||
|
||||
## Implementing an experiment
|
||||
|
||||
[`GLEX`](https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment) - or `Gitlab::Experiment`, the `gitlab-experiment` gem - is the preferred option for implementing an experiment in GitLab.
|
||||
|
||||
For more information, see [Implementing an A/B/n experiment using GLEX](gitlab_experiment.md).
|
||||
For more information, see [Implementing an A/B/n experiment using GLEX](implementing_experiments.md).
|
||||
|
||||
This uses [experiment](../feature_flags/index.md#experiment-type) feature flags.
|
||||
|
||||
|
@ -64,15 +63,3 @@ We recommend the following workflow:
|
|||
1. **If the experiment is a success**, designers add the new icon or illustration to the Pajamas UI kit as part of the cleanup process.
|
||||
Engineers can then add it to the [SVG library](https://gitlab-org.gitlab.io/gitlab-svgs/) and modify the implementation based on the
|
||||
[Frontend Development Guidelines](../fe_guide/icons.md#usage-in-hamlrails-2).
|
||||
|
||||
## Turn off all experiments
|
||||
|
||||
When there is a case on GitLab.com (SaaS) that necessitates turning off all experiments, we have this control.
|
||||
|
||||
You can toggle experiments on SaaS on and off using the `gitlab_experiment` [feature flag](../feature_flags).
|
||||
|
||||
This can be done via chatops:
|
||||
|
||||
- [disable](../feature_flags/controls.md#disabling-feature-flags): `/chatops run feature set gitlab_experiment false`
|
||||
- [enable](../feature_flags/controls.md#process): `/chatops run feature delete gitlab_experiment`
|
||||
- This allows the `default_enabled` [value of true in the yml](https://gitlab.com/gitlab-org/gitlab/-/blob/016430f6751b0c34abb24f74608c80a1a8268f20/config/feature_flags/ops/gitlab_experiment.yml#L8) to be honored.
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
---
|
||||
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
|
||||
---
|
||||
|
||||
# Testing experiments
|
||||
|
||||
## Testing experiments with RSpec
|
||||
|
||||
In the course of working with experiments, you'll probably want to utilize the RSpec
|
||||
tooling that's built in. This happens automatically for files in `spec/experiments`, but
|
||||
for other files and specs you want to include it in, you can specify the `:experiment` type:
|
||||
|
||||
```ruby
|
||||
it "tests experiments nicely", :experiment do
|
||||
end
|
||||
```
|
||||
|
||||
### Stub helpers
|
||||
|
||||
You can stub experiments using `stub_experiments`. Pass it a hash using experiment
|
||||
names as the keys, and the variants you want each to resolve to, as the values:
|
||||
|
||||
```ruby
|
||||
# Ensures the experiments named `:example` & `:example2` are both "enabled" and
|
||||
# that each will resolve to the given variant (`:my_variant` and `:control`
|
||||
# respectively).
|
||||
stub_experiments(example: :my_variant, example2: :control)
|
||||
|
||||
experiment(:example) do |e|
|
||||
e.enabled? # => true
|
||||
e.assigned.name # => 'my_variant'
|
||||
end
|
||||
|
||||
experiment(:example2) do |e|
|
||||
e.enabled? # => true
|
||||
e.assigned.name # => 'control'
|
||||
end
|
||||
```
|
||||
|
||||
### Exclusion, segmentation, and behavior matchers
|
||||
|
||||
You can also test things like the registered behaviors, the exclusions, and
|
||||
segmentations using the matchers.
|
||||
|
||||
```ruby
|
||||
class ExampleExperiment < ApplicationExperiment
|
||||
control { }
|
||||
candidate { '_candidate_' }
|
||||
|
||||
exclude { context.actor.first_name == 'Richard' }
|
||||
segment(variant: :candidate) { context.actor.username == 'jejacks0n' }
|
||||
end
|
||||
|
||||
excluded = double(username: 'rdiggitty', first_name: 'Richard')
|
||||
segmented = double(username: 'jejacks0n', first_name: 'Jeremy')
|
||||
|
||||
# register_behavior matcher
|
||||
expect(experiment(:example)).to register_behavior(:control)
|
||||
expect(experiment(:example)).to register_behavior(:candidate).with('_candidate_')
|
||||
|
||||
# exclude matcher
|
||||
expect(experiment(:example)).to exclude(actor: excluded)
|
||||
expect(experiment(:example)).not_to exclude(actor: segmented)
|
||||
|
||||
# segment matcher
|
||||
expect(experiment(:example)).to segment(actor: segmented).into(:candidate)
|
||||
expect(experiment(:example)).not_to segment(actor: excluded)
|
||||
```
|
||||
|
||||
### Tracking matcher
|
||||
|
||||
Tracking events is a major aspect of experimentation. We try
|
||||
to provide a flexible way to ensure your tracking calls are covered.
|
||||
|
||||
You can do this on the instance level or at an "any instance" level:
|
||||
|
||||
```ruby
|
||||
subject = experiment(:example)
|
||||
|
||||
expect(subject).to track(:my_event)
|
||||
|
||||
subject.track(:my_event)
|
||||
```
|
||||
|
||||
You can use the `on_next_instance` chain method to specify that it will happen
|
||||
on the next instance of the experiment. This helps you if you're calling
|
||||
`experiment(:example).track` downstream:
|
||||
|
||||
```ruby
|
||||
expect(experiment(:example)).to track(:my_event).on_next_instance
|
||||
|
||||
experiment(:example).track(:my_event)
|
||||
```
|
||||
|
||||
A full example of the methods you can chain onto the `track` matcher:
|
||||
|
||||
```ruby
|
||||
expect(experiment(:example)).to track(:my_event, value: 1, property: '_property_')
|
||||
.on_next_instance
|
||||
.with_context(foo: :bar)
|
||||
.for(:variant_name)
|
||||
|
||||
experiment(:example, :variant_name, foo: :bar).track(:my_event, value: 1, property: '_property_')
|
||||
```
|
||||
|
||||
## 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
|
||||
describe('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' });
|
||||
// ...
|
||||
});
|
||||
})
|
||||
```
|
|
@ -193,6 +193,8 @@ pending_job_classes.each { |job_class| Gitlab::BackgroundMigration.steal(job_cla
|
|||
|
||||
GitLab 13.6 introduced an issue where a background migration named `BackfillJiraTrackerDeploymentType2` can be permanently stuck in a **pending** state across upgrades. To clean up this stuck migration, see the [13.6.0 version-specific instructions](#1360).
|
||||
|
||||
GitLab 14.2 introduced an issue where a background migration named `BackfillDraftStatusOnMergeRequests` can be permanently stuck in a **pending** state across upgrades when the instance lacks records that match the migration's target. To clean up this stuck migration, see the [14.2.0 version-specific instructions](#1420).
|
||||
|
||||
GitLab 14.4 introduced an issue where a background migration named `PopulateTopicsTotalProjectsCountCache` can be permanently stuck in a **pending** state across upgrades when the instance lacks records that match the migration's target. To clean up this stuck migration, see the [14.4.0 version-specific instructions](#1440).
|
||||
|
||||
GitLab 14.8 introduced an issue where a background migration named `PopulateTopicsNonPrivateProjectsCount` can be permanently stuck in a **pending** state across upgrades. To clean up this stuck migration, see the [14.8.0 version-specific instructions](#1480).
|
||||
|
@ -616,6 +618,17 @@ for how to proceed.
|
|||
```
|
||||
|
||||
- See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-gitlab-139-to-144).
|
||||
- GitLab 14.2.0 includes a
|
||||
[background migration `BackfillDraftStatusOnMergeRequests`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67687)
|
||||
that may remain stuck permanently in a **pending** state when the instance lacks records that match the migration's target.
|
||||
|
||||
To clean up this stuck job, run the following in the [GitLab Rails Console](../administration/operations/rails_console.md):
|
||||
|
||||
```ruby
|
||||
Gitlab::Database::BackgroundMigrationJob.pending.where(class_name: "BackfillDraftStatusOnMergeRequests").find_each do |job|
|
||||
puts Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded("BackfillDraftStatusOnMergeRequests", job.arguments)
|
||||
end
|
||||
```
|
||||
|
||||
### 14.1.0
|
||||
|
||||
|
|
|
@ -217,7 +217,7 @@ You can [configure](#customizing-the-container-scanning-settings) analyzers by u
|
|||
| `ADDITIONAL_CA_CERT_BUNDLE` | `""` | Bundle of CA certs that you want to trust. See [Using a custom SSL CA certificate authority](#using-a-custom-ssl-ca-certificate-authority) for more details. | All |
|
||||
| `CI_APPLICATION_REPOSITORY` | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` | Docker repository URL for the image to be scanned. | All |
|
||||
| `CI_APPLICATION_TAG` | `$CI_COMMIT_SHA` | Docker repository tag for the image to be scanned. | All |
|
||||
| `CS_ANALYZER_IMAGE` | `registry.gitlab.com/security-products/container-scanning:4` | Docker image of the analyzer. | All |
|
||||
| `CS_ANALYZER_IMAGE` | `registry.gitlab.com/security-products/container-scanning:5` | Docker image of the analyzer. | All |
|
||||
| `CS_DEFAULT_BRANCH_IMAGE` | `""` | The name of the `DOCKER_IMAGE` on the default branch. See [Setting the default branch image](#setting-the-default-branch-image) for more details. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/338877) in GitLab 14.5. | All |
|
||||
| `CS_DISABLE_DEPENDENCY_LIST` | `"false"` | Disable Dependency Scanning for packages installed in the scanned image. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345434) in GitLab 14.6. | All |
|
||||
| `CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN` | `"true"` | Disable scanning for language-specific packages installed in the scanned image. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345434) in GitLab 14.6. | All |
|
||||
|
@ -250,9 +250,9 @@ standard tag plus the `-fips` extension.
|
|||
|
||||
| Scanner name | `CS_ANALYZER_IMAGE` |
|
||||
| --------------- | ------------------- |
|
||||
| Default (Trivy) | `registry.gitlab.com/security-products/container-scanning:4-fips` |
|
||||
| Grype | `registry.gitlab.com/security-products/container-scanning/grype:4-fips` |
|
||||
| Trivy | `registry.gitlab.com/security-products/container-scanning/trivy:4-fips` |
|
||||
| Default (Trivy) | `registry.gitlab.com/security-products/container-scanning:5-fips` |
|
||||
| Grype | `registry.gitlab.com/security-products/container-scanning/grype:5-fips` |
|
||||
| Trivy | `registry.gitlab.com/security-products/container-scanning/trivy:5-fips` |
|
||||
|
||||
NOTE:
|
||||
Prior to GitLab 15.0, the `-ubi` image extension is also available. GitLab 15.0 and later only
|
||||
|
@ -305,9 +305,9 @@ The following options are available:
|
|||
|
||||
| Scanner name | `CS_ANALYZER_IMAGE` |
|
||||
| ------------ | ------------------- |
|
||||
| Default ([Trivy](https://github.com/aquasecurity/trivy)) | `registry.gitlab.com/security-products/container-scanning:4` |
|
||||
| [Grype](https://github.com/anchore/grype) | `registry.gitlab.com/security-products/container-scanning/grype:4` |
|
||||
| Trivy | `registry.gitlab.com/security-products/container-scanning/trivy:4` |
|
||||
| Default ([Trivy](https://github.com/aquasecurity/trivy)) | `registry.gitlab.com/security-products/container-scanning:5` |
|
||||
| [Grype](https://github.com/anchore/grype) | `registry.gitlab.com/security-products/container-scanning/grype:5` |
|
||||
| Trivy | `registry.gitlab.com/security-products/container-scanning/trivy:5` |
|
||||
|
||||
If you're migrating from a GitLab 13.x release to a GitLab 14.x release and have customized the
|
||||
`container_scanning` job or its CI variables, you might need to perform these migration steps in
|
||||
|
@ -320,7 +320,7 @@ your CI file:
|
|||
- `SECURE_ANALYZERS_PREFIX`
|
||||
|
||||
1. Review the `CS_ANALYZER_IMAGE` variable. It no longer depends on the variables above and its new
|
||||
default value is `registry.gitlab.com/security-products/container-scanning:4`. If you have an
|
||||
default value is `registry.gitlab.com/security-products/container-scanning:5`. If you have an
|
||||
offline environment, see
|
||||
[Running container scanning in an offline environment](#running-container-scanning-in-an-offline-environment).
|
||||
|
||||
|
@ -532,9 +532,9 @@ For container scanning, import the following images from `registry.gitlab.com` i
|
|||
[local Docker container registry](../../packages/container_registry/index.md):
|
||||
|
||||
```plaintext
|
||||
registry.gitlab.com/security-products/container-scanning:4
|
||||
registry.gitlab.com/security-products/container-scanning/grype:4
|
||||
registry.gitlab.com/security-products/container-scanning/trivy:4
|
||||
registry.gitlab.com/security-products/container-scanning:5
|
||||
registry.gitlab.com/security-products/container-scanning/grype:5
|
||||
registry.gitlab.com/security-products/container-scanning/trivy:5
|
||||
```
|
||||
|
||||
The process for importing Docker images into a local offline Docker registry depends on
|
||||
|
@ -574,7 +574,7 @@ following `.gitlab-ci.yml` example as a template.
|
|||
|
||||
```yaml
|
||||
variables:
|
||||
SOURCE_IMAGE: registry.gitlab.com/security-products/container-scanning:4
|
||||
SOURCE_IMAGE: registry.gitlab.com/security-products/container-scanning:5
|
||||
TARGET_IMAGE: $CI_REGISTRY/namespace/gitlab-container-scanning
|
||||
|
||||
image: docker:stable
|
||||
|
@ -827,6 +827,7 @@ For information on this, see the [general Application Security troubleshooting s
|
|||
as the default for container scanning, and also [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/326279)
|
||||
an integration with [Grype](https://github.com/anchore/grype)
|
||||
as an alternative scanner.
|
||||
- GitLab 15.0 changed the major analyzer version from `4` to `5`.
|
||||
|
||||
Other changes to the container scanning analyzer can be found in the project's
|
||||
[changelog](https://gitlab.com/gitlab-org/security-products/analyzers/container-scanning/-/blob/master/CHANGELOG.md).
|
||||
|
|
|
@ -182,7 +182,7 @@ administrator to [enable the feature flag](../../../administration/feature_flags
|
|||
On GitLab.com, this feature is not available. This feature is not ready for production use.
|
||||
|
||||
Iteration cadences automate iteration scheduling. You can use them to
|
||||
automate creating iterations every 1, 2, 3, 4, or 6 weeks. You can also
|
||||
automate creating iterations every 1, 2, 3, or 4 weeks. You can also
|
||||
configure iteration cadences to automatically roll over incomplete issues to the next iteration.
|
||||
|
||||
### Create an iteration cadence
|
||||
|
@ -198,7 +198,35 @@ To create an iteration cadence:
|
|||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Issues > Iterations**.
|
||||
1. Select **New iteration cadence**.
|
||||
1. Fill out required fields, and select **Create iteration cadence**. The cadence list page opens.
|
||||
1. Complete the fields.
|
||||
- Enter the title and description of the iteration cadence.
|
||||
- Enter the start date of the iteration cadence.
|
||||
- From the **Duration** dropdown list, select how many weeks each iteration should last.
|
||||
- From the **Future iterations** dropdown list, select how many future iterations should be
|
||||
created and maintained by GitLab.
|
||||
- Optional. To move incomplete issues to the next iteration, select **Roll over issues**.
|
||||
1. Select **Create cadence**. The cadence list page opens.
|
||||
|
||||
### Edit an iteration cadence
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Developer role for a group.
|
||||
|
||||
To edit an iteration cadence:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Issues > Iterations**.
|
||||
1. Select **Edit iteration cadence**.
|
||||
|
||||
When you edit the **Duration**, **Future iterations**, or **Start date** fields,
|
||||
only future iterations are affected.
|
||||
|
||||
You can edit the start date of a cadence if the cadence has not started yet.
|
||||
|
||||
Editing **Future iterations** is a non-destructive action.
|
||||
If ten future iterations already exist, changing the number under **Future iterations** to `2`
|
||||
doesn't delete the eight existing future iterations.
|
||||
|
||||
### Delete an iteration cadence
|
||||
|
||||
|
@ -217,18 +245,38 @@ To delete an iteration cadence:
|
|||
1. Select the three-dot menu (**{ellipsis_v}**) > **Delete cadence** for the cadence you want to delete.
|
||||
1. Select **Delete cadence** in the confirmation modal.
|
||||
|
||||
### Convert manual cadence to use automatic scheduling
|
||||
### Manual iteration cadences
|
||||
|
||||
When you **enable** the iteration cadences feature, all previously
|
||||
created iterations are added to a default iteration cadence.
|
||||
You can continue to add, edit, and remove iterations in
|
||||
this default cadence.
|
||||
|
||||
#### Convert a manual cadence to use automatic scheduling
|
||||
|
||||
WARNING:
|
||||
The upgrade is irreversible. After it's done, manual iteration cadences cannot be created.
|
||||
The upgrade is irreversible. After it's done, a new manual iteration cadence cannot be created.
|
||||
|
||||
When you **enable** the iteration cadences feature, all iterations are added
|
||||
to a default iteration cadence.
|
||||
In this default iteration cadence, you can continue to add, edit, and remove iterations.
|
||||
Prerequisites:
|
||||
|
||||
- You must have created [iterations](#iterations) without cadences before enabling iteration cadences for your group.
|
||||
To upgrade the iteration cadence to use the automation features:
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Issues > Iterations**.
|
||||
1. Select the three-dot menu (**{ellipsis_v}**) > **Edit cadence** for the cadence you want to upgrade.
|
||||
1. Fill out required fields, and select **Save changes**.
|
||||
1. Complete the required fields **Duration** and **Future iterations**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
#### Start dates of converted cadences
|
||||
|
||||
The start date of your converted cadence is set to the start date of its
|
||||
**first** existing iteration.
|
||||
|
||||
If you attempt to set a new start date, the conversion fails with an error message.
|
||||
If your manual cadence is empty, converting it to use automatic scheduling is effectively
|
||||
the same as creating a new automated cadence.
|
||||
|
||||
During the conversion process GitLab does not delete or modify existing **ongoing** or
|
||||
**closed** iterations. If you have iterations with start dates in the future,
|
||||
they are updated to fit your cadence settings.
|
||||
|
|
|
@ -137,7 +137,7 @@ module API
|
|||
end
|
||||
|
||||
def ensure_feature_enabled!
|
||||
not_found! unless Feature.enabled?(:certificate_based_clusters, default_enabled: :yaml, type: :ops)
|
||||
not_found! unless clusterable_instance.certificate_based_clusters_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -139,7 +139,7 @@ module API
|
|||
end
|
||||
|
||||
def ensure_feature_enabled!
|
||||
not_found! unless Feature.enabled?(:certificate_based_clusters, user_group, default_enabled: :yaml, type: :ops)
|
||||
not_found! unless user_group.certificate_based_clusters_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -144,7 +144,9 @@ module API
|
|||
end
|
||||
|
||||
def ensure_feature_enabled!
|
||||
not_found! unless Feature.enabled?(:certificate_based_clusters, user_project.namespace, default_enabled: :yaml, type: :ops)
|
||||
namespace = user_project.namespace
|
||||
|
||||
not_found! unless namespace.certificate_based_clusters_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def connection
|
||||
ActiveRecord::Base.connection
|
||||
ApplicationRecord.connection
|
||||
end
|
||||
|
||||
def process_sub_batch(sub_batch)
|
||||
|
|
|
@ -9,7 +9,7 @@ module Gitlab
|
|||
include Gitlab::Database::DynamicModelHelpers
|
||||
|
||||
def perform(start_id, stop_id, batch_table, batch_column, sub_batch_size, pause_ms)
|
||||
define_batchable_model(batch_table, connection: ActiveRecord::Base.connection).where(batch_column => start_id..stop_id).each_batch(of: sub_batch_size) do |sub_batch|
|
||||
define_batchable_model(batch_table, connection: ApplicationRecord.connection).where(batch_column => start_id..stop_id).each_batch(of: sub_batch_size) do |sub_batch|
|
||||
update_search_data(sub_batch)
|
||||
|
||||
sleep(pause_ms * 0.001)
|
||||
|
|
|
@ -26,7 +26,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
|
||||
define_batchable_model(source_table, connection: ActiveRecord::Base.connection)
|
||||
define_batchable_model(source_table, connection: ApplicationRecord.connection)
|
||||
.joins('INNER JOIN namespaces ON members.source_id = namespaces.id')
|
||||
.where(source_key_column => start_id..stop_id)
|
||||
.where(type: 'GroupMember')
|
||||
|
|
|
@ -27,7 +27,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
|
||||
define_batchable_model(source_table, connection: ActiveRecord::Base.connection)
|
||||
define_batchable_model(source_table, connection: ApplicationRecord.connection)
|
||||
.joins('inner join namespaces on routes.source_id = namespaces.id')
|
||||
.where(source_key_column => start_id..stop_id)
|
||||
.where(namespace_id: nil)
|
||||
|
|
|
@ -13,7 +13,7 @@ module Gitlab
|
|||
cleanup_gin_index('routes')
|
||||
|
||||
batch_metrics.time_operation(:update_all) do
|
||||
ActiveRecord::Base.connection.execute <<~SQL
|
||||
ApplicationRecord.connection.execute <<~SQL
|
||||
WITH route_and_ns(route_id, project_namespace_id) AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
|
||||
#{sub_batch.to_sql}
|
||||
)
|
||||
|
@ -37,15 +37,15 @@ module Gitlab
|
|||
|
||||
def cleanup_gin_index(table_name)
|
||||
sql = "select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%gin%'"
|
||||
index_names = ActiveRecord::Base.connection.select_values(sql)
|
||||
index_names = ApplicationRecord.connection.select_values(sql)
|
||||
|
||||
index_names.each do |index_name|
|
||||
ActiveRecord::Base.connection.execute("select gin_clean_pending_list('#{index_name}')")
|
||||
ApplicationRecord.connection.execute("select gin_clean_pending_list('#{index_name}')")
|
||||
end
|
||||
end
|
||||
|
||||
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
|
||||
define_batchable_model(source_table, connection: ActiveRecord::Base.connection)
|
||||
define_batchable_model(source_table, connection: ApplicationRecord.connection)
|
||||
.joins('INNER JOIN projects ON routes.source_id = projects.id')
|
||||
.where(source_key_column => start_id..stop_id)
|
||||
.where(namespace_id: nil)
|
||||
|
|
|
@ -29,7 +29,7 @@ module Gitlab
|
|||
WHERE namespaces.id = calculated_ids.id
|
||||
AND namespaces.traversal_ids = '{}'
|
||||
SQL
|
||||
ActiveRecord::Base.connection.execute(update_sql)
|
||||
ApplicationRecord.connection.execute(update_sql)
|
||||
|
||||
sleep PAUSE_SECONDS
|
||||
end
|
||||
|
|
|
@ -16,7 +16,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def execute(sql)
|
||||
@connection ||= ::ActiveRecord::Base.connection
|
||||
@connection ||= ApplicationRecord.connection
|
||||
@connection.execute(sql)
|
||||
end
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def connection
|
||||
ActiveRecord::Base.connection
|
||||
ApplicationRecord.connection
|
||||
end
|
||||
|
||||
def relation_scoped_to_range(source_table, source_key_column, start_id, stop_id)
|
||||
|
|
|
@ -15,7 +15,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def orphaned_deployments
|
||||
define_batchable_model('deployments', connection: ActiveRecord::Base.connection)
|
||||
define_batchable_model('deployments', connection: ApplicationRecord.connection)
|
||||
.where('NOT EXISTS (SELECT 1 FROM environments WHERE deployments.environment_id = environments.id)')
|
||||
end
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def execute(sql)
|
||||
ActiveRecord::Base
|
||||
ApplicationRecord
|
||||
.connection
|
||||
.execute(sql)
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ module Gitlab
|
|||
backfill_project_namespaces_service.cleanup_gin_index('projects')
|
||||
|
||||
project_ids.each_slice(SUB_BATCH_SIZE) do |ids|
|
||||
ActiveRecord::Base.connection.execute(update_projects_name_and_path_sql(ids))
|
||||
ApplicationRecord.connection.execute(update_projects_name_and_path_sql(ids))
|
||||
end
|
||||
|
||||
backfill_project_namespaces_service.backfill_project_namespaces
|
||||
|
|
|
@ -14,7 +14,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def create_missing!(from_id, to_id)
|
||||
result = ActiveRecord::Base.connection.select_one(sql(from_id, to_id))
|
||||
result = ApplicationRecord.connection.select_one(sql(from_id, to_id))
|
||||
return 0 unless result
|
||||
|
||||
result['number_of_created_records']
|
||||
|
|
|
@ -120,14 +120,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
def create_missing(from_id, to_id)
|
||||
result = ActiveRecord::Base.connection.select_one(create_sql(from_id, to_id))
|
||||
result = ApplicationRecord.connection.select_one(create_sql(from_id, to_id))
|
||||
return unless result
|
||||
|
||||
logger.info(message: "#{self.class}: created missing services for #{result['number_of_created_records']} projects in id=#{from_id}...#{to_id}")
|
||||
end
|
||||
|
||||
def update_inconsistent(from_id, to_id)
|
||||
result = ActiveRecord::Base.connection.select_one(update_sql(from_id, to_id))
|
||||
result = ApplicationRecord.connection.select_one(update_sql(from_id, to_id))
|
||||
return unless result
|
||||
|
||||
logger.info(message: "#{self.class}: updated inconsistent services for #{result['number_of_updated_records']} projects in id=#{from_id}...#{to_id}")
|
||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
include Gitlab::Database::DynamicModelHelpers
|
||||
|
||||
def perform(start_id, end_id)
|
||||
define_batchable_model('integrations', connection: ::ActiveRecord::Base.connection)
|
||||
define_batchable_model('integrations', connection: ApplicationRecord.connection)
|
||||
.where(id: start_id..end_id, type_new: %w[Integrations::Confluence Integrations::Shimo])
|
||||
.update_all(category: 'third_party_wiki')
|
||||
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
# rubocop:disable Style/Documentation
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class MigrateStageStatus
|
||||
STATUSES = { created: 0, pending: 1, running: 2, success: 3,
|
||||
failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
|
||||
|
||||
class Build < ActiveRecord::Base
|
||||
self.table_name = 'ci_builds'
|
||||
|
||||
scope :latest, -> { where(retried: [false, nil]) }
|
||||
scope :created, -> { where(status: 'created') }
|
||||
scope :running, -> { where(status: 'running') }
|
||||
scope :pending, -> { where(status: 'pending') }
|
||||
scope :success, -> { where(status: 'success') }
|
||||
scope :failed, -> { where(status: 'failed') }
|
||||
scope :canceled, -> { where(status: 'canceled') }
|
||||
scope :skipped, -> { where(status: 'skipped') }
|
||||
scope :manual, -> { where(status: 'manual') }
|
||||
|
||||
scope :failed_but_allowed, -> do
|
||||
where(allow_failure: true, status: [:failed, :canceled])
|
||||
end
|
||||
|
||||
scope :exclude_ignored, -> do
|
||||
where("allow_failure = ? OR status IN (?)",
|
||||
false, %w[created pending running success skipped])
|
||||
end
|
||||
|
||||
def self.status_sql
|
||||
scope_relevant = latest.exclude_ignored
|
||||
scope_warnings = latest.failed_but_allowed
|
||||
|
||||
builds = scope_relevant.select('count(*)').to_sql
|
||||
created = scope_relevant.created.select('count(*)').to_sql
|
||||
success = scope_relevant.success.select('count(*)').to_sql
|
||||
manual = scope_relevant.manual.select('count(*)').to_sql
|
||||
pending = scope_relevant.pending.select('count(*)').to_sql
|
||||
running = scope_relevant.running.select('count(*)').to_sql
|
||||
skipped = scope_relevant.skipped.select('count(*)').to_sql
|
||||
canceled = scope_relevant.canceled.select('count(*)').to_sql
|
||||
warnings = scope_warnings.select('count(*) > 0').to_sql
|
||||
|
||||
<<-SQL.strip_heredoc
|
||||
(CASE
|
||||
WHEN (#{builds}) = (#{skipped}) AND (#{warnings}) THEN #{STATUSES[:success]}
|
||||
WHEN (#{builds}) = (#{skipped}) THEN #{STATUSES[:skipped]}
|
||||
WHEN (#{builds}) = (#{success}) THEN #{STATUSES[:success]}
|
||||
WHEN (#{builds}) = (#{created}) THEN #{STATUSES[:created]}
|
||||
WHEN (#{builds}) = (#{success}) + (#{skipped}) THEN #{STATUSES[:success]}
|
||||
WHEN (#{builds}) = (#{success}) + (#{skipped}) + (#{canceled}) THEN #{STATUSES[:canceled]}
|
||||
WHEN (#{builds}) = (#{created}) + (#{skipped}) + (#{pending}) THEN #{STATUSES[:pending]}
|
||||
WHEN (#{running}) + (#{pending}) > 0 THEN #{STATUSES[:running]}
|
||||
WHEN (#{manual}) > 0 THEN #{STATUSES[:manual]}
|
||||
WHEN (#{created}) > 0 THEN #{STATUSES[:running]}
|
||||
ELSE #{STATUSES[:failed]}
|
||||
END)
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def perform(start_id, stop_id)
|
||||
status_sql = Build
|
||||
.where('ci_builds.commit_id = ci_stages.pipeline_id')
|
||||
.where('ci_builds.stage = ci_stages.name')
|
||||
.status_sql
|
||||
|
||||
sql = <<-SQL
|
||||
UPDATE ci_stages SET status = (#{status_sql})
|
||||
WHERE ci_stages.status IS NULL
|
||||
AND ci_stages.id BETWEEN #{start_id.to_i} AND #{stop_id.to_i}
|
||||
SQL
|
||||
|
||||
ActiveRecord::Base.connection.execute(sql)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,7 +22,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def process_batch(from_id, to_id)
|
||||
ActiveRecord::Base.connection.execute(update_sql(from_id, to_id))
|
||||
ApplicationRecord.connection.execute(update_sql(from_id, to_id))
|
||||
|
||||
logger.info(message: "#{self.class}: Copied container_registry_enabled values for projects with IDs between #{from_id}..#{to_id}")
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ module Gitlab
|
|||
private
|
||||
|
||||
def connection
|
||||
@connection ||= ::ActiveRecord::Base.connection
|
||||
@connection ||= ApplicationRecord.connection
|
||||
end
|
||||
|
||||
def execute(sql)
|
||||
|
|
|
@ -15,7 +15,7 @@ module Gitlab
|
|||
|
||||
def perform(start_id, stop_id)
|
||||
Topic.where(id: start_id..stop_id).each_batch(of: SUB_BATCH_SIZE) do |batch|
|
||||
ActiveRecord::Base.connection.execute(<<~SQL)
|
||||
ApplicationRecord.connection.execute(<<~SQL)
|
||||
WITH batched_relation AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (#{batch.select(:id).limit(SUB_BATCH_SIZE).to_sql})
|
||||
UPDATE topics
|
||||
SET non_private_projects_count = (
|
||||
|
|
|
@ -15,7 +15,7 @@ module Gitlab
|
|||
|
||||
def perform(start_id, stop_id)
|
||||
Topic.where(id: start_id..stop_id).each_batch(of: SUB_BATCH_SIZE) do |batch|
|
||||
ActiveRecord::Base.connection.execute(<<~SQL)
|
||||
ApplicationRecord.connection.execute(<<~SQL)
|
||||
WITH batched_relation AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (#{batch.select(:id).limit(SUB_BATCH_SIZE).to_sql})
|
||||
UPDATE topics
|
||||
SET total_projects_count = (SELECT COUNT(*) FROM project_topics WHERE topic_id = batched_relation.id)
|
||||
|
|
|
@ -26,7 +26,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def connection
|
||||
ActiveRecord::Base.connection
|
||||
ApplicationRecord.connection
|
||||
end
|
||||
|
||||
def insert_query(start_id, end_id)
|
||||
|
|
|
@ -55,10 +55,10 @@ module Gitlab
|
|||
end
|
||||
|
||||
def cleanup_gin_index(table_name)
|
||||
index_names = ActiveRecord::Base.connection.select_values("select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%gin%'")
|
||||
index_names = ApplicationRecord.connection.select_values("select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%gin%'")
|
||||
|
||||
index_names.each do |index_name|
|
||||
ActiveRecord::Base.connection.execute("select gin_clean_pending_list('#{index_name}')")
|
||||
ApplicationRecord.connection.execute("select gin_clean_pending_list('#{index_name}')")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -77,7 +77,7 @@ module Gitlab
|
|||
projects = IsolatedModels::Project.where(id: project_ids)
|
||||
.select("projects.id, projects.name, projects.path, projects.namespace_id, projects.visibility_level, shared_runners_enabled, '#{PROJECT_NAMESPACE_STI_NAME}', now(), now()")
|
||||
|
||||
ActiveRecord::Base.connection.execute <<~SQL
|
||||
ApplicationRecord.connection.execute <<~SQL
|
||||
INSERT INTO namespaces (tmp_project_id, name, path, parent_id, visibility_level, shared_runners_enabled, type, created_at, updated_at)
|
||||
#{projects.to_sql}
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
@ -89,7 +89,7 @@ module Gitlab
|
|||
.joins("INNER JOIN namespaces ON projects.id = namespaces.tmp_project_id")
|
||||
.select("namespaces.id, namespaces.tmp_project_id")
|
||||
|
||||
ActiveRecord::Base.connection.execute <<~SQL
|
||||
ApplicationRecord.connection.execute <<~SQL
|
||||
WITH cte(project_namespace_id, project_id) AS #{::Gitlab::Database::AsWithMaterialized.materialized_if_supported} (
|
||||
#{projects.to_sql}
|
||||
)
|
||||
|
@ -105,7 +105,7 @@ module Gitlab
|
|||
.joins("INNER JOIN namespaces n2 ON namespaces.parent_id = n2.id")
|
||||
.select("namespaces.id as project_namespace_id, n2.traversal_ids")
|
||||
|
||||
ActiveRecord::Base.connection.execute <<~SQL
|
||||
ApplicationRecord.connection.execute <<~SQL
|
||||
UPDATE namespaces
|
||||
SET traversal_ids = array_append(project_namespaces.traversal_ids, project_namespaces.project_namespace_id)
|
||||
FROM (#{namespaces.to_sql}) as project_namespaces(project_namespace_id, traversal_ids)
|
||||
|
|
|
@ -10,7 +10,7 @@ module Gitlab
|
|||
include Gitlab::Database::DynamicModelHelpers
|
||||
|
||||
def perform(start_id, stop_id)
|
||||
define_batchable_model('vulnerability_finding_links', connection: ActiveRecord::Base.connection)
|
||||
define_batchable_model('vulnerability_finding_links', connection: ApplicationRecord.connection)
|
||||
.where(id: start_id..stop_id)
|
||||
.delete_all
|
||||
end
|
||||
|
|
|
@ -28,7 +28,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def connection
|
||||
@connection ||= ::ActiveRecord::Base.connection
|
||||
@connection ||= ApplicationRecord.connection
|
||||
end
|
||||
|
||||
def execute(sql)
|
||||
|
|
|
@ -36,7 +36,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def execute(sql)
|
||||
@connection ||= ::ActiveRecord::Base.connection
|
||||
@connection ||= ApplicationRecord.connection
|
||||
@connection.execute(sql)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ module Gitlab
|
|||
module BackgroundMigration
|
||||
class UpdateUsersWhereTwoFactorAuthRequiredFromGroup # rubocop:disable Metrics/ClassLength
|
||||
def perform(start_id, stop_id)
|
||||
ActiveRecord::Base.connection.execute <<~SQL
|
||||
ApplicationRecord.connection.execute <<~SQL
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
|
|
|
@ -15,7 +15,7 @@ module Gitlab
|
|||
ALLOWED_KEYS =
|
||||
%i[junit codequality sast secret_detection dependency_scanning container_scanning
|
||||
dast performance browser_performance load_performance license_scanning metrics lsif
|
||||
dotenv cobertura terraform accessibility cluster_applications
|
||||
dotenv cobertura terraform accessibility
|
||||
requirements coverage_fuzzing api_fuzzing cluster_image_scanning
|
||||
coverage_report].freeze
|
||||
|
||||
|
@ -48,7 +48,6 @@ module Gitlab
|
|||
validates :cobertura, array_of_strings_or_string: true
|
||||
validates :terraform, array_of_strings_or_string: true
|
||||
validates :accessibility, array_of_strings_or_string: true
|
||||
validates :cluster_applications, array_of_strings_or_string: true # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/333441
|
||||
validates :requirements, array_of_strings_or_string: true
|
||||
end
|
||||
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Graphql
|
||||
module FindArgumentInParent
|
||||
# Searches up the GraphQL AST and returns the first matching argument
|
||||
# passed to a node
|
||||
def self.find(parent, argument, limit_depth: nil)
|
||||
argument = argument.to_s.camelize(:lower).to_sym
|
||||
depth = 0
|
||||
|
||||
while parent.respond_to?(:parent)
|
||||
args = node_args(parent)
|
||||
return args[argument] if args.key?(argument)
|
||||
|
||||
depth += 1
|
||||
return if limit_depth && depth >= limit_depth
|
||||
|
||||
parent = parent.parent
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
private
|
||||
|
||||
def node_args(node)
|
||||
node.irep_node.arguments
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,7 +22,8 @@ module Sidebars
|
|||
override :render?
|
||||
def render?
|
||||
clusterable = context.group
|
||||
Feature.enabled?(:certificate_based_clusters, clusterable, default_enabled: :yaml, type: :ops) &&
|
||||
|
||||
clusterable.certificate_based_clusters_enabled? &&
|
||||
can?(context.current_user, :read_cluster, clusterable)
|
||||
end
|
||||
|
||||
|
|
|
@ -43374,6 +43374,12 @@ msgstr ""
|
|||
msgid "You cannot approve your own deployment."
|
||||
msgstr ""
|
||||
|
||||
msgid "You cannot change the start date after the cadence has started. Please create a new cadence."
|
||||
msgstr ""
|
||||
|
||||
msgid "You cannot change the start date because the first iteration has already started on %{start_date}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You cannot combine replace_ids with add_ids or remove_ids"
|
||||
msgstr ""
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue