mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-07-23 00:47:51 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -1 +1 @@
|
||||
dac25a9c19af0a168a7927784295dd12fb5c8075
|
||||
19109e7090eb6dab2b8c0c168bbef76ceca79a31
|
||||
|
@ -1 +1 @@
|
||||
8.51.0
|
||||
8.52.0
|
||||
|
3
Gemfile
3
Gemfile
@ -307,9 +307,6 @@ gem 'rack-attack', '~> 6.3.0'
|
||||
# Sentry integration
|
||||
gem 'sentry-raven', '~> 3.0'
|
||||
|
||||
# PostgreSQL query parsing
|
||||
gem 'pg_query', '~> 1.2'
|
||||
|
||||
gem 'premailer-rails', '~> 1.10.3'
|
||||
|
||||
# LabKit: Tracing and Correlation
|
||||
|
@ -828,7 +828,6 @@ GEM
|
||||
peek (1.1.0)
|
||||
railties (>= 4.0.0)
|
||||
pg (1.2.3)
|
||||
pg_query (1.2.0)
|
||||
png_quantizator (0.2.1)
|
||||
po_to_json (1.0.1)
|
||||
json (>= 1.6.0)
|
||||
@ -1425,7 +1424,6 @@ DEPENDENCIES
|
||||
parallel (~> 1.19)
|
||||
peek (~> 1.1)
|
||||
pg (~> 1.1)
|
||||
pg_query (~> 1.2)
|
||||
png_quantizator (~> 0.2.1)
|
||||
premailer-rails (~> 1.10.3)
|
||||
prometheus-client-mmap (~> 0.12.0)
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
updateImageDiffNoteOptimisticResponse,
|
||||
toDiffNoteGid,
|
||||
extractDesignNoteId,
|
||||
getPageLayoutElement,
|
||||
} from '../../utils/design_management_utils';
|
||||
import {
|
||||
updateStoreAfterAddImageDiffNote,
|
||||
@ -38,7 +39,7 @@ import {
|
||||
} from '../../utils/error_messages';
|
||||
import { trackDesignDetailView } from '../../utils/tracking';
|
||||
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
|
||||
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
|
||||
import { ACTIVE_DISCUSSION_SOURCE_TYPES, DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../../constants';
|
||||
|
||||
const DEFAULT_SCALE = 1;
|
||||
|
||||
@ -300,6 +301,22 @@ export default {
|
||||
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
|
||||
},
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
const pageEl = getPageLayoutElement();
|
||||
if (pageEl) {
|
||||
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
const pageEl = getPageLayoutElement();
|
||||
if (pageEl) {
|
||||
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
createImageDiffNoteMutation,
|
||||
DESIGNS_ROUTE_NAME,
|
||||
};
|
||||
|
@ -1,9 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import routes from './routes';
|
||||
import { DESIGN_ROUTE_NAME } from './constants';
|
||||
import { getPageLayoutElement } from '~/design_management/utils/design_management_utils';
|
||||
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
@ -13,20 +10,6 @@ export default function createRouter(base) {
|
||||
mode: 'history',
|
||||
routes,
|
||||
});
|
||||
const pageEl = getPageLayoutElement();
|
||||
|
||||
router.beforeEach(({ name }, _, next) => {
|
||||
// apply a fullscreen layout style in Design View (a.k.a design detail)
|
||||
if (pageEl) {
|
||||
if (name === DESIGN_ROUTE_NAME) {
|
||||
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
} else {
|
||||
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
|
||||
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
|
||||
import { validateParams } from '../../utils';
|
||||
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -23,7 +22,7 @@ export default {
|
||||
PipelinesFilteredSearch,
|
||||
GlIcon,
|
||||
},
|
||||
mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()],
|
||||
mixins: [pipelinesMixin, CIPaginationMixin],
|
||||
props: {
|
||||
store: {
|
||||
type: Object,
|
||||
@ -209,9 +208,6 @@ export default {
|
||||
},
|
||||
];
|
||||
},
|
||||
canFilterPipelines() {
|
||||
return this.glFeatures.filterPipelinesSearch;
|
||||
},
|
||||
validatedParams() {
|
||||
return validateParams(this.params);
|
||||
},
|
||||
@ -306,7 +302,6 @@ export default {
|
||||
</div>
|
||||
|
||||
<pipelines-filtered-search
|
||||
v-if="canFilterPipelines"
|
||||
:project-id="projectId"
|
||||
:params="validatedParams"
|
||||
@filterPipelines="filterPipelines"
|
||||
|
@ -1,8 +1,7 @@
|
||||
<script>
|
||||
import $ from 'jquery';
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import Clipboard from 'clipboard';
|
||||
import { __ } from '~/locale';
|
||||
import { uniqueId } from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -17,6 +16,11 @@ export default {
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: () => uniqueId('modal-copy-button-'),
|
||||
},
|
||||
container: {
|
||||
type: String,
|
||||
required: false,
|
||||
@ -52,7 +56,6 @@ export default {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
copySuccessText: __('Copied'),
|
||||
computed: {
|
||||
modalDomId() {
|
||||
return this.modalId ? `#${this.modalId}` : '';
|
||||
@ -68,11 +71,11 @@ export default {
|
||||
});
|
||||
this.clipboard
|
||||
.on('success', e => {
|
||||
this.updateTooltip(e.trigger);
|
||||
this.$root.$emit('bv::hide::tooltip', this.id);
|
||||
this.$emit('success', e);
|
||||
// Clear the selection and blur the trigger so it loses its border
|
||||
e.clearSelection();
|
||||
$(e.trigger).blur();
|
||||
e.trigger.blur();
|
||||
})
|
||||
.on('error', e => this.$emit('error', e));
|
||||
});
|
||||
@ -82,29 +85,11 @@ export default {
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateTooltip(target) {
|
||||
const $target = $(target);
|
||||
const originalTitle = $target.data('originalTitle');
|
||||
|
||||
if ($target.tooltip) {
|
||||
/**
|
||||
* The original tooltip will continue staying there unless we remove it by hand.
|
||||
* $target.tooltip('hide') isn't working.
|
||||
*/
|
||||
$('.tooltip').remove();
|
||||
$target.attr('title', this.$options.copySuccessText);
|
||||
$target.tooltip('_fixTitle');
|
||||
$target.tooltip('show');
|
||||
$target.attr('title', originalTitle);
|
||||
$target.tooltip('_fixTitle');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-button
|
||||
:id="id"
|
||||
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
|
||||
:class="cssClasses"
|
||||
:data-clipboard-target="target"
|
||||
|
@ -12,7 +12,6 @@ class Projects::PipelinesController < Projects::ApplicationController
|
||||
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
|
||||
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
|
||||
before_action do
|
||||
push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
|
||||
push_frontend_feature_flag(:pipelines_security_report_summary, project)
|
||||
push_frontend_feature_flag(:new_pipeline_form, project)
|
||||
|
@ -47,6 +47,8 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
|
||||
payload.transform_values do |value|
|
||||
if value.is_a?(String) || value.is_a?(Integer)
|
||||
value
|
||||
elsif value.nil?
|
||||
''
|
||||
else
|
||||
value.to_json
|
||||
end
|
||||
|
5
changelogs/unreleased/sh-upgrade-workhorse-8-52-0.yml
Normal file
5
changelogs/unreleased/sh-upgrade-workhorse-8-52-0.yml
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
title: Upgrade Workhorse to v8.52.0
|
||||
merge_request: 45778
|
||||
author:
|
||||
type: fixed
|
@ -1,7 +0,0 @@
|
||||
---
|
||||
name: filter_pipelines_search
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
@ -339,12 +339,14 @@ Feature.disable(:value_stream_analytics_create_multiple_value_streams)
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21631) in GitLab 12.6.
|
||||
> - [Chart median line removed](https://gitlab.com/gitlab-org/gitlab/-/issues/235455) in GitLab 13.4.
|
||||
|
||||
This chart visually depicts the total number of days it takes for cycles to be completed.
|
||||
This chart visually depicts the total number of days it takes for cycles to be completed. (Totals are being replaced with averages in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/262070).)
|
||||
|
||||
This chart uses the global page filters for displaying data based on the selected
|
||||
group, projects, and timeframe. In addition, specific stages can be selected
|
||||
from within the chart itself.
|
||||
|
||||
The chart data is limited to the last 500 items.
|
||||
|
||||
### Disabling chart
|
||||
|
||||
This chart is enabled by default. If you have a self-managed instance, an
|
||||
|
@ -123,7 +123,6 @@ module Gitlab
|
||||
end
|
||||
|
||||
extra = sanitize_request_parameters(extra)
|
||||
inject_sql_query_into_extra(exception, extra)
|
||||
|
||||
if sentry && Raven.configuration.server
|
||||
Raven.capture_exception(exception, tags: default_tags, extra: extra)
|
||||
@ -150,12 +149,6 @@ module Gitlab
|
||||
filter.filter(parameters)
|
||||
end
|
||||
|
||||
def inject_sql_query_into_extra(exception, extra)
|
||||
return unless exception.is_a?(ActiveRecord::StatementInvalid)
|
||||
|
||||
extra[:sql] = PgQuery.normalize(exception.sql.to_s)
|
||||
end
|
||||
|
||||
def sentry_dsn
|
||||
return unless Rails.env.production? || Rails.env.development?
|
||||
return unless Gitlab.config.sentry.enabled
|
||||
|
@ -8147,6 +8147,9 @@ msgstr ""
|
||||
msgid "CycleAnalytics|The given date range is larger than 180 days"
|
||||
msgstr ""
|
||||
|
||||
msgid "CycleAnalytics|The total time spent in the selected stage for the items that were completed on each date. Data limited to the last 500 items."
|
||||
msgstr ""
|
||||
|
||||
msgid "CycleAnalytics|Total days to completion"
|
||||
msgstr ""
|
||||
|
||||
|
@ -105,7 +105,8 @@ RSpec.describe Projects::StaticSiteEditorController do
|
||||
foo: 'bar'
|
||||
}
|
||||
},
|
||||
a_boolean: true
|
||||
a_boolean: true,
|
||||
a_nil: nil
|
||||
}
|
||||
end
|
||||
|
||||
@ -130,6 +131,10 @@ RSpec.describe Projects::StaticSiteEditorController do
|
||||
it 'serializes data values which are hashes to JSON' do
|
||||
expect(assigns_data[:a_hash]).to eq('{"a_deeper_hash":{"foo":"bar"}}')
|
||||
end
|
||||
|
||||
it 'serializes data values which are nil to an empty string' do
|
||||
expect(assigns_data[:a_nil]).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -24,7 +24,13 @@ import mockAllVersions from '../../mock_data/all_versions';
|
||||
jest.mock('~/flash');
|
||||
|
||||
const focusInput = jest.fn();
|
||||
|
||||
const mutate = jest.fn().mockResolvedValue();
|
||||
const mockPageLayoutElement = {
|
||||
classList: {
|
||||
add: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
},
|
||||
};
|
||||
const DesignReplyForm = {
|
||||
template: '<div><textarea ref="textarea"></textarea></div>',
|
||||
methods: {
|
||||
@ -37,6 +43,32 @@ const mockDesignNoDiscussions = {
|
||||
nodes: [],
|
||||
},
|
||||
};
|
||||
const newComment = 'new comment';
|
||||
const annotationCoordinates = {
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 100,
|
||||
height: 100,
|
||||
};
|
||||
const createDiscussionMutationVariables = {
|
||||
mutation: createImageDiffNoteMutation,
|
||||
update: expect.anything(),
|
||||
variables: {
|
||||
input: {
|
||||
body: newComment,
|
||||
noteableId: design.id,
|
||||
position: {
|
||||
headSha: 'headSha',
|
||||
baseSha: 'baseSha',
|
||||
startSha: 'startSha',
|
||||
paths: {
|
||||
newPath: 'full-design-path',
|
||||
},
|
||||
...annotationCoordinates,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueRouter);
|
||||
@ -45,35 +77,6 @@ describe('Design management design index page', () => {
|
||||
let wrapper;
|
||||
let router;
|
||||
|
||||
const newComment = 'new comment';
|
||||
const annotationCoordinates = {
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 100,
|
||||
height: 100,
|
||||
};
|
||||
const createDiscussionMutationVariables = {
|
||||
mutation: createImageDiffNoteMutation,
|
||||
update: expect.anything(),
|
||||
variables: {
|
||||
input: {
|
||||
body: newComment,
|
||||
noteableId: design.id,
|
||||
position: {
|
||||
headSha: 'headSha',
|
||||
baseSha: 'baseSha',
|
||||
startSha: 'startSha',
|
||||
paths: {
|
||||
newPath: 'full-design-path',
|
||||
},
|
||||
...annotationCoordinates,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mutate = jest.fn().mockResolvedValue();
|
||||
|
||||
const findDiscussionForm = () => wrapper.find(DesignReplyForm);
|
||||
const findSidebar = () => wrapper.find(DesignSidebar);
|
||||
const findDesignPresentation = () => wrapper.find(DesignPresentation);
|
||||
@ -122,19 +125,44 @@ describe('Design management design index page', () => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('when navigating', () => {
|
||||
it('applies fullscreen layout', () => {
|
||||
const mockEl = {
|
||||
classList: {
|
||||
add: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
},
|
||||
};
|
||||
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockEl);
|
||||
describe('when navigating to component', () => {
|
||||
it('applies fullscreen layout class', () => {
|
||||
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
|
||||
createComponent({ loading: true });
|
||||
|
||||
expect(mockEl.classList.add).toHaveBeenCalledTimes(1);
|
||||
expect(mockEl.classList.add).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
expect(mockPageLayoutElement.classList.add).toHaveBeenCalledTimes(1);
|
||||
expect(mockPageLayoutElement.classList.add).toHaveBeenCalledWith(
|
||||
...DESIGN_DETAIL_LAYOUT_CLASSLIST,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when navigating within the component', () => {
|
||||
it('`scale` prop of DesignPresentation component is 1', async () => {
|
||||
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
|
||||
createComponent({ loading: false }, { data: { design, scale: 2 } });
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(findDesignPresentation().props('scale')).toBe(2);
|
||||
|
||||
DesignIndex.beforeRouteUpdate.call(wrapper.vm, {}, {}, jest.fn());
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findDesignPresentation().props('scale')).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when navigating away from component', () => {
|
||||
it('removes fullscreen layout class', async () => {
|
||||
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
|
||||
createComponent({ loading: true });
|
||||
|
||||
wrapper.vm.$options.beforeRouteLeave[0].call(wrapper.vm, {}, {}, jest.fn());
|
||||
|
||||
expect(mockPageLayoutElement.classList.remove).toHaveBeenCalledTimes(1);
|
||||
expect(mockPageLayoutElement.classList.remove).toHaveBeenCalledWith(
|
||||
...DESIGN_DETAIL_LAYOUT_CLASSLIST,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -19,7 +19,6 @@ import {
|
||||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||
import createRouter from '~/design_management/router';
|
||||
import * as utils from '~/design_management/utils/design_management_utils';
|
||||
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
|
||||
import {
|
||||
designListQueryResponse,
|
||||
designUploadMutationCreatedResponse,
|
||||
@ -682,13 +681,6 @@ describe('Design management index page', () => {
|
||||
});
|
||||
|
||||
describe('when navigating', () => {
|
||||
it('ensures fullscreen layout is not applied', () => {
|
||||
createComponent({ loading: true });
|
||||
|
||||
expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(1);
|
||||
expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
|
||||
});
|
||||
|
||||
it('should trigger a scrollIntoView method if designs route is detected', () => {
|
||||
router.replace({
|
||||
path: '/designs',
|
||||
|
@ -74,7 +74,6 @@ describe('Pipelines', () => {
|
||||
|
||||
const createComponent = (props = defaultProps, methods) => {
|
||||
wrapper = mount(PipelinesComponent, {
|
||||
provide: { glFeatures: { filterPipelinesSearch: true } },
|
||||
propsData: {
|
||||
store: new Store(),
|
||||
projectId: '21',
|
||||
|
@ -1,9 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import modalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
|
||||
import { shallowMount, createWrapper } from '@vue/test-utils';
|
||||
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
|
||||
|
||||
describe('modal copy button', () => {
|
||||
const Component = Vue.extend(modalCopyButton);
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
@ -11,16 +9,18 @@ describe('modal copy button', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(Component, {
|
||||
wrapper = shallowMount(ModalCopyButton, {
|
||||
propsData: {
|
||||
text: 'copy me',
|
||||
title: 'Copy this value',
|
||||
id: 'test-id',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('clipboard', () => {
|
||||
it('should fire a `success` event on click', () => {
|
||||
const root = createWrapper(wrapper.vm.$root);
|
||||
document.execCommand = jest.fn(() => true);
|
||||
window.getSelection = jest.fn(() => ({
|
||||
toString: jest.fn(() => 'test'),
|
||||
@ -31,6 +31,7 @@ describe('modal copy button', () => {
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.emitted().success).not.toBeEmpty();
|
||||
expect(document.execCommand).toHaveBeenCalledWith('copy');
|
||||
expect(root.emitted('bv::hide::tooltip')).toEqual([['test-id']]);
|
||||
});
|
||||
});
|
||||
it("should propagate the clipboard error event if execCommand doesn't work", () => {
|
||||
|
@ -134,8 +134,8 @@ RSpec.describe Resolvers::ProjectsResolver do
|
||||
is_expected.to eq([named_project3, named_project1, named_project2])
|
||||
end
|
||||
|
||||
it 'returns projects not in order of similarity to search if flag is off' do
|
||||
is_expected.not_to eq([named_project3, named_project1, named_project2])
|
||||
it 'returns projects in any order if flag is off' do
|
||||
is_expected.to match_array([named_project3, named_project1, named_project2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -198,39 +198,47 @@ RSpec.describe Gitlab::ErrorTracking do
|
||||
end
|
||||
|
||||
describe '.track_exception' do
|
||||
let(:extra) { { issue_url: issue_url, some_other_info: 'info' } }
|
||||
|
||||
subject(:track_exception) { described_class.track_exception(exception, extra) }
|
||||
|
||||
before do
|
||||
allow(Raven).to receive(:capture_exception).and_call_original
|
||||
allow(Gitlab::ErrorTracking::Logger).to receive(:error)
|
||||
end
|
||||
|
||||
it 'calls Raven.capture_exception' do
|
||||
track_exception
|
||||
expected_extras = {
|
||||
some_other_info: 'info',
|
||||
issue_url: issue_url
|
||||
}
|
||||
|
||||
expect(Raven).to have_received(:capture_exception)
|
||||
.with(exception,
|
||||
tags: a_hash_including(correlation_id: 'cid'),
|
||||
extra: a_hash_including(some_other_info: 'info', issue_url: issue_url))
|
||||
expected_tags = {
|
||||
correlation_id: 'cid'
|
||||
}
|
||||
|
||||
expect(Raven).to receive(:capture_exception)
|
||||
.with(exception,
|
||||
tags: a_hash_including(expected_tags),
|
||||
extra: a_hash_including(expected_extras))
|
||||
|
||||
described_class.track_exception(
|
||||
exception,
|
||||
issue_url: issue_url,
|
||||
some_other_info: 'info'
|
||||
)
|
||||
end
|
||||
|
||||
it 'calls Gitlab::ErrorTracking::Logger.error with formatted payload' do
|
||||
track_exception
|
||||
expect(Gitlab::ErrorTracking::Logger).to receive(:error)
|
||||
.with(a_hash_including(*expected_payload_includes))
|
||||
|
||||
expect(Gitlab::ErrorTracking::Logger).to have_received(:error)
|
||||
.with(a_hash_including(*expected_payload_includes))
|
||||
described_class.track_exception(
|
||||
exception,
|
||||
issue_url: issue_url,
|
||||
some_other_info: 'info'
|
||||
)
|
||||
end
|
||||
|
||||
context 'with filterable parameters' do
|
||||
let(:extra) { { test: 1, my_token: 'test' } }
|
||||
|
||||
it 'filters parameters' do
|
||||
track_exception
|
||||
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
|
||||
hash_including({ 'extra.test' => 1, 'extra.my_token' => '[FILTERED]' }))
|
||||
|
||||
expect(Gitlab::ErrorTracking::Logger).to have_received(:error)
|
||||
.with(hash_including({ 'extra.test' => 1, 'extra.my_token' => '[FILTERED]' }))
|
||||
described_class.track_exception(exception, extra)
|
||||
end
|
||||
end
|
||||
|
||||
@ -239,58 +247,44 @@ RSpec.describe Gitlab::ErrorTracking do
|
||||
let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info, backtrace: caller) }
|
||||
|
||||
it 'includes the extra data from the exception in the tracking information' do
|
||||
track_exception
|
||||
expect(Raven).to receive(:capture_exception)
|
||||
.with(exception, a_hash_including(extra: a_hash_including(extra_info)))
|
||||
|
||||
expect(Raven).to have_received(:capture_exception)
|
||||
.with(exception, a_hash_including(extra: a_hash_including(extra_info)))
|
||||
described_class.track_exception(exception)
|
||||
end
|
||||
end
|
||||
|
||||
context 'the exception implements :sentry_extra_data, which returns nil' do
|
||||
let(:exception) { double(message: 'bang!', sentry_extra_data: nil, backtrace: caller) }
|
||||
let(:extra) { { issue_url: issue_url } }
|
||||
|
||||
it 'just includes the other extra info' do
|
||||
track_exception
|
||||
extra_info = { issue_url: issue_url }
|
||||
expect(Raven).to receive(:capture_exception)
|
||||
.with(exception, a_hash_including(extra: a_hash_including(extra_info)))
|
||||
|
||||
expect(Raven).to have_received(:capture_exception)
|
||||
.with(exception, a_hash_including(extra: a_hash_including(extra)))
|
||||
described_class.track_exception(exception, extra_info)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with sidekiq args' do
|
||||
context 'when the args does not have anything sensitive' do
|
||||
let(:extra) { { sidekiq: { 'class' => 'PostReceive', 'args' => [1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'] } } }
|
||||
it 'ensures extra.sidekiq.args is a string' do
|
||||
extra = { sidekiq: { 'class' => 'PostReceive', 'args' => [1, { 'id' => 2, 'name' => 'hello' }, 'some-value', 'another-value'] } }
|
||||
|
||||
it 'ensures extra.sidekiq.args is a string' do
|
||||
track_exception
|
||||
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
|
||||
hash_including({ 'extra.sidekiq' => { 'class' => 'PostReceive', 'args' => ['1', '{"id"=>2, "name"=>"hello"}', 'some-value', 'another-value'] } }))
|
||||
|
||||
expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(
|
||||
hash_including({ 'extra.sidekiq' => { 'class' => 'PostReceive', 'args' => ['1', '{"id"=>2, "name"=>"hello"}', 'some-value', 'another-value'] } }))
|
||||
end
|
||||
described_class.track_exception(exception, extra)
|
||||
end
|
||||
|
||||
context 'when the args has sensitive information' do
|
||||
let(:extra) { { sidekiq: { 'class' => 'UnknownWorker', 'args' => ['sensitive string', 1, 2] } } }
|
||||
it 'filters sensitive arguments before sending' do
|
||||
extra = { sidekiq: { 'class' => 'UnknownWorker', 'args' => ['sensitive string', 1, 2] } }
|
||||
|
||||
it 'filters sensitive arguments before sending' do
|
||||
track_exception
|
||||
expect(Gitlab::ErrorTracking::Logger).to receive(:error).with(
|
||||
hash_including('extra.sidekiq' => { 'class' => 'UnknownWorker', 'args' => ['[FILTERED]', '1', '2'] }))
|
||||
|
||||
expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2])
|
||||
expect(Gitlab::ErrorTracking::Logger).to have_received(:error).with(
|
||||
hash_including('extra.sidekiq' => { 'class' => 'UnknownWorker', 'args' => ['[FILTERED]', '1', '2'] }))
|
||||
end
|
||||
end
|
||||
end
|
||||
described_class.track_exception(exception, extra)
|
||||
|
||||
context 'when the error is kind of an `ActiveRecord::StatementInvalid`' do
|
||||
let(:exception) { ActiveRecord::StatementInvalid.new(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = 1 AND "users"."foo" = $1') }
|
||||
|
||||
it 'injects the normalized sql query into extra' do
|
||||
track_exception
|
||||
|
||||
expect(Raven).to have_received(:capture_exception)
|
||||
.with(exception, a_hash_including(extra: a_hash_including(sql: 'SELECT "users".* FROM "users" WHERE "users"."id" = $2 AND "users"."foo" = $1')))
|
||||
expect(sentry_event.dig('extra', 'sidekiq', 'args')).to eq(['[FILTERED]', 1, 2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user