diff options
Diffstat (limited to 'lib/error_tracking/sentry_client/issue.rb')
-rw-r--r-- | lib/error_tracking/sentry_client/issue.rb | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/lib/error_tracking/sentry_client/issue.rb b/lib/error_tracking/sentry_client/issue.rb new file mode 100644 index 00000000000..513fb3daabe --- /dev/null +++ b/lib/error_tracking/sentry_client/issue.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +module ErrorTracking + class SentryClient + module Issue + BadRequestError = Class.new(StandardError) + ResponseInvalidSizeError = Class.new(StandardError) + + SENTRY_API_SORT_VALUE_MAP = { + # <accepted_by_client> => <accepted_by_sentry_api> + 'frequency' => 'freq', + 'first_seen' => 'new', + 'last_seen' => nil + }.freeze + + def list_issues(**keyword_args) + response = get_issues(**keyword_args) + + issues = response[:issues] + pagination = response[:pagination] + + validate_size(issues) + + handle_mapping_exceptions do + { + issues: map_to_errors(issues), + pagination: pagination + } + end + end + + def issue_details(issue_id:) + issue = get_issue(issue_id: issue_id) + + map_to_detailed_error(issue) + end + + def update_issue(issue_id:, params:) + http_put(api_urls.issue_url(issue_id), params)[:body] + end + + private + + def get_issues(**keyword_args) + response = http_get( + api_urls.issues_url, + query: list_issue_sentry_query(**keyword_args) + ) + + { + issues: response[:body], + pagination: SentryClient::PaginationParser.parse(response[:headers]) + } + end + + def list_issue_sentry_query(issue_status:, limit:, sort: nil, search_term: '', cursor: nil) + unless SENTRY_API_SORT_VALUE_MAP.key?(sort) + raise BadRequestError, 'Invalid value for sort param' + end + + { + query: "is:#{issue_status} #{search_term}".strip, + limit: limit, + sort: SENTRY_API_SORT_VALUE_MAP[sort], + cursor: cursor + }.compact + end + + def validate_size(issues) + return if Gitlab::Utils::DeepSize.new(issues).valid? + + raise ResponseInvalidSizeError, "Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}." + end + + def get_issue(issue_id:) + http_get(api_urls.issue_url(issue_id))[:body] + end + + def parse_gitlab_issue(issue) + parse_issue_annotations(issue) || parse_plugin_issue(issue) + end + + def parse_issue_annotations(issue) + issue + .fetch('annotations', []) + .reject(&:blank?) + .map { |annotation| Nokogiri.make(annotation) } + .find { |html| html['href']&.starts_with?(Gitlab.config.gitlab.url) } + .try(:[], 'href') + end + + def parse_plugin_issue(issue) + plugin_issues = issue.fetch('pluginIssues', nil) + return unless plugin_issues + + gitlab_plugin = plugin_issues.detect { |item| item['id'] == 'gitlab' } + return unless gitlab_plugin + + gitlab_plugin.dig('issue', 'url') + end + + def issue_url(id) + parse_sentry_url("#{url}/issues/#{id}") + 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!('/') + # Remove trailing slash + uri = uri.to_s.delete_suffix('/') + + uri + end + + def map_to_errors(issues) + issues.map(&method(:map_to_error)) + end + + def map_to_error(issue) + Gitlab::ErrorTracking::Error.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')), + 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') + ) + 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), + tags: extract_tags(issue), + 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'), + gitlab_issue: parse_gitlab_issue(issue), + 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'), + first_release_short_version: issue.dig('firstRelease', 'shortVersion'), + first_release_version: issue.dig('firstRelease', 'version'), + last_release_last_commit: issue.dig('lastRelease', 'lastCommit'), + last_release_short_version: issue.dig('lastRelease', 'shortVersion'), + last_release_version: issue.dig('lastRelease', 'version') + }) + end + + def extract_tags(issue) + { + level: issue.fetch('level', nil), + logger: issue.fetch('logger', nil) + } + end + end + end +end |