Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-06 15:10:05 +00:00
parent 14b5bf2629
commit 07e0fae35c
48 changed files with 770 additions and 326 deletions

View File

@ -393,8 +393,7 @@ db:migrate-from-previous-major-version:
- sed -i -e "s/gem 'google-protobuf', '~> 3.8.0'/gem 'google-protobuf', '~> 3.12'/" Gemfile
- sed -i -e "s/gem 'nokogiri', '~> 1.10.5'/gem 'nokogiri', '~> 1.11.0'/" Gemfile
- sed -i -e "s/gem 'mimemagic', '~> 0.3.2'/gem 'ruby-magic', '~> 0.4.0'/" Gemfile
- run_timed_command "gem install bundler:1.17.3"
- run_timed_command "bundle update google-protobuf nokogiri grpc mimemagic bootsnap"
- run_timed_command "bundle update --bundler google-protobuf nokogiri grpc mimemagic bootsnap"
- SETUP_DB=false USE_BUNDLE_INSTALL=true bash scripts/prepare_build.sh
- run_timed_command "bundle exec rake db:drop db:create db:structure:load db:migrate db:seed_fu"
- git checkout -f $CI_COMMIT_SHA

View File

@ -2,6 +2,7 @@
import { GlAlert } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { ContentEditor } from '../services/content_editor';
import FormattingBubbleMenu from './formatting_bubble_menu.vue';
import TopToolbar from './top_toolbar.vue';
export default {
@ -9,6 +10,7 @@ export default {
GlAlert,
TiptapEditorContent,
TopToolbar,
FormattingBubbleMenu,
},
provide() {
return {
@ -44,6 +46,7 @@ export default {
:class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }"
>
<top-toolbar ref="toolbar" class="gl-mb-4" />
<formatting-bubble-menu />
<tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
</div>
</div>

View File

@ -0,0 +1,67 @@
<script>
import { GlButtonGroup } from '@gitlab/ui';
import { BubbleMenu } from '@tiptap/vue-2';
import { BUBBLE_MENU_TRACKING_ACTION } from '../constants';
import trackUIControl from '../services/track_ui_control';
import ToolbarButton from './toolbar_button.vue';
export default {
components: {
BubbleMenu,
GlButtonGroup,
ToolbarButton,
},
inject: ['tiptapEditor'],
methods: {
trackToolbarControlExecution({ contentType, value }) {
trackUIControl({ action: BUBBLE_MENU_TRACKING_ACTION, property: contentType, value });
},
},
};
</script>
<template>
<bubble-menu class="gl-shadow gl-rounded-base" :editor="tiptapEditor">
<gl-button-group>
<toolbar-button
data-testid="bold"
content-type="bold"
icon-name="bold"
editor-command="toggleBold"
category="primary"
size="medium"
:label="__('Bold text')"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="italic"
content-type="italic"
icon-name="italic"
editor-command="toggleItalic"
category="primary"
size="medium"
:label="__('Italic text')"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="strike"
content-type="strike"
icon-name="strikethrough"
editor-command="toggleStrike"
category="primary"
size="medium"
:label="__('Strikethrough')"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="code"
content-type="code"
icon-name="code"
editor-command="toggleCode"
category="primary"
size="medium"
:label="__('Code')"
@execute="trackToolbarControlExecution"
/>
</gl-button-group>
</bubble-menu>
</template>

View File

@ -29,6 +29,21 @@ export default {
required: false,
default: '',
},
variant: {
type: String,
required: false,
default: 'default',
},
category: {
type: String,
required: false,
default: 'tertiary',
},
size: {
type: String,
required: false,
default: 'small',
},
},
data() {
return {
@ -55,9 +70,9 @@ export default {
<editor-state-observer @transaction="updateActive">
<gl-button
v-gl-tooltip
category="tertiary"
size="small"
class="gl-mx-2"
:variant="variant"
:category="category"
:size="size"
:class="{ active: isActive }"
:aria-label="label"
:title="label"

View File

@ -1,6 +1,5 @@
<script>
import Tracking from '~/tracking';
import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants';
import trackUIControl from '../services/track_ui_control';
import Divider from './divider.vue';
import ToolbarButton from './toolbar_button.vue';
import ToolbarImageButton from './toolbar_image_button.vue';
@ -8,10 +7,6 @@ import ToolbarLinkButton from './toolbar_link_button.vue';
import ToolbarTableButton from './toolbar_table_button.vue';
import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue';
const trackingMixin = Tracking.mixin({
label: CONTENT_EDITOR_TRACKING_LABEL,
});
export default {
components: {
ToolbarButton,
@ -21,13 +16,9 @@ export default {
ToolbarImageButton,
Divider,
},
mixins: [trackingMixin],
methods: {
trackToolbarControlExecution({ contentType: property, value }) {
this.track(TOOLBAR_CONTROL_TRACKING_ACTION, {
property,
value,
});
trackToolbarControlExecution({ contentType, value }) {
trackUIControl({ property: contentType, value });
},
},
};
@ -45,6 +36,7 @@ export default {
data-testid="bold"
content-type="bold"
icon-name="bold"
class="gl-mx-2"
editor-command="toggleBold"
:label="__('Bold text')"
@execute="trackToolbarControlExecution"
@ -53,6 +45,7 @@ export default {
data-testid="italic"
content-type="italic"
icon-name="italic"
class="gl-mx-2"
editor-command="toggleItalic"
:label="__('Italic text')"
@execute="trackToolbarControlExecution"
@ -61,6 +54,7 @@ export default {
data-testid="strike"
content-type="strike"
icon-name="strikethrough"
class="gl-mx-2"
editor-command="toggleStrike"
:label="__('Strikethrough')"
@execute="trackToolbarControlExecution"
@ -69,6 +63,7 @@ export default {
data-testid="code"
content-type="code"
icon-name="code"
class="gl-mx-2"
editor-command="toggleCode"
:label="__('Code')"
@execute="trackToolbarControlExecution"
@ -84,6 +79,7 @@ export default {
data-testid="blockquote"
content-type="blockquote"
icon-name="quote"
class="gl-mx-2"
editor-command="toggleBlockquote"
:label="__('Insert a quote')"
@execute="trackToolbarControlExecution"
@ -92,6 +88,7 @@ export default {
data-testid="code-block"
content-type="codeBlock"
icon-name="doc-code"
class="gl-mx-2"
editor-command="toggleCodeBlock"
:label="__('Insert a code block')"
@execute="trackToolbarControlExecution"
@ -100,6 +97,7 @@ export default {
data-testid="bullet-list"
content-type="bulletList"
icon-name="list-bulleted"
class="gl-mx-2"
editor-command="toggleBulletList"
:label="__('Add a bullet list')"
@execute="trackToolbarControlExecution"
@ -108,6 +106,7 @@ export default {
data-testid="ordered-list"
content-type="orderedList"
icon-name="list-numbered"
class="gl-mx-2"
editor-command="toggleOrderedList"
:label="__('Add a numbered list')"
@execute="trackToolbarControlExecution"
@ -116,6 +115,7 @@ export default {
data-testid="horizontal-rule"
content-type="horizontalRule"
icon-name="dash"
class="gl-mx-2"
editor-command="setHorizontalRule"
:label="__('Add a horizontal rule')"
@execute="trackToolbarControlExecution"

View File

@ -6,6 +6,7 @@ export const PROVIDE_SERIALIZER_OR_RENDERER_ERROR = s__(
export const CONTENT_EDITOR_TRACKING_LABEL = 'content_editor';
export const TOOLBAR_CONTROL_TRACKING_ACTION = 'execute_toolbar_control';
export const BUBBLE_MENU_TRACKING_ACTION = 'execute_bubble_menu_control';
export const KEYBOARD_SHORTCUT_TRACKING_ACTION = 'execute_keyboard_shortcut';
export const INPUT_RULE_TRACKING_ACTION = 'execute_input_rule';
@ -40,3 +41,7 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [
label: __('Normal text'),
},
];
export const LOADING_CONTENT_EVENT = 'loadingContent';
export const LOADING_SUCCESS_EVENT = 'loadingSuccess';
export const LOADING_ERROR_EVENT = 'loadingError';

View File

@ -1,8 +1,11 @@
import eventHubFactory from '~/helpers/event_hub_factory';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
constructor({ tiptapEditor, serializer }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
this._eventHub = eventHubFactory();
}
get tiptapEditor() {
@ -16,12 +19,41 @@ export class ContentEditor {
return doc.childCount === 0 || (doc.childCount === 1 && doc.child(0).childCount === 0);
}
once(type, handler) {
this._eventHub.$once(type, handler);
}
on(type, handler) {
this._eventHub.$on(type, handler);
}
emit(type, params = {}) {
this._eventHub.$emit(type, params);
}
off(type, handler) {
this._eventHub.$off(type, handler);
}
disposeAllEvents() {
this._eventHub.dispose();
}
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor, _serializer: serializer } = this;
editor.commands.setContent(
await serializer.deserialize({ schema: editor.schema, content: serializedContent }),
);
try {
this._eventHub.$emit(LOADING_CONTENT_EVENT);
const document = await serializer.deserialize({
schema: editor.schema,
content: serializedContent,
});
editor.commands.setContent(document);
this._eventHub.$emit(LOADING_SUCCESS_EVENT);
} catch (e) {
this._eventHub.$emit(LOADING_ERROR_EVENT, e);
throw e;
}
}
getSerializedContent() {

View File

@ -0,0 +1,9 @@
import Tracking from '~/tracking';
import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants';
export default ({ action = TOOLBAR_CONTROL_TRACKING_ACTION, property, value } = {}) =>
Tracking.event(undefined, action, {
label: CONTENT_EDITOR_TRACKING_LABEL,
property,
value,
});

View File

@ -18,23 +18,14 @@ module Groups
def authenticate_user_from_jwt_token!
return unless dependency_proxy_for_private_groups?
if Feature.enabled?(:dependency_proxy_deploy_tokens)
authenticate_with_http_token do |token, _|
@authentication_result = EMPTY_AUTH_RESULT
authenticate_with_http_token do |token, _|
@authentication_result = EMPTY_AUTH_RESULT
found_user = user_from_token(token)
sign_in(found_user) if found_user.is_a?(User)
end
request_bearer_token! unless authenticated_user
else
authenticate_with_http_token do |token, _|
user = user_from_token(token)
sign_in(user) if user
end
request_bearer_token! unless current_user
found_user = user_from_token(token)
sign_in(found_user) if found_user.is_a?(User)
end
request_bearer_token! unless authenticated_user
end
private
@ -51,7 +42,6 @@ module Groups
def user_from_token(token)
token_payload = ::DependencyProxy::AuthTokenService.decoded_token_payload(token)
return User.find(token_payload['user_id']) unless Feature.enabled?(:dependency_proxy_deploy_tokens)
if token_payload['user_id']
token_user = User.find(token_payload['user_id'])

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Packages
module Npm
# from "@scope/package-name" return "scope" or nil
def self.scope_of(package_name)
return unless package_name
return unless package_name.starts_with?('@')
return unless package_name.include?('/')
package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
end
end
end

View File

@ -62,7 +62,7 @@ class Packages::Package < ApplicationRecord
validate :valid_conan_package_recipe, if: :conan?
validate :valid_composer_global_name, if: :composer?
validate :package_already_taken, if: :npm?
validate :npm_package_already_taken, if: :npm?
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
validates :name, format: { with: Gitlab::Regex.helm_package_regex }, if: :helm?
@ -320,14 +320,22 @@ class Packages::Package < ApplicationRecord
end
end
def package_already_taken
def npm_package_already_taken
return unless project
return unless follows_npm_naming_convention?
if project.package_already_taken?(name)
if project.package_already_taken?(name, package_type: :npm)
errors.add(:base, _('Package already exists'))
end
end
# https://docs.gitlab.com/ee/user/packages/npm_registry/#package-naming-convention
def follows_npm_naming_convention?
return false unless project&.root_namespace&.path
project.root_namespace.path == ::Packages::Npm.scope_of(name)
end
def unique_debian_package_name
return unless debian_publication&.distribution

View File

@ -2566,12 +2566,15 @@ class Project < ApplicationRecord
[project&.id, root_group&.id]
end
def package_already_taken?(package_name)
def package_already_taken?(package_name, package_type:)
namespace.root_ancestor.all_projects
.joins(:packages)
.where.not(id: id)
.merge(Packages::Package.default_scoped.with_name(package_name))
.exists?
.merge(
Packages::Package.default_scoped
.with_name(package_name)
.with_package_type(package_type)
).exists?
end
def default_branch_or_main

View File

@ -1,8 +0,0 @@
---
name: dependency_proxy_deploy_tokens
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64363
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334565
milestone: '14.2'
type: development
group: group::package
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: multiple_database_metrics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63490
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333227
milestone: '14.1'
type: development
group: group::sharding
default_enabled: false

View File

@ -11,6 +11,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_testing_test_case_parsed
distribution:
- ce
- ee

View File

@ -10,6 +10,20 @@ value_type: number
status: removed
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_testing_test_case_parsed
- i_testing_metrics_report_widget_total
- i_testing_group_code_coverage_visit_total
- i_testing_full_code_quality_report_total
- i_testing_web_performance_widget_total
- i_testing_group_code_coverage_project_click_total
- i_testing_load_performance_widget_total
- i_testing_metrics_report_artifact_uploaders
- i_testing_summary_widget_total
- users_expanding_testing_code_quality_report
- users_expanding_testing_accessibility_report
distribution:
- ce
tier:

View File

@ -12,6 +12,10 @@ milestone: '13.11'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_expanding_testing_code_quality_report
distribution:
- ce
- ee

View File

@ -12,6 +12,10 @@ milestone: '13.11'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_expanding_testing_accessibility_report
distribution:
- ce
- ee

View File

@ -12,6 +12,10 @@ milestone: "13.11"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59316
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_testing_summary_widget_total
distribution:
- ee
- ce

View File

@ -11,6 +11,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_testing_test_case_parsed
distribution:
- ee
- ce

View File

@ -12,6 +12,10 @@ milestone: '13.11'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_expanding_testing_code_quality_report
distribution:
- ce
- ee

View File

@ -12,6 +12,10 @@ milestone: '13.11'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57133
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- users_expanding_testing_accessibility_report
distribution:
- ce
- ee

View File

@ -12,6 +12,10 @@ milestone: "13.11"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59316
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_testing_summary_widget_total
distribution:
- ee
- ce

View File

@ -31,9 +31,9 @@ From left to right, the performance bar displays:
was sent to the read/write primary server. "Replica" means it was sent
to a read-only replica.
- **Config name**: shows up only when the
`multiple_database_metrics` feature flag is enabled. This is used to
distinguish between different databases configured for different GitLab
features. The name shown is the same name used to configure database
`GITLAB_MULTIPLE_DATABASE_METRICS` environment variable is set. This is
used to distinguish between different databases configured for different
GitLab features. The name shown is the same name used to configure database
connections in GitLab.
- **Gitaly calls**: the time taken (in milliseconds) and the total number of
[Gitaly](../../gitaly/index.md) calls. Click to display a modal window with more

View File

@ -49,28 +49,20 @@ module API
when :project
params[:id]
when :instance
namespace_path = namespace_path_from_package_name
package_name = params[:package_name]
namespace_path = ::Packages::Npm.scope_of(package_name)
next unless namespace_path
namespace = Namespace.top_most
.by_path(namespace_path)
next unless namespace
finder = ::Packages::Npm::PackageFinder.new(params[:package_name], namespace: namespace)
finder = ::Packages::Npm::PackageFinder.new(package_name, namespace: namespace)
finder.last&.project_id
end
end
end
# from "@scope/package-name" return "scope" or nil
def namespace_path_from_package_name
package_name = params[:package_name]
return unless package_name.starts_with?('@')
return unless package_name.include?('/')
package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
end
end
end
end

View File

@ -27,7 +27,7 @@ code_quality:
}
- docker pull --quiet "$CODE_QUALITY_IMAGE"
- |
docker run \
docker run --rm \
$(propagate_env_vars \
SOURCE_CODE \
TIMEOUT_SECONDS \

View File

@ -163,6 +163,10 @@ module Gitlab
end
end
def self.db_config_names
::ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).map(&:name)
end
def self.db_config_name(ar_connection)
if ar_connection.respond_to?(:pool) &&
ar_connection.pool.respond_to?(:db_config) &&

View File

@ -42,8 +42,13 @@ module Gitlab
tracking_category: 'Growth::Conversion::Experiment::ContactSalesInApp',
use_backwards_compatible_subject_index: true
},
remove_known_trial_form_fields: {
tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFields'
remove_known_trial_form_fields_welcoming: {
tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFieldsWelcoming',
rollout_strategy: :user
},
remove_known_trial_form_fields_noneditable: {
tracking_category: 'Growth::Conversion::Experiment::RemoveKnownTrialFormFieldsNoneditable',
rollout_strategy: :user
},
invite_members_new_dropdown: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersNewDropdown'

View File

@ -14,20 +14,22 @@ module Gitlab
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/323
def initialize(repo, commit)
@id = commit.id
@additions = 0
@deletions = 0
@total = 0
wrapped_gitaly_errors do
gitaly_stats(repo, commit)
end
additions, deletions = fetch_stats(repo, commit)
@additions = additions.to_i
@deletions = deletions.to_i
@total = @additions + @deletions
end
def gitaly_stats(repo, commit)
stats = repo.gitaly_commit_client.commit_stats(@id)
@additions = stats.additions
@deletions = stats.deletions
@total = @additions + @deletions
def fetch_stats(repo, commit)
Rails.cache.fetch("commit_stats:#{repo.gl_project_path}:#{@id}") do
stats = wrapped_gitaly_errors do
repo.gitaly_commit_client.commit_stats(@id)
end
[stats.additions, stats.deletions]
end
end
end
end

View File

@ -8,17 +8,15 @@ module Gitlab
attach_to :active_record
IGNORABLE_SQL = %w{BEGIN COMMIT}.freeze
DB_COUNTERS = %i{db_count db_write_count db_cached_count}.freeze
DB_COUNTERS = %i{count write_count cached_count}.freeze
SQL_COMMANDS_WITH_COMMENTS_REGEX = %r{\A(/\*.*\*/\s)?((?!(.*[^\w'"](DELETE|UPDATE|INSERT INTO)[^\w'"])))(WITH.*)?(SELECT)((?!(FOR UPDATE|FOR SHARE)).)*$}i.freeze
SQL_DURATION_BUCKET = [0.05, 0.1, 0.25].freeze
TRANSACTION_DURATION_BUCKET = [0.1, 0.25, 1].freeze
DB_LOAD_BALANCING_COUNTERS = %i{
db_replica_count db_replica_cached_count db_replica_wal_count db_replica_wal_cached_count
db_primary_count db_primary_cached_count db_primary_wal_count db_primary_wal_cached_count
}.freeze
DB_LOAD_BALANCING_DURATIONS = %i{db_primary_duration_s db_replica_duration_s}.freeze
DB_LOAD_BALANCING_ROLES = %i{replica primary}.freeze
DB_LOAD_BALANCING_COUNTERS = %i{count cached_count wal_count wal_cached_count}.freeze
DB_LOAD_BALANCING_DURATIONS = %i{duration_s}.freeze
SQL_WAL_LOCATION_REGEX = /(pg_current_wal_insert_lsn\(\)::text|pg_last_wal_replay_lsn\(\)::text)/.freeze
@ -40,9 +38,10 @@ module Gitlab
payload = event.payload
return if ignored_query?(payload)
increment(:db_count)
increment(:db_cached_count) if cached_query?(payload)
increment(:db_write_count) unless select_sql_command?(payload)
db_config_name = db_config_name(event.payload)
increment(:count, db_config_name: db_config_name)
increment(:cached_count, db_config_name: db_config_name) if cached_query?(payload)
increment(:write_count, db_config_name: db_config_name) unless select_sql_command?(payload)
observe(:gitlab_sql_duration_seconds, event) do
buckets SQL_DURATION_BUCKET
@ -61,24 +60,17 @@ module Gitlab
return {} unless Gitlab::SafeRequestStore.active?
{}.tap do |payload|
DB_COUNTERS.each do |counter|
payload[counter] = Gitlab::SafeRequestStore[counter].to_i
db_counter_keys.each do |key|
payload[key] = Gitlab::SafeRequestStore[key].to_i
end
if ::Gitlab::SafeRequestStore.active? && ::Gitlab::Database::LoadBalancing.enable?
DB_LOAD_BALANCING_COUNTERS.each do |counter|
load_balancing_metric_counter_keys.each do |counter|
payload[counter] = ::Gitlab::SafeRequestStore[counter].to_i
end
DB_LOAD_BALANCING_DURATIONS.each do |duration|
payload[duration] = ::Gitlab::SafeRequestStore[duration].to_f.round(3)
end
if Feature.enabled?(:multiple_database_metrics, default_enabled: :yaml)
::Gitlab::SafeRequestStore[:duration_by_database]&.each do |name, duration_by_role|
duration_by_role.each do |db_role, duration|
payload[:"db_#{db_role}_#{name}_duration_s"] = duration.to_f.round(3)
end
end
load_balancing_metric_duration_keys.each do |duration|
payload[duration] = ::Gitlab::SafeRequestStore[duration].to_f.round(3)
end
end
end
@ -92,12 +84,15 @@ module Gitlab
def increment_db_role_counters(db_role, payload)
cached = cached_query?(payload)
increment("db_#{db_role}_count".to_sym)
increment("db_#{db_role}_cached_count".to_sym) if cached
db_config_name = db_config_name(payload)
increment(:count, db_role: db_role, db_config_name: db_config_name)
increment(:cached_count, db_role: db_role, db_config_name: db_config_name) if cached
if wal_command?(payload)
increment("db_#{db_role}_wal_count".to_sym)
increment("db_#{db_role}_wal_cached_count".to_sym) if cached
increment(:wal_count, db_role: db_role, db_config_name: db_config_name)
increment(:wal_cached_count, db_role: db_role, db_config_name: db_config_name) if cached
end
end
@ -109,15 +104,13 @@ module Gitlab
return unless ::Gitlab::SafeRequestStore.active?
duration = event.duration / 1000.0
duration_key = "db_#{db_role}_duration_s".to_sym
duration_key = compose_metric_key(:duration_s, db_role)
::Gitlab::SafeRequestStore[duration_key] = (::Gitlab::SafeRequestStore[duration_key].presence || 0) + duration
# Per database metrics
name = ::Gitlab::Database.db_config_name(event.payload[:connection])
::Gitlab::SafeRequestStore[:duration_by_database] ||= {}
::Gitlab::SafeRequestStore[:duration_by_database][name] ||= {}
::Gitlab::SafeRequestStore[:duration_by_database][name][db_role] ||= 0
::Gitlab::SafeRequestStore[:duration_by_database][name][db_role] += duration
db_config_name = db_config_name(event.payload)
duration_key = compose_metric_key(:duration_s, db_role, db_config_name)
::Gitlab::SafeRequestStore[duration_key] = (::Gitlab::SafeRequestStore[duration_key].presence || 0) + duration
end
def ignored_query?(payload)
@ -132,10 +125,25 @@ module Gitlab
payload[:sql].match(SQL_COMMANDS_WITH_COMMENTS_REGEX)
end
def increment(counter)
current_transaction&.increment("gitlab_transaction_#{counter}_total".to_sym, 1)
def increment(counter, db_config_name:, db_role: nil)
log_key = compose_metric_key(counter, db_role)
Gitlab::SafeRequestStore[counter] = Gitlab::SafeRequestStore[counter].to_i + 1
prometheus_key = if db_role
:"gitlab_transaction_db_#{db_role}_#{counter}_total"
else
:"gitlab_transaction_db_#{counter}_total"
end
current_transaction&.increment(prometheus_key, 1)
Gitlab::SafeRequestStore[log_key] = Gitlab::SafeRequestStore[log_key].to_i + 1
# To avoid confusing log keys we only log the db_config_name metrics
# when we are also logging the db_role. Otherwise it will be hard to
# tell if the log key is referring to a db_role OR a db_config_name.
if db_role.present? && db_config_name.present?
log_key = compose_metric_key(counter, db_role, db_config_name)
Gitlab::SafeRequestStore[log_key] = Gitlab::SafeRequestStore[log_key].to_i + 1
end
end
def observe(histogram, event, &block)
@ -145,6 +153,45 @@ module Gitlab
def current_transaction
::Gitlab::Metrics::WebTransaction.current || ::Gitlab::Metrics::BackgroundTransaction.current
end
def db_config_name(payload)
::Gitlab::Database.db_config_name(payload[:connection])
end
def self.db_counter_keys
DB_COUNTERS.map { |c| compose_metric_key(c) }
end
def self.load_balancing_metric_counter_keys
load_balancing_metric_keys(DB_LOAD_BALANCING_COUNTERS)
end
def self.load_balancing_metric_duration_keys
load_balancing_metric_keys(DB_LOAD_BALANCING_DURATIONS)
end
def self.load_balancing_metric_keys(metrics)
[].tap do |counters|
DB_LOAD_BALANCING_ROLES.each do |role|
metrics.each do |metric|
counters << compose_metric_key(metric, role)
next unless ENV['GITLAB_MULTIPLE_DATABASE_METRICS']
::Gitlab::Database.db_config_names.each do |config_name|
counters << compose_metric_key(metric, role, config_name)
end
end
end
end
end
def compose_metric_key(metric, db_role = nil, db_config_name = nil)
self.class.compose_metric_key(metric, db_role, db_config_name)
end
def self.compose_metric_key(metric, db_role = nil, db_config_name = nil)
[:db, db_role, db_config_name, metric].compact.join("_").to_sym
end
end
end
end

View File

@ -81,7 +81,7 @@ module Peek
end
def format_call_details(call)
if Feature.enabled?(:multiple_database_metrics, default_enabled: :yaml)
if ENV['GITLAB_MULTIPLE_DATABASE_METRICS']
super
else
super.except(:db_config_name)

View File

@ -16337,6 +16337,9 @@ msgstr ""
msgid "Hi %{username}!"
msgstr ""
msgid "Hi%{salutation}, your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information about %{company} to activate your trial."
msgstr ""
msgid "Hide"
msgstr ""
@ -38248,7 +38251,7 @@ msgstr ""
msgid "Your GPG keys (%{count})"
msgstr ""
msgid "Your GitLab Ultimate trial will last 30 days after which point you can keep your free GitLab account forever. We just need some additional information to activate your trial."
msgid "Your GitLab Ultimate trial lasts for 30 days, but you can keep your free GitLab account forever. We just need some additional information to activate your trial."
msgstr ""
msgid "Your GitLab account has been locked due to an excessive amount of unsuccessful sign in attempts. Your account will automatically unlock in %{duration} or you may click the link below to unlock now."
@ -40195,5 +40198,8 @@ msgstr ""
msgid "yaml invalid"
msgstr ""
msgid "your company"
msgstr ""
msgid "your settings"
msgstr ""

View File

@ -65,82 +65,39 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'deploy tokens with dependency_proxy_deploy_tokens disabled' do
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
context 'with deploy token from a different group,' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
context 'with deploy token from a different group,' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'with revoked deploy token' do
let_it_be(:user) { create(:deploy_token, :revoked, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'with expired deploy token' do
let_it_be(:user) { create(:deploy_token, :expired, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'with deploy token with insufficient scopes' do
let_it_be(:user) { create(:deploy_token, :group) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when a group is not found' do
before do
expect(Group).to receive(:find_by_full_path).and_return(nil)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'deploy tokens with dependency_proxy_deploy_tokens enabled' do
context 'with deploy token from a different group,' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
context 'with revoked deploy token' do
let_it_be(:user) { create(:deploy_token, :revoked, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'with expired deploy token' do
let_it_be(:user) { create(:deploy_token, :expired, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'with deploy token with insufficient scopes' do
let_it_be(:user) { create(:deploy_token, :group) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when a group is not found' do
before do
expect(Group).to receive(:find_by_full_path).and_return(nil)
end
context 'with revoked deploy token' do
let_it_be(:user) { create(:deploy_token, :revoked, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'with expired deploy token' do
let_it_be(:user) { create(:deploy_token, :expired, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'with deploy token with insufficient scopes' do
let_it_be(:user) { create(:deploy_token, :group) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when a group is not found' do
before do
expect(Group).to receive(:find_by_full_path).and_return(nil)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when user is not found' do
@ -274,25 +231,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'returning response status', :success
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest_from_cache'
end
context 'with dependency_proxy_deploy_tokens feature flag disabled' do
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
it_behaves_like 'a successful manifest pull'
end
end
context 'a valid deploy token with dependency_proxy_deploy_tokens feature flag disabled' do
let_it_be(:user) { create(:deploy_token, :dependency_proxy_scopes, :group) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'a valid deploy token' do
@ -395,25 +333,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
it_behaves_like 'returning response status', :success
it_behaves_like 'a package tracking event', described_class.name, 'pull_blob_from_cache'
end
context 'with dependency_proxy_deploy_tokens feature flag disabled' do
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
it_behaves_like 'a successful blob pull'
end
end
context 'a valid deploy token with dependency_proxy_deploy_tokens feature flag disabled' do
let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) }
before do
stub_feature_flags(dependency_proxy_deploy_tokens: false)
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'a valid deploy token' do

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`content_editor/components/toolbar_button displays tertiary, small button with a provided label and icon 1`] = `
"<b-button-stub size=\\"sm\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-mx-2 gl-button btn-default-tertiary btn-icon\\">
"<b-button-stub size=\\"sm\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\">
<!---->
<gl-icon-stub name=\\"bold\\" size=\\"16\\" class=\\"gl-button-icon\\"></gl-icon-stub>
<!---->

View File

@ -0,0 +1,80 @@
import { BubbleMenu } from '@tiptap/vue-2';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
import {
BUBBLE_MENU_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
import { createTestEditor } from '../test_utils';
describe('content_editor/components/top_toolbar', () => {
let wrapper;
let trackingSpy;
let tiptapEditor;
const buildEditor = () => {
tiptapEditor = createTestEditor();
jest.spyOn(tiptapEditor, 'isActive');
};
const buildWrapper = () => {
wrapper = shallowMountExtended(FormattingBubbleMenu, {
provide: {
tiptapEditor,
},
});
};
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
buildEditor();
});
afterEach(() => {
wrapper.destroy();
});
it('renders bubble menu component', () => {
buildWrapper();
const bubbleMenu = wrapper.findComponent(BubbleMenu);
expect(bubbleMenu.props().editor).toBe(tiptapEditor);
expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base']);
});
describe.each`
testId | controlProps
${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold', size: 'medium', category: 'primary' }}
${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic', size: 'medium', category: 'primary' }}
${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike', size: 'medium', category: 'primary' }}
${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode', size: 'medium', category: 'primary' }}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
});
it('renders the toolbar control with the provided properties', () => {
expect(wrapper.findByTestId(testId).exists()).toBe(true);
Object.keys(controlProps).forEach((propName) => {
expect(wrapper.findByTestId(testId).props(propName)).toBe(controlProps[propName]);
});
});
it('tracks the execution of toolbar controls', () => {
const eventData = { contentType: 'italic', value: 1 };
const { contentType, value } = eventData;
wrapper.findByTestId(testId).vm.$emit('execute', eventData);
expect(trackingSpy).toHaveBeenCalledWith(undefined, BUBBLE_MENU_TRACKING_ACTION, {
label: CONTENT_EDITOR_TRACKING_LABEL,
property: contentType,
value,
});
});
});
});

View File

@ -50,6 +50,24 @@ describe('content_editor/components/toolbar_button', () => {
expect(findButton().html()).toMatchSnapshot();
});
it('allows customizing the variant, category, size of the button', () => {
const variant = 'danger';
const category = 'secondary';
const size = 'medium';
buildWrapper({
variant,
category,
size,
});
expect(findButton().props()).toMatchObject({
variant,
category,
size,
});
});
it.each`
editorState | outcomeDescription | outcome
${{ isActive: true, isFocused: true }} | ${'button is active'} | ${true}

View File

@ -1,6 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import {
TOOLBAR_CONTROL_TRACKING_ACTION,
@ -12,7 +11,7 @@ describe('content_editor/components/top_toolbar', () => {
let trackingSpy;
const buildWrapper = () => {
wrapper = extendedWrapper(shallowMount(TopToolbar));
wrapper = shallowMountExtended(TopToolbar);
};
beforeEach(() => {
@ -43,17 +42,17 @@ describe('content_editor/components/top_toolbar', () => {
});
it('renders the toolbar control with the provided properties', () => {
expect(wrapper.findByTestId(testId).props()).toEqual({
...controlProps,
expect(wrapper.findByTestId(testId).exists()).toBe(true);
Object.keys(controlProps).forEach((propName) => {
expect(wrapper.findByTestId(testId).props(propName)).toBe(controlProps[propName]);
});
});
it.each`
eventData
${{ contentType: 'bold' }}
${{ contentType: 'blockquote', value: 1 }}
`('tracks the execution of toolbar controls', ({ eventData }) => {
it('tracks the execution of toolbar controls', () => {
const eventData = { contentType: 'blockquote', value: 1 };
const { contentType, value } = eventData;
wrapper.findByTestId(testId).vm.$emit('execute', eventData);
expect(trackingSpy).toHaveBeenCalledWith(undefined, TOOLBAR_CONTROL_TRACKING_ACTION, {

View File

@ -0,0 +1,56 @@
import {
LOADING_CONTENT_EVENT,
LOADING_SUCCESS_EVENT,
LOADING_ERROR_EVENT,
} from '~/content_editor/constants';
import { ContentEditor } from '~/content_editor/services/content_editor';
import { createTestEditor } from '../test_utils';
describe('content_editor/services/content_editor', () => {
let contentEditor;
let serializer;
beforeEach(() => {
const tiptapEditor = createTestEditor();
serializer = { deserialize: jest.fn() };
contentEditor = new ContentEditor({ tiptapEditor, serializer });
});
describe('when setSerializedContent succeeds', () => {
beforeEach(() => {
serializer.deserialize.mockResolvedValueOnce('');
});
it('emits loadingContent and loadingSuccess event', () => {
let loadingContentEmitted = false;
contentEditor.on(LOADING_CONTENT_EVENT, () => {
loadingContentEmitted = true;
});
contentEditor.on(LOADING_SUCCESS_EVENT, () => {
expect(loadingContentEmitted).toBe(true);
});
contentEditor.setSerializedContent('**bold text**');
});
});
describe('when setSerializedContent fails', () => {
const error = 'error';
beforeEach(() => {
serializer.deserialize.mockRejectedValueOnce(error);
});
it('emits loadingError event', async () => {
contentEditor.on(LOADING_ERROR_EVENT, (e) => {
expect(e).toBe('error');
});
await expect(() => contentEditor.setSerializedContent('**bold text**')).rejects.toEqual(
error,
);
});
});
});

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Gitlab::Git::CommitStats, :seed_helper do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) }
def verify_stats!
stats = described_class.new(repository, commit)
expect(stats).to have_attributes(
additions: eq(11),
deletions: eq(6),
total: eq(17)
)
end
it 'returns commit stats and caches them', :use_clean_rails_redis_caching do
expect(repository.gitaly_commit_client).to receive(:commit_stats).with(commit.id).and_call_original
verify_stats!
expect(Rails.cache.fetch("commit_stats:group/project:#{commit.id}")).to eq([11, 6])
expect(repository.gitaly_commit_client).not_to receive(:commit_stats)
verify_stats!
end
end

View File

@ -294,47 +294,38 @@ RSpec.describe Gitlab::SidekiqLogging::StructuredLogger do
context 'when load balancing is enabled', :db_load_balancing do
let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.connection) }
let(:expected_db_payload_defaults) do
metrics =
::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_counter_keys +
::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_duration_keys +
::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_keys +
[:db_duration_s]
metrics.each_with_object({}) do |key, result|
result[key.to_s] = 0
end
end
let(:expected_end_payload_with_db) do
expected_end_payload.merge(
expected_end_payload.merge(expected_db_payload_defaults).merge(
'db_duration_s' => a_value >= 0.1,
'db_count' => a_value >= 1,
'db_cached_count' => 0,
'db_write_count' => 0,
'db_replica_count' => 0,
'db_replica_cached_count' => 0,
'db_replica_wal_count' => 0,
"db_replica_#{db_config_name}_count" => 0,
'db_replica_duration_s' => a_value >= 0,
'db_primary_count' => a_value >= 1,
'db_primary_cached_count' => 0,
'db_primary_wal_count' => 0,
"db_primary_#{db_config_name}_count" => a_value >= 1,
'db_primary_duration_s' => a_value > 0,
"db_primary_#{db_config_name}_duration_s" => a_value > 0,
'db_primary_wal_cached_count' => 0,
'db_replica_wal_cached_count' => 0
"db_primary_#{db_config_name}_duration_s" => a_value > 0
)
end
let(:end_payload) do
start_payload.merge(
start_payload.merge(expected_db_payload_defaults).merge(
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec',
'job_status' => 'done',
'duration_s' => 0.0,
'completed_at' => timestamp.to_f,
'cpu_s' => 1.111112,
'db_duration_s' => 0.0,
'db_cached_count' => 0,
'db_count' => 0,
'db_write_count' => 0,
'db_replica_count' => 0,
'db_replica_cached_count' => 0,
'db_replica_wal_count' => 0,
'db_replica_duration_s' => 0,
'db_primary_count' => 0,
'db_primary_cached_count' => 0,
'db_primary_wal_count' => 0,
'db_primary_wal_cached_count' => 0,
'db_replica_wal_cached_count' => 0,
'db_primary_duration_s' => 0
'cpu_s' => 1.111112
)
end

View File

@ -66,6 +66,16 @@ RSpec.describe Gitlab::UsageDataMetrics do
)
end
it 'includes testing monthly and weekly keys' do
expect(subject[:redis_hll_counters][:testing]).to include(
:i_testing_test_case_parsed_monthly, :i_testing_test_case_parsed_weekly,
:users_expanding_testing_code_quality_report_monthly, :users_expanding_testing_code_quality_report_weekly,
:users_expanding_testing_accessibility_report_monthly, :users_expanding_testing_accessibility_report_weekly,
:i_testing_summary_widget_total_monthly, :i_testing_summary_widget_total_weekly,
:testing_total_unique_counts_monthly
)
end
it 'includes source_code monthly and weekly keys' do
expect(subject[:redis_hll_counters][:source_code].keys).to contain_exactly(*[
:wiki_action_monthly, :wiki_action_weekly,

View File

@ -109,9 +109,9 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
)
end
context 'when the multiple_database_metrics feature flag is disabled' do
context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do
before do
stub_feature_flags(multiple_database_metrics: false)
stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil)
end
it 'does not include db_config_name field' do
@ -191,9 +191,9 @@ RSpec.describe Peek::Views::ActiveRecord, :request_store do
)
end
context 'when the multiple_database_metrics feature flag is disabled' do
context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do
before do
stub_feature_flags(multiple_database_metrics: false)
stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil)
end
it 'does not include db_config_name field' do

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Npm do
using RSpec::Parameterized::TableSyntax
describe '.scope_of' do
subject { described_class.scope_of(package_name) }
where(:package_name, :expected_result) do
nil | nil
'test' | nil
'@test' | nil
'test/package' | nil
'@/package' | nil
'@test/package' | 'test'
'@test/' | nil
end
with_them do
it { is_expected.to eq(expected_result) }
end
end
end

View File

@ -418,7 +418,7 @@ RSpec.describe Packages::Package, type: :model do
end
end
describe '#package_already_taken' do
describe '#npm_package_already_taken' do
context 'maven package' do
let!(:package) { create(:maven_package) }
@ -428,6 +428,43 @@ RSpec.describe Packages::Package, type: :model do
expect(new_package).to be_valid
end
end
context 'npm package' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:second_project) { create(:project, namespace: group)}
let(:package) { build(:npm_package, project: project, name: name) }
let(:second_package) { build(:npm_package, project: second_project, name: name, version: '5.0.0') }
context 'following the naming convention' do
let(:name) { "@#{group.path}/test" }
it 'will allow the first package' do
expect(package).to be_valid
end
it 'will not allow npm package with duplicate name' do
package.save!
expect(second_package).not_to be_valid
end
end
context 'not following the naming convention' do
let(:name) { '@foobar/test' }
it 'will allow the first package' do
expect(package).to be_valid
end
it 'will allow npm package with duplicate name' do
package.save!
expect(second_package).to be_valid
end
end
end
end
context "recipe uniqueness for conan packages" do

View File

@ -6839,29 +6839,36 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#package_already_taken?' do
let(:namespace) { create(:namespace) }
let(:project) { create(:project, :public, namespace: namespace) }
let!(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") }
let_it_be(:namespace) { create(:namespace) }
let_it_be(:project) { create(:project, :public, namespace: namespace) }
let_it_be(:package) { create(:npm_package, project: project, name: "@#{namespace.path}/foo") }
context 'no package exists with the same name' do
it 'returns false' do
result = project.package_already_taken?("@#{namespace.path}/bar")
result = project.package_already_taken?("@#{namespace.path}/bar", package_type: :npm)
expect(result).to be false
end
it 'returns false if it is the project that the package belongs to' do
result = project.package_already_taken?("@#{namespace.path}/foo")
result = project.package_already_taken?("@#{namespace.path}/foo", package_type: :npm)
expect(result).to be false
end
end
context 'a package already exists with the same name' do
let(:alt_project) { create(:project, :public, namespace: namespace) }
let_it_be(:alt_project) { create(:project, :public, namespace: namespace) }
it 'returns true' do
result = alt_project.package_already_taken?("@#{namespace.path}/foo")
result = alt_project.package_already_taken?(package.name, package_type: :npm)
expect(result).to be true
end
context 'for a different package type' do
it 'returns false' do
result = alt_project.package_already_taken?(package.name, package_type: :nuget)
expect(result).to be false
end
end
end
end

View File

@ -228,6 +228,31 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'handling upload with different authentications'
end
context 'with an existing package' do
let_it_be(:second_project) { create(:project, namespace: namespace) }
context 'following the naming convention' do
let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@#{group.path}/test") }
let(:package_name) { "@#{group.path}/test" }
it_behaves_like 'handling invalid record with 400 error'
end
context 'not following the naming convention' do
let_it_be(:second_package) { create(:npm_package, project: second_project, name: "@any_scope/test") }
let(:package_name) { "@any_scope/test" }
it "uploads the package" do
expect { upload_package_with_token(package_name, params) }
.to change { project.packages.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
end
end
end
end
context 'package creation fails' do

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
RSpec.configure do |config|
config.before do
# Enable this by default in all tests so it behaves like a FF
stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', '1')
end
end

View File

@ -1,70 +1,80 @@
# frozen_string_literal: true
RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.connection) }
let(:expected_payload_defaults) do
metrics =
::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_counter_keys +
::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_duration_keys +
::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_keys
metrics.each_with_object({}) do |key, result|
result[key] = 0
end
end
it 'prevents db counters from leaking to the next transaction' do
2.times do
Gitlab::WithRequestStore.with_request_store do
subscriber.sql(event)
connection = event.payload[:connection]
if db_role == :primary
expected = {
db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0,
db_primary_cached_count: record_cached_query ? 1 : 0,
db_primary_count: record_query ? 1 : 0,
db_primary_duration_s: record_query ? 0.002 : 0,
db_replica_cached_count: 0,
db_replica_count: 0,
db_replica_duration_s: 0.0,
db_primary_wal_count: record_wal_query ? 1 : 0,
db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
db_replica_wal_cached_count: 0,
db_replica_wal_count: 0
}
expected[:"db_primary_#{::Gitlab::Database.db_config_name(connection)}_duration_s"] = 0.002 if record_query
elsif db_role == :replica
expected = {
db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0,
db_primary_cached_count: 0,
db_primary_count: 0,
db_primary_duration_s: 0.0,
db_replica_cached_count: record_cached_query ? 1 : 0,
db_replica_count: record_query ? 1 : 0,
db_replica_duration_s: record_query ? 0.002 : 0,
db_replica_wal_count: record_wal_query ? 1 : 0,
db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
db_primary_wal_cached_count: 0,
db_primary_wal_count: 0
}
expected[:"db_replica_#{::Gitlab::Database.db_config_name(connection)}_duration_s"] = 0.002 if record_query
else
expected = {
db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0
}
end
expected = if db_role == :primary
expected_payload_defaults.merge({
db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0,
db_primary_cached_count: record_cached_query ? 1 : 0,
"db_primary_#{db_config_name}_cached_count": record_cached_query ? 1 : 0,
db_primary_count: record_query ? 1 : 0,
"db_primary_#{db_config_name}_count": record_query ? 1 : 0,
db_primary_duration_s: record_query ? 0.002 : 0,
"db_primary_#{db_config_name}_duration_s": record_query ? 0.002 : 0,
db_primary_wal_count: record_wal_query ? 1 : 0,
"db_primary_#{db_config_name}_wal_count": record_wal_query ? 1 : 0,
db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
"db_primary_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0
})
elsif db_role == :replica
expected_payload_defaults.merge({
db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0,
db_replica_cached_count: record_cached_query ? 1 : 0,
"db_replica_#{db_config_name}_cached_count": record_cached_query ? 1 : 0,
db_replica_count: record_query ? 1 : 0,
"db_replica_#{db_config_name}_count": record_query ? 1 : 0,
db_replica_duration_s: record_query ? 0.002 : 0,
"db_replica_#{db_config_name}_duration_s": record_query ? 0.002 : 0,
db_replica_wal_count: record_wal_query ? 1 : 0,
"db_replica_#{db_config_name}_wal_count": record_wal_query ? 1 : 0,
db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
"db_replica_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0
})
else
{
db_count: record_query ? 1 : 0,
db_write_count: record_write_query ? 1 : 0,
db_cached_count: record_cached_query ? 1 : 0
}
end
expect(described_class.db_counter_payload).to eq(expected)
end
end
end
context 'when multiple_database_metrics is disabled' do
context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do
before do
stub_feature_flags(multiple_database_metrics: false)
stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil)
end
it 'does not include per database metrics' do
Gitlab::WithRequestStore.with_request_store do
subscriber.sql(event)
connection = event.payload[:connection]
expect(described_class.db_counter_payload).not_to include(:"db_replica_#{::Gitlab::Database.db_config_name(connection)}_duration_s")
expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_duration_s")
expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_count")
end
end
end