diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ea45b5e3ec7..015f0519c72 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -39,10 +39,10 @@ export default {
required: false,
default: false,
},
- pipelineLayers: {
- type: Array,
+ computedPipelineInfo: {
+ type: Object,
required: false,
- default: () => [],
+ default: () => ({}),
},
type: {
type: String,
@@ -81,7 +81,10 @@ export default {
layout() {
return this.isStageView
? this.pipeline.stages
- : generateColumnsFromLayersListMemoized(this.pipeline, this.pipelineLayers);
+ : generateColumnsFromLayersListMemoized(
+ this.pipeline,
+ this.computedPipelineInfo.pipelineLayers,
+ );
},
hasDownstreamPipelines() {
return Boolean(this.pipeline?.downstream?.length > 0);
@@ -92,6 +95,9 @@ export default {
isStageView() {
return this.viewType === STAGE_VIEW;
},
+ linksData() {
+ return this.computedPipelineInfo?.linksData ?? null;
+ },
metricsConfig() {
return {
path: this.configPaths.metricsPath,
@@ -188,6 +194,7 @@ export default {
:container-id="containerId"
:container-measurements="measurements"
:highlighted-job="hoveredJobName"
+ :links-data="linksData"
:metrics-config="metricsConfig"
:show-links="showJobLinks"
:view-type="viewType"
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index 5d51d97eaee..8462fb752b7 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -9,11 +9,11 @@ import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql';
import { reportToSentry, reportMessageToSentry } from '../../utils';
-import { listByLayers } from '../parsing_utils';
import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
import PipelineGraph from './graph_component.vue';
import GraphViewSelector from './graph_view_selector.vue';
import {
+ calculatePipelineLayersInfo,
getQueryHeaders,
serializeLoadErrors,
toggleQueryPollingByVisibility,
@@ -51,10 +51,10 @@ export default {
return {
alertType: null,
callouts: [],
+ computedPipelineInfo: null,
currentViewType: STAGE_VIEW,
canRefetchHeaderPipeline: false,
pipeline: null,
- pipelineLayers: null,
showAlert: false,
showLinks: false,
};
@@ -214,12 +214,16 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
methods: {
- getPipelineLayers() {
- if (this.currentViewType === LAYER_VIEW && !this.pipelineLayers) {
- this.pipelineLayers = listByLayers(this.pipeline);
+ getPipelineInfo() {
+ if (this.currentViewType === LAYER_VIEW && !this.computedPipelineInfo) {
+ this.computedPipelineInfo = calculatePipelineLayersInfo(
+ this.pipeline,
+ this.$options.name,
+ this.metricsPath,
+ );
}
- return this.pipelineLayers;
+ return this.computedPipelineInfo;
},
handleTipDismissal() {
try {
@@ -288,7 +292,7 @@ export default {
v-if="pipeline"
:config-paths="configPaths"
:pipeline="pipeline"
- :pipeline-layers="getPipelineLayers()"
+ :computed-pipeline-info="getPipelineInfo()"
:show-links="showLinks"
:view-type="graphViewType"
@error="reportFailure"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 52ee40bd982..d251e0d8bd8 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -2,10 +2,10 @@
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { LOAD_FAILURE } from '../../constants';
import { reportToSentry } from '../../utils';
-import { listByLayers } from '../parsing_utils';
import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants';
import LinkedPipeline from './linked_pipeline.vue';
import {
+ calculatePipelineLayersInfo,
getQueryHeaders,
serializeLoadErrors,
toggleQueryPollingByVisibility,
@@ -138,7 +138,11 @@ export default {
},
getPipelineLayers(id) {
if (this.viewType === LAYER_VIEW && !this.pipelineLayers[id]) {
- this.pipelineLayers[id] = listByLayers(this.currentPipeline);
+ this.pipelineLayers[id] = calculatePipelineLayersInfo(
+ this.currentPipeline,
+ this.$options.name,
+ this.configPaths.metricsPath,
+ );
}
return this.pipelineLayers[id];
@@ -223,7 +227,7 @@ export default {
class="d-inline-block gl-mt-n2"
:config-paths="configPaths"
:pipeline="currentPipeline"
- :pipeline-layers="getPipelineLayers(pipeline.id)"
+ :computed-pipeline-info="getPipelineLayers(pipeline.id)"
:show-links="showLinks"
:is-linked-pipeline="true"
:view-type="graphViewType"
diff --git a/app/assets/javascripts/pipelines/components/graph/perf_utils.js b/app/assets/javascripts/pipelines/components/graph/perf_utils.js
new file mode 100644
index 00000000000..3737a209f5c
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph/perf_utils.js
@@ -0,0 +1,50 @@
+import {
+ PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
+ PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
+ PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
+ PIPELINES_DETAIL_LINK_DURATION,
+ PIPELINES_DETAIL_LINKS_TOTAL,
+ PIPELINES_DETAIL_LINKS_JOB_RATIO,
+} from '~/performance/constants';
+
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import { reportPerformance } from '../graph_shared/api';
+
+export const beginPerfMeasure = () => {
+ performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
+};
+
+export const finishPerfMeasureAndSend = (numLinks, numGroups, metricsPath) => {
+ performanceMarkAndMeasure({
+ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
+ measures: [
+ {
+ name: PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
+ start: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
+ },
+ ],
+ });
+
+ window.requestAnimationFrame(() => {
+ const duration = window.performance.getEntriesByName(
+ PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
+ )[0]?.duration;
+
+ if (!duration) {
+ return;
+ }
+
+ const data = {
+ histograms: [
+ { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
+ { name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
+ {
+ name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
+ value: numLinks / numGroups,
+ },
+ ],
+ };
+
+ reportPerformance(metricsPath, data);
+ });
+};
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js
index 163b3898c28..3acfc10108b 100644
--- a/app/assets/javascripts/pipelines/components/graph/utils.js
+++ b/app/assets/javascripts/pipelines/components/graph/utils.js
@@ -1,7 +1,10 @@
import { isEmpty } from 'lodash';
import Visibility from 'visibilityjs';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { reportToSentry } from '../../utils';
+import { listByLayers } from '../parsing_utils';
import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
+import { beginPerfMeasure, finishPerfMeasureAndSend } from './perf_utils';
const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
return {
@@ -10,6 +13,28 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
};
};
+const calculatePipelineLayersInfo = (pipeline, componentName, metricsPath) => {
+ const shouldCollectMetrics = Boolean(metricsPath.length);
+
+ if (shouldCollectMetrics) {
+ beginPerfMeasure();
+ }
+
+ let layers = null;
+
+ try {
+ layers = listByLayers(pipeline);
+
+ if (shouldCollectMetrics) {
+ finishPerfMeasureAndSend(layers.linksData.length, layers.numGroups, metricsPath);
+ }
+ } catch (err) {
+ reportToSentry(componentName, err);
+ }
+
+ return layers;
+};
+
/* eslint-disable @gitlab/require-i18n-strings */
const getQueryHeaders = (etagResource) => {
return {
@@ -106,6 +131,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => {
const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0;
export {
+ calculatePipelineLayersInfo,
getQueryHeaders,
serializeGqlErr,
serializeLoadErrors,
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
index 83f2466f0bf..d6d9ea94c13 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
@@ -13,7 +13,7 @@ export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobNam
* @returns {Array} Links that contain all the information about them
*/
-export const generateLinksData = ({ links }, containerID, modifier = '') => {
+export const generateLinksData = (links, containerID, modifier = '') => {
const containerEl = document.getElementById(containerID);
return links.map((link) => {
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
index 5c775df7b48..1189c2ebad8 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
@@ -17,8 +17,8 @@ export default {
type: Object,
required: true,
},
- parsedData: {
- type: Object,
+ linksData: {
+ type: Array,
required: true,
},
pipelineId: {
@@ -95,7 +95,7 @@ export default {
highlightedJobs(jobs) {
this.$emit('highlightedJobsChange', jobs);
},
- parsedData() {
+ linksData() {
this.calculateLinkData();
},
viewType() {
@@ -112,7 +112,7 @@ export default {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
mounted() {
- if (!isEmpty(this.parsedData)) {
+ if (!isEmpty(this.linksData)) {
this.calculateLinkData();
}
},
@@ -122,7 +122,7 @@ export default {
},
calculateLinkData() {
try {
- this.links = generateLinksData(this.parsedData, this.containerId, `-${this.pipelineId}`);
+ this.links = generateLinksData(this.linksData, this.containerId, `-${this.pipelineId}`);
} catch (err) {
this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false });
reportToSentry(this.$options.name, err);
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
index 81409752621..ef24694e494 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
@@ -1,20 +1,16 @@
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
index b36c9c0d049..7e7f0572faf 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/parsing_utils.js
@@ -175,7 +175,7 @@ export const listByLayers = ({ stages }) => {
const parsedData = parseData(arrayOfJobs);
const dataWithLayers = createSankey()(parsedData);
- return dataWithLayers.nodes.reduce((acc, { layer, name }) => {
+ const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => {
/* sort groups by layer */
if (!acc[layer]) {
@@ -186,6 +186,12 @@ export const listByLayers = ({ stages }) => {
return acc;
}, []);
+
+ return {
+ linksData: parsedData.links,
+ numGroups: arrayOfJobs.length,
+ pipelineLayers,
+ };
};
export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipelineLayers) => {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index d343ba700ab..3ed9de6c133 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,5 +1,5 @@
-
+ />
diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
index 1b20ae57563..5cd2018bb8c 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
@@ -1,12 +1,12 @@
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index a93c70c75d3..fa235f72e35 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -98,7 +98,6 @@
}
.note-action-button,
-.toolbar-btn,
.dropdown-toggle-caret {
@include transition(color);
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 2a97009e605..99b1e44f23b 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -131,36 +131,6 @@
width: 100%;
}
-.toolbar-btn {
- float: left;
- padding: 0 7px;
- background: transparent;
- border: 0;
- outline: 0;
-
- svg {
- width: 14px;
- height: 14px;
- vertical-align: middle;
- fill: $gl-text-color-secondary;
- }
-
- &:hover,
- &:focus {
- svg {
- fill: $blue-600;
- }
- }
-}
-
-.toolbar-fullscreen-btn {
- margin-right: -5px;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- }
-}
-
.md-suggestion-diff {
display: table !important;
border: 1px solid $border-color !important;
diff --git a/app/finders/error_tracking/errors_finder.rb b/app/finders/error_tracking/errors_finder.rb
new file mode 100644
index 00000000000..fb2d4b14dfa
--- /dev/null
+++ b/app/finders/error_tracking/errors_finder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ErrorsFinder
+ def initialize(current_user, project, params)
+ @current_user = current_user
+ @project = project
+ @params = params
+ end
+
+ def execute
+ return ErrorTracking::Error.none unless authorized?
+
+ collection = project.error_tracking_errors
+ collection = by_status(collection)
+
+ # Limit collection until pagination implemented
+ collection.limit(20)
+ end
+
+ private
+
+ attr_reader :current_user, :project, :params
+
+ def by_status(collection)
+ if params[:status].present? && ErrorTracking::Error.statuses.key?(params[:status])
+ collection.for_status(params[:status])
+ else
+ collection
+ end
+ end
+
+ def authorized?
+ Ability.allowed?(current_user, :read_sentry_issue, project)
+ end
+ end
+end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 7525481047e..f185d6cd002 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -250,7 +250,7 @@ module MarkupHelper
data = options[:data].merge({ container: 'body' })
content_tag :button,
type: 'button',
- class: 'toolbar-btn js-md has-tooltip',
+ class: 'gl-button btn btn-default-tertiary btn-icon js-md has-tooltip',
data: data,
title: options[:title],
aria: { label: options[:title] } do
diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb
index 012dcc4418f..32932c4d045 100644
--- a/app/models/error_tracking/error.rb
+++ b/app/models/error_tracking/error.rb
@@ -5,10 +5,19 @@ class ErrorTracking::Error < ApplicationRecord
has_many :events, class_name: 'ErrorTracking::ErrorEvent'
+ scope :for_status, -> (status) { where(status: status) }
+
validates :project, presence: true
validates :name, presence: true
validates :description, presence: true
validates :actor, presence: true
+ validates :status, presence: true
+
+ enum status: {
+ unresolved: 0,
+ resolved: 1,
+ ignored: 2
+ }
def self.report_error(name:, description:, actor:, platform:, timestamp:)
safe_find_or_create_by(
@@ -20,4 +29,64 @@ class ErrorTracking::Error < ApplicationRecord
error.update!(last_seen_at: timestamp)
end
end
+
+ def title
+ if description.present?
+ "#{name} #{description}"
+ else
+ name
+ end
+ end
+
+ def title_truncated
+ title.truncate(64)
+ end
+
+ # For compatibility with sentry integration
+ def to_sentry_error
+ Gitlab::ErrorTracking::Error.new(
+ id: id,
+ title: title_truncated,
+ message: description,
+ culprit: actor,
+ first_seen: first_seen_at,
+ last_seen: last_seen_at,
+ status: status,
+ count: events_count
+ )
+ end
+
+ # For compatibility with sentry integration
+ def to_sentry_detailed_error
+ Gitlab::ErrorTracking::DetailedError.new(
+ id: id,
+ title: title_truncated,
+ message: description,
+ culprit: actor,
+ first_seen: first_seen_at.to_s,
+ last_seen: last_seen_at.to_s,
+ count: events_count,
+ user_count: 0, # we don't support user count yet.
+ project_id: project.id,
+ status: status,
+ tags: { level: nil, logger: nil },
+ external_url: external_url,
+ external_base_url: external_base_url
+ )
+ end
+
+ private
+
+ # For compatibility with sentry integration
+ def external_url
+ Gitlab::Routing.url_helpers.details_namespace_project_error_tracking_index_url(
+ namespace_id: project.namespace,
+ project_id: project,
+ issue_id: id)
+ end
+
+ # For compatibility with sentry integration
+ def external_base_url
+ Gitlab::Routing.url_helpers.root_url
+ end
end
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index ed14a1bce41..4de13de7e2e 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -8,4 +8,69 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
validates :error, presence: true
validates :description, presence: true
validates :occurred_at, presence: true
+
+ def stacktrace
+ @stacktrace ||= build_stacktrace
+ end
+
+ # For compatibility with sentry integration
+ def to_sentry_error_event
+ Gitlab::ErrorTracking::ErrorEvent.new(
+ issue_id: error_id,
+ date_received: occurred_at,
+ stack_trace_entries: stacktrace
+ )
+ end
+
+ private
+
+ def build_stacktrace
+ raw_stacktrace = find_stacktrace_from_payload
+
+ return [] unless raw_stacktrace
+
+ raw_stacktrace.map do |entry|
+ {
+ 'lineNo' => entry['lineno'],
+ 'context' => build_stacktrace_context(entry),
+ 'filename' => entry['filename'],
+ 'function' => entry['function'],
+ 'colNo' => 0 # we don't support colNo yet.
+ }
+ end
+ end
+
+ def find_stacktrace_from_payload
+ exception_entry = payload.dig('exception')
+
+ if exception_entry
+ exception_values = exception_entry.dig('values')
+ stack_trace_entry = exception_values&.detect { |h| h['stacktrace'].present? }
+ stack_trace_entry&.dig('stacktrace', 'frames')
+ end
+ end
+
+ def build_stacktrace_context(entry)
+ context = []
+ error_line = entry['context_line']
+ error_line_no = entry['lineno']
+ pre_context = entry['pre_context']
+ post_context = entry['post_context']
+
+ context += lines_with_position(pre_context, error_line_no - pre_context.size)
+ context += lines_with_position([error_line], error_line_no)
+ context += lines_with_position(post_context, error_line_no + 1)
+
+ context.reject(&:blank?)
+ end
+
+ def lines_with_position(lines, position)
+ return [] if lines.blank?
+
+ lines.map.with_index do |line, index|
+ next unless line
+
+ [position + index, line]
+ end
+ end
end
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index c729b002852..c5a77427588 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -31,12 +31,13 @@ module ErrorTracking
validates :api_url, length: { maximum: 255 }, public_url: { enforce_sanitization: true, ascii_only: true }, allow_nil: true
validates :enabled, inclusion: { in: [true, false] }
+ validates :integrated, inclusion: { in: [true, false] }
- validates :api_url, presence: { message: 'is a required field' }, if: :enabled
-
- validate :validate_api_url_path, if: :enabled
-
- validates :token, presence: { message: 'is a required field' }, if: :enabled
+ with_options if: :sentry_enabled do
+ validates :api_url, presence: { message: 'is a required field' }
+ validates :token, presence: { message: 'is a required field' }
+ validate :validate_api_url_path
+ end
attr_encrypted :token,
mode: :per_attribute_iv,
@@ -45,6 +46,14 @@ module ErrorTracking
after_save :clear_reactive_cache!
+ def sentry_enabled
+ enabled && !integrated_client?
+ end
+
+ def integrated_client?
+ integrated && ::Feature.enabled?(:integrated_error_tracking, project)
+ end
+
def api_url=(value)
super
clear_memoization(:api_url_slugs)
@@ -79,7 +88,7 @@ module ErrorTracking
def sentry_client
strong_memoize(:sentry_client) do
- ErrorTracking::SentryClient.new(api_url, token)
+ ::ErrorTracking::SentryClient.new(api_url, token)
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 60d5d80a1e2..1d0aa54c1c0 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -52,7 +52,7 @@ class GroupPolicy < BasePolicy
condition(:dependency_proxy_access_allowed) do
if Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true)
- access_level >= GroupMember::GUEST || valid_dependency_proxy_deploy_token
+ access_level(for_any_session: true) >= GroupMember::GUEST || valid_dependency_proxy_deploy_token
else
can?(:read_group)
end
@@ -240,14 +240,14 @@ class GroupPolicy < BasePolicy
enable :read_label
end
- def access_level
+ def access_level(for_any_session: false)
return GroupMember::NO_ACCESS if @user.nil?
return GroupMember::NO_ACCESS unless user_is_user?
- @access_level ||= lookup_access_level!
+ @access_level ||= lookup_access_level!(for_any_session: for_any_session)
end
- def lookup_access_level!
+ def lookup_access_level!(for_any_session: false)
@subject.max_member_access_for_user(@user)
end
diff --git a/app/services/error_tracking/issue_details_service.rb b/app/services/error_tracking/issue_details_service.rb
index 0068a9e9b6d..1614c597a8e 100644
--- a/app/services/error_tracking/issue_details_service.rb
+++ b/app/services/error_tracking/issue_details_service.rb
@@ -8,7 +8,7 @@ module ErrorTracking
private
def perform
- response = project_error_tracking_setting.issue_details(issue_id: params[:issue_id])
+ response = find_issue_details(params[:issue_id])
compose_response(response) do
# The gitlab_issue attribute can contain an absolute GitLab url from the Sentry Client
@@ -36,5 +36,29 @@ module ErrorTracking
def parse_response(response)
{ issue: response[:issue] }
end
+
+ def find_issue_details(issue_id)
+ # There are 2 types of the data source for the error tracking feature:
+ #
+ # * When integrated error tracking is enabled, we use the application database
+ # to read and save error tracking data.
+ #
+ # * When integrated error tracking is disabled we call
+ # project_error_tracking_setting method which works with Sentry API.
+ #
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
+ #
+ if project_error_tracking_setting.integrated_client?
+ error = project.error_tracking_errors.find(issue_id)
+
+ # We use the same response format as project_error_tracking_setting
+ # method below for compatibility with existing code.
+ {
+ issue: error.to_sentry_detailed_error
+ }
+ else
+ project_error_tracking_setting.issue_details(issue_id: issue_id)
+ end
+ end
end
end
diff --git a/app/services/error_tracking/issue_latest_event_service.rb b/app/services/error_tracking/issue_latest_event_service.rb
index a39f1cde1b2..1bf86c658fc 100644
--- a/app/services/error_tracking/issue_latest_event_service.rb
+++ b/app/services/error_tracking/issue_latest_event_service.rb
@@ -5,7 +5,7 @@ module ErrorTracking
private
def perform
- response = project_error_tracking_setting.issue_latest_event(issue_id: params[:issue_id])
+ response = find_issue_latest_event(params[:issue_id])
compose_response(response)
end
@@ -13,5 +13,30 @@ module ErrorTracking
def parse_response(response)
{ latest_event: response[:latest_event] }
end
+
+ def find_issue_latest_event(issue_id)
+ # There are 2 types of the data source for the error tracking feature:
+ #
+ # * When integrated error tracking is enabled, we use the application database
+ # to read and save error tracking data.
+ #
+ # * When integrated error tracking is disabled we call
+ # project_error_tracking_setting method which works with Sentry API.
+ #
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
+ #
+ if project_error_tracking_setting.integrated_client?
+ error = project.error_tracking_errors.find(issue_id)
+ event = error.events.last
+
+ # We use the same response format as project_error_tracking_setting
+ # method below for compatibility with existing code.
+ {
+ latest_event: event.to_sentry_error_event
+ }
+ else
+ project_error_tracking_setting.issue_latest_event(issue_id: issue_id)
+ end
+ end
end
end
diff --git a/app/services/error_tracking/issue_update_service.rb b/app/services/error_tracking/issue_update_service.rb
index 2f8bbfddef0..624e5f94dde 100644
--- a/app/services/error_tracking/issue_update_service.rb
+++ b/app/services/error_tracking/issue_update_service.rb
@@ -5,10 +5,12 @@ module ErrorTracking
private
def perform
- response = project_error_tracking_setting.update_issue(
+ update_opts = {
issue_id: params[:issue_id],
params: update_params
- )
+ }
+
+ response = update_issue(update_opts)
compose_response(response) do
project_error_tracking_setting.expire_issues_cache
@@ -69,5 +71,31 @@ module ErrorTracking
return error('Error Tracking is not enabled') unless enabled?
return error('Access denied', :unauthorized) unless can_update?
end
+
+ def update_issue(opts)
+ # There are 2 types of the data source for the error tracking feature:
+ #
+ # * When integrated error tracking is enabled, we use the application database
+ # to read and save error tracking data.
+ #
+ # * When integrated error tracking is disabled we call
+ # project_error_tracking_setting method which works with Sentry API.
+ #
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
+ #
+ if project_error_tracking_setting.integrated_client?
+ error = project.error_tracking_errors.find(opts[:issue_id])
+ error.status = opts[:params][:status]
+ error.save!
+
+ # We use the same response format as project_error_tracking_setting
+ # method below for compatibility with existing code.
+ {
+ updated: true
+ }
+ else
+ project_error_tracking_setting.update_issue(**opts)
+ end
+ end
end
end
diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb
index 7087e3825d6..5ddba748fd4 100644
--- a/app/services/error_tracking/list_issues_service.rb
+++ b/app/services/error_tracking/list_issues_service.rb
@@ -22,13 +22,15 @@ module ErrorTracking
def perform
return invalid_status_error unless valid_status?
- response = project_error_tracking_setting.list_sentry_issues(
+ sentry_opts = {
issue_status: issue_status,
limit: limit,
search_term: params[:search_term].presence,
sort: sort,
cursor: params[:cursor].presence
- )
+ }
+
+ response = list_issues(sentry_opts)
compose_response(response)
end
@@ -56,5 +58,36 @@ module ErrorTracking
def sort
params[:sort] || DEFAULT_SORT
end
+
+ def list_issues(opts)
+ # There are 2 types of the data source for the error tracking feature:
+ #
+ # * When integrated error tracking is enabled, we use the application database
+ # to read and save error tracking data.
+ #
+ # * When integrated error tracking is disabled we call
+ # project_error_tracking_setting method which works with Sentry API.
+ #
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
+ #
+ if project_error_tracking_setting.integrated_client?
+ # We are going to support more options in the future.
+ # For now we implement the bare minimum for rendering the list in UI.
+ filter_opts = {
+ status: opts[:issue_status]
+ }
+
+ errors = ErrorTracking::ErrorsFinder.new(current_user, project, filter_opts).execute
+
+ # We use the same response format as project_error_tracking_setting
+ # method below for compatibility with existing code.
+ {
+ issues: errors.map(&:to_sentry_error),
+ pagination: {}
+ }
+ else
+ project_error_tracking_setting.list_sentry_issues(**opts)
+ end
+ end
end
end
diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml
index 033ed69da41..e02c24b93f1 100644
--- a/app/views/shared/blob/_markdown_buttons.html.haml
+++ b/app/views/shared/blob/_markdown_buttons.html.haml
@@ -24,5 +24,5 @@
title: _("Add a collapsible section") })
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
- if show_fullscreen_button
- %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
+ %button.gl-button.btn.btn-default-tertiary.btn-icon.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
= sprite_icon("maximize")
diff --git a/config/feature_flags/development/dast_meta_tag_validation.yml b/config/feature_flags/development/dast_meta_tag_validation.yml
new file mode 100644
index 00000000000..ebe30192043
--- /dev/null
+++ b/config/feature_flags/development/dast_meta_tag_validation.yml
@@ -0,0 +1,8 @@
+---
+name: dast_meta_tag_validation
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67945
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337711
+milestone: '14.2'
+type: development
+group: group::dynamic analysis
+default_enabled: false
diff --git a/db/fixtures/development/31_error_tracking.rb b/db/fixtures/development/31_error_tracking.rb
new file mode 100644
index 00000000000..60e288696f8
--- /dev/null
+++ b/db/fixtures/development/31_error_tracking.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class Gitlab::Seeder::ErrorTrackingSeeder
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def seed
+ parsed_event = Gitlab::Json.parse(read_fixture_file('parsed_event.json'))
+
+ ErrorTracking::CollectErrorService
+ .new(project, nil, event: parsed_event)
+ .execute
+ end
+
+ private
+
+ def read_fixture_file(file)
+ File.read(fixture_path(file))
+ end
+
+ def fixture_path(file)
+ Rails.root.join('spec', 'fixtures', 'error_tracking', file)
+ end
+end
+
+
+Gitlab::Seeder.quiet do
+ admin_user = User.admins.first
+
+ Project.not_mass_generated.visible_to_user(admin_user).sample(1).each do |project|
+ puts "\nActivating integrated error tracking for the '#{project.full_path}' project"
+
+ unless Feature.enabled?(:integrated_error_tracking, project)
+ puts '- enabling feature flag'
+ Feature.enable(:integrated_error_tracking, project)
+ end
+
+ puts '- enabling in settings'
+ project.error_tracking_setting || project.create_error_tracking_setting
+ project.error_tracking_setting.update!(enabled: true, integrated: true)
+
+ puts '- seeding an error'
+ seeder = Gitlab::Seeder::ErrorTrackingSeeder.new(project)
+ seeder.seed
+ end
+end
diff --git a/db/migrate/20210726134950_add_integrated_to_error_tracking_setting.rb b/db/migrate/20210726134950_add_integrated_to_error_tracking_setting.rb
new file mode 100644
index 00000000000..5fd558e0c1b
--- /dev/null
+++ b/db/migrate/20210726134950_add_integrated_to_error_tracking_setting.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddIntegratedToErrorTrackingSetting < ActiveRecord::Migration[6.1]
+ def up
+ add_column :project_error_tracking_settings, :integrated, :boolean, null: false, default: false
+ end
+
+ def down
+ remove_column :project_error_tracking_settings, :integrated
+ end
+end
diff --git a/db/migrate/20210728110654_add_status_to_error_tracking_error.rb b/db/migrate/20210728110654_add_status_to_error_tracking_error.rb
new file mode 100644
index 00000000000..035f97dc963
--- /dev/null
+++ b/db/migrate/20210728110654_add_status_to_error_tracking_error.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddStatusToErrorTrackingError < ActiveRecord::Migration[6.1]
+ def up
+ add_column :error_tracking_errors, :status, :integer, null: false, default: 0, limit: 2
+ end
+
+ def down
+ remove_column :error_tracking_errors, :status
+ end
+end
diff --git a/db/post_migrate/20210809143931_finalize_job_id_conversion_to_bigint_for_ci_job_artifacts.rb b/db/post_migrate/20210809143931_finalize_job_id_conversion_to_bigint_for_ci_job_artifacts.rb
new file mode 100644
index 00000000000..bb12045b1de
--- /dev/null
+++ b/db/post_migrate/20210809143931_finalize_job_id_conversion_to_bigint_for_ci_job_artifacts.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+class FinalizeJobIdConversionToBigintForCiJobArtifacts < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = 'ci_job_artifacts'
+
+ def up
+ ensure_batched_background_migration_is_finished(
+ job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
+ table_name: TABLE_NAME,
+ column_name: 'id',
+ job_arguments: [%w[id job_id], %w[id_convert_to_bigint job_id_convert_to_bigint]]
+ )
+
+ swap
+ end
+
+ def down
+ swap
+ end
+
+ private
+
+ def swap
+ # This is to replace the existing "index_ci_job_artifacts_on_expire_at_and_job_id" btree (expire_at, job_id)
+ add_concurrent_index TABLE_NAME, [:expire_at, :job_id_convert_to_bigint], name: 'index_ci_job_artifacts_on_expire_at_and_job_id_bigint'
+ # This is to replace the existing "index_ci_job_artifacts_on_job_id_and_file_type" btree (job_id, file_type)
+ add_concurrent_index TABLE_NAME, [:job_id_convert_to_bigint, :file_type], name: 'index_ci_job_artifacts_on_job_id_and_file_type_bigint', unique: true
+
+ # # Add a FK on `job_id_convert_to_bigint` to `ci_builds(id)`, the old FK (fk_rails_c5137cb2c1)
+ # # is removed below since it won't be dropped automatically.
+ fk_ci_builds_job_id = concurrent_foreign_key_name(TABLE_NAME, :job_id, prefix: 'fk_rails_')
+ fk_ci_builds_job_id_tmp = "#{fk_ci_builds_job_id}_tmp"
+
+ add_concurrent_foreign_key TABLE_NAME, :ci_builds,
+ column: :job_id_convert_to_bigint,
+ name: fk_ci_builds_job_id_tmp,
+ on_delete: :cascade,
+ reverse_lock_order: true
+
+ with_lock_retries(raise_on_exhaustion: true) do
+ # We'll need ACCESS EXCLUSIVE lock on the related tables,
+ # lets make sure it can be acquired from the start
+
+ execute "LOCK TABLE ci_builds, #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE"
+
+ temp_name = 'job_id_tmp'
+ execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:job_id)} TO #{quote_column_name(temp_name)}"
+ execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:job_id_convert_to_bigint)} TO #{quote_column_name(:job_id)}"
+ execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(temp_name)} TO #{quote_column_name(:job_id_convert_to_bigint)}"
+
+ # We need to update the trigger function in order to make PostgreSQL to
+ # regenerate the execution plan for it. This is to avoid type mismatch errors like
+ # "type of parameter 15 (bigint) does not match that when preparing the plan (integer)"
+ function_name = Gitlab::Database::UnidirectionalCopyTrigger.on_table(TABLE_NAME).name([:id, :job_id], [:id_convert_to_bigint, :job_id_convert_to_bigint])
+ execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL"
+
+ # Swap defaults
+ change_column_default TABLE_NAME, :job_id, nil
+ change_column_default TABLE_NAME, :job_id_convert_to_bigint, 0
+
+ # Rename the rest of the indexes (we already hold an exclusive lock, so no need to use DROP INDEX CONCURRENTLY here
+ execute 'DROP INDEX index_ci_job_artifacts_on_expire_at_and_job_id'
+ rename_index TABLE_NAME, 'index_ci_job_artifacts_on_expire_at_and_job_id_bigint', 'index_ci_job_artifacts_on_expire_at_and_job_id'
+ execute 'DROP INDEX index_ci_job_artifacts_on_job_id_and_file_type'
+ rename_index TABLE_NAME, 'index_ci_job_artifacts_on_job_id_and_file_type_bigint', 'index_ci_job_artifacts_on_job_id_and_file_type'
+
+ # Drop original FK on the old int4 `job_id` (fk_rails_c5137cb2c1)
+ remove_foreign_key TABLE_NAME, name: fk_ci_builds_job_id
+
+ # We swapped the columns but the FK for job_id is still using the temporary name for the job_id_convert_to_bigint column
+ # So we have to also swap the FK name now that we dropped the other one with the same
+ rename_constraint(TABLE_NAME, fk_ci_builds_job_id_tmp, fk_ci_builds_job_id)
+ end
+ end
+end
diff --git a/db/schema_migrations/20210726134950 b/db/schema_migrations/20210726134950
new file mode 100644
index 00000000000..73f298e04a7
--- /dev/null
+++ b/db/schema_migrations/20210726134950
@@ -0,0 +1 @@
+d989534193566d90f1d4d61a0a588f3204670b67e049e875011a06b32ffd941a
\ No newline at end of file
diff --git a/db/schema_migrations/20210728110654 b/db/schema_migrations/20210728110654
new file mode 100644
index 00000000000..3dd51a29bb7
--- /dev/null
+++ b/db/schema_migrations/20210728110654
@@ -0,0 +1 @@
+8c317e202b9fb5fc3733325fd2447f65283c3752fcb314033f5d3b2b28484f71
\ No newline at end of file
diff --git a/db/schema_migrations/20210809143931 b/db/schema_migrations/20210809143931
new file mode 100644
index 00000000000..294c62d54d8
--- /dev/null
+++ b/db/schema_migrations/20210809143931
@@ -0,0 +1 @@
+37cac2c3c5c5c22a34e0a77733c5330a32101090ac47b46260123c3362a9e36f
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index cffe2d249d0..870277556bb 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10787,7 +10787,7 @@ ALTER SEQUENCE ci_instance_variables_id_seq OWNED BY ci_instance_variables.id;
CREATE TABLE ci_job_artifacts (
id integer NOT NULL,
project_id integer NOT NULL,
- job_id integer NOT NULL,
+ job_id_convert_to_bigint integer DEFAULT 0 NOT NULL,
file_type integer NOT NULL,
size bigint,
created_at timestamp with time zone NOT NULL,
@@ -10799,7 +10799,7 @@ CREATE TABLE ci_job_artifacts (
file_format smallint,
file_location smallint,
id_convert_to_bigint bigint DEFAULT 0 NOT NULL,
- job_id_convert_to_bigint bigint DEFAULT 0 NOT NULL,
+ job_id bigint NOT NULL,
CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL))
);
@@ -12922,6 +12922,7 @@ CREATE TABLE error_tracking_errors (
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
events_count bigint DEFAULT 0 NOT NULL,
+ status smallint DEFAULT 0 NOT NULL,
CONSTRAINT check_18a758e537 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_b5cb4d3888 CHECK ((char_length(actor) <= 255)),
CONSTRAINT check_c739788b12 CHECK ((char_length(description) <= 1024)),
@@ -17005,7 +17006,8 @@ CREATE TABLE project_error_tracking_settings (
encrypted_token character varying,
encrypted_token_iv character varying,
project_name character varying,
- organization_name character varying
+ organization_name character varying,
+ integrated boolean DEFAULT false NOT NULL
);
CREATE TABLE project_export_jobs (
diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md
index bc958a27c90..4b87f1c28f1 100644
--- a/doc/development/elasticsearch.md
+++ b/doc/development/elasticsearch.md
@@ -135,10 +135,10 @@ This is not applicable yet as multiple indices functionality is not fully implem
Currently GitLab can only handle a single version of setting. Any setting/schema changes would require reindexing everything from scratch. Since reindexing can take a long time, this can cause search functionality downtime.
To avoid downtime, GitLab is working to support multiple indices that
-can function at the same time. Whenever the schema changes, the admin
+can function at the same time. Whenever the schema changes, the administrator
will be able to create a new index and reindex to it, while searches
continue to go to the older, stable index. Any data updates will be
-forwarded to both indices. Once the new index is ready, an admin can
+forwarded to both indices. Once the new index is ready, an administrator can
mark it active, which will direct all searches to it, and remove the old
index.
diff --git a/doc/development/geo.md b/doc/development/geo.md
index 38245e5f4e5..7d5aae49749 100644
--- a/doc/development/geo.md
+++ b/doc/development/geo.md
@@ -96,7 +96,7 @@ projects that need updating. Those projects can be:
- Updated recently: Projects that have a `last_repository_updated_at`
timestamp that is more recent than the `last_repository_successful_sync_at`
timestamp in the `Geo::ProjectRegistry` model.
-- Manual: The admin can manually flag a repository to resync in the
+- Manual: The administrator can manually flag a repository to resync in the
[Geo admin panel](../user/admin_area/geo_nodes.md).
When we fail to fetch a repository on the secondary `RETRIES_BEFORE_REDOWNLOAD`
diff --git a/doc/development/snowplow/index.md b/doc/development/snowplow/index.md
index 552249344c7..59361e5206c 100644
--- a/doc/development/snowplow/index.md
+++ b/doc/development/snowplow/index.md
@@ -279,7 +279,8 @@ export default {
```
The event data can be provided with a `tracking` object, declared in the `data` function,
-or as a `computed property`.
+or as a `computed property`. A `tracking` object is convenient when the default
+event properties are dynamic or provided at runtime.
```javascript
export default {
@@ -292,6 +293,7 @@ export default {
// category: '',
// property: '',
// value: '',
+ // experiment: '',
// extra: {},
},
};
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 5abc4bd3122..ca024f685f1 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -277,7 +277,7 @@ On the Route 53 dashboard, click **Hosted zones** in the left navigation bar:
1. Click **Create**.
1. If you registered your domain through Route 53, you're done. If you used a different domain registrar, you need to update your DNS records with your domain registrar. You'll need to:
1. Click on **Hosted zones** and select the domain you added above.
- 1. You'll see a list of `NS` records. From your domain registrar's admin panel, add each of these as `NS` records to your domain's DNS records. These steps may vary between domain registrars. If you're stuck, Google **"name of your registrar" add DNS records** and you should find a help article specific to your domain registrar.
+ 1. You'll see a list of `NS` records. From your domain registrar's administrator panel, add each of these as `NS` records to your domain's DNS records. These steps may vary between domain registrars. If you're stuck, Google **"name of your registrar" add DNS records** and you should find a help article specific to your domain registrar.
The steps for doing this vary depending on which registrar you use and is beyond the scope of this guide.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 66a961cefb8..a627f04fa46 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -117,6 +117,7 @@ SSO has the following effects when enabled:
even if the project is forked.
- For a Git activity, users must be signed-in through SSO before they can push to or
pull from a GitLab repository.
+- Users must be signed-in through SSO before they can pull images using the [Dependency Proxy](../../packages/dependency_proxy/index.md).
## Providers
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index dd7ad7d4f8d..cd504851b1f 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -68,11 +68,6 @@ The requirement to authenticate is a breaking change added in 13.7. An [administ
disable it](../../../administration/packages/dependency_proxy.md#disabling-authentication) if it
has disrupted your existing Dependency Proxy usage.
-WARNING:
-If [SSO enforcement](../../group/saml_sso/index.md#sso-enforcement)
-is enabled for your Group, requests to the dependency proxy will fail. This bug is being tracked in
-[this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/294018).
-
Because the Dependency Proxy is storing Docker images in a space associated with your group,
you must authenticate against the Dependency Proxy.
@@ -91,6 +86,12 @@ You can authenticate using:
- A [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `read_registry` and `write_registry`.
- A [group deploy token](../../../user/project/deploy_tokens/index.md#group-deploy-token) with the scope set to `read_registry` and `write_registry`.
+#### SAML SSO
+
+When [SSO enforcement](../../group/saml_sso/index.md#sso-enforcement)
+is enabled, users must be signed-in through SSO before they can pull images through the Dependency
+Proxy.
+
#### Authenticate within CI/CD
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280582) in GitLab 13.7.
diff --git a/doc/user/project/import/clearcase.md b/doc/user/project/import/clearcase.md
index 27a84476590..120c64e00f2 100644
--- a/doc/user/project/import/clearcase.md
+++ b/doc/user/project/import/clearcase.md
@@ -31,7 +31,7 @@ _Taken from the slides [ClearCase and the journey to Git](https://docplayer.net/
## Why migrate
-ClearCase can be difficult to manage both from a user and an admin perspective.
+ClearCase can be difficult to manage both from a user and an administrator perspective.
Migrating to Git/GitLab there is:
- **No licensing costs**, Git is GPL while ClearCase is proprietary.
diff --git a/doc/user/report_abuse.md b/doc/user/report_abuse.md
index 2b585315326..a8107d77113 100644
--- a/doc/user/report_abuse.md
+++ b/doc/user/report_abuse.md
@@ -64,5 +64,5 @@ in the abuse report's **Message** field.
## Managing abuse reports
-Admins are able to view and resolve abuse reports.
+Administrators are able to view and resolve abuse reports.
For more information, see [abuse reports administration documentation](admin_area/review_abuse_reports.md).
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index f2ae23bd6f0..92d01e6a43e 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -306,10 +306,10 @@ GitLab instance.
## Search settings
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292941) in GitLab 13.8 behind a feature flag, disabled by default.
-> - [Added to Group, Admin, and User settings](https://gitlab.com/groups/gitlab-org/-/epics/4842) in GitLab 13.9.
+> - [Added to Group, Administrator, and User settings](https://gitlab.com/groups/gitlab-org/-/epics/4842) in GitLab 13.9.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/294025) in GitLab 13.11.
-You can search inside a Project, Group, Admin, or User's settings by entering
+You can search inside a Project, Group, Administrator, or User's settings by entering
a search term in the search box located at the top of the page. The search results
appear highlighted in the sections that match the search term.
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 24d84ac36bf..305ffc6ab96 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -17,13 +17,13 @@ module Gitlab
Config::Yaml::Tags::TagError
].freeze
- attr_reader :root, :context, :ref, :source
+ attr_reader :root, :context, :source_ref_path, :source
- def initialize(config, project: nil, sha: nil, user: nil, parent_pipeline: nil, ref: nil, source: nil)
- @context = build_context(project: project, sha: sha, user: user, parent_pipeline: parent_pipeline)
+ def initialize(config, project: nil, sha: nil, user: nil, parent_pipeline: nil, source_ref_path: nil, source: nil)
+ @context = build_context(project: project, sha: sha, user: user, parent_pipeline: parent_pipeline, ref: source_ref_path)
@context.set_deadline(TIMEOUT_SECONDS)
- @ref = ref
+ @source_ref_path = source_ref_path
@source = source
@config = expand_config(config)
@@ -108,13 +108,13 @@ module Gitlab
end
end
- def build_context(project:, sha:, user:, parent_pipeline:)
+ def build_context(project:, sha:, user:, parent_pipeline:, ref:)
Config::External::Context.new(
project: project,
sha: sha || find_sha(project),
user: user,
parent_pipeline: parent_pipeline,
- variables: build_variables(project: project, ref: sha))
+ variables: build_variables(project: project, ref: ref))
end
def build_variables(project:, ref:)
diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb
index 49ec1250a5f..5251dd3d40a 100644
--- a/lib/gitlab/ci/pipeline/chain/config/process.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/process.rb
@@ -14,7 +14,7 @@ module Gitlab
result = ::Gitlab::Ci::YamlProcessor.new(
@command.config_content, {
project: project,
- ref: @pipeline.ref,
+ source_ref_path: @pipeline.source_ref_path,
sha: @pipeline.sha,
source: @pipeline.source,
user: current_user,
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index dcefa5234a8..5633194a8f8 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -294,6 +294,7 @@ excluded_attributes:
- :encrypted_token
- :encrypted_token_iv
- :enabled
+ - :integrated
service_desk_setting:
- :outgoing_name
priorities:
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index e52023c4612..2ed87f30b96 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -8,7 +8,8 @@ module Gitlab
@project = project
@repository_ref = repository_ref.presence
- super(current_user, query, [project], order_by: order_by, sort: sort, filters: filters)
+ # use the default filter for project searches since we are already limiting by a single project
+ super(current_user, query, [project], order_by: order_by, sort: sort, filters: filters, default_project_filter: true)
end
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index e6851af8264..90513e346f2 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -168,7 +168,7 @@ module Gitlab
issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute
unless default_project_filter
- issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord
+ issues = issues.in_projects(project_ids_relation)
end
apply_sort(issues, scope: 'issues')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index eedf84beaf1..683f4331dbe 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10438,6 +10438,9 @@ msgstr ""
msgid "DastSiteValidation|Copy HTTP header to clipboard"
msgstr ""
+msgid "DastSiteValidation|Copy Meta tag to clipboard"
+msgstr ""
+
msgid "DastSiteValidation|Could not create validation token. Please try again."
msgstr ""
@@ -10450,6 +10453,9 @@ msgstr ""
msgid "DastSiteValidation|Header validation"
msgstr ""
+msgid "DastSiteValidation|Meta tag validation"
+msgstr ""
+
msgid "DastSiteValidation|Retry validation"
msgstr ""
@@ -10462,12 +10468,18 @@ msgstr ""
msgid "DastSiteValidation|Step 2 - Add following HTTP header to your site"
msgstr ""
+msgid "DastSiteValidation|Step 2 - Add following meta tag to your site"
+msgstr ""
+
msgid "DastSiteValidation|Step 2 - Add following text to the target site"
msgstr ""
msgid "DastSiteValidation|Step 3 - Confirm header location and validate"
msgstr ""
+msgid "DastSiteValidation|Step 3 - Confirm meta tag location and validate"
+msgstr ""
+
msgid "DastSiteValidation|Step 3 - Confirm text file location and validate"
msgstr ""
@@ -15892,7 +15904,7 @@ msgstr ""
msgid "GroupSAML|Enable SAML authentication for this group"
msgstr ""
-msgid "GroupSAML|Enforce SSO-only authentication for Git activity for this group"
+msgid "GroupSAML|Enforce SSO-only authentication for Git and Dependency Proxy activity for this group"
msgstr ""
msgid "GroupSAML|Enforce SSO-only authentication for web activity for this group"
@@ -30587,9 +30599,6 @@ msgstr ""
msgid "Show latest version"
msgstr ""
-msgid "Show links anyways"
-msgstr ""
-
msgid "Show list"
msgstr ""
@@ -33930,9 +33939,6 @@ msgstr ""
msgid "This field is required."
msgstr ""
-msgid "This graph has a large number of jobs and showing the links between them may have performance implications."
-msgstr ""
-
msgid "This group"
msgstr ""
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
index dc25d75ee54..7415c2860c8 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -147,25 +147,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
subject { get_manifest }
- shared_examples 'a successful manifest pull' do
- it 'sends a file' do
- expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
-
- subject
- end
-
- it 'returns Content-Disposition: attachment', :aggregate_failures do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
- expect(response.headers['Content-Length']).to eq(manifest.size)
- expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
- expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
- expect(response.headers['Content-Disposition']).to match(/^attachment/)
- end
- end
-
context 'feature enabled' do
before do
enable_dependency_proxy
@@ -272,21 +253,6 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
end
- shared_examples 'a successful blob pull' do
- it 'sends a file' do
- expect(controller).to receive(:send_file).with(blob.file.path, {})
-
- subject
- end
-
- it 'returns Content-Disposition: attachment', :aggregate_failures do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['Content-Disposition']).to match(/^attachment/)
- end
- end
-
subject { get_blob }
context 'feature enabled' do
diff --git a/spec/factories/error_tracking/error.rb b/spec/factories/error_tracking/error.rb
index cd2df351abe..bebdffb3614 100644
--- a/spec/factories/error_tracking/error.rb
+++ b/spec/factories/error_tracking/error.rb
@@ -35,5 +35,10 @@ FactoryBot.define do
platform { 'ruby' }
first_seen_at { Time.now.iso8601 }
last_seen_at { Time.now.iso8601 }
+ status { 'unresolved' }
+
+ trait :resolved do
+ status { 'resolved' }
+ end
end
end
diff --git a/spec/features/projects/tags/user_edits_tags_spec.rb b/spec/features/projects/tags/user_edits_tags_spec.rb
index 7a8a685f3d9..9f66b7274e8 100644
--- a/spec/features/projects/tags/user_edits_tags_spec.rb
+++ b/spec/features/projects/tags/user_edits_tags_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe 'Project > Tags', :js do
note_textarea = page.find('.js-gfm-input')
# Click on Bold button
- page.find('.md-header-toolbar button.toolbar-btn:first-child').click
+ page.find('.md-header-toolbar button:first-child').click
expect(note_textarea.value).to eq('****')
end
diff --git a/spec/finders/error_tracking/errors_finder_spec.rb b/spec/finders/error_tracking/errors_finder_spec.rb
new file mode 100644
index 00000000000..2df5f1653e0
--- /dev/null
+++ b/spec/finders/error_tracking/errors_finder_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ErrorTracking::ErrorsFinder do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { project.creator }
+ let_it_be(:error) { create(:error_tracking_error, project: project) }
+ let_it_be(:error_resolved) { create(:error_tracking_error, :resolved, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe '#execute' do
+ let(:params) { {} }
+
+ subject { described_class.new(user, project, params).execute }
+
+ it { is_expected.to contain_exactly(error, error_resolved) }
+
+ context 'with status parameter' do
+ let(:params) { { status: 'resolved' } }
+
+ it { is_expected.to contain_exactly(error_resolved) }
+ end
+ end
+end
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 30914ba99a5..1fba3823161 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -4,8 +4,8 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
+import { calculatePipelineLayersInfo } from '~/pipelines/components/graph/utils';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
-import { listByLayers } from '~/pipelines/components/parsing_utils';
import {
generateResponse,
mockPipelineResponse,
@@ -150,7 +150,7 @@ describe('graph component', () => {
},
props: {
viewType: LAYER_VIEW,
- pipelineLayers: listByLayers(defaultProps.pipeline),
+ computedPipelineInfo: calculatePipelineLayersInfo(defaultProps.pipeline, 'layer', ''),
},
});
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index ce507c2413e..1cf5f67f9a9 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -1,11 +1,19 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
+import axios from '~/lib/utils/axios_utils';
+import {
+ PIPELINES_DETAIL_LINK_DURATION,
+ PIPELINES_DETAIL_LINKS_TOTAL,
+ PIPELINES_DETAIL_LINKS_JOB_RATIO,
+} from '~/performance/constants';
+import * as perfUtils from '~/performance/utils';
import {
IID_FAILURE,
LAYER_VIEW,
@@ -16,9 +24,11 @@ import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
+import * as Api from '~/pipelines/components/graph_shared/api';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
import * as parsingUtils from '~/pipelines/components/parsing_utils';
import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
+import * as sentryUtils from '~/pipelines/utils';
import { mockRunningPipelineHeaderData } from '../mock_data';
import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data';
@@ -480,4 +490,112 @@ describe('Pipeline graph wrapper', () => {
});
});
});
+
+ describe('performance metrics', () => {
+ const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json';
+ let markAndMeasure;
+ let reportToSentry;
+ let reportPerformance;
+ let mock;
+
+ beforeEach(() => {
+ jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
+ markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure');
+ reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry');
+ reportPerformance = jest.spyOn(Api, 'reportPerformance');
+ });
+
+ describe('with no metrics path', () => {
+ beforeEach(async () => {
+ createComponentWithApollo();
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('is not called', () => {
+ expect(markAndMeasure).not.toHaveBeenCalled();
+ expect(reportToSentry).not.toHaveBeenCalled();
+ expect(reportPerformance).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with metrics path', () => {
+ const duration = 875;
+ const numLinks = 7;
+ const totalGroups = 8;
+ const metricsData = {
+ histograms: [
+ { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
+ { name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
+ {
+ name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
+ value: numLinks / totalGroups,
+ },
+ ],
+ };
+
+ describe('when no duration is obtained', () => {
+ beforeEach(async () => {
+ jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
+ return [];
+ });
+
+ createComponentWithApollo({
+ provide: {
+ metricsPath,
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ data: {
+ currentViewType: LAYER_VIEW,
+ },
+ });
+
+ jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+ });
+
+ it('attempts to collect metrics', () => {
+ expect(markAndMeasure).toHaveBeenCalled();
+ expect(reportPerformance).not.toHaveBeenCalled();
+ expect(reportToSentry).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with duration and no error', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onPost(metricsPath).reply(200, {});
+
+ jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
+ return [{ duration }];
+ });
+
+ createComponentWithApollo({
+ provide: {
+ metricsPath,
+ glFeatures: {
+ pipelineGraphLayersView: true,
+ },
+ },
+ data: {
+ currentViewType: LAYER_VIEW,
+ },
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('it calls reportPerformance with expected arguments', () => {
+ expect(markAndMeasure).toHaveBeenCalled();
+ expect(reportPerformance).toHaveBeenCalled();
+ expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData);
+ expect(reportToSentry).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
index 8f39c8c2405..be422fac92c 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -31,7 +31,7 @@ describe('Links Inner component', () => {
propsData: {
...defaultProps,
...props,
- parsedData: parseData(currentPipelineData.flatMap(({ groups }) => groups)),
+ linksData: parseData(currentPipelineData.flatMap(({ groups }) => groups)).links,
},
});
};
diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index 932a19f2f00..44ab60cbee7 100644
--- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
@@ -1,16 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import {
- PIPELINES_DETAIL_LINK_DURATION,
- PIPELINES_DETAIL_LINKS_TOTAL,
- PIPELINES_DETAIL_LINKS_JOB_RATIO,
-} from '~/performance/constants';
-import * as perfUtils from '~/performance/utils';
-import * as Api from '~/pipelines/components/graph_shared/api';
import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
-import * as sentryUtils from '~/pipelines/utils';
import { generateResponse, mockPipelineResponse } from '../graph/mock_data';
describe('links layer component', () => {
@@ -94,139 +84,4 @@ describe('links layer component', () => {
expect(findLinksInner().exists()).toBe(false);
});
});
-
- describe('performance metrics', () => {
- const metricsPath = '/root/project/-/ci/prometheus_metrics/histograms.json';
- let markAndMeasure;
- let reportToSentry;
- let reportPerformance;
- let mock;
-
- beforeEach(() => {
- jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => cb());
- markAndMeasure = jest.spyOn(perfUtils, 'performanceMarkAndMeasure');
- reportToSentry = jest.spyOn(sentryUtils, 'reportToSentry');
- reportPerformance = jest.spyOn(Api, 'reportPerformance');
- });
-
- describe('with no metrics config object', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('is not called', () => {
- expect(markAndMeasure).not.toHaveBeenCalled();
- expect(reportToSentry).not.toHaveBeenCalled();
- expect(reportPerformance).not.toHaveBeenCalled();
- });
- });
-
- describe('with metrics config set to false', () => {
- beforeEach(() => {
- createComponent({
- props: {
- metricsConfig: {
- collectMetrics: false,
- metricsPath: '/path/to/metrics',
- },
- },
- });
- });
-
- it('is not called', () => {
- expect(markAndMeasure).not.toHaveBeenCalled();
- expect(reportToSentry).not.toHaveBeenCalled();
- expect(reportPerformance).not.toHaveBeenCalled();
- });
- });
-
- describe('with no metrics path', () => {
- beforeEach(() => {
- createComponent({
- props: {
- metricsConfig: {
- collectMetrics: true,
- metricsPath: '',
- },
- },
- });
- });
-
- it('is not called', () => {
- expect(markAndMeasure).not.toHaveBeenCalled();
- expect(reportToSentry).not.toHaveBeenCalled();
- expect(reportPerformance).not.toHaveBeenCalled();
- });
- });
-
- describe('with metrics path and collect set to true', () => {
- const duration = 875;
- const numLinks = 7;
- const totalGroups = 8;
- const metricsData = {
- histograms: [
- { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
- { name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
- {
- name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
- value: numLinks / totalGroups,
- },
- ],
- };
-
- describe('when no duration is obtained', () => {
- beforeEach(() => {
- jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
- return [];
- });
-
- createComponent({
- props: {
- metricsConfig: {
- collectMetrics: true,
- path: metricsPath,
- },
- },
- });
- });
-
- it('attempts to collect metrics', () => {
- expect(markAndMeasure).toHaveBeenCalled();
- expect(reportPerformance).not.toHaveBeenCalled();
- expect(reportToSentry).not.toHaveBeenCalled();
- });
- });
-
- describe('with duration and no error', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onPost(metricsPath).reply(200, {});
-
- jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
- return [{ duration }];
- });
-
- createComponent({
- props: {
- metricsConfig: {
- collectMetrics: true,
- path: metricsPath,
- },
- },
- });
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('it calls reportPerformance with expected arguments', () => {
- expect(markAndMeasure).toHaveBeenCalled();
- expect(reportPerformance).toHaveBeenCalled();
- expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData);
- expect(reportToSentry).not.toHaveBeenCalled();
- });
- });
- });
- });
});
diff --git a/spec/frontend/pipelines/parsing_utils_spec.js b/spec/frontend/pipelines/parsing_utils_spec.js
index 074009ae056..3a270c1c1b5 100644
--- a/spec/frontend/pipelines/parsing_utils_spec.js
+++ b/spec/frontend/pipelines/parsing_utils_spec.js
@@ -120,8 +120,8 @@ describe('DAG visualization parsing utilities', () => {
describe('generateColumnsFromLayersList', () => {
const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo');
- const layers = listByLayers(pipeline);
- const columns = generateColumnsFromLayersListBare(pipeline, layers);
+ const { pipelineLayers } = listByLayers(pipeline);
+ const columns = generateColumnsFromLayersListBare(pipeline, pipelineLayers);
it('returns stage-like objects with default name, id, and status', () => {
columns.forEach((col, idx) => {
@@ -136,7 +136,7 @@ describe('DAG visualization parsing utilities', () => {
it('creates groups that match the list created in listByLayers', () => {
columns.forEach((col, idx) => {
const groupNames = col.groups.map(({ name }) => name);
- expect(groupNames).toEqual(layers[idx]);
+ expect(groupNames).toEqual(pipelineLayers[idx]);
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
index 786dfabb990..19e4f2d8c92 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
@@ -1,3 +1,4 @@
+import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
@@ -25,7 +26,7 @@ describe('toolbar_button', () => {
});
const getButtonShortcutsAttr = () => {
- return wrapper.find('button').attributes('data-md-shortcuts');
+ return wrapper.find(GlButton).attributes('data-md-shortcuts');
};
describe('keyboard shortcuts', () => {
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index e8c127f0444..62de4d2e96d 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -107,7 +107,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do
context 'when ref is protected' do
before do
allow(project).to receive(:protected_for?).with('master').and_return(true)
- allow(project).to receive(:protected_for?).with('b83d6e391c22777fca1ed3012fce84f633d7fed0').and_return(true)
allow(project).to receive(:protected_for?).with('refs/heads/master').and_return(true)
dependencies.map(&:perform!)
diff --git a/spec/models/error_tracking/error_event_spec.rb b/spec/models/error_tracking/error_event_spec.rb
index 331661f88cc..8e20eb25353 100644
--- a/spec/models/error_tracking/error_event_spec.rb
+++ b/spec/models/error_tracking/error_event_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ErrorTracking::ErrorEvent, type: :model do
+ let_it_be(:event) { create(:error_tracking_error_event) }
+
describe 'relationships' do
it { is_expected.to belong_to(:error) }
end
@@ -11,4 +13,33 @@ RSpec.describe ErrorTracking::ErrorEvent, type: :model do
it { is_expected.to validate_presence_of(:description) }
it { is_expected.to validate_presence_of(:occurred_at) }
end
+
+ describe '#stacktrace' do
+ it 'generates a correct stacktrace in expected format' do
+ expected_context = [
+ [132, " end\n"],
+ [133, "\n"],
+ [134, " begin\n"],
+ [135, " block.call(work, *extra)\n"],
+ [136, " rescue Exception => e\n"],
+ [137, " STDERR.puts \"Error reached top of thread-pool: #\{e.message\} (#\{e.class\})\"\n"],
+ [138, " end\n"]
+ ]
+
+ expected_entry = {
+ 'lineNo' => 135,
+ 'context' => expected_context,
+ 'filename' => 'puma/thread_pool.rb',
+ 'function' => 'block in spawn_thread',
+ 'colNo' => 0
+ }
+
+ expect(event.stacktrace).to be_kind_of(Array)
+ expect(event.stacktrace.first).to eq(expected_entry)
+ end
+ end
+
+ describe '#to_sentry_error_event' do
+ it { expect(event.to_sentry_error_event).to be_kind_of(Gitlab::ErrorTracking::ErrorEvent) }
+ end
end
diff --git a/spec/models/error_tracking/error_spec.rb b/spec/models/error_tracking/error_spec.rb
index 8591802d15c..57899985daf 100644
--- a/spec/models/error_tracking/error_spec.rb
+++ b/spec/models/error_tracking/error_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe ErrorTracking::Error, type: :model do
+ let_it_be(:error) { create(:error_tracking_error) }
+
describe 'relationships' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:events) }
@@ -13,4 +15,16 @@ RSpec.describe ErrorTracking::Error, type: :model do
it { is_expected.to validate_presence_of(:description) }
it { is_expected.to validate_presence_of(:actor) }
end
+
+ describe '#title' do
+ it { expect(error.title).to eq('ActionView::MissingTemplate Missing template posts/edit') }
+ end
+
+ describe '#to_sentry_error' do
+ it { expect(error.to_sentry_error).to be_kind_of(Gitlab::ErrorTracking::Error) }
+ end
+
+ describe '#to_sentry_detailed_error' do
+ it { expect(error.to_sentry_detailed_error).to be_kind_of(Gitlab::ErrorTracking::DetailedError) }
+ end
end
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index bafa62c9543..7be61f4950e 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -54,20 +54,22 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
valid_api_url = 'http://example.com/api/0/projects/org-slug/proj-slug/'
valid_token = 'token'
- where(:enabled, :token, :api_url, :valid?) do
- true | nil | nil | false
- true | nil | valid_api_url | false
- true | valid_token | nil | false
- true | valid_token | valid_api_url | true
- false | nil | nil | true
- false | nil | valid_api_url | true
- false | valid_token | nil | true
- false | valid_token | valid_api_url | true
+ where(:enabled, :integrated, :token, :api_url, :valid?) do
+ true | true | nil | nil | true
+ true | false | nil | nil | false
+ true | false | nil | valid_api_url | false
+ true | false | valid_token | nil | false
+ true | false | valid_token | valid_api_url | true
+ false | false | nil | nil | true
+ false | false | nil | valid_api_url | true
+ false | false | valid_token | nil | true
+ false | false | valid_token | valid_api_url | true
end
with_them do
before do
subject.enabled = enabled
+ subject.integrated = integrated
subject.token = token
subject.api_url = api_url
end
@@ -472,4 +474,25 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
expect(subject.list_sentry_issues(params)).to eq(nil)
end
end
+
+ describe '#sentry_enabled' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:enabled, :integrated, :feature_flag, :sentry_enabled) do
+ true | false | false | true
+ true | true | false | true
+ true | true | true | false
+ false | false | false | false
+ end
+
+ with_them do
+ before do
+ subject.enabled = enabled
+ subject.integrated = integrated
+ stub_feature_flags(integrated_error_tracking: feature_flag)
+ end
+
+ it { expect(subject.sentry_enabled).to eq(sentry_enabled) }
+ end
+ end
end
diff --git a/spec/services/error_tracking/issue_details_service_spec.rb b/spec/services/error_tracking/issue_details_service_spec.rb
index 2880697e2ae..8cc2688d198 100644
--- a/spec/services/error_tracking/issue_details_service_spec.rb
+++ b/spec/services/error_tracking/issue_details_service_spec.rb
@@ -39,6 +39,21 @@ RSpec.describe ErrorTracking::IssueDetailsService do
include_examples 'error tracking service data not ready', :issue_details
include_examples 'error tracking service sentry error handling', :issue_details
include_examples 'error tracking service http status handling', :issue_details
+
+ context 'integrated error tracking' do
+ let_it_be(:error) { create(:error_tracking_error, project: project) }
+
+ let(:params) { { issue_id: error.id } }
+
+ before do
+ error_tracking_setting.update!(integrated: true)
+ end
+
+ it 'returns the error in detailed format' do
+ expect(result[:status]).to eq(:success)
+ expect(result[:issue].to_json).to eq(error.to_sentry_detailed_error.to_json)
+ end
+ end
end
include_examples 'error tracking service unauthorized user'
diff --git a/spec/services/error_tracking/issue_latest_event_service_spec.rb b/spec/services/error_tracking/issue_latest_event_service_spec.rb
index 82579418c10..e914cb1241e 100644
--- a/spec/services/error_tracking/issue_latest_event_service_spec.rb
+++ b/spec/services/error_tracking/issue_latest_event_service_spec.rb
@@ -5,7 +5,9 @@ require 'spec_helper'
RSpec.describe ErrorTracking::IssueLatestEventService do
include_context 'sentry error tracking context'
- subject { described_class.new(project, user) }
+ let(:params) { {} }
+
+ subject { described_class.new(project, user, params) }
describe '#execute' do
context 'with authorized user' do
@@ -25,6 +27,22 @@ RSpec.describe ErrorTracking::IssueLatestEventService do
include_examples 'error tracking service data not ready', :issue_latest_event
include_examples 'error tracking service sentry error handling', :issue_latest_event
include_examples 'error tracking service http status handling', :issue_latest_event
+
+ context 'integrated error tracking' do
+ let_it_be(:error) { create(:error_tracking_error, project: project) }
+ let_it_be(:event) { create(:error_tracking_error_event, error: error) }
+
+ let(:params) { { issue_id: error.id } }
+
+ before do
+ error_tracking_setting.update!(integrated: true)
+ end
+
+ it 'returns the latest event in expected format' do
+ expect(result[:status]).to eq(:success)
+ expect(result[:latest_event].to_json).to eq(event.to_sentry_error_event.to_json)
+ end
+ end
end
include_examples 'error tracking service unauthorized user'
diff --git a/spec/services/error_tracking/issue_update_service_spec.rb b/spec/services/error_tracking/issue_update_service_spec.rb
index 9ed24038ed8..31a66654100 100644
--- a/spec/services/error_tracking/issue_update_service_spec.rb
+++ b/spec/services/error_tracking/issue_update_service_spec.rb
@@ -114,6 +114,21 @@ RSpec.describe ErrorTracking::IssueUpdateService do
end
include_examples 'error tracking service sentry error handling', :update_issue
+
+ context 'integrated error tracking' do
+ let(:error) { create(:error_tracking_error, project: project) }
+ let(:arguments) { { issue_id: error.id, status: 'resolved' } }
+ let(:update_issue_response) { { updated: true, status: :success, closed_issue_iid: nil } }
+
+ before do
+ error_tracking_setting.update!(integrated: true)
+ end
+
+ it 'resolves the error and responds with expected format' do
+ expect(update_service.execute).to eq(update_issue_response)
+ expect(error.reload.status).to eq('resolved')
+ end
+ end
end
include_examples 'error tracking service unauthorized user'
diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb
index 518f2a80826..b49095ab8b9 100644
--- a/spec/services/error_tracking/list_issues_service_spec.rb
+++ b/spec/services/error_tracking/list_issues_service_spec.rb
@@ -52,6 +52,20 @@ RSpec.describe ErrorTracking::ListIssuesService do
include_examples 'error tracking service unauthorized user'
include_examples 'error tracking service disabled'
+
+ context 'integrated error tracking' do
+ let_it_be(:error) { create(:error_tracking_error, project: project) }
+
+ before do
+ error_tracking_setting.update!(integrated: true)
+ end
+
+ it 'returns the error in expected format' do
+ expect(result[:status]).to eq(:success)
+ expect(result[:issues].size).to eq(1)
+ expect(result[:issues].first.to_json).to eq(error.to_sentry_error.to_json)
+ end
+ end
end
describe '#external_url' do
diff --git a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
new file mode 100644
index 00000000000..d29c677a962
--- /dev/null
+++ b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a successful blob pull' do
+ it 'sends a file' do
+ expect(controller).to receive(:send_file).with(blob.file.path, {})
+
+ subject
+ end
+
+ it 'returns Content-Disposition: attachment', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Content-Disposition']).to match(/^attachment/)
+ end
+end
+
+RSpec.shared_examples 'a successful manifest pull' do
+ it 'sends a file' do
+ expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
+
+ subject
+ end
+
+ it 'returns Content-Disposition: attachment', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
+ expect(response.headers['Content-Length']).to eq(manifest.size)
+ expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
+ expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
+ expect(response.headers['Content-Disposition']).to match(/^attachment/)
+ end
+end
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
index 56154c7cd03..8212f14d6be 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
@@ -23,6 +23,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
end
click_on_protect
+ wait_for_requests
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])