Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6c4491e936
commit
082a21ddd4
4
Gemfile
4
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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
|||
<gl-pagination
|
||||
v-show="!loading"
|
||||
v-if="paginationRequired"
|
||||
:prev-page="$options.PREV_PAGE"
|
||||
:next-page="$options.NEXT_PAGE"
|
||||
:prev-page="previousPage"
|
||||
:next-page="nextPage"
|
||||
:value="pageValue"
|
||||
align="center"
|
||||
@input="goToPage"
|
||||
|
|
|
@ -16,7 +16,7 @@ class ErrorTracking::ClientKey < ApplicationRecord
|
|||
end
|
||||
|
||||
def sentry_dsn
|
||||
@sentry_dsn ||= ErrorTracking::Collector::Dsn.build_url(public_key, project_id)
|
||||
@sentry_dsn ||= ::Gitlab::ErrorTracking::ErrorRepository.build(project).dsn_url(public_key)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
module ErrorTracking
|
||||
class ErrorEntity < Grape::Entity
|
||||
expose :id, :title, :type, :user_count, :count,
|
||||
expose :id do |error|
|
||||
error.id.to_s
|
||||
end
|
||||
|
||||
expose :title, :type, :user_count, :count,
|
||||
:first_seen, :last_seen, :message, :culprit,
|
||||
:external_url, :project_id, :project_name, :project_slug,
|
||||
:short_id, :status, :frequency
|
||||
|
|
|
@ -75,6 +75,7 @@ module ErrorTracking
|
|||
# For now we implement the bare minimum for rendering the list in UI.
|
||||
list_opts = {
|
||||
filters: { status: opts[:issue_status] },
|
||||
query: opts[:search_term],
|
||||
sort: opts[:sort],
|
||||
limit: opts[:limit],
|
||||
cursor: opts[:cursor]
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ErrorTracking
|
||||
module Collector
|
||||
class Dsn
|
||||
# Build a sentry compatible DSN URL for GitLab collector.
|
||||
#
|
||||
# The expected URL looks like that:
|
||||
# https://PUBLIC_KEY@gitlab.example.com/api/v4/error_tracking/collector/PROJECT_ID
|
||||
#
|
||||
def self.build_url(public_key, project_id)
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,7 +15,12 @@ module Gitlab
|
|||
#
|
||||
# @return [self]
|
||||
def self.build(project)
|
||||
strategy = ActiveRecordStrategy.new(project)
|
||||
strategy =
|
||||
if Feature.enabled?(:use_click_house_database_for_error_tracking, project)
|
||||
OpenApiStrategy.new(project)
|
||||
else
|
||||
ActiveRecordStrategy.new(project)
|
||||
end
|
||||
|
||||
new(strategy)
|
||||
end
|
||||
|
@ -72,14 +77,15 @@ module Gitlab
|
|||
# @param sort [String] order list by 'first_seen', 'last_seen', or 'frequency'
|
||||
# @param filters [Hash<Symbol, String>] 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<Array<Gitlab::ErrorTracking::Error>, 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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=(?<cursor>[^&]+).*; rel="(?<direction>\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
|
|
@ -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')
|
||||
|
|
|
@ -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
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
|
@ -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' => [
|
||||
'<url?cursor=next_cursor¶m>; rel="next"',
|
||||
'<url?cursor=prev_cursor¶m>; 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
|
|
@ -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) { {} }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue