Setting up Airbrake on an Ember application - javascript

How do you set up Airbrake such that it gets context information from unhandled Javascript errors that occur in an Ember application?

Assuming you've included Airbrake-js you can hook on Ember's onerror handler and push errors.
Ember.onerror = function(err) { // any ember error
Airbrake.push(err);
//any other error handling
};
Ember.RSVP.configure('onerror',function(err){ // any promise error
Airbrake.push(err);
console.error(e.message);
console.error(e.stack);
//any other generic promise error handling
};
window.onerror = function(err){ // window general errors.
Airbrake.push(err);
//generic error handling that might not be Airbrake related.
};
You can see more options of different parameters of the data sent in the airbrake-js GitHub repository docs.

I don't know if this answers your question, but I hope it helps.
To handle server thrown errors you can define an "error" function in the application's route and push it in Airbrake:
App.ApplicationRoute = Ember.Route.extend({
actions: {
error: function(error) {
// handle the error
Airbreak.push(error)
}
}
});
Moreover, if you catch errors somewhere else and have the same handling, you can make a mixin and pass the error:
App.ErrorHandlerMixin = Ember.Mixin.create({
handleError: function(error){
//make different stuff
Airbreak.push(error)
}
});
App.ApplicationRoute = Ember.Route.extend(App.ErrorHandlerMixin, {
actions: {
error: function(error, transition) {
this.handleError(error);
}
}
});
App.ApplicationController = Ember.ObjectController.extend((App.ErrorHandlerMixin, {
someFunction: function () {
this.handleError(randomError);
}
});
This way you have all the error handling in a single place.

Related

Javascript / React window.onerror fired twice

I want to globally catch errors in my React application.
But every time the error is caught/forwarded twice to my registered function.
Example code:
window.onerror = (msg, url, lineNo, columnNo, error) => {
console.log(msg)
alert(msg)
}
class TodoApp extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<button onClick={(e)=>{
console.log("clicked")
null.bla
}}>
Create an error
</button>
)
}
}
ReactDOM.render(<TodoApp />, document.querySelector("#app"))
Here is a JS-fiddle:
https://jsfiddle.net/dmxur0rc/4/
The console only shows one 'clicked' log, so it's not the button that fires twice, but the error event.
It is known react error, related with implementation of error boundaries.
I found a basic solution to this that should work in all scenarios.
It turns out that the object is identical in all calls, you could set up something to match them exactly, or you could just attach a custom attribute to the error object...
Admittedly this may only work with window.addEventListener('error', function...), as you are given the genuine error object as an argument, as opposed to window.onerror = function... which gets the data parts, such as message & lineNumber as opposed to the real error.
This is basically how I'm using it:
window.addEventListener('error', function (event) {
if (event.error.hasBeenCaught !== undefined){
return false
}
event.error.hasBeenCaught = true
// ... your useful code here
})
If this is called with the same error twice it will exit before getting to your useful code, only executing the useful code once per error.
You need to return true from your error handler otherwise the default error handler will fire:
https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror
When the function returns true, this prevents the firing of the default event handler.
Also note that other error handlers may be in place via addEventHandler.
As mentioned in other answers, the problem is in React in DEV mode. In this mode it re-throws all exceptions to "improve debugging experience".
I see 4 different error scenarios
Normal JS errors (for example, from an event handler, like in the question).
These are sent to window.onerror twice by React's invokeGuardedCallbackDev.
JS errors that happen during render, and there is no React's error boundary in the components tree.
The same as scenario 1.
JS errors that happen during render, and there is an error boundary somewhere in the components tree.
These are sent to window.onerror once by invokeGuardedCallbackDev, but are also caught by the error boundary's componentDidCatch.
JS errors inside promises, that were not handled.
These aren't sent to window.onerror, but rather to window.onunhandledrejection. And that happens only once, so no problem with this.
My workaround
window.addEventListener('error', function (event) {
const { error } = event;
// Skip the first error, it is always irrelevant in the DEV mode.
if (error.stack?.indexOf('invokeGuardedCallbackDev') >= 0 && !error.alreadySeen) {
error.alreadySeen = true;
event.preventDefault();
return;
}
// Normal error handling.
}, { capture: true });
I use this error boundary to handle both React and global errors. Here is some advice from the React documentation:
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.
A class component becomes an error boundary if it defines either (or
both) of the lifecycle methods static getDerivedStateFromError() or
componentDidCatch().
Only use error boundaries for recovering from unexpected exceptions;
don’t try to use them for control flow.
Note that error boundaries only catch errors in the components below them in the tree; An error boundary can’t catch an error within itself.
class ErrorBoundary extends React.Component {
state = {
error: null,
};
lastError = null;
// This lifecycle is invoked after an error has been thrown by a descendant component. It receives the error that was thrown as a parameter and should return a value to update state.
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return {
error,
};
}
componentDidMount() {
window.onerror = (msg, url, line, column, error) => {
this.logError({
error,
});
};
}
// getDerivedStateFromError() is called during the “render” phase, so side-effects are not permitted. For those use cases, use componentDidCatch() instead.
// This lifecycle is invoked after an error has been thrown by a descendant component. It receives two parameters:
// error - The error that was thrown.
// info - An object with a componentStack key containing
componentDidCatch(error, info) {
// avoid calling log error twice
if (this.lastError && this.lastError.message === this.state.error.message) {
return true;
}
// Example "componentStack":
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
// logComponentStackToMyService(info.componentStack);
this.logError({
error,
info,
});
}
async logError({
error,
info
}) {
this.lastError = error;
try {
await fetch('/error', {
method: 'post',
body: JSON.stringify(error),
});
} catch (e) {}
}
render() {
if (this.state.error) {
return display error ;
}
return this.props.children;
}
}
Another way is to store the last error's message in state and check when it happens for the second time.
export default MyComponent extends React.Component{
constructor(props){
super(props);
this.state = {message: null};
}
componentDidMount(){
const root = this;
window.onerror = function(msg, url, line, column, error){
if(root.state.message !== msg){
root.setState({message: msg});
// do rest of the logic
}
}
}
}
But anyways it is good idea to use React Error Boundaries. And you can
implement this global javascript error handling inside the error
boundary component. Where you can both catch js errors (with
window.onerror) and React errors (with componendDidCatch).
My workaround: Apply debouncing (in typescript):
import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
let errorObserver: any;
window.onerror = (msg, url, lineNo, columnNo, error) => {
if (!errorObserver) {
new Observable(observer => {
errorObserver = observer
}).pipe(debounceTime(300)) // wait 300ms after the last event before emitting last event
.pipe(distinctUntilChanged()) // only emit if value is different from previous value
.subscribe(handleOnError)
}
errorObserver.next(
{
msg,
url,
lineNo,
columnNo,
error
}
)
return true
}
const handleOnError = (value: any) => {
console.log('handleOnError', value)
}
This looks like it's probably firing twice due to the nature of running it on JSFiddle. In a normal build process (with webpack and babel) code with a script error like that should fail to transpile.

AngularJS - Use Toaster Notification for Custom Exception Handling and Logging

Using AngularJS Toaster for notification handling.
Now for custom exception handling, defined in index.html like below
<toaster-container toaster-options="{'time-out': 3000, 'position-class': 'toast-top-right'}"></toaster-container>
Using it in controller like below for custom exception
myService.serviceName().then(function (data) {
//do some processing
}).catch(function (error) {
toaster.pop({
type: 'error',
title: 'Custom exception!'
});
});
How can I use Angularjs-Toaster inside the decorator of
$exceptionHandler ?
How can I use Angularjs-Toaster for logging specific errors ?
How can I use a common service for toaster notification for
success,error and other messages ?
AngularJS has its own exception handler. You can override it with a
Decorator that helps you to extend the functionality of an object in
AngularJS.
Move the code of showing toastr to custom exception handler, and in your service throw an exception which is handled by custom exception handler.
So your service looks like these,
myService.serviceName().then(function (data) {
//Handle success
}).catch(function (error) {
// throw exception from catch handler
throw new Error(error.msg);
});
Custom Exception Handler for showing toastr messages.
angular.module('exceptionHandlingApp')
.config(function($provide) {
$provide.decorator('$exceptionHandler',
['$delegate', 'toastr', function extendExceptionHandler($delegate, toastr) {
return function (exception, cause) {
exception.message = '[Error: ]' + exception.message;
// Use toastr service to show toastr msg.
toastr.pop({
type: 'error',
title: exception.message
});
$delegate(exception, cause);
};
}]);
});

Server side account creation error

I have been trying to get server side account user creating to work but I have come across an issue with the check() method I am using server side. (I am using simple-schema for this)
When the password is empty, this causes check() to throw an error, and rightly so. However, this is a server-side error and I am not quite sure how to propagate this to the client to be caught and dealth with.
The exception that I can see from my browser console is as follows:
Exception while simulating the effect of invoking 'createUserAccount' Meteor.makeErrorType.errorClass {message: "Match error: One or more properties do not match the schema.", path: "", sanitizedError: Meteor.makeErrorType.errorClass, errorType: "Match.Error", stack: (...)…} Error: Match error: One or more properties do not match the schema.
at SimpleSchema.condition (http://localhost:3000/packages/aldeed_simple-schema.js?8fda161c43c0ba62801a10b0dfcc3eab75c6db88:2450:11)
at checkSubtree (http://localhost:3000/packages/check.js?ac81167b8513b85b926c167bba423981b0c4cf9c:255:17)
at check (http://localhost:3000/packages/check.js?ac81167b8513b85b926c167bba423981b0c4cf9c:67:5)
at Meteor.methods.createUserAccount (http://localhost:3000/both/methods/accounts.js?c418120e76666f0ca774a281caafc39bc2c3a59d:4:27)
at http://localhost:3000/packages/ddp.js?41b62dcceb3ce0de6ca79c6aed088cccde6a44d8:4244:25
at _.extend.withValue (http://localhost:3000/packages/meteor.js?81e2f06cff198adaa81b3bc09fc4f3728b7370ec:949:17)
at _.extend.apply (http://localhost:3000/packages/ddp.js?41b62dcceb3ce0de6ca79c6aed088cccde6a44d8:4235:54)
at _.extend.call (http://localhost:3000/packages/ddp.js?41b62dcceb3ce0de6ca79c6aed088cccde6a44d8:4113:17)
at Object.Template.PasswordRegister.events.submit form (http://localhost:3000/client/views/shared/accounts/accounts.js?ac573d92938a2b3d6107ea19e50065f7ac5d41b3:36:20)
at null. (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:3147:18)
Here is how my client code looks like :
Template.PasswordRegister.events({
'submit form': function(event, template) {
event.preventDefault();
var user = {
email: template.find('#email').value,
password: template.find('#password').value
};
Meteor.call('createUserAccount', user, function(error) {
if (error) {
console.log("CONSOLE : " + error);
//TODO DO SOMETHING
// return alert(error.reason);
} else {
Meteor.loginWithPassword(user.email, user.password, function(error) {
if (error) {
console.log("CONSOLE : " + error);
//TODO DO SOMETHING
// return alert(error.reason);
}
});
}
});
}
});
and here is my server side code:
Meteor.methods({
createUserAccount: function(user) {
// Important server-side check for security and data integrity
check(user, Schema.registration);
var email = user.email;
var password = user.password;
this.unblock();
return Accounts.createUser({
email: email,
password: password
});
}
});
I've tried wrapping client-side code with a normal try catch block but didn't make any difference; that console error still shows.
As the error message says, you have a method stub for 'createUserAccount' defined on the client. It is that client stub that is throwing the exception.
Wrap the method shown with if (Meteor.isServer) to keep it from running on the client.
if (Meteor.isServer ){
Meteor.methods({
createUserAccount: function (user) { ... }
});
}
If that doesn't work search your project for the client code defining the method stub.
To clarify what is happening I have made a meteorpad with a method incorrectly stubbed on the client that throws the error you see in the browser console. I have then added a second method, 'creatUserAccount1', which is only defined on server. When this second method is called, its error is handled by callback and does not cause an exception. I think that is the behaviour you want.

PhoneGap/Cordova with client-side routing?

I've inherited a Cordova/PhoneGap app running Cordova 3.4. My first task was to implement a Client-Side Routing framework to make it easier to navigate between pages. I chose Flatiron Director as my client-side router, but when I went to implement it I started to get weird functionality out of the app.
My first router setup:
var routing = {
testHandler: function(){
console.log('Route ran');
},
routes: function(){
return {
"/testhandler": testHandler
}
}
};
console.log('Routes added');
The routes are added (at least based on the console output). When I attempt to hit the /testhandler hash, I receive a "Failed to load resource: file:///testhandler" error when I set window.location.hash to "/testhandler". I noticed the "Route ran" statement was never printed.
My next attempt was just using the hashchange event with jQuery.
$(window).on('hashchange', function(){ console.log('Ran'); });
On this attempt, regardless of what I change the hash to, I see the 'Ran' output, but I still receive the "Failed to load resource: " error.
Is this a problem with PhoneGap/Cordova? Or our implementation? Is it just not possible to use client-side routing with Cordova? What am I doing wrong?
I know that this doesn't answer your question directly but you may consider making your own provisional router. This may help you to debug your app and to figure out what's the problem.
Something like this for example:
var router = (function (routes) {
var onRouteChange = function () {
// removes hash from the route
var route = location.hash.slice(1);
if (route in routes) {
routes[route]();
} else {
console.log('Route not defined');
}
};
window.addEventListener('hashchange', onRouteChange, false);
return {
addRoute: function (hashRoute, callback) {
routes[hashRoute] = callback;
},
removeRoute: function (hashRoute) {
delete routes[hashRoute];
}
};
})({
route1: function () {
console.log('Route 1');
document.getElementById('view').innerHTML = '<div><h1>Route 1</h1><p>Para 1</p><p>Para 2</p></div>';
},
route2: function () {
console.log('Route 2');
document.getElementById('view').innerHTML = '<div><h1>Route 1</h1><p>Para 1</p><p>Para 2</p></div>';
}
});

How to transition to a route from an Ember Data Adapter

In my application, I've got an ApplicationAdapter whose ajaxError method is customized. Within that method, I'd like to be able to transition to a given route. How can I do this?
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR) {
var error = this._super(jqXHR);
if (jqXHR) {
switch(jqXHR.status) {
// [...]
case 401:
// How can I transitionTo('login') here?
}
// [...]
}
}
});
Instead of transition in the adapter, isn't a good pratice IMHO, you can return an instance of Error and handle it in the error action of the current route:
App.UnauthorizedError // create a custom Error class
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR) {
var defaultAjaxError = this._super(jqXHR);
if (jqXHR) {
switch(jqXHR.status) {
case 401:
return new App.UnauthorizedError()
}
}
return defaultAjaxError;
}
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('person');
},
actions: {
error: function(reason) {
// all errors will be propagated to here, we check the instance to handle the UnauthorizedError
if (reason instanceof App.UnauthorizedError) {
this.transitionTo('login')
}
}
}
});
If you want to use this for all routes, you can put the unauthorized transition in the ApplicationRoute. Because the ApplicationRoute is the parent of all routes, and not handled actions, or actions that return true, will bubble to the parent routes.
App.ApplicationRoute = Ember.Route.extend({
actions: {
error: function(reason) {
if (reason instanceof App.UnauthorizedError) {
this.transitionTo('login')
}
}
}
});
App.BarRoute = Ember.Route.extend({
actions: {
error: function(reason) {
// handle errors of bar route
// bubble to application route
return true;
}
}
});
This is a fiddle with this sample http://jsfiddle.net/SkCH5/
Throw the error and allow the error hook on the route to catch it and transition from there. Additionally you can make a mixin with this logic and add the mixin to all of your routes.
Machty has additional information in his gist talking about the new router: https://gist.github.com/machty/5647589
App.AuthenticatedRoute = Ember.Route.extend({
beforeModel: function(transition) {
if (!authTokenPresent) {
return RSVP.reject();
// Could also just throw an error here too...
// it'll do the same thing as returning a rejecting promise.
// Note that we could put the redirecting `transitionTo`
// in here, but it's a better pattern to put this logic
// into `error` so that errors with resolving the model
// (say, the server tells us the auth token expired)
// can also get handled by the same redirect-to-login logic.
}
},
error: function(reason, transition) {
// This hook will be called for any errors / rejected promises
// from any of the other hooks or provided transitionTo promises.
// Redirect to `login` but save the attempted Transition
var loginController = this.controllerFor('login')
loginController.set('afterLoginTransition', transition);
this.transitionTo('login');
}
});

Categories