Commit 125a1c92 authored by James Lopez's avatar James Lopez

Merge branch 'refactor-sentry-response' into 'master'

Provide pagination data in the Sentry's response

See merge request gitlab-org/gitlab!20305
parents 0fb95bbe d3e623ee
...@@ -55,6 +55,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -55,6 +55,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
render json: { render json: {
errors: serialize_errors(result[:issues]), errors: serialize_errors(result[:issues]),
pagination: result[:pagination],
external_url: service.external_url external_url: service.external_url
} }
end end
...@@ -111,7 +112,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController ...@@ -111,7 +112,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end end
def list_issues_params def list_issues_params
params.permit([:search_term, :sort]) params.permit(:search_term, :sort, :cursor)
end end
def list_projects_params def list_projects_params
......
...@@ -104,7 +104,7 @@ module ErrorTracking ...@@ -104,7 +104,7 @@ module ErrorTracking
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) } sentry_client.list_issues(**opts.symbolize_keys)
when 'issue_details' when 'issue_details'
{ {
issue: sentry_client.issue_details(**opts.symbolize_keys) issue: sentry_client.issue_details(**opts.symbolize_keys)
......
...@@ -6,37 +6,24 @@ module ErrorTracking ...@@ -6,37 +6,24 @@ module ErrorTracking
DEFAULT_LIMIT = 20 DEFAULT_LIMIT = 20
DEFAULT_SORT = 'last_seen' DEFAULT_SORT = 'last_seen'
def execute
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,
search_term: search_term,
sort: sort
)
# our results are not yet ready
unless result
return error('Not ready. Try again later', :no_content)
end
if result[:error].present?
return error(result[:error], http_status_for(result[:error_type]))
end
success(issues: result[:issues])
end
def external_url def external_url
project_error_tracking_setting&.sentry_external_url project_error_tracking_setting&.sentry_external_url
end end
private private
def fetch
project_error_tracking_setting.list_sentry_issues(
issue_status: issue_status,
limit: limit,
search_term: params[:search_term].presence,
sort: sort,
cursor: params[:cursor].presence
)
end
def parse_response(response) def parse_response(response)
{ issues: response[:issues] } response.slice(:issues, :pagination)
end end
def issue_status def issue_status
...@@ -47,18 +34,6 @@ module ErrorTracking ...@@ -47,18 +34,6 @@ module ErrorTracking
params[:limit] || DEFAULT_LIMIT params[:limit] || DEFAULT_LIMIT
end end
def search_term
params[:search_term].presence
end
def enabled?
project_error_tracking_setting&.enabled?
end
def can_read?
can?(current_user, :read_sentry_issue, project)
end
def sort def sort
params[:sort] || DEFAULT_SORT params[:sort] || DEFAULT_SORT
end end
......
...@@ -34,12 +34,18 @@ module Sentry ...@@ -34,12 +34,18 @@ module Sentry
end end
def list_issues(**keyword_args) def list_issues(**keyword_args)
issues = get_issues(keyword_args) response = get_issues(keyword_args)
issues = response[:issues]
pagination = response[:pagination]
validate_size(issues) validate_size(issues)
handle_mapping_exceptions do handle_mapping_exceptions do
map_to_errors(issues) {
issues: map_to_errors(issues),
pagination: pagination
}
end end
end end
...@@ -83,36 +89,40 @@ module Sentry ...@@ -83,36 +89,40 @@ module Sentry
end end
def get_issues(**keyword_args) def get_issues(**keyword_args)
http_get( response = http_get(
issues_api_url, issues_api_url,
query: list_issue_sentry_query(keyword_args) query: list_issue_sentry_query(keyword_args)
) )
{
issues: response[:body],
pagination: Sentry::PaginationParser.parse(response[:headers])
}
end end
def list_issue_sentry_query(issue_status:, limit:, sort: nil, search_term: '') def list_issue_sentry_query(issue_status:, limit:, sort: nil, search_term: '', cursor: nil)
unless SENTRY_API_SORT_VALUE_MAP.key?(sort) unless SENTRY_API_SORT_VALUE_MAP.key?(sort)
raise BadRequestError, 'Invalid value for sort param' raise BadRequestError, 'Invalid value for sort param'
end end
query_params = { {
query: "is:#{issue_status} #{search_term}".strip, query: "is:#{issue_status} #{search_term}".strip,
limit: limit, limit: limit,
sort: SENTRY_API_SORT_VALUE_MAP[sort] sort: SENTRY_API_SORT_VALUE_MAP[sort],
} cursor: cursor
}.compact
query_params.compact
end end
def get_issue(issue_id:) def get_issue(issue_id:)
http_get(issue_api_url(issue_id)) http_get(issue_api_url(issue_id))[:body]
end end
def get_issue_latest_event(issue_id:) def get_issue_latest_event(issue_id:)
http_get(issue_latest_event_api_url(issue_id)) http_get(issue_latest_event_api_url(issue_id))[:body]
end end
def get_projects def get_projects
http_get(projects_api_url) http_get(projects_api_url)[:body]
end end
def handle_request_exceptions def handle_request_exceptions
...@@ -138,7 +148,7 @@ module Sentry ...@@ -138,7 +148,7 @@ module Sentry
raise_error "Sentry response status code: #{response.code}" raise_error "Sentry response status code: #{response.code}"
end end
response.parsed_response { body: response.parsed_response, headers: response.headers }
end end
def raise_error(message) def raise_error(message)
......
# frozen_string_literal: true
module Sentry
module PaginationParser
PATTERN = /rel=\"(?<direction>\w+)\";\sresults=\"(?<results>\w+)\";\scursor=\"(?<cursor>.+)\"/.freeze
def self.parse(headers)
links = headers['link'].to_s.split(',')
links.map { |link| parse_link(link) }.compact.to_h
end
def self.parse_link(link)
match = link.match(PATTERN)
return unless match
return if match['results'] != "true"
[match['direction'], { 'cursor' => match['cursor'] }]
end
private_class_method :parse_link
end
end
...@@ -50,8 +50,6 @@ describe Projects::ErrorTrackingController do ...@@ -50,8 +50,6 @@ describe Projects::ErrorTrackingController do
let(:external_url) { 'http://example.com' } let(:external_url) { 'http://example.com' }
context 'no data' do context 'no data' do
let(:params) { project_params(format: :json) }
let(:permitted_params) do let(:permitted_params) do
ActionController::Parameters.new({}).permit! ActionController::Parameters.new({}).permit!
end end
...@@ -72,11 +70,13 @@ describe Projects::ErrorTrackingController do ...@@ -72,11 +70,13 @@ describe Projects::ErrorTrackingController do
end end
end end
context 'with a search_term and sort params' do context 'with extra params' do
let(:params) { project_params(format: :json, search_term: 'something', sort: 'last_seen') } let(:cursor) { '1572959139000:0:0' }
let(:search_term) { 'something' }
let(:sort) { 'last_seen' }
let(:params) { project_params(format: :json, search_term: search_term, sort: sort, cursor: cursor) }
let(:permitted_params) do let(:permitted_params) do
ActionController::Parameters.new(search_term: 'something', sort: 'last_seen').permit! ActionController::Parameters.new(search_term: search_term, sort: sort, cursor: cursor).permit!
end end
before do before do
...@@ -88,7 +88,7 @@ describe Projects::ErrorTrackingController do ...@@ -88,7 +88,7 @@ describe Projects::ErrorTrackingController do
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)
.and_return(status: :success, issues: [error]) .and_return(status: :success, issues: [error], pagination: {})
expect(list_issues_service).to receive(:external_url) expect(list_issues_service).to receive(:external_url)
.and_return(external_url) .and_return(external_url)
end end
...@@ -100,13 +100,16 @@ describe Projects::ErrorTrackingController do ...@@ -100,13 +100,16 @@ describe Projects::ErrorTrackingController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/index') expect(response).to match_response_schema('error_tracking/index')
expect(json_response['external_url']).to eq(external_url) expect(json_response).to eq(
expect(json_response['errors']).to eq([error].as_json) 'errors' => [error].as_json,
'pagination' => {},
'external_url' => external_url
)
end end
end end
end end
context 'without params' do context 'without extra params' do
before do before do
expect(ErrorTracking::ListIssuesService) expect(ErrorTracking::ListIssuesService)
.to receive(:new).with(project, user, {}) .to receive(:new).with(project, user, {})
...@@ -116,7 +119,7 @@ describe Projects::ErrorTrackingController do ...@@ -116,7 +119,7 @@ describe Projects::ErrorTrackingController do
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)
.and_return(status: :success, issues: [error]) .and_return(status: :success, issues: [error], pagination: {})
expect(list_issues_service).to receive(:external_url) expect(list_issues_service).to receive(:external_url)
.and_return(external_url) .and_return(external_url)
end end
...@@ -128,8 +131,11 @@ describe Projects::ErrorTrackingController do ...@@ -128,8 +131,11 @@ describe Projects::ErrorTrackingController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('error_tracking/index') expect(response).to match_response_schema('error_tracking/index')
expect(json_response['external_url']).to eq(external_url) expect(json_response).to eq(
expect(json_response['errors']).to eq([error].as_json) 'errors' => [error].as_json,
'pagination' => {},
'external_url' => external_url
)
end end
end end
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
"type": "object", "type": "object",
"required": [ "required": [
"external_url", "external_url",
"pagination",
"errors" "errors"
], ],
"properties": { "properties": {
...@@ -9,6 +10,9 @@ ...@@ -9,6 +10,9 @@
"errors": { "errors": {
"type": "array", "type": "array",
"items": { "$ref": "error.json" } "items": { "$ref": "error.json" }
},
"pagination": {
"type": "object"
} }
}, },
"additionalProperties": false "additionalProperties": false
......
...@@ -54,10 +54,20 @@ describe Sentry::Client do ...@@ -54,10 +54,20 @@ describe Sentry::Client do
end end
end end
shared_examples 'issues has correct return type' do |klass|
it "returns objects of type #{klass}" do
expect(subject[:issues]).to all( be_a(klass) )
end
end
shared_examples 'has correct length' do |length| shared_examples 'has correct length' do |length|
it { expect(subject.length).to eq(length) } it { expect(subject.length).to eq(length) }
end end
shared_examples 'issues has correct length' do |length|
it { expect(subject[:issues].length).to eq(length) }
end
# Requires sentry_api_request and subject to be defined # Requires sentry_api_request and subject to be defined
shared_examples 'calls sentry api' do shared_examples 'calls sentry api' do
it 'calls sentry api' do it 'calls sentry api' do
...@@ -95,26 +105,44 @@ describe Sentry::Client do ...@@ -95,26 +105,44 @@ describe Sentry::Client do
let(:issue_status) { 'unresolved' } let(:issue_status) { 'unresolved' }
let(:limit) { 20 } let(:limit) { 20 }
let(:search_term) { '' } let(:search_term) { '' }
let(:cursor) { nil }
let(:sort) { 'last_seen' }
let(:sentry_api_response) { issues_sample_response } let(:sentry_api_response) { issues_sample_response }
let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' } let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }
let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) } let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }
subject { client.list_issues(issue_status: issue_status, limit: limit, search_term: search_term, sort: 'last_seen') } subject { client.list_issues(issue_status: issue_status, limit: limit, search_term: search_term, sort: sort, cursor: cursor) }
it_behaves_like 'calls sentry api' it_behaves_like 'calls sentry api'
it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'has correct length', 1 it_behaves_like 'issues has correct length', 1
shared_examples 'has correct external_url' do shared_examples 'has correct external_url' do
context 'external_url' do context 'external_url' do
it 'is constructed correctly' do it 'is constructed correctly' do
expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11') expect(subject[:issues][0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11')
end end
end end
end end
context 'when response has a pagination info' do
let(:headers) do
{
link: '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
}
end
let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response, headers: headers) }
it 'parses the pagination' do
expect(subject[:pagination]).to eq(
'previous' => { 'cursor' => '1573556671000:0:1' },
'next' => { 'cursor' => '1572959139000:0:0' }
)
end
end
context 'error object created from sentry response' do context 'error object created from sentry response' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
...@@ -137,7 +165,7 @@ describe Sentry::Client do ...@@ -137,7 +165,7 @@ describe Sentry::Client do
end end
with_them do with_them do
it { expect(subject[0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) } it { expect(subject[:issues][0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) }
end end
it_behaves_like 'has correct external_url' it_behaves_like 'has correct external_url'
...@@ -210,8 +238,8 @@ describe Sentry::Client do ...@@ -210,8 +238,8 @@ describe Sentry::Client do
it_behaves_like 'calls sentry api' it_behaves_like 'calls sentry api'
it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'has correct length', 1 it_behaves_like 'issues has correct length', 1
it_behaves_like 'has correct external_url' it_behaves_like 'has correct external_url'
end end
...@@ -240,13 +268,23 @@ describe Sentry::Client do ...@@ -240,13 +268,23 @@ describe Sentry::Client do
it_behaves_like 'maps exceptions' it_behaves_like 'maps exceptions'
context 'when search term is present' do context 'when search term is present' do
let(:search_term) { 'NoMethodError'} let(:search_term) { 'NoMethodError' }
let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&query=is:unresolved NoMethodError" } let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&query=is:unresolved NoMethodError" }
it_behaves_like 'calls sentry api' it_behaves_like 'calls sentry api'
it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'has correct length', 1 it_behaves_like 'issues has correct length', 1
end
context 'when cursor is present' do
let(:cursor) { '1572959139000:0:0' }
let(:sentry_request_url) { "#{sentry_url}/issues/?limit=20&cursor=#{cursor}&query=is:unresolved" }
it_behaves_like 'calls sentry api'
it_behaves_like 'issues has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'issues has correct length', 1
end end
end end
......
# frozen_string_literal: true
require 'fast_spec_helper'
require 'support/helpers/fixture_helpers'
describe Sentry::PaginationParser do
include FixtureHelpers
describe '.parse' do
subject { described_class.parse(headers) }
context 'when headers do not have "link" param' do
let(:headers) { {} }
it 'returns empty hash' do
is_expected.to eq({})
end
end
context 'when headers.link has previous and next pages' do
let(:headers) do
{
'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
}
end
it 'returns info about both pages' do
is_expected.to eq(
'previous' => { 'cursor' => '1573556671000:0:1' },
'next' => { 'cursor' => '1572959139000:0:0' }
)
end
end
context 'when headers.link has only next page' do
let(:headers) do
{
'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="false"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="true"; cursor="1572959139000:0:0"'
}
end
it 'returns only info about the next page' do
is_expected.to eq(
'next' => { 'cursor' => '1572959139000:0:0' }
)
end
end
context 'when headers.link has only previous page' do
let(:headers) do
{
'link' => '<https://sentrytest.gitlab.com>; rel="previous"; results="true"; cursor="1573556671000:0:1", <https://sentrytest.gitlab.com>; rel="next"; results="false"; cursor="1572959139000:0:0"'
}
end
it 'returns only info about the previous page' do
is_expected.to eq(
'previous' => { 'cursor' => '1573556671000:0:1' }
)
end
end
end
end
...@@ -153,9 +153,9 @@ describe ErrorTracking::ProjectErrorTrackingSetting do ...@@ -153,9 +153,9 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
it 'returns cached issues' do it 'returns cached issues' do
expect(sentry_client).to receive(:list_issues).with(opts) expect(sentry_client).to receive(:list_issues).with(opts)
.and_return(issues) .and_return(issues: issues, pagination: {})
expect(result).to eq(issues: issues) expect(result).to eq(issues: issues, pagination: {})
end end
end end
......
...@@ -5,13 +5,14 @@ require 'spec_helper' ...@@ -5,13 +5,14 @@ require 'spec_helper'
describe ErrorTracking::ListIssuesService do describe ErrorTracking::ListIssuesService do
set(:user) { create(:user) } set(:user) { create(:user) }
set(:project) { create(:project) } set(:project) { create(:project) }
let(:params) { { search_term: 'something', sort: 'last_seen' } } let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }
let(:list_sentry_issues_args) do let(:list_sentry_issues_args) do
{ {
issue_status: 'unresolved', issue_status: 'unresolved',
limit: 20, limit: 20,
search_term: params[:search_term], search_term: 'something',
sort: params[:sort] sort: 'last_seen',
cursor: 'some-cursor'
} }
end end
...@@ -40,11 +41,11 @@ describe ErrorTracking::ListIssuesService do ...@@ -40,11 +41,11 @@ describe ErrorTracking::ListIssuesService do
expect(error_tracking_setting) expect(error_tracking_setting)
.to receive(:list_sentry_issues) .to receive(:list_sentry_issues)
.with(list_sentry_issues_args) .with(list_sentry_issues_args)
.and_return(issues: issues) .and_return(issues: issues, pagination: {})
end end
it 'returns the issues' do it 'returns the issues' do
expect(result).to eq(status: :success, issues: issues) expect(result).to eq(status: :success, pagination: {}, issues: issues)
end 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