Commit 3cd814f9 authored by Sean Arnold's avatar Sean Arnold Committed by Douglas Barbosa Alexandre

Add WIP implementation of error details

- API call for Basic error data
- API call for last occurance
- Use reactive caching
- Add route endpoint
parent e1eb198b
...@@ -15,6 +15,23 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -15,6 +15,23 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end end
end end
def details
respond_to do |format|
format.html
format.json do
render_issue_detail_json
end
end
end
def stack_trace
respond_to do |format|
format.json do
render_issue_stack_trace_json
end
end
end
def list_projects def list_projects
respond_to do |format| respond_to do |format|
format.json do format.json do
...@@ -29,10 +46,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -29,10 +46,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
service = ErrorTracking::ListIssuesService.new(project, current_user) service = ErrorTracking::ListIssuesService.new(project, current_user)
result = service.execute result = service.execute
unless result[:status] == :success return if handle_errors(result)
return render json: { message: result[:message] },
status: result[:http_status] || :bad_request
end
render json: { render json: {
errors: serialize_errors(result[:issues]), errors: serialize_errors(result[:issues]),
...@@ -40,6 +54,28 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -40,6 +54,28 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
} }
end end
def render_issue_detail_json
service = ErrorTracking::IssueDetailsService.new(project, current_user, issue_details_params)
result = service.execute
return if handle_errors(result)
render json: {
error: serialize_detailed_error(result[:issue])
}
end
def render_issue_stack_trace_json
service = ErrorTracking::IssueLatestEventService.new(project, current_user, issue_details_params)
result = service.execute
return if handle_errors(result)
render json: {
error: serialize_error_event(result[:latest_event])
}
end
def render_project_list_json def render_project_list_json
service = ErrorTracking::ListProjectsService.new( service = ErrorTracking::ListProjectsService.new(
project, project,
...@@ -62,10 +98,21 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -62,10 +98,21 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end end
end end
def handle_errors(result)
unless result[:status] == :success
render json: { message: result[:message] },
status: result[:http_status] || :bad_request
end
end
def list_projects_params def list_projects_params
params.require(:error_tracking_setting).permit([:api_host, :token]) params.require(:error_tracking_setting).permit([:api_host, :token])
end end
def issue_details_params
params.permit(:issue_id)
end
def set_polling_interval def set_polling_interval
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
end end
...@@ -76,6 +123,18 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -76,6 +123,18 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
.represent(errors) .represent(errors)
end end
def serialize_detailed_error(error)
ErrorTracking::DetailedErrorSerializer
.new(project: project, user: current_user)
.represent(error)
end
def serialize_error_event(event)
ErrorTracking::ErrorEventSerializer
.new(project: project, user: current_user)
.represent(event)
end
def serialize_projects(projects) def serialize_projects(projects)
ErrorTracking::ProjectSerializer ErrorTracking::ProjectSerializer
.new(project: project, user: current_user) .new(project: project, user: current_user)
......
...@@ -13,4 +13,13 @@ module Projects::ErrorTrackingHelper ...@@ -13,4 +13,13 @@ module Projects::ErrorTrackingHelper
'illustration-path' => image_path('illustrations/cluster_popover.svg') 'illustration-path' => image_path('illustrations/cluster_popover.svg')
} }
end end
def error_details_data(project, issue)
opts = [project, issue, { format: :json }]
{
'issue-details-path' => details_namespace_project_error_tracking_index_path(*opts),
'issue-stack-trace-path' => stack_trace_namespace_project_error_tracking_index_path(*opts)
}
end
end end
...@@ -87,10 +87,30 @@ module ErrorTracking ...@@ -87,10 +87,30 @@ module ErrorTracking
{ projects: sentry_client.list_projects } { projects: sentry_client.list_projects }
end end
def issue_details(opts = {})
with_reactive_cache('issue_details', opts.stringify_keys) do |result|
result
end
end
def issue_latest_event(opts = {})
with_reactive_cache('issue_latest_event', opts.stringify_keys) do |result|
result
end
end
def calculate_reactive_cache(request, opts) def calculate_reactive_cache(request, opts)
case request case request
when 'list_issues' when 'list_issues'
{ issues: sentry_client.list_issues(**opts.symbolize_keys) } { issues: sentry_client.list_issues(**opts.symbolize_keys) }
when 'issue_details'
{
issue: sentry_client.issue_details(**opts.symbolize_keys)
}
when 'issue_latest_event'
{
latest_event: sentry_client.issue_latest_event(**opts.symbolize_keys)
}
end end
rescue Sentry::Client::Error => e rescue Sentry::Client::Error => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE } { error: e.message, error_type: SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE }
......
# frozen_string_literal: true
module ErrorTracking
class DetailedErrorEntity < Grape::Entity
expose :count,
:culprit,
:external_base_url,
:external_url,
:first_release_last_commit,
:first_release_short_version,
:first_seen,
:frequency,
:id,
:last_release_last_commit,
:last_release_short_version,
:last_seen,
:message,
:project_id,
:project_name,
:project_slug,
:short_id,
:status,
:title,
:type,
:user_count
end
end
# frozen_string_literal: true
module ErrorTracking
class DetailedErrorSerializer < BaseSerializer
entity DetailedErrorEntity
end
end
# frozen_string_literal: true
module ErrorTracking
class ErrorEventEntity < Grape::Entity
expose :issue_id, :date_received, :stack_trace_entries
end
end
# frozen_string_literal: true
module ErrorTracking
class ErrorEventSerializer < BaseSerializer
entity ErrorEventEntity
end
end
# frozen_string_literal: true
module ErrorTracking
class BaseService < ::BaseService
def execute
unauthorized = check_permissions
return unauthorized if unauthorized
begin
response = fetch
rescue Sentry::Client::Error => e
return error(e.message, :bad_request)
rescue Sentry::Client::MissingKeysError => e
return error(e.message, :internal_server_error)
end
errors = parse_errors(response)
return errors if errors
success(parse_response(response))
end
private
def fetch
raise NotImplementedError,
"#{self.class} does not implement #{__method__}"
end
def parse_response(response)
raise NotImplementedError,
"#{self.class} does not implement #{__method__}"
end
def check_permissions
return error('Error Tracking is not enabled') unless enabled?
return error('Access denied', :unauthorized) unless can_read?
end
def parse_errors(response)
return error('Not ready. Try again later', :no_content) unless response
return error(response[:error], http_status_for(response[:error_type])) if response[:error].present?
end
def http_status_for(error_type)
case error_type
when ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
:internal_server_error
else
:bad_request
end
end
def project_error_tracking_setting
project.error_tracking_setting
end
def enabled?
project_error_tracking_setting&.enabled?
end
def can_read?
can?(current_user, :read_sentry_issue, project)
end
end
end
# frozen_string_literal: true
module ErrorTracking
class IssueDetailsService < ErrorTracking::BaseService
private
def fetch
project_error_tracking_setting.issue_details(issue_id: params[:issue_id])
end
def parse_response(response)
{ issue: response[:issue] }
end
end
end
# frozen_string_literal: true
module ErrorTracking
class IssueLatestEventService < ErrorTracking::BaseService
private
def fetch
project_error_tracking_setting.issue_latest_event(issue_id: params[:issue_id])
end
def parse_response(response)
{ latest_event: response[:latest_event] }
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module ErrorTracking module ErrorTracking
class ListIssuesService < ::BaseService class ListIssuesService < ErrorTracking::BaseService
DEFAULT_ISSUE_STATUS = 'unresolved' DEFAULT_ISSUE_STATUS = 'unresolved'
DEFAULT_LIMIT = 20 DEFAULT_LIMIT = 20
def execute private
return error('Error Tracking is not enabled') unless enabled?
return error('Access denied', :unauthorized) unless can_read?
result = project_error_tracking_setting
.list_sentry_issues(issue_status: issue_status, limit: limit)
# our results are not yet ready
unless result
return error('Not ready. Try again later', :no_content)
end
if result[:error].present? def fetch
return error(result[:error], http_status_from_error_type(result[:error_type])) project_error_tracking_setting.list_sentry_issues(issue_status: issue_status, limit: limit)
end end
success(issues: result[:issues]) def parse_response(response)
{ issues: response[:issues] }
end end
def external_url def external_url
project_error_tracking_setting&.sentry_external_url project_error_tracking_setting&.sentry_external_url
end end
private
def http_status_from_error_type(error_type)
case error_type
when ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
:internal_server_error
else
:bad_request
end
end
def project_error_tracking_setting
project.error_tracking_setting
end
def issue_status def issue_status
params[:issue_status] || DEFAULT_ISSUE_STATUS params[:issue_status] || DEFAULT_ISSUE_STATUS
end end
...@@ -50,13 +26,5 @@ module ErrorTracking ...@@ -50,13 +26,5 @@ module ErrorTracking
def limit def limit
params[:limit] || DEFAULT_LIMIT params[:limit] || DEFAULT_LIMIT
end end
def enabled?
project_error_tracking_setting&.enabled?
end
def can_read?
can?(current_user, :read_sentry_issue, project)
end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module ErrorTracking module ErrorTracking
class ListProjectsService < ::BaseService class ListProjectsService < ErrorTracking::BaseService
def execute def execute
return error('access denied') unless can_read? unless project_error_tracking_setting.valid?
return error(project_error_tracking_setting.errors.full_messages.join(', '), :bad_request)
setting = project_error_tracking_setting
unless setting.valid?
return error(setting.errors.full_messages.join(', '), :bad_request)
end end
begin super
result = setting.list_sentry_projects
rescue Sentry::Client::Error => e
return error(e.message, :bad_request)
rescue Sentry::Client::MissingKeysError => e
return error(e.message, :internal_server_error)
end
success(projects: result[:projects])
end end
private private
def project_error_tracking_setting def fetch
(project.error_tracking_setting || project.build_error_tracking_setting).tap do |setting| project_error_tracking_setting.list_sentry_projects
setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from( end
api_host: params[:api_host],
organization_slug: 'org', def parse_response(response)
project_slug: 'proj' { projects: response[:projects] }
)
setting.token = token(setting)
setting.enabled = true
end
end end
def can_read? def project_error_tracking_setting
can?(current_user, :read_sentry_issue, project) @project_error_tracking_setting ||= begin
(super || project.build_error_tracking_setting).tap do |setting|
setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
api_host: params[:api_host],
organization_slug: 'org',
project_slug: 'proj'
)
setting.token = token(setting)
setting.enabled = true
end
end
end end
def token(setting) def token(setting)
......
- page_title _('Error Details')
#js-error_tracking{ data: error_details_data(@current_user, @project) }
---
title: API for stack trace & detail view of Sentry error in GitLab
merge_request: 19137
author:
type: added
...@@ -613,6 +613,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -613,6 +613,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :error_tracking, only: [:index], controller: :error_tracking do resources :error_tracking, only: [:index], controller: :error_tracking do
collection do collection do
get ':issue_id/details',
to: 'error_tracking#details',
as: 'details'
get ':issue_id/stack_trace',
to: 'error_tracking#stack_trace',
as: 'stack_trace'
post :list_projects post :list_projects
end end
end end
......
# frozen_string_literal: true
module Gitlab
module ErrorTracking
class DetailedError
include ActiveModel::Model
attr_accessor :count,
:culprit,
:external_base_url,
:external_url,
:first_release_last_commit,
:first_release_short_version,
:first_seen,
:frequency,
:id,
:last_release_last_commit,
:last_release_short_version,
:last_seen,
:message,
:project_id,
:project_name,
:project_slug,
:short_id,
:status,
:title,
:type,
:user_count
end
end
end
# frozen_string_literal: true
module Gitlab
module ErrorTracking
class ErrorEvent
include ActiveModel::Model
attr_accessor :issue_id, :date_received, :stack_trace_entries
end
end
end
...@@ -12,6 +12,18 @@ module Sentry ...@@ -12,6 +12,18 @@ module Sentry
@token = token @token = token
end end
def issue_details(issue_id:)
issue = get_issue(issue_id: issue_id)
map_to_detailed_error(issue)
end
def issue_latest_event(issue_id:)
latest_event = get_issue_latest_event(issue_id: issue_id)
map_to_event(latest_event)
end
def list_issues(issue_status:, limit:) def list_issues(issue_status:, limit:)
issues = get_issues(issue_status: issue_status, limit: limit) issues = get_issues(issue_status: issue_status, limit: limit)
...@@ -61,6 +73,14 @@ module Sentry ...@@ -61,6 +73,14 @@ module Sentry
}) })
end end
def get_issue(issue_id:)
http_get(issue_api_url(issue_id))
end
def get_issue_latest_event(issue_id:)
http_get(issue_latest_event_api_url(issue_id))
end
def get_projects def get_projects
http_get(projects_api_url) http_get(projects_api_url)
end end
...@@ -102,6 +122,20 @@ module Sentry ...@@ -102,6 +122,20 @@ module Sentry
projects_url projects_url
end end
def issue_api_url(issue_id)
issue_url = URI(@url)
issue_url.path = "/api/0/issues/#{issue_id}/"
issue_url
end
def issue_latest_event_api_url(issue_id)
latest_event_url = URI(@url)
latest_event_url.path = "/api/0/issues/#{issue_id}/events/latest/"
latest_event_url
end
def issues_api_url def issues_api_url
issues_url = URI(@url + '/issues/') issues_url = URI(@url + '/issues/')
issues_url.path.squeeze!('/') issues_url.path.squeeze!('/')
...@@ -119,38 +153,87 @@ module Sentry ...@@ -119,38 +153,87 @@ module Sentry
def issue_url(id) def issue_url(id)
issues_url = @url + "/issues/#{id}" issues_url = @url + "/issues/#{id}"
issues_url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(issues_url)
uri = URI(issues_url) parse_sentry_url(issues_url)
end
def project_url
parse_sentry_url(@url)
end
def parse_sentry_url(api_url)
url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(api_url)
uri = URI(url)
uri.path.squeeze!('/') uri.path.squeeze!('/')
# Remove trailing spaces
uri = uri.to_s.gsub(/\/\z/, '')
uri.to_s uri
end end
def map_to_error(issue) def map_to_event(event)
id = issue.fetch('id') stack_trace = parse_stack_trace(event)
Gitlab::ErrorTracking::ErrorEvent.new(
issue_id: event.dig('groupID'),
date_received: event.dig('dateReceived'),
stack_trace_entries: stack_trace
)
end
count = issue.fetch('count', nil) def parse_stack_trace(event)
exception_entry = event.dig('entries')&.detect { |h| h['type'] == 'exception' }
return unless exception_entry
frequency = issue.dig('stats', '24h') exception_values = exception_entry.dig('data', 'values')
message = issue.dig('metadata', 'value') stack_trace_entry = exception_values&.detect { |h| h['stacktrace'].present? }
return unless stack_trace_entry
external_url = issue_url(id) stack_trace_entry.dig('stacktrace', 'frames')
end
def map_to_detailed_error(issue)
Gitlab::ErrorTracking::DetailedError.new(
id: issue.fetch('id'),
first_seen: issue.fetch('firstSeen', nil),
last_seen: issue.fetch('lastSeen', nil),
title: issue.fetch('title', nil),
type: issue.fetch('type', nil),
user_count: issue.fetch('userCount', nil),
count: issue.fetch('count', nil),
message: issue.dig('metadata', 'value'),
culprit: issue.fetch('culprit', nil),
external_url: issue_url(issue.fetch('id')),
external_base_url: project_url,
short_id: issue.fetch('shortId', nil),
status: issue.fetch('status', nil),
frequency: issue.dig('stats', '24h'),
project_id: issue.dig('project', 'id'),
project_name: issue.dig('project', 'name'),
project_slug: issue.dig('project', 'slug'),
first_release_last_commit: issue.dig('firstRelease', 'lastCommit'),
last_release_last_commit: issue.dig('lastRelease', 'lastCommit'),
first_release_short_version: issue.dig('firstRelease', 'shortVersion'),
last_release_short_version: issue.dig('lastRelease', 'shortVersion')
)
end
def map_to_error(issue)
Gitlab::ErrorTracking::Error.new( Gitlab::ErrorTracking::Error.new(
id: id, id: issue.fetch('id'),
first_seen: issue.fetch('firstSeen', nil), first_seen: issue.fetch('firstSeen', nil),
last_seen: issue.fetch('lastSeen', nil), last_seen: issue.fetch('lastSeen', nil),
title: issue.fetch('title', nil), title: issue.fetch('title', nil),
type: issue.fetch('type', nil), type: issue.fetch('type', nil),
user_count: issue.fetch('userCount', nil), user_count: issue.fetch('userCount', nil),
count: count, count: issue.fetch('count', nil),
message: message, message: issue.dig('metadata', 'value'),
culprit: issue.fetch('culprit', nil), culprit: issue.fetch('culprit', nil),
external_url: external_url, external_url: issue_url(issue.fetch('id')),
short_id: issue.fetch('shortId', nil), short_id: issue.fetch('shortId', nil),
status: issue.fetch('status', nil), status: issue.fetch('status', nil),
frequency: frequency, frequency: issue.dig('stats', '24h'),
project_id: issue.dig('project', 'id'), project_id: issue.dig('project', 'id'),
project_name: issue.dig('project', 'name'), project_name: issue.dig('project', 'name'),
project_slug: issue.dig('project', 'slug') project_slug: issue.dig('project', 'slug')
......
...@@ -6583,6 +6583,9 @@ msgstr "" ...@@ -6583,6 +6583,9 @@ msgstr ""
msgid "Error" msgid "Error"
msgstr "" msgstr ""
msgid "Error Details"
msgstr ""
msgid "Error Tracking" msgid "Error Tracking"
msgstr "" msgstr ""
......
...@@ -46,17 +46,6 @@ describe Projects::ErrorTrackingController do ...@@ -46,17 +46,6 @@ describe Projects::ErrorTrackingController do
end end
describe 'format json' do describe 'format json' do
shared_examples 'no data' do
it 'returns no data' do
get :index, params: project_params(format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/index')
expect(json_response['external_url']).to be_nil
expect(json_response['errors']).to eq([])
end
end
let(:list_issues_service) { spy(:list_issues_service) } let(:list_issues_service) { spy(:list_issues_service) }
let(:external_url) { 'http://example.com' } let(:external_url) { 'http://example.com' }
...@@ -66,6 +55,19 @@ describe Projects::ErrorTrackingController do ...@@ -66,6 +55,19 @@ describe Projects::ErrorTrackingController do
.and_return(list_issues_service) .and_return(list_issues_service)
end end
context 'no data' do
before do
expect(list_issues_service).to receive(:execute)
.and_return(status: :error, http_status: :no_content)
end
it 'returns no data' do
get :index, params: project_params(format: :json)
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'service result is successful' do context 'service result is successful' do
before do before do
expect(list_issues_service).to receive(:execute) expect(list_issues_service).to receive(:execute)
...@@ -232,8 +234,186 @@ describe Projects::ErrorTrackingController do ...@@ -232,8 +234,186 @@ describe Projects::ErrorTrackingController do
end end
end end
describe 'GET #issue_details' do
let_it_be(:issue_id) { 1234 }
let(:issue_details_service) { spy(:issue_details_service) }
let(:permitted_params) do
ActionController::Parameters.new(
{ issue_id: issue_id.to_s }
).permit!
end
before do
expect(ErrorTracking::IssueDetailsService)
.to receive(:new).with(project, user, permitted_params)
.and_return(issue_details_service)
end
describe 'format json' do
context 'no data' do
before do
expect(issue_details_service).to receive(:execute)
.and_return(status: :error, http_status: :no_content)
end
it 'returns no data' do
get :details, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'service result is successful' do
before do
expect(issue_details_service).to receive(:execute)
.and_return(status: :success, issue: error)
end
let(:error) { build(:detailed_error_tracking_error) }
it 'returns an error' do
get :details, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/issue_detailed')
expect(json_response['error']).to eq(error.as_json)
end
end
context 'service result is erroneous' do
let(:error_message) { 'error message' }
context 'without http_status' do
before do
expect(issue_details_service).to receive(:execute)
.and_return(status: :error, message: error_message)
end
it 'returns 400 with message' do
get :details, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq(error_message)
end
end
context 'with explicit http_status' do
let(:http_status) { :no_content }
before do
expect(issue_details_service).to receive(:execute).and_return(
status: :error,
message: error_message,
http_status: http_status
)
end
it 'returns http_status with message' do
get :details, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(http_status)
expect(json_response['message']).to eq(error_message)
end
end
end
end
end
describe 'GET #stack_trace' do
let_it_be(:issue_id) { 1234 }
let(:issue_stack_trace_service) { spy(:issue_stack_trace_service) }
let(:permitted_params) do
ActionController::Parameters.new(
{ issue_id: issue_id.to_s }
).permit!
end
before do
expect(ErrorTracking::IssueLatestEventService)
.to receive(:new).with(project, user, permitted_params)
.and_return(issue_stack_trace_service)
end
describe 'format json' do
context 'awaiting data' do
before do
expect(issue_stack_trace_service).to receive(:execute)
.and_return(status: :error, http_status: :no_content)
end
it 'returns no data' do
get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'service result is successful' do
before do
expect(issue_stack_trace_service).to receive(:execute)
.and_return(status: :success, latest_event: error_event)
end
let(:error_event) { build(:error_tracking_error_event) }
it 'returns an error' do
get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/issue_stack_trace')
expect(json_response['error']).to eq(error_event.as_json)
end
end
context 'service result is erroneous' do
let(:error_message) { 'error message' }
context 'without http_status' do
before do
expect(issue_stack_trace_service).to receive(:execute)
.and_return(status: :error, message: error_message)
end
it 'returns 400 with message' do
get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq(error_message)
end
end
context 'with explicit http_status' do
let(:http_status) { :no_content }
before do
expect(issue_stack_trace_service).to receive(:execute).and_return(
status: :error,
message: error_message,
http_status: http_status
)
end
it 'returns http_status with message' do
get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
expect(response).to have_gitlab_http_status(http_status)
expect(json_response['message']).to eq(error_message)
end
end
end
end
end
private private
def issue_params(opts = {})
project_params.reverse_merge(opts)
end
def project_params(opts = {}) def project_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace, project_id: project) opts.reverse_merge(namespace_id: project.namespace, project_id: project)
end end
......
# frozen_string_literal: true
FactoryBot.define do
factory :detailed_error_tracking_error, class: Gitlab::ErrorTracking::DetailedError do
id { 'id' }
title { 'title' }
type { 'error' }
user_count { 1 }
count { 2 }
first_seen { Time.now }
last_seen { Time.now }
message { 'message' }
culprit { 'culprit' }
external_url { 'http://example.com/id' }
external_base_url { 'http://example.com' }
project_id { 'project1' }
project_name { 'project name' }
project_slug { 'project_name' }
short_id { 'ID' }
status { 'unresolved' }
frequency { [] }
first_release_last_commit { '68c914da9' }
last_release_last_commit { '9ad419c86' }
first_release_short_version { 'abc123' }
last_release_short_version { 'abc123' }
skip_create
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :error_tracking_error_event, class: Gitlab::ErrorTracking::ErrorEvent do
issue_id { 'id' }
date_received { Time.now.iso8601 }
stack_trace_entries do
{
'stacktrace' =>
{
'frames' => [{ 'file' => 'test.rb' }]
}
}
end
skip_create
end
end
...@@ -4,7 +4,14 @@ ...@@ -4,7 +4,14 @@
"external_url", "external_url",
"last_seen", "last_seen",
"message", "message",
"type" "type",
"title",
"project_id",
"project_name",
"project_slug",
"short_id",
"status",
"frequency"
], ],
"properties" : { "properties" : {
"id": { "type": "string"}, "id": { "type": "string"},
...@@ -15,7 +22,14 @@ ...@@ -15,7 +22,14 @@
"culprit": { "type": "string" }, "culprit": { "type": "string" },
"count": { "type": "integer"}, "count": { "type": "integer"},
"external_url": { "type": "string" }, "external_url": { "type": "string" },
"user_count": { "type": "integer"} "user_count": { "type": "integer"},
"title": { "type": "string"},
"project_id": { "type": "string"},
"project_name": { "type": "string"},
"project_slug": { "type": "string"},
"short_id": { "type": "string"},
"status": { "type": "string"},
"frequency": { "type": "array"}
}, },
"additionalProperties": true "additionalProperties": false
} }
{
"type": "object",
"required" : [
"external_url",
"external_base_url",
"last_seen",
"message",
"type",
"title",
"project_id",
"project_name",
"project_slug",
"short_id",
"status",
"frequency",
"first_release_last_commit",
"last_release_last_commit",
"first_release_short_version",
"last_release_short_version"
],
"properties" : {
"id": { "type": "string"},
"first_seen": { "type": "string", "format": "date-time" },
"last_seen": { "type": "string", "format": "date-time" },
"type": { "type": "string" },
"message": { "type": "string" },
"culprit": { "type": "string" },
"count": { "type": "integer"},
"external_url": { "type": "string" },
"external_base_url": { "type": "string" },
"user_count": { "type": "integer"},
"title": { "type": "string"},
"project_id": { "type": "string"},
"project_name": { "type": "string"},
"project_slug": { "type": "string"},
"short_id": { "type": "string"},
"status": { "type": "string"},
"frequency": { "type": "array"},
"first_release_last_commit": { "type": ["string", "null"] },
"last_release_last_commit": { "type": ["string", "null"] },
"first_release_short_version": { "type": ["string", "null"] },
"last_release_short_version": { "type": ["string", "null"] }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"issue_id",
"stack_trace_entries",
"date_received"
],
"properties": {
"issue_id": { "type": ["string", "integer"] },
"stack_trace_entries": { "type": "object" },
"date_received": { "type": "string" }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"error"
],
"properties": {
"error": { "$ref": "error_detailed.json" }
},
"additionalProperties": false
}
{
"type": "object",
"required": [
"error"
],
"properties": {
"error": { "$ref": "error_stack_trace.json" }
},
"additionalProperties": false
}
# frozen_string_literal: true
require 'spec_helper'
describe ErrorTracking::IssueDetailsService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
let(:result) { subject.execute }
let(:error_tracking_setting) do
create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project)
end
subject { described_class.new(project, user) }
before do
expect(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting)
project.add_reporter(user)
end
describe '#execute' do
context 'with authorized user' do
context 'when issue_details returns a detailed error' do
let(:detailed_error) { build(:detailed_error_tracking_error) }
before do
expect(error_tracking_setting)
.to receive(:issue_details).and_return(issue: detailed_error)
end
it 'returns the detailed error' do
expect(result).to eq(status: :success, issue: detailed_error)
end
end
include_examples 'error tracking service data not ready', :issue_details
include_examples 'error tracking service sentry error handling', :issue_details
include_examples 'error tracking service http status handling', :issue_details
end
include_examples 'error tracking service unauthorized user'
include_examples 'error tracking service disabled'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ErrorTracking::IssueLatestEventService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
let(:result) { subject.execute }
let(:error_tracking_setting) do
create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project)
end
subject { described_class.new(project, user) }
before do
expect(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting)
project.add_reporter(user)
end
describe '#execute' do
context 'with authorized user' do
context 'when issue_latest_event returns an error event' do
let(:error_event) { build(:error_tracking_error_event) }
before do
expect(error_tracking_setting)
.to receive(:issue_latest_event).and_return(latest_event: error_event)
end
it 'returns the error event' do
expect(result).to eq(status: :success, latest_event: error_event)
end
end
include_examples 'error tracking service data not ready', :issue_latest_event
include_examples 'error tracking service sentry error handling', :issue_latest_event
include_examples 'error tracking service http status handling', :issue_latest_event
end
include_examples 'error tracking service unauthorized user'
include_examples 'error tracking service disabled'
end
end
...@@ -37,93 +37,12 @@ describe ErrorTracking::ListIssuesService do ...@@ -37,93 +37,12 @@ describe ErrorTracking::ListIssuesService do
end end
end end
context 'when list_sentry_issues returns nil' do include_examples 'error tracking service data not ready', :list_sentry_issues
before do include_examples 'error tracking service sentry error handling', :list_sentry_issues
expect(error_tracking_setting) include_examples 'error tracking service http status handling', :list_sentry_issues
.to receive(:list_sentry_issues).and_return(nil)
end
it 'result is not ready' do
expect(result).to eq(
status: :error, http_status: :no_content, message: 'Not ready. Try again later')
end
end
context 'when list_sentry_issues returns error' do
before do
allow(error_tracking_setting)
.to receive(:list_sentry_issues)
.and_return(
error: 'Sentry response status code: 401',
error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE
)
end
it 'returns the error' do
expect(result).to eq(
status: :error,
http_status: :bad_request,
message: 'Sentry response status code: 401'
)
end
end
context 'when list_sentry_issues returns error with http_status' do
before do
allow(error_tracking_setting)
.to receive(:list_sentry_issues)
.and_return(
error: 'Sentry API response is missing keys. key not found: "id"',
error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
)
end
it 'returns the error with correct http_status' do
expect(result).to eq(
status: :error,
http_status: :internal_server_error,
message: 'Sentry API response is missing keys. key not found: "id"'
)
end
end
end end
context 'with unauthorized user' do include_examples 'error tracking service unauthorized user'
let(:unauthorized_user) { create(:user) } include_examples 'error tracking service disabled'
subject { described_class.new(project, unauthorized_user) }
it 'returns error' do
result = subject.execute
expect(result).to include(
status: :error,
message: 'Access denied',
http_status: :unauthorized
)
end
end
context 'with error tracking disabled' do
before do
error_tracking_setting.enabled = false
end
it 'raises error' do
result = subject.execute
expect(result).to include(status: :error, message: 'Error Tracking is not enabled')
end
end
end
describe '#sentry_external_url' do
let(:external_url) { 'https://sentrytest.gitlab.com/sentry-org/sentry-project' }
it 'calls ErrorTracking::ProjectErrorTrackingSetting' do
expect(error_tracking_setting).to receive(:sentry_external_url).and_call_original
subject.external_url
end
end end
end end
...@@ -127,7 +127,7 @@ describe ErrorTracking::ListProjectsService do ...@@ -127,7 +127,7 @@ describe ErrorTracking::ListProjectsService do
end end
it 'returns error' do it 'returns error' do
expect(result).to include(status: :error, message: 'access denied') expect(result).to include(status: :error, message: 'Access denied', http_status: :unauthorized)
end end
end end
......
# frozen_string_literal: true
shared_examples 'error tracking service data not ready' do |service_call|
context "when #{service_call} returns nil" do
before do
expect(error_tracking_setting)
.to receive(service_call).and_return(nil)
end
it 'result is not ready' do
expect(result).to eq(
status: :error, http_status: :no_content, message: 'Not ready. Try again later')
end
end
end
shared_examples 'error tracking service sentry error handling' do |service_call|
context "when #{service_call} returns error" do
before do
allow(error_tracking_setting)
.to receive(service_call)
.and_return(
error: 'Sentry response status code: 401',
error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE
)
end
it 'returns the error' do
expect(result).to eq(
status: :error,
http_status: :bad_request,
message: 'Sentry response status code: 401'
)
end
end
end
shared_examples 'error tracking service http status handling' do |service_call|
context "when #{service_call} returns error with http_status" do
before do
allow(error_tracking_setting)
.to receive(service_call)
.and_return(
error: 'Sentry API response is missing keys. key not found: "id"',
error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
)
end
it 'returns the error with correct http_status' do
expect(result).to eq(
status: :error,
http_status: :internal_server_error,
message: 'Sentry API response is missing keys. key not found: "id"'
)
end
end
end
shared_examples 'error tracking service unauthorized user' do
context 'with unauthorized user' do
let(:unauthorized_user) { create(:user) }
subject { described_class.new(project, unauthorized_user) }
it 'returns error' do
result = subject.execute
expect(result).to include(
status: :error,
message: 'Access denied',
http_status: :unauthorized
)
end
end
end
shared_examples 'error tracking service disabled' do
context 'with error tracking disabled' do
before do
error_tracking_setting.enabled = false
end
it 'raises error' do
result = subject.execute
expect(result).to include(status: :error, message: 'Error Tracking is not enabled')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment