Sentry/Raven with Meteor on Server

I’ve been really impressed with Sentry’s exception handling for client side so was trying to get it work for the server too. Has anyone found a nice way to catch all server side exceptions and send them to Sentry via the Raven node client?

I can manually send them using Raven.captureException but I can’t seem to setup a global hook. In their docs they recommend running your whole server process inside a Raven context.

Raven.config('___DSN___').install();
Raven.context(function () {
  // all your stuff goes here
});

By the way - the docs are out of date. This forum post explains it best.

Is something like that possible with Meteor?

I tried looking through Kadira’s code to see if there were any clues but I’m not familiar enough with the internals of Node/Meteor to figure it out.

2 Likes

Apologies. Giving this a Monday morning bump before it disappears into oblivion. Any sentry users out there? :slight_smile:

Caveat: I’m not sure this code catches every error but it’s catching some and that’s better than none. I have an identical code block for the server main.js import.

import { Meteor } from 'meteor/meteor';

if (Meteor.isDevelopment) {
  require('../imports/client/main'); // eslint-disable-line global-require
} else {
  try {
    require('../imports/client/main'); // eslint-disable-line global-require
  } catch (e) {
    RavenLogger.captureException(e);
  }
}

The RavenLogger global is exported by a package I wrote that looks like this - https://gist.github.com/TimFletcher/57e2b9b81501a9e5326cf4899ff010f5

1 Like

Thanks for that Tim. We still haven’t made the switch to ES6 though so can’t readily make use of your suggestion. It has got me thinking a bit though. I had another look at what Kadira does and adapted their code:

let wrapMethodHanderForErrors = function(name, originalHandler, methodMap) {
    methodMap[name] = function() {
        try{
            return originalHandler.apply(this, arguments);
        } catch(ex) {
            Raven.captureException(ex);
            throw ex;
        }
    }
}

export const wrapMethods = function() {
    var originalMeteorMethods = Meteor.methods;
    // wrap future method handlers for capturing errors
    Meteor.methods = function(methodMap) {
        _.each(methodMap, function(handler, name) {
            wrapMethodHanderForErrors(name, handler, methodMap);
        });
        originalMeteorMethods(methodMap);
    };

    // wrap existing method handlers for capturing errors
    _.each(Meteor.default_server.method_handlers, function(handler, name) {
        wrapMethodHanderForErrors(name, handler, Meteor.default_server.method_handlers);
    });
}


I’ve called wrapMethods in my startup on the server and it works well. Only issue is that it conflicts (obviously enough) with Kadira. So my options are either to forego Kadira (which is still the best performance monitor by far for Meteor) or make a local clone of Kadira with the error handling stuff switched off (edit: this is what we’ve done).

I’ll still be missing exceptions from publications but I guess I could do something similar, again, following the example from Kadira.

Would be preferable to just have a global handler for uncaught exceptions though so if anyone knows a ‘classic’ meteor compatible solution it would be useful.

2 Likes

Have you been using Raven on the client side to track errors? Could you share your code for this?

Apologies @asad, I was on holidays :slight_smile:

Yes. It’s really simple on the client side.

In your client startup you will need something like this:

import ravenClient from 'raven-js';

Meteor.startup(function () {

    let sentryURL = 'https://' + Meteor.settings.public.sentryPublicKey + "@sentry.io/" + Meteor.settings.public.sentryAppId;

    ravenClient.config(sentryURL, {
        release: Meteor.settings.public.version,
        environment: Meteor.settings.public.environment,
        shouldSendCallback: function(data) {
            return Meteor.settings.public.environment !== "development";
        }
    }).install();
})

I’ve left our specific keys we use for keeping track of the version and environment and the callback we use so sentry doesn’t send anything when we’re in development mode.

Then, to add a little context add the following to your onLogin/Logout overrides.

Accounts.onLogin(function() {
    ravenClient.setUserContext({
        email: Meteor.user().emails[0].address,
        id: Meteor.userId()
    });
});

Accounts.onLogout(function() {
    ravenClient.setUserContext();
});
1 Like

Does this catch all errors or does Meteor swallow most errors and prevent them from being captured by Sentry? Because that has been my experience so far.

Yes - this gets all client side errors. Basically any exception that shows up in your browser console.

Gotta try this!

Does it play well with Kadira, or do I have to choose?

For using it on the server you have to remove the official meteorhacks:kadira, clone it locally and make the changes. Then you can keep sending performance related metrics to Kadira and send exceptions to Sentry.

1 Like

Ive been using sentry

Actually @asad. I’ve just noticed it doesn’t get all the errors, apologies. Specifically an error coming from a Tracker recompute which was asking Blaze to rerender. I had been focused on getting the server bits re-wired using modified Kadira code but didn’t spot that on the client there is also a part that needs re-wiring. Here’s the Kadira code un-modified. I will just change the part that sends the error to Kadira and re-route to Sentry in my local clone of Kadira.

var originalMeteorDebug = Meteor._debug;

Meteor._debug = function(m, s) {
  // We need to asign variables like this. Otherwise, 
  // we can't see proper error messages.
  // See: https://github.com/meteorhacks/kadira/issues/193
  var message = m;
  var stack = s;
  
  // track only if error tracking is enabled
  if(!Kadira.options.enableErrorTracking) {
    return originalMeteorDebug(message, stack);
  }

  // do not track if a zone is available (let zone handle the error)
  if(window.zone) {
    return originalMeteorDebug(message, stack);
  }

  // We hate Meteor._debug (no single usage pattern)
  if(message instanceof Error) {
    stack = message.stack;
    message = message.message
  } else if(typeof message == 'string' && stack === undefined) {
    stack = getStackFromMessage(message);
    message = firstLine(message);
  }

  // sometimes Meteor._debug is called with the stack concat to the message
  // FIXME Meteor._debug can be called in many ways
  if(message && stack === undefined) {
    stack = getStackFromMessage(message);
    message = firstLine(message);
  }

  var now = (new Date().getTime());
  Kadira.errors.sendError({
    appId : Kadira.options.appId,
    name : message,
    type : 'client',
    startTime : now,
    subType : 'meteor._debug',
    info : getBrowserInfo(),
    stacks : JSON.stringify([{at: now, events: [], stack: stack}]),
  });

  return originalMeteorDebug.apply(this, arguments);
};

var stackRegex = /^\s+at\s.+$/gm;
function getStackFromMessage (message) {
  // add empty string to add the empty line at start
  var stack = [''];
  var match;
  while(match = stackRegex.exec(message)) {
    stack.push(match[0]);
  }
  return stack.join('\n');
}

function firstLine (message) {
  return message.split('\n')[0];
}

Ok, for anyone interested, I think I have all client side errors now. If you’re using meteorhacks:kadira you’ll have to disable to the wrapping of the window.onerror and also Meteor._debug it does (in your local clone). window.onerror is then handled automatically by Raven and you can plug into Meteor._debug like this. Just call this function from your client startup. I left the original Kadira comments/issues in there.

import ravenClient from 'raven-js';

export const wrapMeteorDebug = function()  {

    let originalMeteorDebug = Meteor._debug;
    Meteor._debug = function(m, s) {
        // We need to asign variables like this. Otherwise, 
        // we can't see proper error messages.
        // See: https://github.com/meteorhacks/kadira/issues/193
        var message = m;
        var stack = s;
        
        // We hate Meteor._debug (no single usage pattern)
        if(message instanceof Error) {
            ravenClient.captureException(message);
        } else if(typeof message === 'string') { 
            let extra = {level: "error"};
            //meteor._debug never seems to receive a stack here but just incase let's add it as context.
            if(stack) {
                extra.stack = stack;
            } else {
                //otherwise let's generate a stack trace
                extra.stacktrace = true;
            }
            ravenClient.captureMessage(message, extra);
        }
        return originalMeteorDebug.apply(this, arguments);
    };
}
5 Likes

It can be usefull to do a proper meteor package to configure correctly Sentry with actual version of Meteor, isn’t it ? I had a look at https://github.com/deepwell/meteor-raven but it does not suit Meteor actual best parctices (es6…)

Unfortunately, I do not have time to dive in such thing actually…

That package is really out of date and does very little unfortunately.

For a really useful solution I think the best bet would be to fork meteorhacks:kadira and replace all the calls to the kadira server with ones to the sentry one. We could also replace all the trace calls with the equivalent to add sentry breadcrumbs which would be really cool.

I had started by just ripping pieces out of kadira as I needed them but I soon found that kadira was doing lots of other necessary things that I would need - the main example being accessing things like Meteor.userId from outside fibers. Kadira solves this by attaching the info when publications or methods are run and then it can use it internally when an error occurs.

So yeah, meteorhacks:kadira would be the best starting point I believe.

The best would be to refactor in a way to be able to choose our monitoring system (Sentry, AppDynamics, New Relic…)

Yeah, I totally agree.

Wouldn’t it be lovely if this was something Meteor provided an easy way to hook into.

Now that Kadira APM is open-sourced, could it be worth taking another shot at this?

3 Likes

The post by @marklynch works beautifully!

1 Like

I’m glad to hear that @batist , but I don’t understand @marklynch’s instructions. Could you explain them step by step?