Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-11 21:10:33 +00:00
parent b54cbe2c73
commit 63fd08e6b4
69 changed files with 1116 additions and 538 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,20 +1,16 @@
<script>
import { isEmpty } from 'lodash';
import { __ } from '~/locale';
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 { memoize } from 'lodash';
import { reportToSentry } from '../../utils';
import { parseData } from '../parsing_utils';
import { reportPerformance } from './api';
import LinksInner from './links_inner.vue';
const parseForLinksBare = (pipeline) => {
const arrayOfJobs = pipeline.flatMap(({ groups }) => groups);
return parseData(arrayOfJobs).links;
};
const parseForLinks = memoize(parseForLinksBare);
export default {
name: 'LinksLayer',
components: {
@ -29,10 +25,10 @@ export default {
type: Array,
required: true,
},
metricsConfig: {
type: Object,
linksData: {
type: Array,
required: false,
default: () => ({}),
default: () => [],
},
showLinks: {
type: Boolean,
@ -40,30 +36,16 @@ export default {
default: true,
},
},
data() {
return {
alertDismissed: false,
parsedData: {},
showLinksOverride: false,
};
},
i18n: {
showLinksAnyways: __('Show links anyways'),
tooManyJobs: __(
'This graph has a large number of jobs and showing the links between them may have performance implications.',
),
},
computed: {
containerZero() {
return !this.containerMeasurements.width || !this.containerMeasurements.height;
},
numGroups() {
return this.pipelineData.reduce((acc, { groups }) => {
return acc + Number(groups.length);
}, 0);
},
shouldCollectMetrics() {
return this.metricsConfig.collectMetrics && this.metricsConfig.path;
getLinksData() {
if (this.linksData.length > 0) {
return this.linksData;
}
return parseForLinks(this.pipelineData);
},
showLinkedLayers() {
return this.showLinks && !this.containerZero;
@ -72,77 +54,14 @@ export default {
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
},
mounted() {
if (!isEmpty(this.pipelineData)) {
window.requestAnimationFrame(() => {
this.prepareLinkData();
});
}
},
methods: {
beginPerfMeasure() {
if (this.shouldCollectMetrics) {
performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
}
},
finishPerfMeasureAndSend(numLinks) {
if (this.shouldCollectMetrics) {
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 / this.numGroups,
},
],
};
reportPerformance(this.metricsConfig.path, data);
});
},
prepareLinkData() {
this.beginPerfMeasure();
let numLinks;
try {
const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups);
this.parsedData = parseData(arrayOfJobs);
numLinks = this.parsedData.links.length;
} catch (err) {
reportToSentry(this.$options.name, err);
}
this.finishPerfMeasureAndSend(numLinks);
},
},
};
</script>
<template>
<links-inner
v-if="showLinkedLayers"
:container-measurements="containerMeasurements"
:parsed-data="parsedData"
:links-data="getLinksData"
:pipeline-data="pipelineData"
:total-groups="numGroups"
v-bind="$attrs"
v-on="$listeners"
>

View file

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

View file

@ -1,5 +1,5 @@
<script>
import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
import $ from 'jquery';
import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings';
import { getSelectedFragment } from '~/lib/utils/common_utils';
@ -10,7 +10,6 @@ import ToolbarButton from './toolbar_button.vue';
export default {
components: {
ToolbarButton,
GlIcon,
GlPopover,
GlButton,
},
@ -46,6 +45,7 @@ export default {
data() {
return {
tag: '> ',
suggestPopoverVisible: false,
};
},
computed: {
@ -76,15 +76,27 @@ export default {
return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
},
},
watch: {
showSuggestPopover() {
this.updateSuggestPopoverVisibility();
},
},
mounted() {
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
this.updateSuggestPopoverVisibility();
},
beforeDestroy() {
$(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
methods: {
async updateSuggestPopoverVisibility() {
await this.$nextTick();
this.suggestPopoverVisible = this.showSuggestPopover && this.canSuggest;
},
isValid(form) {
return (
!form ||
@ -153,127 +165,114 @@ export default {
</button>
</li>
<li :class="{ active: !previewMarkdown }" class="md-header-toolbar">
<div class="d-inline-block">
<toolbar-button
tag="**"
:button-title="
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
"
:shortcuts="$options.shortcuts.bold"
icon="bold"
/>
<toolbar-button
tag="_"
:button-title="
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
"
:shortcuts="$options.shortcuts.italic"
icon="italic"
/>
<toolbar-button
tag="**"
:button-title="
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
"
:shortcuts="$options.shortcuts.bold"
icon="bold"
/>
<toolbar-button
tag="_"
:button-title="
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
"
:shortcuts="$options.shortcuts.italic"
icon="italic"
/>
<toolbar-button
:prepend="true"
:tag="tag"
:button-title="__('Insert a quote')"
icon="quote"
@click="handleQuote"
/>
<template v-if="canSuggest">
<toolbar-button
ref="suggestButton"
:tag="mdSuggestion"
:prepend="true"
:tag="tag"
:button-title="__('Insert a quote')"
icon="quote"
@click="handleQuote"
:button-title="__('Insert suggestion')"
:cursor-offset="4"
:tag-content="lineContent"
icon="doc-code"
data-qa-selector="suggestion_button"
class="js-suggestion-btn"
@click="handleSuggestDismissed"
/>
</div>
<div class="d-inline-block ml-md-2 ml-0">
<template v-if="canSuggest">
<toolbar-button
ref="suggestButton"
:tag="mdSuggestion"
:prepend="true"
:button-title="__('Insert suggestion')"
:cursor-offset="4"
:tag-content="lineContent"
icon="doc-code"
data-qa-selector="suggestion_button"
class="js-suggestion-btn"
@click="handleSuggestDismissed"
/>
<gl-popover
v-if="showSuggestPopover && $refs.suggestButton"
:target="$refs.suggestButton"
:css-classes="['diff-suggest-popover']"
placement="bottom"
:show="showSuggestPopover"
>
<strong>{{ __('New! Suggest changes directly') }}</strong>
<p class="mb-2">
{{
__(
'Suggest code changes which can be immediately applied in one click. Try it out!',
)
}}
</p>
<gl-button
variant="info"
category="primary"
size="sm"
@click="handleSuggestDismissed"
>
{{ __('Got it') }}
</gl-button>
</gl-popover>
</template>
<toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
<toolbar-button
tag="[{text}](url)"
tag-select="url"
:button-title="
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
"
:shortcuts="$options.shortcuts.link"
icon="link"
/>
</div>
<div class="d-inline-block ml-md-2 ml-0">
<toolbar-button
:prepend="true"
tag="- "
:button-title="__('Add a bullet list')"
icon="list-bulleted"
/>
<toolbar-button
:prepend="true"
tag="1. "
:button-title="__('Add a numbered list')"
icon="list-numbered"
/>
<toolbar-button
:prepend="true"
tag="- [ ] "
:button-title="__('Add a task list')"
icon="list-task"
/>
<toolbar-button
:tag="mdCollapsibleSection"
:prepend="true"
tag-select="Click to expand"
:button-title="__('Add a collapsible section')"
icon="details-block"
/>
<toolbar-button
:tag="mdTable"
:prepend="true"
:button-title="__('Add a table')"
icon="table"
/>
</div>
<div class="d-inline-block ml-md-2 ml-0">
<button
v-gl-tooltip
:aria-label="__('Go full screen')"
class="toolbar-btn toolbar-fullscreen-btn js-zen-enter"
data-container="body"
tabindex="-1"
:title="__('Go full screen')"
type="button"
<gl-popover
v-if="suggestPopoverVisible"
:target="$refs.suggestButton.$el"
:css-classes="['diff-suggest-popover']"
placement="bottom"
:show="suggestPopoverVisible"
>
<gl-icon name="maximize" />
</button>
</div>
<strong>{{ __('New! Suggest changes directly') }}</strong>
<p class="mb-2">
{{
__(
'Suggest code changes which can be immediately applied in one click. Try it out!',
)
}}
</p>
<gl-button
variant="info"
category="primary"
size="small"
@click="handleSuggestDismissed"
>
{{ __('Got it') }}
</gl-button>
</gl-popover>
</template>
<toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
<toolbar-button
tag="[{text}](url)"
tag-select="url"
:button-title="
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
"
:shortcuts="$options.shortcuts.link"
icon="link"
/>
<toolbar-button
:prepend="true"
tag="- "
:button-title="__('Add a bullet list')"
icon="list-bulleted"
/>
<toolbar-button
:prepend="true"
tag="1. "
:button-title="__('Add a numbered list')"
icon="list-numbered"
/>
<toolbar-button
:prepend="true"
tag="- [ ] "
:button-title="__('Add a task list')"
icon="list-task"
/>
<toolbar-button
:tag="mdCollapsibleSection"
:prepend="true"
tag-select="Click to expand"
:button-title="__('Add a collapsible section')"
icon="details-block"
/>
<toolbar-button
:tag="mdTable"
:prepend="true"
:button-title="__('Add a table')"
icon="table"
/>
<toolbar-button
class="js-zen-enter"
:prepend="true"
:button-title="__('Go full screen')"
icon="maximize"
/>
</li>
</ul>
</div>

View file

@ -1,9 +1,9 @@
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -19,7 +19,8 @@ export default {
},
tag: {
type: String,
required: true,
required: false,
default: '',
},
tagBlock: {
type: String,
@ -71,7 +72,7 @@ export default {
</script>
<template>
<button
<gl-button
v-gl-tooltip
:data-md-tag="tag"
:data-md-cursor-offset="cursorOffset"
@ -82,11 +83,11 @@ export default {
:data-md-shortcuts="shortcutsString"
:title="buttonTitle"
:aria-label="buttonTitle"
:icon="icon"
type="button"
class="toolbar-btn js-md"
category="tertiary"
class="js-md"
data-container="body"
@click="() => $emit('click')"
>
<gl-icon :name="icon" />
</button>
/>
</template>

View file

@ -1,12 +1,12 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Vue from 'vue';
import Tracking from '~/tracking';
export default {
directives: {
SafeHtml,
},
mixins: [Tracking.mixin()],
props: {
title: {
type: String,
@ -17,16 +17,6 @@ export default {
required: true,
},
},
created() {
const trackingMixin = Tracking.mixin();
const trackingInstance = new Vue({
...trackingMixin,
render() {
return null;
},
});
this.track = trackingInstance.track;
},
};
</script>
<template>

View file

@ -98,7 +98,6 @@
}
.note-action-button,
.toolbar-btn,
.dropdown-toggle-caret {
@include transition(color);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
d989534193566d90f1d4d61a0a588f3204670b67e049e875011a06b32ffd941a

View file

@ -0,0 +1 @@
8c317e202b9fb5fc3733325fd2447f65283c3752fcb314033f5d3b2b28484f71

View file

@ -0,0 +1 @@
37cac2c3c5c5c22a34e0a77733c5330a32101090ac47b46260123c3362a9e36f

View file

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

View file

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

View file

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

View file

@ -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: {},
},
};

View file

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

View file

@ -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).
<!-- Add bullet for API activity when https://gitlab.com/gitlab-org/gitlab/-/issues/9152 is complete -->
## Providers

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -294,6 +294,7 @@ excluded_attributes:
- :encrypted_token
- :encrypted_token_iv
- :enabled
- :integrated
service_desk_setting:
- :outgoing_name
priorities:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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', ''),
},
});
});

View file

@ -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();
});
});
});
});
});

View file

@ -31,7 +31,7 @@ describe('Links Inner component', () => {
propsData: {
...defaultProps,
...props,
parsedData: parseData(currentPipelineData.flatMap(({ groups }) => groups)),
linksData: parseData(currentPipelineData.flatMap(({ groups }) => groups)).links,
},
});
};

View file

@ -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();
});
});
});
});
});

View file

@ -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]);
});
});

View file

@ -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', () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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