Commit 6235833f authored by Alex Kalderimis's avatar Alex Kalderimis

Merge branch 'dz-error-tracking-store-collector' into 'master'

Add support for another et collector endpoint

See merge request gitlab-org/gitlab!69660
parents d5e2568b b79a4e9b
...@@ -9,9 +9,9 @@ module ErrorTracking ...@@ -9,9 +9,9 @@ module ErrorTracking
error = project.error_tracking_errors.report_error( error = project.error_tracking_errors.report_error(
name: exception['type'], # Example: ActionView::MissingTemplate name: exception['type'], # Example: ActionView::MissingTemplate
description: exception['value'], # Example: Missing template posts/show in... description: exception['value'], # Example: Missing template posts/show in...
actor: event['transaction'], # Example: PostsController#show actor: actor, # Example: PostsController#show
platform: event['platform'], # Example: ruby platform: event['platform'], # Example: ruby
timestamp: event['timestamp'] timestamp: timestamp
) )
# The payload field contains all the data on error including stacktrace in jsonb. # The payload field contains all the data on error including stacktrace in jsonb.
...@@ -20,7 +20,7 @@ module ErrorTracking ...@@ -20,7 +20,7 @@ module ErrorTracking
environment: event['environment'], environment: event['environment'],
description: exception['value'], description: exception['value'],
level: event['level'], level: event['level'],
occurred_at: event['timestamp'], occurred_at: timestamp,
payload: event payload: event
) )
end end
...@@ -34,5 +34,29 @@ module ErrorTracking ...@@ -34,5 +34,29 @@ module ErrorTracking
def exception def exception
event['exception']['values'].first event['exception']['values'].first
end end
def actor
return event['transaction'] if event['transaction']
# Some SDK do not have transaction attribute.
# So we build it by combining function name and module name from
# the last item in stacktrace.
last_line = exception.dig('stacktrace', 'frames').last
"#{last_line['function']}(#{last_line['module']})"
end
def timestamp
return @timestamp if @timestamp
@timestamp = (event['timestamp'] || Time.zone.now)
# Some SDK send timestamp in numeric format like '1630945472.13'.
if @timestamp.to_s =~ /\A\d+(\.\d+)?\z/
@timestamp = Time.zone.at(@timestamp.to_f)
end
@timestamp
end
end end
end end
...@@ -2,48 +2,15 @@ ...@@ -2,48 +2,15 @@
"description": "Error tracking event payload", "description": "Error tracking event payload",
"type": "object", "type": "object",
"required": [], "required": [],
"modules": {
"type": "object"
},
"properties": { "properties": {
"event_id": {
"type": "string"
},
"level": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"release": {
"type": "string"
},
"environment": { "environment": {
"type": "string" "type": "string"
}, },
"server_name": {
"type": "string"
},
"message": {
"type": "string"
},
"contexts": {
"type": "object"
},
"platform": { "platform": {
"type": "string" "type": "string"
}, },
"sdk": { "sdk": {
"type": "object", "type": "object"
"required": [],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
}
}
}, },
"exception": { "exception": {
"type": "object", "type": "object",
......
...@@ -8,6 +8,8 @@ module API ...@@ -8,6 +8,8 @@ module API
feature_category :error_tracking feature_category :error_tracking
content_type :envelope, 'application/x-sentry-envelope' content_type :envelope, 'application/x-sentry-envelope'
content_type :json, 'application/json'
content_type :txt, 'text/plain'
default_format :envelope default_format :envelope
before do before do
...@@ -33,17 +35,24 @@ module API ...@@ -33,17 +35,24 @@ module API
end end
def active_client_key? def active_client_key?
public_key = extract_public_key
find_client_key(public_key)
end
def extract_public_key
# Some SDK send public_key as a param. In this case we don't need to parse headers.
return params[:sentry_key] if params[:sentry_key].present?
begin begin
public_key = ::ErrorTracking::Collector::SentryAuthParser.parse(request)[:public_key] ::ErrorTracking::Collector::SentryAuthParser.parse(request)[:public_key]
rescue StandardError rescue StandardError
bad_request!('Failed to parse sentry request') bad_request!('Failed to parse sentry request')
end end
find_client_key(public_key)
end end
end end
desc 'Submit error tracking event to the project' do desc 'Submit error tracking event to the project as envelope' do
detail 'This feature was introduced in GitLab 14.1.' detail 'This feature was introduced in GitLab 14.1.'
end end
params do params do
...@@ -89,5 +98,38 @@ module API ...@@ -89,5 +98,38 @@ module API
# it is safe only for submission of new events. # it is safe only for submission of new events.
no_content! no_content!
end end
desc 'Submit error tracking event to the project' do
detail 'This feature was introduced in GitLab 14.1.'
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
post 'error_tracking/collector/api/:id/store' do
# There is a reason why we have such uncommon path.
# We depend on a client side error tracking software which
# modifies URL for its own reasons.
#
# When we give user a URL like this
# HOST/api/v4/error_tracking/collector/123
#
# Then error tracking software will convert it like this:
# HOST/api/v4/error_tracking/collector/api/123/store/
begin
parsed_body = Gitlab::Json.parse(request.body.read)
rescue StandardError
bad_request!('Failed to parse sentry request')
end
::ErrorTracking::CollectErrorService
.new(project, nil, event: parsed_body)
.execute
# Collector should never return any information back.
# Because DSN and public key are designed for public use,
# it is safe only for submission of new events.
no_content!
end
end end
end end
...@@ -7,6 +7,30 @@ RSpec.describe API::ErrorTrackingCollector do ...@@ -7,6 +7,30 @@ RSpec.describe API::ErrorTrackingCollector do
let_it_be(:setting) { create(:project_error_tracking_setting, :integrated, project: project) } let_it_be(:setting) { create(:project_error_tracking_setting, :integrated, project: project) }
let_it_be(:client_key) { create(:error_tracking_client_key, project: project) } let_it_be(:client_key) { create(:error_tracking_client_key, project: project) }
RSpec.shared_examples 'not found' do
it 'reponds with 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
RSpec.shared_examples 'bad request' do
it 'responds with 400' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
RSpec.shared_examples 'successful request' do
it 'writes to the database and returns no content' do
expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1)
expect(response).to have_gitlab_http_status(:no_content)
end
end
describe "POST /error_tracking/collector/api/:id/envelope" do describe "POST /error_tracking/collector/api/:id/envelope" do
let_it_be(:raw_event) { fixture_file('error_tracking/event.txt') } let_it_be(:raw_event) { fixture_file('error_tracking/event.txt') }
let_it_be(:url) { "/error_tracking/collector/api/#{project.id}/envelope" } let_it_be(:url) { "/error_tracking/collector/api/#{project.id}/envelope" }
...@@ -16,22 +40,6 @@ RSpec.describe API::ErrorTrackingCollector do ...@@ -16,22 +40,6 @@ RSpec.describe API::ErrorTrackingCollector do
subject { post api(url), params: params, headers: headers } subject { post api(url), params: params, headers: headers }
RSpec.shared_examples 'not found' do
it 'reponds with 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
RSpec.shared_examples 'bad request' do
it 'responds with 400' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'error tracking feature is disabled' do context 'error tracking feature is disabled' do
before do before do
setting.update!(enabled: false) setting.update!(enabled: false)
...@@ -88,10 +96,53 @@ RSpec.describe API::ErrorTrackingCollector do ...@@ -88,10 +96,53 @@ RSpec.describe API::ErrorTrackingCollector do
end end
end end
it 'writes to the database and returns no content' do it_behaves_like 'successful request'
expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1) end
expect(response).to have_gitlab_http_status(:no_content) describe "POST /error_tracking/collector/api/:id/store" do
let_it_be(:raw_event) { fixture_file('error_tracking/parsed_event.json') }
let_it_be(:url) { "/error_tracking/collector/api/#{project.id}/store" }
let(:params) { raw_event }
let(:headers) { { 'X-Sentry-Auth' => "Sentry sentry_key=#{client_key.public_key}" } }
subject { post api(url), params: params, headers: headers }
it_behaves_like 'successful request'
context 'empty headers' do
let(:headers) { {} }
it_behaves_like 'bad request'
end
context 'empty body' do
let(:params) { '' }
it_behaves_like 'bad request'
end
context 'sentry_key as param and empty headers' do
let(:url) { "/error_tracking/collector/api/#{project.id}/store?sentry_key=#{sentry_key}" }
let(:headers) { {} }
context 'key is wrong' do
let(:sentry_key) { 'glet_1fedb514e17f4b958435093deb02048c' }
it_behaves_like 'not found'
end
context 'key is empty' do
let(:sentry_key) { '' }
it_behaves_like 'bad request'
end
context 'key is correct' do
let(:sentry_key) { client_key.public_key }
it_behaves_like 'successful request'
end
end end
end end
end end
...@@ -40,5 +40,29 @@ RSpec.describe ErrorTracking::CollectErrorService do ...@@ -40,5 +40,29 @@ RSpec.describe ErrorTracking::CollectErrorService do
expect(event.environment).to eq 'development' expect(event.environment).to eq 'development'
expect(event.payload).to eq parsed_event expect(event.payload).to eq parsed_event
end end
context 'unusual payload' do
let(:modified_event) { parsed_event }
context 'missing transaction' do
it 'builds actor from stacktrace' do
modified_event.delete('transaction')
event = described_class.new(project, nil, event: modified_event).execute
expect(event.error.actor).to eq 'find()'
end
end
context 'timestamp is numeric' do
it 'parses timestamp' do
modified_event['timestamp'] = '1631015580.50'
event = described_class.new(project, nil, event: modified_event).execute
expect(event.occurred_at).to eq '2021-09-07T11:53:00.5'
end
end
end
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