Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-05 09:08:00 +00:00
parent cd9bbd8a3e
commit 17ef30f3df
119 changed files with 1163 additions and 1153 deletions

View File

@ -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.

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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: {

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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 ||= {}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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 -->

View File

@ -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.

View File

@ -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.

View File

@ -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' });
// ...
});
})
```

View File

@ -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

View File

@ -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).

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -22,7 +22,7 @@ module Gitlab
private
def connection
ActiveRecord::Base.connection
ApplicationRecord.connection
end
def process_sub_batch(sub_batch)

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -16,7 +16,7 @@ module Gitlab
private
def execute(sql)
@connection ||= ::ActiveRecord::Base.connection
@connection ||= ApplicationRecord.connection
@connection.execute(sql)
end

View File

@ -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)

View File

@ -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

View File

@ -32,7 +32,7 @@ module Gitlab
private
def execute(sql)
ActiveRecord::Base
ApplicationRecord
.connection
.execute(sql)
end

View File

@ -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

View File

@ -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']

View File

@ -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}")

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -33,7 +33,7 @@ module Gitlab
private
def connection
@connection ||= ::ActiveRecord::Base.connection
@connection ||= ApplicationRecord.connection
end
def execute(sql)

View File

@ -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 = (

View File

@ -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)

View File

@ -26,7 +26,7 @@ module Gitlab
end
def connection
ActiveRecord::Base.connection
ApplicationRecord.connection
end
def insert_query(start_id, end_id)

View File

@ -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)

View File

@ -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

View File

@ -28,7 +28,7 @@ module Gitlab
end
def connection
@connection ||= ::ActiveRecord::Base.connection
@connection ||= ApplicationRecord.connection
end
def execute(sql)

View File

@ -36,7 +36,7 @@ module Gitlab
end
def execute(sql)
@connection ||= ::ActiveRecord::Base.connection
@connection ||= ApplicationRecord.connection
@connection.execute(sql)
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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