From 082a21ddd4d13dd00fc443ecc29c6cc5c1229423 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Sat, 2 Jul 2022 12:08:31 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- Gemfile | 4 + Gemfile.lock | 7 + .../components/error_tracking_list.vue | 10 +- app/models/error_tracking/client_key.rb | 2 +- .../error_tracking/error_entity.rb | 6 +- .../error_tracking/list_issues_service.rb | 1 + lib/error_tracking/collector/dsn.rb | 30 -- lib/gitlab/error_tracking/error_repository.rb | 16 +- .../active_record_strategy.rb | 21 +- .../error_repository/open_api_strategy.rb | 247 ++++++++++ lib/tasks/gems.rake | 2 + spec/factories/error_tracking/open_api.rb | 41 ++ .../components/error_tracking_list_spec.js | 24 + spec/lib/error_tracking/collector/dsn_spec.rb | 34 -- .../open_api_strategy_spec.rb | 436 ++++++++++++++++++ spec/requests/api/internal/base_spec.rb | 5 + spec/spec_helper.rb | 4 + vendor/gems/error_tracking_open_api/README.md | 2 +- .../error_tracking_open_api.gemspec | 4 +- 19 files changed, 821 insertions(+), 75 deletions(-) delete mode 100644 lib/error_tracking/collector/dsn.rb create mode 100644 lib/gitlab/error_tracking/error_repository/open_api_strategy.rb create mode 100644 spec/factories/error_tracking/open_api.rb delete mode 100644 spec/lib/error_tracking/collector/dsn_spec.rb create mode 100644 spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb diff --git a/Gemfile b/Gemfile index e4e871039cc..52cd1bd953d 100644 --- a/Gemfile +++ b/Gemfile @@ -549,3 +549,7 @@ gem 'parslet', '~> 1.8' gem 'ipynbdiff', path: 'vendor/gems/ipynbdiff' gem 'ed25519', '~> 1.3.0' + +# Error Tracking OpenAPI client +# See https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/rake_tasks.md#update-openapi-client-for-error-tracking-feature +gem 'error_tracking_open_api', path: 'vendor/gems/error_tracking_open_api' diff --git a/Gemfile.lock b/Gemfile.lock index 53cb0789513..6f6b98c0876 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,9 @@ +PATH + remote: vendor/gems/error_tracking_open_api + specs: + error_tracking_open_api (1.0.0) + typhoeus (~> 1.0, >= 1.0.1) + PATH remote: vendor/gems/ipynbdiff specs: @@ -1520,6 +1526,7 @@ DEPENDENCIES elasticsearch-rails (~> 7.2) email_reply_trimmer (~> 0.1) email_spec (~> 2.2.0) + error_tracking_open_api! erubi (~> 1.9.0) escape_utils (~> 1.1) factory_bot_rails (~> 6.2.0) diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue index d29d5aa0671..a07428dafea 100644 --- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -150,6 +150,12 @@ export default { paginationRequired() { return !isEmpty(this.pagination); }, + previousPage() { + return this.pagination.previous ? this.$options.PREV_PAGE : null; + }, + nextPage() { + return this.pagination.next ? this.$options.NEXT_PAGE : null; + }, errorTrackingHelpUrl() { return helpPagePath('operations/error_tracking'); }, @@ -430,8 +436,8 @@ export default { ] filter list by # @option filters [String] :status error status + # @params query [String, nil] free text search # @param limit [Integer, String] limit result # @param cursor [Hash] pagination information # # @return [Array, Pagination>] - def list_errors(sort: 'last_seen', filters: {}, limit: 20, cursor: {}) + def list_errors(sort: 'last_seen', filters: {}, query: nil, limit: 20, cursor: {}) limit = [limit.to_i, 100].min - strategy.list_errors(filters: filters, sort: sort, limit: limit, cursor: cursor) + strategy.list_errors(filters: filters, query: query, sort: sort, limit: limit, cursor: cursor) end # Fetches last event for error +id+. @@ -105,6 +111,10 @@ module Gitlab strategy.update_error(id, status: status) end + def dsn_url(public_key) + strategy.dsn_url(public_key) + end + private attr_reader :strategy diff --git a/lib/gitlab/error_tracking/error_repository/active_record_strategy.rb b/lib/gitlab/error_tracking/error_repository/active_record_strategy.rb index e5b532ee0f0..01e7fbda384 100644 --- a/lib/gitlab/error_tracking/error_repository/active_record_strategy.rb +++ b/lib/gitlab/error_tracking/error_repository/active_record_strategy.rb @@ -39,11 +39,12 @@ module Gitlab handle_exceptions(e) end - def list_errors(filters:, sort:, limit:, cursor:) + def list_errors(filters:, query:, sort:, limit:, cursor:) errors = project_errors errors = filter_by_status(errors, filters[:status]) errors = sort(errors, sort) errors = errors.keyset_paginate(cursor: cursor, per_page: limit) + # query is not supported pagination = ErrorRepository::Pagination.new(errors.cursor_for_next_page, errors.cursor_for_previous_page) @@ -60,6 +61,24 @@ module Gitlab project_error(id).update(attributes) end + def dsn_url(public_key) + gitlab = Settings.gitlab + + custom_port = Settings.gitlab_on_standard_port? ? nil : ":#{gitlab.port}" + + base_url = [ + gitlab.protocol, + "://", + public_key, + '@', + gitlab.host, + custom_port, + gitlab.relative_url_root + ].join('') + + "#{base_url}/api/v4/error_tracking/collector/#{project.id}" + end + private attr_reader :project diff --git a/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb b/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb new file mode 100644 index 00000000000..5aa1a366f58 --- /dev/null +++ b/lib/gitlab/error_tracking/error_repository/open_api_strategy.rb @@ -0,0 +1,247 @@ +# frozen_string_literal: true + +module Gitlab + module ErrorTracking + class ErrorRepository + class OpenApiStrategy + def initialize(project) + @project = project + + api_url = configured_api_url + + open_api.configure do |config| + config.scheme = api_url.scheme + config.host = [api_url.host, api_url.port].compact.join(':') + config.server_index = nil + config.logger = Gitlab::AppLogger + end + end + + def report_error( + name:, description:, actor:, platform:, + environment:, level:, occurred_at:, payload: + ) + raise NotImplementedError, 'Use ingestion endpoint' + end + + def find_error(id) + api = open_api::ErrorsApi.new + error = api.get_error(project_id, id) + + to_sentry_detailed_error(error) + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + nil + end + + def list_errors(filters:, query:, sort:, limit:, cursor:) + opts = { + sort: "#{sort}_desc", + status: filters[:status], + query: query, + cursor: cursor, + limit: limit + }.compact + + api = open_api::ErrorsApi.new + errors, _status, headers = api.list_errors_with_http_info(project_id, opts) + pagination = pagination_from_headers(headers) + + if errors.size < limit + # Don't show next link if amount of errors is less then requested. + # This a workaround until the Golang backend returns link cursor + # only if there is a next page. + pagination.next = nil + end + + [errors.map { to_sentry_error(_1) }, pagination] + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + [[], ErrorRepository::Pagination.new] + end + + def last_event_for(id) + event = newest_event_for(id) + return unless event + + api = open_api::ErrorsApi.new + error = api.get_error(project_id, id) + return unless error + + to_sentry_error_event(event, error) + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + nil + end + + def update_error(id, **attributes) + opts = attributes.slice(:status) + + body = open_api::ErrorUpdatePayload.new(opts) + + api = open_api::ErrorsApi.new + api.update_error(project_id, id, body) + + true + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + false + end + + def dsn_url(public_key) + config = open_api::Configuration.default + + base_url = [ + config.scheme, + "://", + public_key, + '@', + config.host, + config.base_path + ].join('') + + "#{base_url}/projects/api/#{project_id}" + end + + private + + def event_for(id, sort:) + opts = { sort: sort, limit: 1 } + + api = open_api::ErrorsApi.new + api.list_events(project_id, id, opts).first + rescue ErrorTrackingOpenAPI::ApiError => e + log_exception(e) + nil + end + + def newest_event_for(id) + event_for(id, sort: 'occurred_at_desc') + end + + def oldest_event_for(id) + event_for(id, sort: 'occurred_at_asc') + end + + def to_sentry_error(error) + Gitlab::ErrorTracking::Error.new( + id: error.fingerprint.to_s, + title: error.name, + message: error.description, + culprit: error.actor, + first_seen: error.first_seen_at, + last_seen: error.last_seen_at, + status: error.status, + count: error.event_count, + user_count: error.approximated_user_count + ) + end + + def to_sentry_detailed_error(error) + Gitlab::ErrorTracking::DetailedError.new( + id: error.fingerprint.to_s, + title: error.name, + message: error.description, + culprit: error.actor, + first_seen: error.first_seen_at.to_s, + last_seen: error.last_seen_at.to_s, + count: error.event_count, + user_count: error.approximated_user_count, + project_id: error.project_id, + status: error.status, + tags: { level: nil, logger: nil }, + external_url: external_url(error.fingerprint), + external_base_url: external_base_url, + integrated: true, + first_release_version: release_from(oldest_event_for(error.fingerprint)), + last_release_version: release_from(newest_event_for(error.fingerprint)) + ) + end + + def to_sentry_error_event(event, error) + Gitlab::ErrorTracking::ErrorEvent.new( + issue_id: event.fingerprint.to_s, + date_received: error.last_seen_at, + stack_trace_entries: build_stacktrace(event) + ) + end + + def pagination_from_headers(headers) + links = headers['link'].to_s.split(', ') + + pagination_hash = links.map { parse_pagination_link(_1) }.compact.to_h + + ErrorRepository::Pagination.new(pagination_hash['next'], pagination_hash['prev']) + end + + LINK_PATTERN = %r{cursor=(?[^&]+).*; rel="(?\w+)"}.freeze + + def parse_pagination_link(content) + match = LINK_PATTERN.match(content) + return unless match + + [match['direction'], CGI.unescape(match['cursor'])] + end + + def build_stacktrace(event) + payload = parse_json(event.payload) + return [] unless payload + + ::ErrorTracking::StacktraceBuilder.new(payload).stacktrace + end + + def parse_json(payload) + Gitlab::Json.parse(payload) + rescue JSON::ParserError + end + + def release_from(event) + return unless event + + payload = parse_json(event.payload) + return unless payload + + payload['release'] + end + + def project_id + @project.id + end + + def open_api + ErrorTrackingOpenAPI + end + + # For compatibility with sentry integration + def external_url(id) + 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.project_url(@project) + end + + def configured_api_url + url = ENV.fetch('ERROR_TRACKING_API_URL', 'http://localhost:8080') + + Gitlab::UrlBlocker.validate!(url, schemes: %w[http https], allow_localhost: true) + + URI(url) + end + + def log_exception(exception) + params = { + http_code: exception.code, + response_body: exception.response_body&.truncate(100) + } + + Gitlab::AppLogger.error(Gitlab::Utils::InlineHash.merge_keys(params, prefix: 'open_api')) + end + end + end + end +end diff --git a/lib/tasks/gems.rake b/lib/tasks/gems.rake index b06b6e2bf3c..27ea735059e 100644 --- a/lib/tasks/gems.rake +++ b/lib/tasks/gems.rake @@ -55,6 +55,8 @@ namespace :gems do write_file(gem_dir / 'LICENSE', license) write_file(gem_dir / "#{gem_name}.gemspec") do |content| replace_string(content, 'Unlicense', 'MIT') + replace_string(content, /(\.files\s*=).*/, '\1 Dir.glob("lib/**/*")') + replace_string(content, /(\.test_files\s*=).*/, '\1 []') end remove_entry_secure(gem_dir / '.rubocop.yml') diff --git a/spec/factories/error_tracking/open_api.rb b/spec/factories/error_tracking/open_api.rb new file mode 100644 index 00000000000..ad134701fd0 --- /dev/null +++ b/spec/factories/error_tracking/open_api.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :error_tracking_open_api_error, class: 'ErrorTrackingOpenAPI::Error' do + fingerprint { 1 } + project_id { 2 } + name { 'ActionView::MissingTemplate' } + description { 'Missing template posts/edit' } + actor { 'PostsController#edit' } + event_count { 3 } + approximated_user_count { 4 } + first_seen_at { Time.now.iso8601 } + last_seen_at { Time.now.iso8601 } + status { 'unresolved' } + + skip_create + end + + factory :error_tracking_open_api_error_event, class: 'ErrorTrackingOpenAPI::ErrorEvent' do + fingerprint { 1 } + project_id { 2 } + payload { File.read(Rails.root.join('spec/fixtures/error_tracking/parsed_event.json')) } + name { 'ActionView::MissingTemplate' } + description { 'Missing template posts/edit' } + actor { 'PostsController#edit' } + environment { 'development' } + platform { 'ruby' } + + trait :golang do + payload { File.read(Rails.root.join('spec/fixtures/error_tracking/go_parsed_event.json')) } + platform { 'go' } + end + + trait :browser do + payload { File.read(Rails.root.join('spec/fixtures/error_tracking/browser_event.json')) } + platform { 'javascript' } + end + + skip_create + end +end diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index 5e0f0ca9bef..23d448f3964 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -398,6 +398,30 @@ describe('ErrorTrackingList', () => { }); describe('When pagination is required', () => { + describe('and previous cursor is not available', () => { + beforeEach(async () => { + store.state.list.loading = false; + delete store.state.list.pagination.previous; + mountComponent(); + }); + + it('disables Prev button in the pagination', async () => { + expect(findPagination().props('prevPage')).toBe(null); + expect(findPagination().props('nextPage')).not.toBe(null); + }); + }); + describe('and next cursor is not available', () => { + beforeEach(async () => { + store.state.list.loading = false; + delete store.state.list.pagination.next; + mountComponent(); + }); + + it('disables Next button in the pagination', async () => { + expect(findPagination().props('prevPage')).not.toBe(null); + expect(findPagination().props('nextPage')).toBe(null); + }); + }); describe('and the user is not on the first page', () => { describe('and the previous button is clicked', () => { beforeEach(async () => { diff --git a/spec/lib/error_tracking/collector/dsn_spec.rb b/spec/lib/error_tracking/collector/dsn_spec.rb deleted file mode 100644 index 3aa8719fe38..00000000000 --- a/spec/lib/error_tracking/collector/dsn_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ErrorTracking::Collector::Dsn do - describe '.build_url' do - let(:setting) do - { - protocol: 'https', - https: true, - port: 443, - host: 'gitlab.example.com', - relative_url_root: nil - } - end - - subject { described_class.build_url('abcdef1234567890', 778) } - - it 'returns a valid URL without explicit port' do - stub_config_setting(setting) - - is_expected.to eq('https://abcdef1234567890@gitlab.example.com/api/v4/error_tracking/collector/778') - end - - context 'with non-standard port' do - it 'returns a valid URL with custom port' do - setting[:port] = 4567 - stub_config_setting(setting) - - is_expected.to eq('https://abcdef1234567890@gitlab.example.com:4567/api/v4/error_tracking/collector/778') - end - end - end -end diff --git a/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb new file mode 100644 index 00000000000..608040eaefb --- /dev/null +++ b/spec/lib/gitlab/error_tracking/error_repository/open_api_strategy_spec.rb @@ -0,0 +1,436 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ErrorTracking::ErrorRepository::OpenApiStrategy do + include AfterNextHelpers + + let(:project) { build_stubbed(:project) } + let(:api_exception) { ErrorTrackingOpenAPI::ApiError.new(code: 500, response_body: 'b' * 101) } + + subject(:repository) { Gitlab::ErrorTracking::ErrorRepository.build(project) } + + before do + # Disabled in spec_helper by default thus we need to enable it here. + stub_feature_flags(use_click_house_database_for_error_tracking: true) + end + + shared_examples 'exception logging' do + it 'logs error' do + expect(Gitlab::AppLogger).to receive(:error).with( + 'open_api.http_code' => api_exception.code, + 'open_api.response_body' => api_exception.response_body.truncate(100) + ) + + subject + end + end + + shared_examples 'no logging' do + it 'does not log anything' do + expect(Gitlab::AppLogger).not_to receive(:debug) + expect(Gitlab::AppLogger).not_to receive(:info) + expect(Gitlab::AppLogger).not_to receive(:error) + end + end + + describe '#report_error' do + let(:params) do + { + name: 'anything', + description: 'anything', + actor: 'anything', + platform: 'anything', + environment: 'anything', + level: 'anything', + occurred_at: Time.zone.now, + payload: {} + } + end + + subject { repository.report_error(**params) } + + it 'is not implemented' do + expect { subject }.to raise_error(NotImplementedError, 'Use ingestion endpoint') + end + end + + describe '#find_error' do + let(:error) { build(:error_tracking_open_api_error, project_id: project.id) } + + subject { repository.find_error(error.fingerprint) } + + before do + allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api| + allow(open_api).to receive(:get_error).with(project.id, error.fingerprint) + .and_return(error) + + allow(open_api).to receive(:list_events) + .with(project.id, error.fingerprint, sort: 'occurred_at_asc', limit: 1) + .and_return(list_events_asc) + + allow(open_api).to receive(:list_events) + .with(project.id, error.fingerprint, sort: 'occurred_at_desc', limit: 1) + .and_return(list_events_desc) + end + end + + context 'when request succeeds' do + context 'without events returned' do + let(:list_events_asc) { [] } + let(:list_events_desc) { [] } + + include_examples 'no logging' + + it 'returns detailed error' do + is_expected.to have_attributes( + id: error.fingerprint.to_s, + title: error.name, + message: error.description, + culprit: error.actor, + first_seen: error.first_seen_at.to_s, + last_seen: error.last_seen_at.to_s, + count: error.event_count, + user_count: error.approximated_user_count, + project_id: error.project_id, + status: error.status, + tags: { level: nil, logger: nil }, + external_url: "http://localhost/#{project.full_path}/-/error_tracking/#{error.fingerprint}/details", + external_base_url: "http://localhost/#{project.full_path}", + integrated: true + ) + end + + it 'returns no first and last release version' do + is_expected.to have_attributes( + first_release_version: nil, + last_release_version: nil + ) + end + end + + context 'with events returned' do + let(:first_event) { build(:error_tracking_open_api_error_event, project_id: project.id) } + let(:first_release) { parse_json(first_event.payload).fetch('release') } + let(:last_event) { build(:error_tracking_open_api_error_event, :golang, project_id: project.id) } + let(:last_release) { parse_json(last_event.payload).fetch('release') } + + let(:list_events_asc) { [first_event] } + let(:list_events_desc) { [last_event] } + + include_examples 'no logging' + + it 'returns first and last release version' do + expect(first_release).to be_present + expect(last_release).to be_present + + is_expected.to have_attributes( + first_release_version: first_release, + last_release_version: last_release + ) + end + + def parse_json(content) + Gitlab::Json.parse(content) + end + end + end + + context 'when request fails' do + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:get_error) + .with(project.id, error.fingerprint) + .and_raise(api_exception) + end + + include_examples 'exception logging' + + it { is_expected.to be_nil } + end + end + + describe '#list_errors' do + let(:errors) { [] } + let(:response_with_info) { [errors, 200, headers] } + let(:result_errors) { result.first } + let(:result_pagination) { result.last } + + let(:headers) do + { + 'link' => [ + '; rel="next"', + '; rel="prev"' + ].join(', ') + } + end + + subject(:result) { repository.list_errors(**params) } + + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info) + .with(project.id, kind_of(Hash)) + .and_return(response_with_info) + end + + context 'with errors' do + let(:limit) { 3 } + let(:params) { { limit: limit } } + let(:errors_size) { limit } + let(:errors) { build_list(:error_tracking_open_api_error, errors_size, project_id: project.id) } + + include_examples 'no logging' + + it 'maps errors to models' do + # All errors are identical + error = errors.first + + expect(result_errors).to all( + have_attributes( + id: error.fingerprint.to_s, + title: error.name, + message: error.description, + culprit: error.actor, + first_seen: error.first_seen_at, + last_seen: error.last_seen_at, + status: error.status, + count: error.event_count, + user_count: error.approximated_user_count + )) + end + + context 'when n errors are returned' do + let(:errors_size) { limit } + + include_examples 'no logging' + + it 'returns the amount of errors' do + expect(result_errors.size).to eq(3) + end + + it 'cursor links are preserved' do + expect(result_pagination).to have_attributes( + prev: 'prev_cursor', + next: 'next_cursor' + ) + end + end + + context 'when less errors than requested are returned' do + let(:errors_size) { limit - 1 } + + include_examples 'no logging' + + it 'returns the amount of errors' do + expect(result_errors.size).to eq(2) + end + + it 'cursor link for next is removed' do + expect(result_pagination).to have_attributes( + prev: 'prev_cursor', + next: nil + ) + end + end + end + + context 'with params' do + let(:params) do + { + filters: { status: 'resolved', something: 'different' }, + query: 'search term', + sort: 'first_seen', + limit: 2, + cursor: 'abc' + } + end + + include_examples 'no logging' + + it 'passes provided params to client' do + passed_params = { + sort: 'first_seen_desc', + status: 'resolved', + query: 'search term', + cursor: 'abc', + limit: 2 + } + + expect_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info) + .with(project.id, passed_params) + .and_return(response_with_info) + + subject + end + end + + context 'without explicit params' do + let(:params) { {} } + + include_examples 'no logging' + + it 'passes default params to client' do + passed_params = { + sort: 'last_seen_desc', + limit: 20, + cursor: {} + } + + expect_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info) + .with(project.id, passed_params) + .and_return(response_with_info) + + subject + end + end + + context 'when request fails' do + let(:params) { {} } + + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info) + .with(project.id, kind_of(Hash)) + .and_raise(api_exception) + end + + include_examples 'exception logging' + + specify do + expect(result_errors).to eq([]) + expect(result_pagination).to have_attributes( + next: nil, + prev: nil + ) + end + end + end + + describe '#last_event_for' do + let(:params) { { sort: 'occurred_at_desc', limit: 1 } } + let(:event) { build(:error_tracking_open_api_error_event, project_id: project.id) } + let(:error) { build(:error_tracking_open_api_error, project_id: project.id, fingerprint: event.fingerprint) } + + subject { repository.last_event_for(error.fingerprint) } + + context 'when both event and error is returned' do + before do + allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api| + allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params) + .and_return([event]) + + allow(open_api).to receive(:get_error).with(project.id, error.fingerprint) + .and_return(error) + end + end + + include_examples 'no logging' + + it 'returns mapped error event' do + is_expected.to have_attributes( + issue_id: event.fingerprint.to_s, + date_received: error.last_seen_at, + stack_trace_entries: kind_of(Array) + ) + end + end + + context 'when event is not returned' do + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_events) + .with(project.id, event.fingerprint, params) + .and_return([]) + end + + include_examples 'no logging' + + it { is_expected.to be_nil } + end + + context 'when list_events request fails' do + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_events) + .with(project.id, event.fingerprint, params) + .and_raise(api_exception) + end + + include_examples 'exception logging' + + it { is_expected.to be_nil } + end + + context 'when error is not returned' do + before do + allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api| + allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params) + .and_return([event]) + + allow(open_api).to receive(:get_error).with(project.id, error.fingerprint) + .and_return(nil) + end + end + + include_examples 'no logging' + + it { is_expected.to be_nil } + end + + context 'when get_error request fails' do + before do + allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api| + allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params) + .and_return([event]) + + allow(open_api).to receive(:get_error).with(project.id, error.fingerprint) + .and_raise(api_exception) + end + end + + include_examples 'exception logging' + + it { is_expected.to be_nil } + end + end + + describe '#update_error' do + let(:error) { build(:error_tracking_open_api_error, project_id: project.id) } + let(:update_params) { { status: 'resolved' } } + let(:passed_body) { ErrorTrackingOpenAPI::ErrorUpdatePayload.new(update_params) } + + subject { repository.update_error(error.fingerprint, **update_params) } + + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:update_error) + .with(project.id, error.fingerprint, passed_body) + .and_return(:anything) + end + + context 'when update succeeds' do + include_examples 'no logging' + + it { is_expected.to eq(true) } + end + + context 'when update fails' do + before do + allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:update_error) + .with(project.id, error.fingerprint, passed_body) + .and_raise(api_exception) + end + + include_examples 'exception logging' + + it { is_expected.to eq(false) } + end + end + + describe '#dsn_url' do + let(:public_key) { 'abc' } + let(:config) { ErrorTrackingOpenAPI::Configuration.default } + + subject { repository.dsn_url(public_key) } + + it do + is_expected + .to eq("#{config.scheme}://#{public_key}@#{config.host}/errortracking/api/v1/projects/api/#{project.id}") + end + end +end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index faff5fdd6a7..ba34827da62 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -63,6 +63,11 @@ RSpec.describe API::Internal::Base do post api('/internal/error_tracking/allowed'), params: params, headers: headers end + before do + # Because the feature flag is disabled in specs we have to enable it explicitly. + stub_feature_flags(use_click_house_database_for_error_tracking: true) + end + context 'when the secret header is missing' do let(:headers) { {} } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d01af996141..06d6a8d9c0f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -289,6 +289,10 @@ RSpec.configure do |config| stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: false) stub_feature_flags(ci_queueing_disaster_recovery_disable_quota: false) + # It's disabled in specs because we don't support certain features which + # cause spec failures. + stub_feature_flags(use_click_house_database_for_error_tracking: false) + enable_rugged = example.metadata[:enable_rugged].present? # Disable Rugged features by default diff --git a/vendor/gems/error_tracking_open_api/README.md b/vendor/gems/error_tracking_open_api/README.md index a26eb616ea1..1bd234c8ddb 100644 --- a/vendor/gems/error_tracking_open_api/README.md +++ b/vendor/gems/error_tracking_open_api/README.md @@ -1,4 +1,4 @@ -# Generated by `rake gems:error_tracking_open_api:generate` at 2022-07-01 +# Generated by `rake gems:error_tracking_open_api:generate` on 2022-07-02 See https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/rake_tasks.md#update-openapi-client-for-error-tracking-feature diff --git a/vendor/gems/error_tracking_open_api/error_tracking_open_api.gemspec b/vendor/gems/error_tracking_open_api/error_tracking_open_api.gemspec index 53804855886..45f07204725 100644 --- a/vendor/gems/error_tracking_open_api/error_tracking_open_api.gemspec +++ b/vendor/gems/error_tracking_open_api/error_tracking_open_api.gemspec @@ -31,8 +31,8 @@ Gem::Specification.new do |s| s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0' - s.files = `find *`.split("\n").uniq.sort.select { |f| !f.empty? } - s.test_files = `find spec/*`.split("\n") + s.files = Dir.glob("lib/**/*") + s.test_files = [] s.executables = [] s.require_paths = ["lib"] end