Identify wrong index name

This commit is contained in:
Pixelastic 2017-11-16 17:08:28 +01:00
parent a10b2d385b
commit 8e35d8eb10
3 changed files with 231 additions and 18 deletions

View File

@ -1,5 +1,6 @@
require 'verbal_expressions'
require 'filesize'
require 'cgi'
module Jekyll
module Algolia
@ -19,12 +20,23 @@ module Jekyll
exit 1
end
# Public: Will identify the error and return its internal name
#
# error - The caught error
# context - A hash of additional information that can be passed from the
# code intercepting the user
#
# It will parse in order all potential known issues until it finds one
# that matches. Returns false if no match, or a hash of :name and :details
# further identifying the issue.
def self.identify(error, context = {})
known_errors = %w[
unknown_application_id
invalid_credentials_for_tmp_index
invalid_credentials
record_too_big
unknown_settings
invalid_index_name
]
# Checking the errors against our known list
@ -39,6 +51,12 @@ module Jekyll
false
end
# Public: Parses an Algolia error message into a hash of its content
#
# message - The raw message as returned by the API
#
# Returns a hash of all parts of the message, to be more easily consumed
# by our error matchers
def self.error_hash(message)
message = message.delete("\n")
@ -57,9 +75,17 @@ module Jekyll
find '/'
capture('api_section') { word }
find '/'
capture('index_name') { word }
capture('index_name') do
anything_but('/')
end
find '/'
capture('api_action') { word }
capture do
capture('api_action') { word }
maybe '?'
capture('query_parameters') do
anything_but(':')
end
end
find ': '
capture('json') do
find '{'
@ -82,10 +108,26 @@ module Jekyll
hash['api_version'] = hash['api_version'].to_i
hash['http_error'] = hash['http_error'].to_i
hash['json'] = JSON.parse(hash['json'])
# Merging the JSON key directly in the answer
hash = hash.merge(JSON.parse(hash['json']))
hash.delete('json')
# Merging the query parameters in the answer
CGI.parse(hash['query_parameters']).each do |key, values|
hash[key] = values[0]
end
hash.delete('query_parameters')
hash
end
# Public: Check if the application id is available
#
# _context - Not used
#
# If the call to the cluster fails, chances are that the application ID
# is invalid. As we cannot actually contact the server, the error is raw
# and does not follow our error spec
def self.unknown_application_id?(error, _context = {})
message = error.message
return false if message !~ /^Cannot reach any host/
@ -95,6 +137,13 @@ module Jekyll
{ 'application_id' => matches[1] }
end
# Public: Check if credentials specifically can't access the _tmp index
#
# _context - Not used
#
# If the error happens on a _tmp folder, it might mean that the key does
# not have access to the _tmp indices and the error message will reflect
# that.
def self.invalid_credentials_for_tmp_index?(error, _context = {})
return false unless invalid_credentials?(error)
@ -109,10 +158,15 @@ module Jekyll
}
end
# Public: Check if the credentials are working
#
# _context - Not used
#
# Application ID and API key submitted don't match any credentials known
def self.invalid_credentials?(error, _context = {})
details = error_hash(error.message)
if details['json']['message'] != 'Invalid Application-ID or API key'
if details['message'] != 'Invalid Application-ID or API key'
return false
end
@ -122,16 +176,23 @@ module Jekyll
}
end
# Public: Check if the sent records are not too big
#
# context[:records] - list of records to push
#
# Records cannot weight more that 10Kb. If we're getting this error it
# means that one of the records is too big, so we'll try to give
# informations about it so the user can debug it.
def self.record_too_big?(error, context = {})
details = error_hash(error.message)
message = details['json']['message']
message = details['message']
return false if message !~ /^Record .* is too big .*/
# Getting the record size
size, = /.*size=(.*) bytes.*/.match(message).captures
size = Filesize.from("#{size} B").pretty
object_id = details['json']['objectID']
object_id = details['objectID']
# Getting record details
record = Utils.find_by_key(context[:records], :objectID, object_id)
@ -145,6 +206,44 @@ module Jekyll
'size_limit' => '10 Kb'
}
end
# Public: Check if one of the index settings is invalid
#
# context[:settings] - The settings passed to update the index
#
# The API will block any call that tries to update a setting value that is
# not available. We'll tell the user which one so they can fix their
# issue.
def self.unknown_settings?(error, context = {})
details = error_hash(error.message)
message = details['message']
return false if message !~ /^Invalid object attributes.*/
# Getting the unknown setting name
regex = /^Invalid object attributes: (.*) near line.*/
setting_name, = regex.match(message).captures
setting_value = context[:settings][setting_name]
{
'setting_name' => setting_name,
'setting_value' => setting_value
}
end
# Public: Check if the index name is invalid
#
# Some characters are forbidden in index names
def self.invalid_index_name?(error, _context = {})
details = error_hash(error.message)
message = details['message']
return false if message !~ /^indexName is not valid.*/
{
'index_name' => Configurator.index_name
}
end
end
end
end

View File

@ -75,7 +75,12 @@ module Jekyll
def self.delete_records_by_id(index, ids)
Logger.log("I:Deleting #{ids.length} records")
return if Configurator.dry_run?
index.delete_objects!(ids)
begin
index.delete_objects!(ids)
rescue StandardError => error
ErrorHandler.stop(error)
end
end
# Public: Returns an array of all the objectIDs in the index
@ -108,7 +113,11 @@ module Jekyll
def self.update_settings(index, settings)
Logger.verbose('I:Updating settings')
return if Configurator.dry_run?
index.set_settings(settings)
begin
index.set_settings(settings)
rescue StandardError => error
ErrorHandler.stop(error, settings: settings)
end
end
# Public: Index content following the `diff` indexing mode
@ -118,10 +127,13 @@ module Jekyll
# The `diff` indexing mode will only push new content to the index and
# remove old content from it. It won't touch records that haven't been
# updated. It will be a bit slower as it will first need to get the list
# of all records in the index, but it will consume less operations than
# the `atomic` indexing mode.
# of all records in the index, but it will consume less operations.
def self.run_diff_mode(records)
index = index(Configurator.index_name)
# Update settings
update_settings(index, Configurator.settings)
# Getting list of objectID in remote and locally
remote_ids = remote_object_ids(index)
local_ids = local_object_ids(records)
@ -135,9 +147,6 @@ module Jekyll
new_records_ids.include?(record[:objectID])
end
update_records(index, new_records)
# Update settings
update_settings(index, Configurator.settings)
end
# Public: Get the settings of the remote index
@ -145,6 +154,8 @@ module Jekyll
# index - The Algolia Index
def self.remote_settings(index)
index.get_settings
rescue StandardError => error
ErrorHandler.stop(error)
end
# Public: Rename an index
@ -156,7 +167,11 @@ module Jekyll
def self.rename_index(old_name, new_name)
Logger.verbose("I:Renaming `#{old_name}` to `#{new_name}`")
return if Configurator.dry_run?
::Algolia.move_index(old_name, new_name)
begin
::Algolia.move_index(old_name, new_name)
rescue StandardError => error
ErrorHandler.stop(error, new_name: new_name)
end
end
# Public: Index content following the `atomic` indexing mode
@ -177,14 +192,14 @@ module Jekyll
Logger.verbose("I:Using `#{index_tmp_name}` as temporary index")
# Pushing everthing to a brand new index
update_records(index_tmp, records)
# Copying original settings to the new index
remote_settings = remote_settings(index)
new_settings = remote_settings.merge(Configurator.settings)
update_settings(index_tmp, new_settings)
# Pushing everthing to a brand new index
update_records(index_tmp, records)
# Renaming the new index in place of the old
rename_index(index_tmp_name, index_name)
end

View File

@ -5,6 +5,63 @@ describe(Jekyll::Algolia::ErrorHandler) do
let(:current) { Jekyll::Algolia::ErrorHandler }
let(:configurator) { Jekyll::Algolia::Configurator }
describe '.error_hash' do
subject { current.error_hash(message) }
context 'with a regular error message' do
let(:message) do
'Cannot POST to '\
'https://MY_APP_ID.algolia.net/1/section/index_name/action: '\
'{"message":"Custom message","status":403}'\
"\n (403)"
end
it do
should include('verb' => 'POST')
should include('scheme' => 'https')
should include('application_id' => 'MY_APP_ID')
should include('api_version' => 1)
should include('api_section' => 'section')
should include('index_name' => 'index_name')
should include('api_action' => 'action')
should include('message' => 'Custom message')
should include('status' => 403)
end
end
context 'with a message with query parameters' do
let(:message) do
'Cannot POST to '\
'https://MY_APP_ID.algolia.net/1/section/index_name/action?foo=bar: '\
'{"message":"Custom message","status":403}'\
"\n (403)"
end
it do
should include('foo' => 'bar')
end
end
context 'with an error message with weird characaters' do
let(:message) do
'Cannot POST to '\
'https://MY_APP_ID.algolia.net/1/section/index_name$`!</action: '\
'{"message":"Custom message","status":403}'\
"\n (403)"
end
it do
should include('index_name' => 'index_name$`!<')
end
end
context 'with a malformed error message' do
let(:message) { 'Unable to even parse this' }
it { should eq false }
end
end
describe '.identify' do
subject { current.identify(error, context) }
@ -72,7 +129,7 @@ describe(Jekyll::Algolia::ErrorHandler) do
context 'with a record too big' do
let(:message) do
'400: Cannot POST to '\
'https://MXM0JWJNIW.algolia.net/1/indexes/my_index/batch: '\
'https://MY_APP_ID.algolia.net/1/indexes/my_index/batch: '\
'{"message":"Record at the position 3 '\
'objectID=deadbeef is too big size=1091966 bytes. '\
'Contact us if you need an extended quota","position":3,'\
@ -102,5 +159,47 @@ describe(Jekyll::Algolia::ErrorHandler) do
expect(details).to include('size_limit' => '10 Kb')
end
end
context 'with an unknown setting' do
let(:message) do
'400: Cannot PUT to '\
'https://MY_APP_ID.algolia.net/1/indexes/my_index/settings: '\
'{"message":"Invalid object attributes: deadbeef near line:1 column:456",'\
'"status":400} (400)'
end
let(:context) do
{ settings:
{
'searchableAttributes' => %w[foo bar],
'deadbeef' => 'foofoo'
} }
end
it { should include(name: 'unknown_settings') }
it do
details = subject[:details]
expect(details).to include('setting_name' => 'deadbeef')
expect(details).to include('setting_value' => 'foofoo')
end
end
context 'with an invalid index name' do
before do
allow(configurator)
.to receive(:index_name)
.and_return('invalid_index_name')
end
let(:message) do
'400: Cannot GET to '\
'https://MY_APP_ID-dsn.algolia.net/1/indexes/invalid_index_name/settings?getVersion=2: '\
'{"message":"indexName is not valid","status":400} (400)'
end
it { should include(name: 'invalid_index_name') }
it do
details = subject[:details]
expect(details).to include('index_name' => 'invalid_index_name')
end
end
end
end