Sentry.js + Stimulus + Asset Pipeline

TLDR; if I want to modify the stimulus Application in an asset based rails project so it applies to ALL controllers, where do I do that?

I am working on a basic app that a) uses hotwire, b) does not touch Webpack.

This has been challenging and I’ve had to make compromises (like moving Tailwind compilation to its own project) but I’m really struggling with Sentry. After many hours I found this PR that explained a lot of what I was seeing, because Stimulus is overriding the default Sentry window.onerror.

Now I’m trying to figure out what to do about that. I found this bit about a global error handler (and other similar examples), but these all appear to be using the webpack method of stimulus - loading up via the index.js.

I also found this, which seemed like the correct choice for overriding the default handler to hook up to Sentry. But again, this is using the index.js / webpack method of loading stimulus.

(The index.js thing threw me for a while. I’m not sure why the stimulus:install puts that in there if its an asset pipeline project, since it doesn’t appear to be used. (unless I broke it somehow).)

But I can’t figure out where I would do the error handler override in an asset manager based project. Namely, I can’t figure out what loads the importmap controllers and how the app hooks them up, so I don’t know where to inject the code to override the error handler. I’ve tried all sorts of things and now I’m so tied up in Javascript knots I don’t know which way is up.

Note: I do NOT want to have to manually trap errors in my controllers in the global application controller method. To me, this kills the whole point of having Sentry and having it capture errors you completely missed or ones that happen due to unforeseen library conflicts. If I have to use a try / catch and call Sentry manually, I’ve already lost.

1 Like

Well, I’ve dug into the stimulus-rails gem and I see what’s going on and how the Application gets loaded with the autoloader, which runs a specific autoloader.js to start the javascript app. And I can’t think of any way to jack in or monkey code that loader in order to do what I want. I could fork and modify the gem, but that’s not really viable either.

I think my only hope is to wait for that PR to get integrated in to make stimulus window.onerror aware. But until then, I don’t think there’s anything I can do for now.

Happy to hear other ideas. In the meantime I think the way you solve this problem is to just have to manually call Sentry when exceptions occur and pattern your controllers to wrap functions in try catch.

As I started working through my Application Controller, I had an idea. This is crappy, imo, but it does seem to work. Maybe someone can chime in with a less icky way:

I created an application controller at controllers/application_controller.js and gave it in “init” function:

import { Controller } from "stimulus"

export default class extends Controller {

  init() {
    console.log("Application Controller")
    if (this.application.errorShunt) return

    console.log('modifying error handler')
    const defaultErrorHandler = this.application.handleError.bind(this.application)
    const sentryErrorHandler = (error, message, detail = {}) => {
      defaultErrorHandler(error, message, detail)
      Sentry.captureException(error, { message, ...detail })
    }
    this.application.handleError = sentryErrorHandler
    this.application.errorShunt = true
  }
}

This init will modify the application’s error handler function to add in the Sentry method. (just like the Global Error Handler article) It also sets a flag to know that its been set already, so that additional controllers on the page don’t cause it to keep running and adding a loops of handlers.

Then in my two test controllers I did:

hello_controller.js

import ApplicationController from "application_controller"

export default class extends ApplicationController {

  connect() {
    super.init()
    console.log("hello loading")
  }

  error(e) {
    e.stopPropagation()
    console.log('hello fail')
    helloThisDoesNotExist()
  }
}

goodbye_controller.js

import ApplicationController from "application_controller"

export default class extends ApplicationController {

  connect() {
    super.init()
    console.log('goodbye loading')
  }

  error(e) {
    e.stopPropagation()
    console.log('goodbye fail')
    goodbyeThisDoesNotExist()
  }
}

Finally, my tiny html fragment is:

<div data-controller="hello">
  <a data-action="click->hello#error" href="#">Hello Error</a>
</div>

<div data-controller="goodbye">
  <a data-action="click->goodbye#error" href="#">Goodbye Error</a>
</div>

So when each controller loads, it calls the init on the super. The first one causes the handler to get replaced. The second controller call the init but the handler has already been replaced so it just moves on.

Tested and Sentry received both errors as I clicked the link. Without a try…catch or any other handling within the controllers. That’ll do for now I guess.