summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorReuben Pereira <rpereira@gitlab.com>2019-01-31 10:05:29 +0000
committerSean McGivern <sean@gitlab.com>2019-01-31 10:05:29 +0000
commit5841a7d5efe772a9a8f09144b84fcb9aee906c4c (patch)
treee966d385aea7c9a669ef7cd6d77a8874c14bb5de
parentab8b77a87c99a5b83050e2e9d588d4940288d754 (diff)
downloadgitlab-ce-5841a7d5efe772a9a8f09144b84fcb9aee906c4c.tar.gz
Update Sentry client to get project list
-rw-r--r--app/serializers/error_tracking/project_entity.rb7
-rw-r--r--app/serializers/error_tracking/project_serializer.rb7
-rw-r--r--lib/gitlab/error_tracking/project.rb16
-rw-r--r--lib/sentry/client.rb58
-rw-r--r--spec/factories/error_tracking/project.rb15
-rw-r--r--spec/fixtures/api/schemas/error_tracking/list_projects.json13
-rw-r--r--spec/fixtures/api/schemas/error_tracking/project.json19
-rw-r--r--spec/fixtures/sentry/list_projects_sample_response.json81
-rw-r--r--spec/lib/sentry/client_spec.rb159
9 files changed, 334 insertions, 41 deletions
diff --git a/app/serializers/error_tracking/project_entity.rb b/app/serializers/error_tracking/project_entity.rb
new file mode 100644
index 00000000000..405d87ca0d0
--- /dev/null
+++ b/app/serializers/error_tracking/project_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectEntity < Grape::Entity
+ expose(*Gitlab::ErrorTracking::Project::ACCESSORS)
+ end
+end
diff --git a/app/serializers/error_tracking/project_serializer.rb b/app/serializers/error_tracking/project_serializer.rb
new file mode 100644
index 00000000000..b2406f4d631
--- /dev/null
+++ b/app/serializers/error_tracking/project_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ProjectSerializer < BaseSerializer
+ entity ProjectEntity
+ end
+end
diff --git a/lib/gitlab/error_tracking/project.rb b/lib/gitlab/error_tracking/project.rb
new file mode 100644
index 00000000000..93e81da5034
--- /dev/null
+++ b/lib/gitlab/error_tracking/project.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ErrorTracking
+ class Project
+ include ActiveModel::Model
+
+ ACCESSORS = [
+ :id, :name, :status, :slug, :organization_name,
+ :organization_id, :organization_slug
+ ].freeze
+
+ attr_accessor(*ACCESSORS)
+ end
+ end
+end
diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb
index 343f2c49a7f..4187014d49e 100644
--- a/lib/sentry/client.rb
+++ b/lib/sentry/client.rb
@@ -3,6 +3,7 @@
module Sentry
class Client
Error = Class.new(StandardError)
+ SentryError = Class.new(StandardError)
attr_accessor :url, :token
@@ -16,6 +17,13 @@ module Sentry
map_to_errors(issues)
end
+ def list_projects
+ projects = get_projects
+ map_to_projects(projects)
+ rescue KeyError => e
+ raise Client::SentryError, "Sentry API response is missing keys. #{e.message}"
+ end
+
private
def request_params
@@ -27,18 +35,23 @@ module Sentry
}
end
- def get_issues(issue_status:, limit:)
- resp = Gitlab::HTTP.get(
- issues_api_url,
- **request_params.merge(query: {
- query: "is:#{issue_status}",
- limit: limit
- })
- )
+ def http_get(url, params = {})
+ resp = Gitlab::HTTP.get(url, **request_params.merge(params))
handle_response(resp)
end
+ def get_issues(issue_status:, limit:)
+ http_get(issues_api_url, query: {
+ query: "is:#{issue_status}",
+ limit: limit
+ })
+ end
+
+ def get_projects
+ http_get(projects_api_url)
+ end
+
def handle_response(response)
unless response.code == 200
raise Client::Error, "Sentry response error: #{response.code}"
@@ -47,6 +60,13 @@ module Sentry
response.as_json
end
+ def projects_api_url
+ projects_url = URI(@url)
+ projects_url.path = '/api/0/projects/'
+
+ projects_url
+ end
+
def issues_api_url
issues_url = URI(@url + '/issues/')
issues_url.path.squeeze!('/')
@@ -55,9 +75,11 @@ module Sentry
end
def map_to_errors(issues)
- issues.map do |issue|
- map_to_error(issue)
- end
+ issues.map(&method(:map_to_error))
+ end
+
+ def map_to_projects(projects)
+ projects.map(&method(:map_to_project))
end
def issue_url(id)
@@ -100,5 +122,19 @@ module Sentry
project_slug: project.fetch('slug', nil)
)
end
+
+ def map_to_project(project)
+ organization = project.fetch('organization')
+
+ Gitlab::ErrorTracking::Project.new(
+ id: project.fetch('id'),
+ name: project.fetch('name'),
+ slug: project.fetch('slug'),
+ status: project.dig('status'),
+ organization_name: organization.fetch('name'),
+ organization_id: organization.fetch('id'),
+ organization_slug: organization.fetch('slug')
+ )
+ end
end
end
diff --git a/spec/factories/error_tracking/project.rb b/spec/factories/error_tracking/project.rb
new file mode 100644
index 00000000000..5e9219b241f
--- /dev/null
+++ b/spec/factories/error_tracking/project.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :error_tracking_project, class: Gitlab::ErrorTracking::Project do
+ id '1'
+ name 'Sentry Example'
+ slug 'sentry-example'
+ status 'active'
+ organization_name 'Sentry'
+ organization_id '1'
+ organization_slug 'sentry'
+
+ skip_create
+ end
+end
diff --git a/spec/fixtures/api/schemas/error_tracking/list_projects.json b/spec/fixtures/api/schemas/error_tracking/list_projects.json
new file mode 100644
index 00000000000..2aaa525e38f
--- /dev/null
+++ b/spec/fixtures/api/schemas/error_tracking/list_projects.json
@@ -0,0 +1,13 @@
+{
+ "type": "object",
+ "required": [
+ "projects"
+ ],
+ "properties": {
+ "projects": {
+ "type": "array",
+ "items": { "$ref": "project.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/error_tracking/project.json b/spec/fixtures/api/schemas/error_tracking/project.json
new file mode 100644
index 00000000000..f6d611133c7
--- /dev/null
+++ b/spec/fixtures/api/schemas/error_tracking/project.json
@@ -0,0 +1,19 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "slug",
+ "organization_slug",
+ "name"
+ ],
+ "properties" : {
+ "id": { "type": "string"},
+ "name": { "type": "string" },
+ "slug": { "type": "string" },
+ "status": { "type": "string" },
+ "organization_name": { "type": "string" },
+ "organization_slug": { "type": "string" },
+ "organization_id": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/sentry/list_projects_sample_response.json b/spec/fixtures/sentry/list_projects_sample_response.json
new file mode 100644
index 00000000000..fd79b0d0f30
--- /dev/null
+++ b/spec/fixtures/sentry/list_projects_sample_response.json
@@ -0,0 +1,81 @@
+[
+ {
+ "status": "active",
+ "features": [
+ "data-forwarding",
+ "rate-limits",
+ "releases"
+ ],
+ "color": "#5c3fbf",
+ "isInternal": false,
+ "isPublic": false,
+ "dateCreated": "2018-12-11T10:41:22.476Z",
+ "id": "2",
+ "slug": "sentry-example",
+ "name": "sentry-example",
+ "hasAccess": true,
+ "isBookmarked": false,
+ "platform": "node",
+ "firstEvent": "2018-12-12T15:07:18Z",
+ "avatar": {
+ "avatarUuid": null,
+ "avatarType": "letter_avatar"
+ },
+ "isMember": true,
+ "organization": {
+ "status": {
+ "id": "active",
+ "name": "active"
+ },
+ "require2FA": false,
+ "avatar": {
+ "avatarUuid": null,
+ "avatarType": "letter_avatar"
+ },
+ "name": "Sentry",
+ "dateCreated": "2018-12-11T10:21:47.431Z",
+ "id": "1",
+ "isEarlyAdopter": false,
+ "slug": "sentry"
+ }
+ },
+ {
+ "status": "active",
+ "features": [
+ "data-forwarding",
+ "rate-limits"
+ ],
+ "color": "#bf873f",
+ "isInternal": true,
+ "isPublic": false,
+ "dateCreated": "2018-12-11T10:21:47.440Z",
+ "id": "1",
+ "slug": "internal",
+ "name": "Internal",
+ "hasAccess": true,
+ "isBookmarked": false,
+ "platform": null,
+ "firstEvent": "2018-12-11T10:54:35Z",
+ "avatar": {
+ "avatarUuid": null,
+ "avatarType": "letter_avatar"
+ },
+ "isMember": true,
+ "organization": {
+ "status": {
+ "id": "active",
+ "name": "active"
+ },
+ "require2FA": false,
+ "avatar": {
+ "avatarUuid": null,
+ "avatarType": "letter_avatar"
+ },
+ "name": "Sentry",
+ "dateCreated": "2018-12-11T10:21:47.431Z",
+ "id": "1",
+ "isEarlyAdopter": false,
+ "slug": "sentry"
+ }
+ }
+]
diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb
index b36be0fd9c1..6fbf60a6222 100644
--- a/spec/lib/sentry/client_spec.rb
+++ b/spec/lib/sentry/client_spec.rb
@@ -3,30 +3,76 @@
require 'spec_helper'
describe Sentry::Client do
- let(:issue_status) { 'unresolved' }
- let(:limit) { 20 }
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
- let(:sample_response) do
+ let(:issues_sample_response) do
Gitlab::Utils.deep_indifferent_access(
- JSON.parse(File.read(Rails.root.join('spec/fixtures/sentry/issues_sample_response.json')))
+ JSON.parse(fixture_file('sentry/issues_sample_response.json'))
+ )
+ end
+
+ let(:projects_sample_response) do
+ Gitlab::Utils.deep_indifferent_access(
+ JSON.parse(fixture_file('sentry/list_projects_sample_response.json'))
)
end
subject(:client) { described_class.new(sentry_url, token) }
- describe '#list_issues' do
- subject { client.list_issues(issue_status: issue_status, limit: limit) }
+ # Requires sentry_api_url and subject to be defined
+ shared_examples 'no redirects' do
+ let(:redirect_to) { 'https://redirected.example.com' }
+ let(:other_url) { 'https://other.example.org' }
+
+ let!(:redirected_req_stub) { stub_sentry_request(other_url) }
+
+ let!(:redirect_req_stub) do
+ stub_sentry_request(
+ sentry_api_url,
+ status: 302,
+ headers: { location: redirect_to }
+ )
+ end
- before do
- stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: sample_response)
+ it 'does not follow redirects' do
+ expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302')
+ expect(redirect_req_stub).to have_been_requested
+ expect(redirected_req_stub).not_to have_been_requested
end
+ end
- it 'returns objects of type ErrorTracking::Error' do
- expect(subject.length).to eq(1)
- expect(subject[0]).to be_a(Gitlab::ErrorTracking::Error)
+ shared_examples 'has correct return type' do |klass|
+ it "returns objects of type #{klass}" do
+ expect(subject).to all( be_a(klass) )
end
+ end
+
+ shared_examples 'has correct length' do |length|
+ it { expect(subject.length).to eq(length) }
+ end
+
+ # Requires sentry_api_request and subject to be defined
+ shared_examples 'calls sentry api' do
+ it 'calls sentry api' do
+ subject
+
+ expect(sentry_api_request).to have_been_requested
+ end
+ end
+
+ describe '#list_issues' do
+ let(:issue_status) { 'unresolved' }
+ let(:limit) { 20 }
+
+ let!(:sentry_api_request) { stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: issues_sample_response) }
+
+ subject { client.list_issues(issue_status: issue_status, limit: limit) }
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'has correct length', 1
context 'error object created from sentry response' do
using RSpec::Parameterized::TableSyntax
@@ -50,7 +96,7 @@ describe Sentry::Client do
end
with_them do
- it { expect(subject[0].public_send(error_object)).to eq(sample_response[0].dig(*sentry_response)) }
+ it { expect(subject[0].public_send(error_object)).to eq(issues_sample_response[0].dig(*sentry_response)) }
end
context 'external_url' do
@@ -61,24 +107,9 @@ describe Sentry::Client do
end
context 'redirects' do
- let(:redirect_to) { 'https://redirected.example.com' }
- let(:other_url) { 'https://other.example.org' }
-
- let!(:redirected_req_stub) { stub_sentry_request(other_url) }
-
- let!(:redirect_req_stub) do
- stub_sentry_request(
- sentry_url + '/issues/?limit=20&query=is:unresolved',
- status: 302,
- headers: { location: redirect_to }
- )
- end
+ let(:sentry_api_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }
- it 'does not follow redirects' do
- expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302')
- expect(redirect_req_stub).to have_been_requested
- expect(redirected_req_stub).not_to have_been_requested
- end
+ it_behaves_like 'no redirects'
end
# Sentry API returns 404 if there are extra slashes in the URL!
@@ -99,7 +130,75 @@ describe Sentry::Client do
anything
).and_call_original
- client.list_issues(issue_status: issue_status, limit: limit)
+ subject
+
+ expect(valid_req_stub).to have_been_requested
+ end
+ end
+ end
+
+ describe '#list_projects' do
+ let(:sentry_list_projects_url) { 'https://sentrytest.gitlab.com/api/0/projects/' }
+
+ let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: projects_sample_response) }
+
+ subject { client.list_projects }
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project
+ it_behaves_like 'has correct length', 2
+
+ context 'keys missing in API response' do
+ it 'raises exception' do
+ projects_sample_response[0].delete(:slug)
+
+ stub_sentry_request(sentry_list_projects_url, body: projects_sample_response)
+
+ expect { subject }.to raise_error(Sentry::Client::SentryError, 'Sentry API response is missing keys. key not found: "slug"')
+ end
+ end
+
+ context 'error object created from sentry response' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:sentry_project_object, :sentry_response) do
+ :id | :id
+ :name | :name
+ :status | :status
+ :slug | :slug
+ :organization_name | [:organization, :name]
+ :organization_id | [:organization, :id]
+ :organization_slug | [:organization, :slug]
+ end
+
+ with_them do
+ it { expect(subject[0].public_send(sentry_project_object)).to eq(projects_sample_response[0].dig(*sentry_response)) }
+ end
+ end
+
+ context 'redirects' do
+ let(:sentry_api_url) { sentry_list_projects_url }
+
+ it_behaves_like 'no redirects'
+ end
+
+ # Sentry API returns 404 if there are extra slashes in the URL!
+ context 'extra slashes in URL' do
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api//0/projects//' }
+ let(:client) { described_class.new(sentry_url, token) }
+
+ let!(:valid_req_stub) do
+ stub_sentry_request(sentry_list_projects_url)
+ end
+
+ it 'removes extra slashes in api url' do
+ expect(Gitlab::HTTP).to receive(:get).with(
+ URI(sentry_list_projects_url),
+ anything
+ ).and_call_original
+
+ subject
expect(valid_req_stub).to have_been_requested
end