Improved email error logging (#16184)

no issue

Logs errors to Sentry and adds error codes.
This commit is contained in:
Simon Backx 2023-01-25 14:57:10 +01:00 committed by Daniel Lockyer
parent fc990e856a
commit 846d033e20
No known key found for this signature in database
5 changed files with 105 additions and 12 deletions

View File

@ -196,8 +196,11 @@ module.exports = {
// log any error that didn't come from the provider which would have already logged it // log any error that didn't come from the provider which would have already logged it
if (!error.code || error.code !== 'BULK_EMAIL_SEND_FAILED') { if (!error.code || error.code !== 'BULK_EMAIL_SEND_FAILED') {
let ghostError = new errors.InternalServerError({ let ghostError = new errors.EmailError({
err: error err: error,
code: 'BULK_EMAIL_SEND_FAILED',
message: `Error sending email batch ${emailBatchId}`,
context: error.message
}); });
sentry.captureException(ghostError); sentry.captureException(ghostError);
logging.error(ghostError); logging.error(ghostError);
@ -274,7 +277,7 @@ module.exports = {
}); });
sentry.captureException(ghostError); sentry.captureException(ghostError);
logging.warn(ghostError); logging.error(ghostError);
debug(`failed to send message (${Date.now() - startTime}ms)`); debug(`failed to send message (${Date.now() - startTime}ms)`);
throw ghostError; throw ghostError;

View File

@ -88,7 +88,8 @@ class EmailServiceWrapper {
jobsService, jobsService,
emailSegmenter, emailSegmenter,
emailRenderer, emailRenderer,
db db,
sentry
}); });
this.service = new EmailService({ this.service = new EmailService({

View File

@ -29,6 +29,7 @@ class BatchSendingService {
#jobsService; #jobsService;
#models; #models;
#db; #db;
#sentry;
/** /**
* @param {Object} dependencies * @param {Object} dependencies
@ -42,6 +43,7 @@ class BatchSendingService {
* @param {Email} dependencies.models.Email * @param {Email} dependencies.models.Email
* @param {object} dependencies.models.Member * @param {object} dependencies.models.Member
* @param {object} dependencies.db * @param {object} dependencies.db
* @param {object} [dependencies.sentry]
*/ */
constructor({ constructor({
emailRenderer, emailRenderer,
@ -49,7 +51,8 @@ class BatchSendingService {
jobsService, jobsService,
emailSegmenter, emailSegmenter,
models, models,
db db,
sentry
}) { }) {
this.#emailRenderer = emailRenderer; this.#emailRenderer = emailRenderer;
this.#sendingService = sendingService; this.#sendingService = sendingService;
@ -57,6 +60,7 @@ class BatchSendingService {
this.#emailSegmenter = emailSegmenter; this.#emailSegmenter = emailSegmenter;
this.#models = models; this.#models = models;
this.#db = db; this.#db = db;
this.#sentry = sentry;
} }
/** /**
@ -97,7 +101,17 @@ class BatchSendingService {
error: null error: null
}, {patch: true, autoRefresh: false}); }, {patch: true, autoRefresh: false});
} catch (e) { } catch (e) {
logging.error(`Error sending email ${email.id}: ${e.message}`); const ghostError = new errors.EmailError({
err: e,
code: 'BULK_EMAIL_SEND_FAILED',
message: `Error sending email ${email.id}`
});
logging.error(ghostError);
if (this.#sentry) {
// Log the original error to Sentry
this.#sentry.captureException(e);
}
// Edge case: Store error in email model (that are not caught by the batch) // Edge case: Store error in email model (that are not caught by the batch)
await email.save({ await email.save({
@ -324,8 +338,21 @@ class BatchSendingService {
}, {patch: true, require: false, autoRefresh: false}); }, {patch: true, require: false, autoRefresh: false});
succeeded = true; succeeded = true;
} catch (err) { } catch (err) {
logging.error(`Error sending email batch ${batch.id}`); if (!err.code || err.code !== 'BULK_EMAIL_SEND_FAILED') {
logging.error(err); // BULK_EMAIL_SEND_FAILED are already logged in mailgun-email-provider
const ghostError = new errors.EmailError({
err,
code: 'BULK_EMAIL_SEND_FAILED',
message: `Error sending email batch ${batch.id}`,
context: err.message
});
logging.error(ghostError);
if (this.#sentry) {
// Log the original error to Sentry
this.#sentry.captureException(err);
}
}
await batch.save({ await batch.save({
status: 'failed', status: 'failed',

View File

@ -168,7 +168,7 @@ class MailgunEmailProvider {
}); });
} }
logging.warn(ghostError); logging.error(ghostError);
debug(`failed to send message (${Date.now() - startTime}ms)`); debug(`failed to send message (${Date.now() - startTime}ms)`);
// log error to custom error handler. ex sentry // log error to custom error handler. ex sentry

View File

@ -125,8 +125,12 @@ describe('Batch Sending Service', function () {
status: 'pending' status: 'pending'
} }
}); });
const captureException = sinon.stub();
const service = new BatchSendingService({ const service = new BatchSendingService({
models: {Email} models: {Email},
sentry: {
captureException
}
}); });
let emailModel; let emailModel;
let afterEmailModel; let afterEmailModel;
@ -141,6 +145,16 @@ describe('Batch Sending Service', function () {
assert.equal(result, undefined); assert.equal(result, undefined);
sinon.assert.calledOnce(errorLog); sinon.assert.calledOnce(errorLog);
sinon.assert.calledOnce(sendEmail); sinon.assert.calledOnce(sendEmail);
sinon.assert.calledOnce(captureException);
// Check error code
const error = errorLog.firstCall.args[0];
assert.equal(error.code, 'BULK_EMAIL_SEND_FAILED');
// Check error
const sentryError = captureException.firstCall.args[0];
assert.equal(sentryError.message, '');
assert.equal(emailModel.status, 'submitting', 'The email status is submitting while sending'); assert.equal(emailModel.status, 'submitting', 'The email status is submitting while sending');
assert.equal(afterEmailModel.get('status'), 'failed', 'The email status is failed after sending'); assert.equal(afterEmailModel.get('status'), 'failed', 'The email status is failed after sending');
assert.equal(afterEmailModel.get('error'), 'Something went wrong while sending the email'); assert.equal(afterEmailModel.get('error'), 'Something went wrong while sending the email');
@ -621,7 +635,7 @@ describe('Batch Sending Service', function () {
}); });
assert.equal(result, false); assert.equal(result, false);
sinon.assert.calledTwice(errorLog); sinon.assert.calledOnce(errorLog);
sinon.assert.calledOnce(sendingService.send); sinon.assert.calledOnce(sendingService.send);
sinon.assert.calledOnce(findOne); sinon.assert.calledOnce(findOne);
@ -632,6 +646,54 @@ describe('Batch Sending Service', function () {
assert.equal(batch.get('error_data'), null); assert.equal(batch.get('error_data'), null);
}); });
it('Does log error to Sentry', async function () {
const EmailBatch = createModelClass({
findOne: {
status: 'pending',
member_segment: null
}
});
const sendingService = {
send: sinon.stub().rejects(new Error('Test error'))
};
const findOne = sinon.spy(EmailBatch, 'findOne');
const captureException = sinon.stub();
const service = new BatchSendingService({
models: {EmailBatch, EmailRecipient},
sendingService,
sentry: {
captureException
}
});
const result = await service.sendBatch({
email: createModel({}),
batch: createModel({}),
post: createModel({}),
newsletter: createModel({})
});
assert.equal(result, false);
sinon.assert.calledOnce(errorLog);
sinon.assert.calledOnce(sendingService.send);
sinon.assert.calledOnce(captureException);
const sentryExeption = captureException.firstCall.args[0];
assert.equal(sentryExeption.message, 'Test error');
const loggedExeption = errorLog.firstCall.args[0];
assert.match(loggedExeption.message, /Error sending email batch/);
assert.equal(loggedExeption.context, 'Test error');
assert.equal(loggedExeption.code, 'BULK_EMAIL_SEND_FAILED');
sinon.assert.calledOnce(findOne);
const batch = await findOne.firstCall.returnValue;
assert.equal(batch.get('status'), 'failed');
assert.equal(batch.get('error_status_code'), null);
assert.equal(batch.get('error_message'), 'Test error');
assert.equal(batch.get('error_data'), null);
});
it('Does save EmailError', async function () { it('Does save EmailError', async function () {
const EmailBatch = createModelClass({ const EmailBatch = createModelClass({
findOne: { findOne: {
@ -664,7 +726,7 @@ describe('Batch Sending Service', function () {
}); });
assert.equal(result, false); assert.equal(result, false);
sinon.assert.calledTwice(errorLog); sinon.assert.notCalled(errorLog);
sinon.assert.calledOnce(sendingService.send); sinon.assert.calledOnce(sendingService.send);
sinon.assert.calledOnce(findOne); sinon.assert.calledOnce(findOne);