diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-22 09:08:26 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-22 09:08:26 +0000 |
commit | 9dab4d7b6492628eb9222f14954fdd8889bd6e34 (patch) | |
tree | ae7723aff502efd04449be1692b4488cbd198de7 | |
parent | f0fa9f7acf4c23f9a9fb4903fd7c41678a20c028 (diff) | |
download | gitlab-ce-9dab4d7b6492628eb9222f14954fdd8889bd6e34.tar.gz |
Add latest changes from gitlab-org/gitlab@master
22 files changed, 172 insertions, 181 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 12d13315ca4..5059b30b278 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -dac25a9c19af0a168a7927784295dd12fb5c8075 +19109e7090eb6dab2b8c0c168bbef76ceca79a31 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index f66efe38785..89a8ef8d8fc 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.51.0 +8.52.0 @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 241d21d4288..93670d22920 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 6a96b06dcd8..642d51fbc7a 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -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, }; diff --git a/app/assets/javascripts/design_management/router/index.js b/app/assets/javascripts/design_management/router/index.js index cbeb2f7ce42..12692612bbc 100644 --- a/app/assets/javascripts/design_management/router/index.js +++ b/app/assets/javascripts/design_management/router/index.js @@ -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; } diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index adba86d384b..00ccf59e37c 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -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" diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue index cad4439ecea..de9c84dd157 100644 --- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue +++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue @@ -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" diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 953dce4d63c..fa150412285 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -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) diff --git a/app/controllers/projects/static_site_editor_controller.rb b/app/controllers/projects/static_site_editor_controller.rb index 7e2e32a843f..58cda84ebe7 100644 --- a/app/controllers/projects/static_site_editor_controller.rb +++ b/app/controllers/projects/static_site_editor_controller.rb @@ -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 diff --git a/changelogs/unreleased/sh-upgrade-workhorse-8-52-0.yml b/changelogs/unreleased/sh-upgrade-workhorse-8-52-0.yml new file mode 100644 index 00000000000..0d5081113ad --- /dev/null +++ b/changelogs/unreleased/sh-upgrade-workhorse-8-52-0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Workhorse to v8.52.0 +merge_request: 45778 +author: +type: fixed diff --git a/config/feature_flags/development/filter_pipelines_search.yml b/config/feature_flags/development/filter_pipelines_search.yml deleted file mode 100644 index 57bf61552d8..00000000000 --- a/config/feature_flags/development/filter_pipelines_search.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: filter_pipelines_search -introduced_by_url: -rollout_issue_url: -group: -type: development -default_enabled: true diff --git a/doc/user/analytics/value_stream_analytics.md b/doc/user/analytics/value_stream_analytics.md index 86525587b30..5a498dea7bb 100644 --- a/doc/user/analytics/value_stream_analytics.md +++ b/doc/user/analytics/value_stream_analytics.md @@ -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 diff --git a/lib/gitlab/error_tracking.rb b/lib/gitlab/error_tracking.rb index a5ace2be773..803acef9a40 100644 --- a/lib/gitlab/error_tracking.rb +++ b/lib/gitlab/error_tracking.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f6fdfb1706f..352ccdf4d1e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/controllers/projects/static_site_editor_controller_spec.rb b/spec/controllers/projects/static_site_editor_controller_spec.rb index 6ea730cbf27..867b2b51039 100644 --- a/spec/controllers/projects/static_site_editor_controller_spec.rb +++ b/spec/controllers/projects/static_site_editor_controller_spec.rb @@ -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 diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js index d9f7146d258..d0c14c4c7eb 100644 --- a/spec/frontend/design_management/pages/design/index_spec.js +++ b/spec/frontend/design_management/pages/design/index_spec.js @@ -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, + ); }); }); diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js index 27a91b11448..279403789f1 100644 --- a/spec/frontend/design_management/pages/index_spec.js +++ b/spec/frontend/design_management/pages/index_spec.js @@ -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', diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index 1298a2a1524..5e04f9a6433 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -74,7 +74,6 @@ describe('Pipelines', () => { const createComponent = (props = defaultProps, methods) => { wrapper = mount(PipelinesComponent, { - provide: { glFeatures: { filterPipelinesSearch: true } }, propsData: { store: new Store(), projectId: '21', diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js index e5a8860f42e..ca9f8ff54d4 100644 --- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js +++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js @@ -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", () => { diff --git a/spec/graphql/resolvers/projects_resolver_spec.rb b/spec/graphql/resolvers/projects_resolver_spec.rb index 83a26062957..3de54c7e410 100644 --- a/spec/graphql/resolvers/projects_resolver_spec.rb +++ b/spec/graphql/resolvers/projects_resolver_spec.rb @@ -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 diff --git a/spec/lib/gitlab/error_tracking_spec.rb b/spec/lib/gitlab/error_tracking_spec.rb index 68a46b11487..2cc9ff36c99 100644 --- a/spec/lib/gitlab/error_tracking_spec.rb +++ b/spec/lib/gitlab/error_tracking_spec.rb @@ -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 - track_exception - - 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 + it 'filters sensitive arguments before sending' do + extra = { sidekiq: { 'class' => 'UnknownWorker', 'args' => ['sensitive string', 1, 2] } } - 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') } + expect(Gitlab::ErrorTracking::Logger).to receive(:error).with( + hash_including('extra.sidekiq' => { 'class' => 'UnknownWorker', 'args' => ['[FILTERED]', '1', '2'] })) - it 'injects the normalized sql query into extra' do - track_exception + described_class.track_exception(exception, extra) - 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 |