Last active
March 11, 2016 20:51
-
-
Save chriskrycho/27cfac58c103e35c2562 to your computer and use it in GitHub Desktop.
Ember error service with a bit of a problem.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ----- 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