Improve error reporting for MSS sending

[MAILPOET-1445]
This commit is contained in:
Pavel Dohnal
2018-07-10 12:42:14 +01:00
parent 42b353fa59
commit 27b9857e6a
7 changed files with 217 additions and 17 deletions

View File

@ -164,10 +164,11 @@ class SendingQueue {
);
// log error message and schedule retry/pause sending
if($send_result['response'] === false) {
MailerLog::processError(
$send_result['operation'],
$send_result['error_message']
);
if(isset($send_result['retry_interval'])) {
MailerLog::processNonBlockingError($send_result['operation'], $send_result['error_message'], $send_result['retry_interval']);
} else {
MailerLog::processError($send_result['operation'], $send_result['error_message']);
}
}
// update processed/to process list
if(!$queue->updateProcessedSubscribers($prepared_subscribers_ids)) {

View File

@ -75,16 +75,43 @@ class MailerLog {
return self::resetMailerLog();
}
/**
* Process error, doesn't increase retry_attempt so it will not block sending
*
* @param string $operation
* @param string $error_message
* @param int $retry_interval
*
* @throws \Exception
*/
static function processNonBlockingError($operation, $error_message, $retry_interval = self::RETRY_INTERVAL) {
$mailer_log = self::getMailerLog();
$mailer_log['retry_at'] = time() + $retry_interval;
$mailer_log = self::setError($mailer_log, $operation, $error_message);
self::updateMailerLog($mailer_log);
self::enforceExecutionRequirements();
}
/**
* Process error, increase retry_attempt and block sending if it goes above RETRY_INTERVAL
*
* @param string $operation
* @param string $error_message
* @param string $error_code
* @param bool $pause_sending
*
* @throws \Exception
*/
static function processError($operation, $error_message, $error_code = null, $pause_sending = false) {
$mailer_log = self::getMailerLog();
(int)$mailer_log['retry_attempt']++;
$mailer_log['retry_attempt']++;
$mailer_log['retry_at'] = time() + self::RETRY_INTERVAL;
$mailer_log = self::setError($mailer_log, $operation, $error_message, $error_code);
self::updateMailerLog($mailer_log);
if($pause_sending) {
self::pauseSending($mailer_log);
}
return self::enforceExecutionRequirements();
self::enforceExecutionRequirements();
}
static function setError($mailer_log, $operation, $error_message, $error_code = null) {

View File

@ -9,6 +9,9 @@ use MailPoet\Services\Bridge\API;
if(!defined('ABSPATH')) exit;
class MailPoet {
const TEMPORARY_UNAVAILABLE_RETRY_INTERVAL = 300; // seconds
public $api;
public $sender;
public $reply_to;
@ -34,16 +37,37 @@ class MailPoet {
case API::SENDING_STATUS_CONNECTION_ERROR:
return Mailer::formatMailerConnectionErrorResult($result['message']);
case API::SENDING_STATUS_SEND_ERROR:
if(!empty($result['code']) && $result['code'] === API::RESPONSE_CODE_KEY_INVALID) {
Bridge::invalidateKey();
}
return Mailer::formatMailerSendErrorResult($result['message']);
return $this->processSendError($result, $subscriber);
case API::SENDING_STATUS_OK:
default:
return Mailer::formatMailerSendSuccessResult();
}
}
function processSendError($result, $subscriber) {
if(!empty($result['code'])) {
switch($result['code']) {
case API::RESPONSE_CODE_NOT_ARRAY:
return Mailer::formatMailerSendErrorResult(__('JSON input is not an array', 'mailpoet'));
case API::RESPONSE_CODE_PAYLOAD_TOO_BIG:
return Mailer::formatMailerSendErrorResult($result['message']);
case API::RESPONSE_CODE_PAYLOAD_ERROR:
$error = $this->parseErrorResponse($result['message'], $subscriber);
return Mailer::formatMailerSendErrorResult($error);
case API::RESPONSE_CODE_TEMPORARY_UNAVAILABLE:
$error = Mailer::formatMailerSendErrorResult(__('Email service is temporarily not available, please try again in a few minutes.', 'mailpoet'));
$error['retry_interval'] = self::TEMPORARY_UNAVAILABLE_RETRY_INTERVAL;
return $error;
case API::RESPONSE_CODE_KEY_INVALID:
Bridge::invalidateKey();
break;
default:
return Mailer::formatMailerSendErrorResult($result['message']);
}
}
return Mailer::formatMailerSendErrorResult($result['message']);
}
function processSubscriber($subscriber) {
preg_match('!(?P<name>.*?)\s<(?P<email>.*?)>!', $subscriber, $subscriber_data);
if(!isset($subscriber_data['email'])) {
@ -104,4 +128,37 @@ class MailPoet {
}
return $body;
}
private function parseErrorResponse($result, $subscriber) {
$result_parsed = json_decode($result, true);
$errors = [];
if(is_array($result_parsed)) {
foreach($result_parsed as $result_error) {
$errors[] = $this->processSingleSubscriberError($result_error, $subscriber);
}
}
if(!empty($errors)) {
return __('Error while sending: ', 'mailpoet') . join(', ', $errors);
} else {
return __('Error while sending newsletters. ', 'mailpoet') . $result;
}
}
private function processSingleSubscriberError($result_error, $subscriber) {
$error = '';
if(is_array($result_error)) {
$subscriber_errors = [];
if(isset($result_error['errors']) && is_array($result_error['errors'])) {
array_walk_recursive($result_error['errors'], function($item) use (&$subscriber_errors) {
$subscriber_errors[] = $item;
});
}
$error .= join(', ', $subscriber_errors);
if(isset($result_error['index']) && isset($subscriber[$result_error['index']])) {
$error = '(' . $subscriber[$result_error['index']] . ': ' . $error . ')';
}
}
return $error;
}
}

View File

@ -16,6 +16,10 @@ class API {
const RESPONSE_CODE_KEY_INVALID = 401;
const RESPONSE_CODE_STATS_SAVED = 204;
const RESPONSE_CODE_TEMPORARY_UNAVAILABLE = 503;
const RESPONSE_CODE_NOT_ARRAY = 422;
const RESPONSE_CODE_PAYLOAD_TOO_BIG = 413;
const RESPONSE_CODE_PAYLOAD_ERROR = 400;
private $api_key;

View File

@ -87,7 +87,7 @@ class MailerLogTest extends \MailPoetTest {
expect(MailerLog::isSendingLimitReached())->true();
}
function testitChecksWhenSendingIsPaused() {
function testItChecksWhenSendingIsPaused() {
$mailer_log = array('status' => MailerLog::STATUS_PAUSED);
expect(MailerLog::isSendingPaused($mailer_log))->true();
$mailer_log = array('status' => false);
@ -142,13 +142,14 @@ class MailerLogTest extends \MailPoetTest {
expect($mailer_log['retry_at'])->null();
}
function itProcessesSendingError() {
function testItProcessesSendingError() {
// retry-related mailer values should be null
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['retry_attempt'])->null();
expect($mailer_log['retry_at'])->null();
expect($mailer_log['error'])->null();
// retry attempt should be incremented, error logged, retry attempt scheduled
$this->setExpectedException('\Exception');
MailerLog::processError($operation = 'send', $error = 'email rejected');
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['retry_attempt'])->equals(1);
@ -161,6 +162,24 @@ class MailerLogTest extends \MailPoetTest {
);
}
function testItProcessesNonBlockingSendingError() {
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['retry_attempt'])->null();
expect($mailer_log['retry_at'])->null();
expect($mailer_log['error'])->null();
$this->setExpectedException('\Exception');
MailerLog::processNonBlockingError($operation = 'send', $error = 'email rejected');
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['retry_attempt'])->equals(1);
expect($mailer_log['retry_at'])->greaterThan(time());
expect($mailer_log['error'])->equals(
array(
'operation' => 'send',
'error_message' => $error
)
);
}
function testItPausesSendingAfterProcessingSendingError() {
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['error'])->null();

View File

@ -4,6 +4,7 @@ namespace MailPoet\Test\Mailer\Methods;
use Codeception\Util\Stub;
use MailPoet\Config\ServicesChecker;
use MailPoet\Mailer\Methods\MailPoet;
use MailPoet\Services\Bridge\API;
class MailPoetAPITest extends \MailPoetTest {
function _before() {
@ -149,4 +150,95 @@ class MailPoetAPITest extends \MailPoetTest {
);
expect($result['response'])->true();
}
function testFormatConnectionError() {
$this->mailer->api = Stub::makeEmpty(
'MailPoet\Services\Bridge\API',
array('sendMessages' => [
'status' => API::SENDING_STATUS_CONNECTION_ERROR,
'message' => 'connection error',
]),
$this
);
$result = $this->mailer->send($this->newsletter, $this->subscriber);
expect($result)->equals([
'response' => false,
'operation' => 'connect',
'error_message' => 'connection error',
]);
}
function testFormatErrorNotArray() {
$this->mailer->api = Stub::makeEmpty(
'MailPoet\Services\Bridge\API',
array('sendMessages' => [
'code' => API::RESPONSE_CODE_NOT_ARRAY,
'status' => API::SENDING_STATUS_SEND_ERROR,
'message' => 'error not array',
]),
$this
);
$result = $this->mailer->send($this->newsletter, $this->subscriber);
expect($result)->equals([
'response' => false,
'operation' => 'send',
'error_message' => 'JSON input is not an array',
]);
}
function testFormatErrorTooBig() {
$this->mailer->api = Stub::makeEmpty(
'MailPoet\Services\Bridge\API',
array('sendMessages' => [
'code' => API::RESPONSE_CODE_PAYLOAD_TOO_BIG,
'status' => API::SENDING_STATUS_SEND_ERROR,
'message' => 'error too big',
]),
$this
);
$result = $this->mailer->send($this->newsletter, $this->subscriber);
expect($result)->equals([
'response' => false,
'operation' => 'send',
'error_message' => 'error too big',
]);
}
function testFormatPayloadError() {
$this->mailer->api = Stub::makeEmpty(
'MailPoet\Services\Bridge\API',
array('sendMessages' => [
'code' => API::RESPONSE_CODE_PAYLOAD_ERROR,
'status' => API::SENDING_STATUS_SEND_ERROR,
'message' => 'Api Error',
]),
$this
);
$result = $this->mailer->send([$this->newsletter, $this->newsletter], ['a@example.com', 'c d <b@example.com>']);
expect($result)->equals([
'response' => false,
'operation' => 'send',
'error_message' => 'Error while sending newsletters. Api Error',
]);
}
function testFormatPayloadErrorWithErrorMessage() {
$this->mailer->api = Stub::makeEmpty(
'MailPoet\Services\Bridge\API',
array('sendMessages' => [
'code' => API::RESPONSE_CODE_PAYLOAD_ERROR,
'status' => API::SENDING_STATUS_SEND_ERROR,
'message' => '[{"index":0,"errors":{"subject":"subject is missing"}},{"index":1,"errors":{"subject":"subject is missing"}}]'
]),
$this
);
$result = $this->mailer->send([$this->newsletter, $this->newsletter], ['a@example.com', 'c d <b@example.com>']);
expect($result)->equals([
'response' => false,
'operation' => 'send',
'error_message' => 'Error while sending: (a@example.com: subject is missing), (c d <b@example.com>: subject is missing)',
]);
}
}