Commit b2a93504 authored by Will Meek's avatar Will Meek Committed by Andrejs Cunskis

Adds ability to log sentry urls for failed API calls

Adds some extra handling around failed api calls that maps
a correlation id to a sentry url for live environments.
If the test is run in a dyanmic environment, the correlation id
will still be logged.
parent eee5d278
......@@ -54,7 +54,7 @@ module QA
body)
unless response.code == HTTP_STATUS_OK
raise ResourceFabricationFailedError, "Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
raise ResourceFabricationFailedError, "Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
end
process_api_response(parse_body(response))
......@@ -91,9 +91,9 @@ module QA
response = get(request.url)
if response.code == HTTP_STATUS_SERVER_ERROR
raise InternalServerError, "Failed to GET #{request.mask_url} - (#{response.code}): `#{response}`."
raise InternalServerError, "Failed to GET #{request.mask_url} - (#{response.code}): `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
elsif response.code != HTTP_STATUS_OK
raise ResourceNotFoundError, "Resource at #{request.mask_url} could not be found (#{response.code}): `#{response}`."
raise ResourceNotFoundError, "Resource at #{request.mask_url} could not be found (#{response.code}): `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
end
@api_fabrication_http_method = :get # rubocop:disable Gitlab/ModuleWithInstanceVariables
......@@ -114,6 +114,7 @@ module QA
unless graphql_response.code == HTTP_STATUS_OK && (body[:errors].nil? || body[:errors].empty?)
raise(ResourceFabricationFailedError, <<~MSG)
Fabrication of #{self.class.name} using the API failed (#{graphql_response.code}) with `#{graphql_response}`.
#{QA::Support::Loglinking.failure_metadata(graphql_response.headers[:x_request_id])}
MSG
end
......@@ -126,7 +127,7 @@ module QA
unless response.code == HTTP_STATUS_CREATED
raise(
ResourceFabricationFailedError,
"Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
"Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
)
end
......@@ -145,7 +146,7 @@ module QA
response = delete(request.url)
unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code
raise ResourceNotDeletedError, "Resource at #{request.mask_url} could not be deleted (#{response.code}): `#{response}`."
raise ResourceNotDeletedError, "Resource at #{request.mask_url} could not be deleted (#{response.code}): `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
end
response
......
# frozen_string_literal: true
module QA
module Support
module Loglinking
# Static address variables declared for mapping environment to logging URLs
STAGING_ADDRESS = 'https://staging.gitlab.com'
STAGING_REF_ADDRESS = 'https://staging-ref.gitlab.com'
PRODUCTION_ADDRESS = 'https://www.gitlab.com'
PRE_PROD_ADDRESS = 'https://pre.gitlab.com'
SENTRY_ENVIRONMENTS = {
staging: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
staging_canary: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
staging_ref: 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
pre: 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
canary: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
production: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny'
}.freeze
KIBANA_ENVIRONMENTS = {
staging: 'https://nonprod-log.gitlab.net/',
staging_canary: 'https://nonprod-log.gitlab.net/',
canary: 'https://log.gprd.gitlab.net/',
production: 'https://log.gprd.gitlab.net/'
}.freeze
def self.failure_metadata(correlation_id)
return if correlation_id.blank?
sentry_uri = sentry_url
kibana_uri = kibana_url
errors = ["Correlation Id: #{correlation_id}"]
errors << "Sentry Url: #{sentry_uri}&query=correlation_id%3A%22#{correlation_id}%22" if sentry_uri
errors << "Kibana Url: #{kibana_uri}app/discover#/?_a=(query:(language:kuery,query:'json.correlation_id%20:%20#{correlation_id}'))" if kibana_uri
errors.join("\n")
end
def self.sentry_url
return unless logging_environment?
SENTRY_ENVIRONMENTS[logging_environment]
end
def self.kibana_url
return unless logging_environment?
KIBANA_ENVIRONMENTS[logging_environment]
end
def self.logging_environment
address = QA::Runtime::Scenario.attributes[:gitlab_address]
return if address.nil?
case address
when STAGING_ADDRESS
canary? ? :staging_canary : :staging
when STAGING_REF_ADDRESS
:staging_ref
when PRODUCTION_ADDRESS
canary? ? :canary : :production
when PRE_PROD_ADDRESS
:pre
else
nil
end
end
def self.logging_environment?
!logging_environment.nil?
end
def self.cookies
browser_cookies = Capybara.current_session.driver.browser.manage.all_cookies
# rubocop:disable Rails/IndexBy
browser_cookies.each_with_object({}) do |cookie, memo|
memo[cookie[:name]] = cookie
end
# rubocop:enable Rails/IndexBy
end
def self.canary?
cookies.dig('gitlab_canary', :value) == 'true'
end
end
end
end
......@@ -5,14 +5,28 @@ module QA
class PageErrorChecker
class << self
def report!(page, error_code)
request_id_string = ''
if error_code == 500
request_id = parse_five_c_page_request_id(page)
if request_id
request_id_string = "\n\n" + Loglinking.failure_metadata(request_id)
end
end
report = if QA::Runtime::Env.browser == :chrome
return_chrome_errors(page, error_code)
else
status_code_report(error_code)
end
raise "#{report}\n\n"\
"Path: #{page.current_path}"
raise "Error Code #{error_code}\n\n"\
"#{report}\n\n"\
"Path: #{page.current_path}"\
"#{request_id_string}"
end
def parse_five_c_page_request_id(page)
Nokogiri::HTML.parse(page.html).xpath("/html/body/div/p[1]/code").children.first
end
def return_chrome_errors(page, error_code)
......
......@@ -108,15 +108,58 @@ RSpec.describe QA::Resource::ApiFabricator do
context 'when the POST fails' do
let(:post_response) { { error: "Name already taken." } }
let(:raw_post) { double('Raw POST response', code: 400, body: post_response.to_json) }
let(:raw_post) { double('Raw POST response', code: 400, body: post_response.to_json, headers: {}) }
it 'raises a ResourceFabricationFailedError exception' do
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(nil)
expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.")
expect { subject.fabricate_via_api! }.to raise_error do |error|
expect(error.class).to eql(described_class::ResourceFabricationFailedError)
expect(error.to_s).to eql(<<~ERROR.chomp)
Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.\n
ERROR
end
expect(subject.api_resource).to be_nil
end
it 'logs a correlation id' do
response = double('Raw POST response', code: 400, body: post_response.to_json, headers: { x_request_id: 'foobar' })
allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(nil)
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(response)
expect { subject.fabricate_via_api! }.to raise_error do |error|
expect(error.class).to eql(described_class::ResourceFabricationFailedError)
expect(error.to_s).to eql(<<~ERROR.chomp)
Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.
Correlation Id: foobar
ERROR
end
end
it 'logs a sentry url from staging' do
response = double('Raw POST response', code: 400, body: post_response.to_json, headers: { x_request_id: 'foobar' })
cookies = [{ name: 'Foo', value: 'Bar' }, { name: 'gitlab_canary', value: 'true' }]
allow(Capybara.current_session).to receive_message_chain(:driver, :browser, :manage, :all_cookies).and_return(cookies)
allow(QA::Runtime::Scenario).to receive(:attributes).and_return({ gitlab_address: 'https://staging.gitlab.com' })
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(response)
expect { subject.fabricate_via_api! }.to raise_error do |error|
expect(error.class).to eql(described_class::ResourceFabricationFailedError)
expect(error.to_s).to eql(<<~ERROR.chomp)
Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.
Correlation Id: foobar
Sentry Url: https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny&query=correlation_id%3A%22foobar%22
Kibana Url: https://nonprod-log.gitlab.net/app/discover#/?_a=(query:(language:kuery,query:'json.correlation_id%20:%20foobar'))
ERROR
end
end
end
end
......
# frozen_string_literal: true
RSpec.describe QA::Support::Loglinking do
describe '.failure_metadata' do
context 'return nil string' do
it 'if correlation_id is empty' do
expect(QA::Support::Loglinking.failure_metadata('')).to eq(nil)
end
it 'if correlation_id is nil' do
expect(QA::Support::Loglinking.failure_metadata(nil)).to eq(nil)
end
end
context 'return error string' do
it 'with sentry URL' do
allow(QA::Support::Loglinking).to receive(:sentry_url).and_return('https://sentry.address/?environment=bar')
allow(QA::Support::Loglinking).to receive(:kibana_url).and_return(nil)
expect(QA::Support::Loglinking.failure_metadata('foo123')).to eql(<<~ERROR.chomp)
Correlation Id: foo123
Sentry Url: https://sentry.address/?environment=bar&query=correlation_id%3A%22foo123%22
ERROR
end
it 'with kibana URL' do
allow(QA::Support::Loglinking).to receive(:sentry_url).and_return(nil)
allow(QA::Support::Loglinking).to receive(:kibana_url).and_return('https://kibana.address/')
expect(QA::Support::Loglinking.failure_metadata('foo123')).to eql(<<~ERROR.chomp)
Correlation Id: foo123
Kibana Url: https://kibana.address/app/discover#/?_a=(query:(language:kuery,query:'json.correlation_id%20:%20foo123'))
ERROR
end
end
end
describe '.sentry_url' do
let(:url_hash) do
{
:staging => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
:staging_canary => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
:staging_ref => 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
:pre => 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
:canary => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
:production => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny',
:foo => nil,
nil => nil
}
end
it 'returns sentry URL if environment found' do
url_hash.each do |environment, url|
allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(environment)
expect(QA::Support::Loglinking.sentry_url).to eq(url)
end
end
end
describe '.kibana_url' do
let(:url_hash) do
{
:staging => 'https://nonprod-log.gitlab.net/',
:staging_canary => 'https://nonprod-log.gitlab.net/',
:staging_ref => nil,
:pre => nil,
:canary => 'https://log.gprd.gitlab.net/',
:production => 'https://log.gprd.gitlab.net/',
:foo => nil,
nil => nil
}
end
it 'returns kibana URL if environment found' do
url_hash.each do |environment, url|
allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(environment)
expect(QA::Support::Loglinking.kibana_url).to eq(url)
end
end
end
describe '.logging_environment' do
let(:staging_address) { 'https://staging.gitlab.com' }
let(:staging_ref_address) { 'https://staging-ref.gitlab.com' }
let(:production_address) { 'https://www.gitlab.com' }
let(:pre_prod_address) { 'https://pre.gitlab.com' }
let(:logging_env_array) do
[
{
address: staging_address,
canary: false,
expected_env: :staging
},
{
address: staging_address,
canary: true,
expected_env: :staging_canary
},
{
address: staging_ref_address,
canary: true,
expected_env: :staging_ref
},
{
address: production_address,
canary: false,
expected_env: :production
},
{
address: production_address,
canary: true,
expected_env: :canary
},
{
address: pre_prod_address,
canary: true,
expected_env: :pre
},
{
address: 'https://foo.com',
canary: true,
expected_env: nil
}
]
end
it 'returns logging environment if environment found' do
logging_env_array.each do |logging_env_hash|
allow(QA::Runtime::Scenario).to receive(:attributes).and_return({ gitlab_address: logging_env_hash[:address] })
allow(QA::Support::Loglinking).to receive(:canary?).and_return(logging_env_hash[:canary])
expect(QA::Support::Loglinking.logging_environment).to eq(logging_env_hash[:expected_env])
end
end
end
describe '.logging_environment?' do
context 'returns boolean' do
it 'returns true if logging_environment is not nil' do
allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(:staging)
expect(QA::Support::Loglinking.logging_environment?).to eq(true)
end
it 'returns false if logging_environment is nil' do
allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(nil)
expect(QA::Support::Loglinking.logging_environment?).to eq(false)
end
end
end
describe '.cookies' do
let(:cookies) { [{ name: 'Foo', value: 'Bar' }, { name: 'gitlab_canary', value: 'true' }] }
it 'returns browser cookies' do
allow(Capybara.current_session).to receive_message_chain(:driver, :browser, :manage, :all_cookies).and_return(cookies)
expect(QA::Support::Loglinking.cookies).to eq({ "Foo" => { name: "Foo", value: "Bar" }, "gitlab_canary" => { name: "gitlab_canary", value: "true" } })
end
end
describe '.canary?' do
context 'gitlab_canary cookie is present' do
it 'and true returns true' do
allow(QA::Support::Loglinking).to receive(:cookies).and_return({ 'gitlab_canary' => { name: 'gitlab_canary', value: 'true' } })
expect(QA::Support::Loglinking.canary?).to eq(true)
end
it 'and not true returns false' do
allow(QA::Support::Loglinking).to receive(:cookies).and_return({ 'gitlab_canary' => { name: 'gitlab_canary', value: 'false' } })
expect(QA::Support::Loglinking.canary?).to eq(false)
end
end
context 'gitlab_canary cookie is not present' do
it 'returns false' do
allow(QA::Support::Loglinking).to receive(:cookies).and_return({ 'foo' => { name: 'foo', path: '/pathname' } })
expect(QA::Support::Loglinking.canary?).to eq(false)
end
end
end
end
......@@ -8,16 +8,28 @@ RSpec.describe QA::Support::PageErrorChecker do
describe '.report!' do
context 'reports errors' do
let(:expected_chrome_error) do
"Error Code 500\n\n"\
"chrome errors\n\n"\
"Path: #{test_path}"
"Path: #{test_path}\n\n"\
"Logging: foo123"
end
let(:expected_basic_error) do
"Error Code 500\n\n"\
"foo status\n\n"\
"Path: #{test_path}\n\n"\
"Logging: foo123"
end
let(:expected_basic_404) do
"Error Code 404\n\n"\
"foo status\n\n"\
"Path: #{test_path}"
end
it 'reports error message on chrome browser' do
allow(QA::Support::PageErrorChecker).to receive(:parse_five_c_page_request_id).and_return('foo123')
allow(QA::Support::Loglinking).to receive(:failure_metadata).with('foo123').and_return('Logging: foo123')
allow(QA::Support::PageErrorChecker).to receive(:return_chrome_errors).and_return('chrome errors')
allow(page).to receive(:current_path).and_return(test_path)
allow(QA::Runtime::Env).to receive(:browser).and_return(:chrome)
......@@ -26,12 +38,64 @@ RSpec.describe QA::Support::PageErrorChecker do
end
it 'reports basic message on non-chrome browser' do
allow(QA::Support::PageErrorChecker).to receive(:parse_five_c_page_request_id).and_return('foo123')
allow(QA::Support::Loglinking).to receive(:failure_metadata).with('foo123').and_return('Logging: foo123')
allow(QA::Support::PageErrorChecker).to receive(:status_code_report).and_return('foo status')
allow(page).to receive(:current_path).and_return(test_path)
allow(QA::Runtime::Env).to receive(:browser).and_return(:firefox)
expect { QA::Support::PageErrorChecker.report!(page, 500) }.to raise_error(RuntimeError, expected_basic_error)
end
it 'does not report failure metadata on non 500 error' do
allow(QA::Support::PageErrorChecker).to receive(:parse_five_c_page_request_id).and_return('foo123')
expect(QA::Support::Loglinking).not_to receive(:failure_metadata)
allow(QA::Support::PageErrorChecker).to receive(:status_code_report).and_return('foo status')
allow(page).to receive(:current_path).and_return(test_path)
allow(QA::Runtime::Env).to receive(:browser).and_return(:firefox)
expect { QA::Support::PageErrorChecker.report!(page, 404) }.to raise_error(RuntimeError, expected_basic_404)
end
end
end
describe '.parse_five_c_page_request_id' do
context 'parse correlation ID' do
require 'nokogiri'
before do
nokogiri_parse = Class.new do
def self.parse(str)
Nokogiri::HTML.parse(str)
end
end
stub_const('NokogiriParse', nokogiri_parse)
end
let(:error_500_str) do
"<html><body><div><p><code>"\
"req678"\
"</code></p></div></body></html>"
end
let(:error_500_no_code_str) do
"<html><body>"\
"The code you are looking for is not here"\
"</body></html>"
end
it 'returns code is present' do
allow(page).to receive(:html).and_return(error_500_str)
allow(Nokogiri::HTML).to receive(:parse).with(error_500_str).and_return(NokogiriParse.parse(error_500_str))
expect(QA::Support::PageErrorChecker.parse_five_c_page_request_id(page).to_str).to eq('req678')
end
it 'returns nil if not present' do
allow(page).to receive(:html).and_return(error_500_no_code_str)
allow(Nokogiri::HTML).to receive(:parse).with(error_500_no_code_str).and_return(NokogiriParse.parse(error_500_no_code_str))
expect(QA::Support::PageErrorChecker.parse_five_c_page_request_id(page)).to be_nil
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