Skip to content

Instantly share code, notes, and snippets.

@chriskrycho
Last active March 11, 2016 20:51
Show Gist options
  • Save chriskrycho/27cfac58c103e35c2562 to your computer and use it in GitHub Desktop.
Save chriskrycho/27cfac58c103e35c2562 to your computer and use it in GitHub Desktop.
Ember error service with a bit of a problem.
// ----- services/error.js ----- //
import Ember from 'ember';
import ENV from "mobile-web/config/environment";
import DS from 'ember-data';
import { UnauthorizedError } from 'ember-ajax/errors';
// TODO: set this text configurably/get input from marketing/etc.?
const fooErr = "Sorry, we were unable to do that thing.";
export default Ember.Service.extend({
externalLogger: Ember.inject.service(), // SEE BELOW
userMessages: Ember.inject.service(), // SEE BELOW
// WARNING: This is private API and may break during upgrades. It should be
// replaced as soon as there is a public routing API.
_routing: Ember.inject.service('-routing'),
/**
*/
redirectToLogin() {
Ember.run(() => { this.get('_routing').transitionTo('login'); });
},
/**
Send a message to the user. If the error has an associated `detail` field,
use it as the message to the user; otherwise, default to `fooErr`.
@private
@param {Object} err The error object to us.
@param {String} [defaultMsg] Override the default message.
*/
_sendUserMessage(err, defaultMsg) {
let message;
if (err.detail) {
message = err.detail;
} else if (defaultMsg) {
message = defaultMsg;
} else {
message = fooErr;
}
this.get('userMessages').add('error', message);
},
/**
Ember application error, not handled any other way.
@method appError
@param {Object} err
@param {String} [err.detail] The associated message, if any.
@param {Number} [err.status] The associated HTTP status code, if any.
@param {Object} [context]
@param {} context.cause A description of the error cause.
@param {} context.stack The stack trace for the error.
*/
appError(err, context) {
if (ENV.APP.LOG_ALL_ERRS) {
console.error(`Application error: ${err} in context ${context}`); // eslint-disable-line no-console
}
this._sendUserMessage(err);
this.get('externalLogger').logError(err, context);
},
/**
API errors correspond to `FooExceptions` returned by the server-side API,
which should correspond to `DS.InvalidError` results from Ember Data. The
[docs](http://emberjs.com/api/data/classes/DS.InvalidError.html) read:
> A `DS.InvalidError` is used by an adapter to signal the external API was
> unable to process a request because the content was not semantically
> correct or meaningful per the API. Usually this means a record failed some
> form of server side validation. When a promise from an adapter is rejected
> with a `DS.InvalidError` the record will transition to the invalid state
> and the errors will be set to the errors property on the record.
These errors can be associated with models, if the adapter supports it. That
they trigger correctly should be the subject of an integration test.
@method apiError
@param {Object} err
@param {String|null} err.detail The associated message, if any.
@param {Number|null} err.status The associated HTTP status code, if any.
*/
apiError(err) {
if (ENV.APP.LOG_ALL_ERRS) { console.error(`API error: ${err}`); } // eslint-disable-line no-console
this._sendUserMessage(err);
this.get('externalLogger').logError(err);
},
/**
Handle top-level 401s triggered by e.g. other services rather than models
attached to routes.
@param {Object} err
@param {String|null} err.detail The associated message, if any.
@param {Number|null} err.status The associated status, if any. Must be
`401` if included.
@param {String|null} message
*/
unauthorizedError(err, message) {
if (ENV.APP.LOG_ALL_ERRS) { console.error(`API error: ${err}`); } // eslint-disable-line no-console
if (err.status && err.status !== 401) {
const detail = "unauthorizedError was called with a non-401 status";
this.appError({ detail });
return;
}
this._sendUserMessage(err, message);
this.redirectToLogin();
},
/**
Validation errors are the result of a client- or server-side validation of
the user input into a form.
@param {Object} err
@param {String} [err.detail] The associated message, if any.
@param {Number} [err.status] The associated HTTP status code, if any.
*/
validationError(err) {
if (ENV.APP.LOG_ALL_ERRS) { console.error(`Validation error: ${err}`); } // eslint-disable-line no-console
// QUESTION: should we set this in a configuration or similar somewhere, so
// that it's not a detail of the implementation? If so, where?
const defaultMessage =
'Sorry, we had a problem validating your order. Please try again.';
const message = err && err.detail ? err.detail : defaultMessage;
this._sendUserMessage(err, message);
},
/**
Capture all otherwise unbound errors.
*/
_handleGenericError(error) {
if (error instanceof UnauthorizedError) {
this.unauthorizedError(arguments);
} else {
this.appError(arguments);
}
},
/**
Handle RSVP Promise rejection.
*/
_handlePromiseError(error) {
// We allow TransitionAborted "error" Promises to pass.
if (error.name === 'TransitionAborted') { return; }
if (error instanceof DS.InvalidError) {
this.apiError(error);
} else if (error instanceof UnauthorizedError) {
this.unauthorizedError(error, 'You must be logged in to do that.');
} else {
this.appError(error);
}
},
/**
Log errors with structure appropriate to the service.
*/
_customErrorLogger(message, cause, stack) {
const err = { detail: `${message} (${cause})` };
const context = { cause, stack };
this.get('externalLogger').logError(err, context);
},
/**
Instantiation behavior. Note that the `onerror` and `Logger.error` items
must be set as closures which call the class items, not binding the methods
directly, so that the call site (and therefore `this` context within the
methods) is correct.
*/
init() {
this.set('originalOnError', Ember.onerror);
Ember.onerror = (error, ...args) => {
Ember.run(() => { this._handleGenericError(error, ...args); });
};
Ember.RSVP.on('error', (...args) => {
Ember.run(() => { this._handlePromiseError(...args); });
});
this.set('originalErrorLogger', Ember.Logger.error);
Ember.Logger.error = (message, cause, stack) => { this._customErrorLogger(message, cause, stack); };
},
/**
Tear-down. (Really only needed for tests. But needed.)
*/
willDestroy() {
Ember.onerror = this.get('originalOnError');
Ember.RSVP.off('error', this._handlePromiseError);
Ember.Logger.error = this.get('originalErrorLogger');
}
});
// ----- services/user-messages.js ----- //
import Ember from 'ember';
export default Ember.Service.extend({
messages: [],
add(type, detail) {
if (!(type === 'error' || type === 'info' || type === 'warning')) {
throw new Error(`Tried to call 'userMessage.add' with bad 'type' argument: ${type}`);
}
this.get('messages').pushObject({ type, detail });
}
});
// ----- services/external-logger.js ----- //
/* global Raygun */
import Ember from 'ember';
// Note that we close over the Raygun instance; it is not publicly visible on
// the service.
export default Ember.Service.extend({
/**
The main public function: log against the external logging API.
@param {Object} err Any object you wish to log as an error.
@param {Object} context
@param {String} context.cause
@param {String} context.stack
*/
logError(err, context) {
Raygun.send(err, null, context);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment