Back in 2017 I implemented error reporting for a client. Exceptions were sent to Rollbar and could be fixed quickly. My client and I were happy with this integration.
But after a while ago rather odd exceptions popped up. They were similar to this:
Here’s what I thought:
Why is IntelliJ reporting this? It’s clearly some internal IntelliJ stuff. Probably, this doOkAction() method is buggy. It really does not come from this plugin!
Errors of this kind did not stop, though. But my own plugins were still receiving stack traces as usual. So, I assumed that all of this is not my fault and that I couldn’t fix this in the plugin.
Recently, I implemented error reporting for the upcoming BashSupport Pro. I decided to use a self-hosted Sentry installation for this, because I wanted to be in control of the data. And guess what? The exceptions looked broken, just like the one above.
This article will explain what error reporting in a plugin can do for you. You’ll also learn how to implement it for your own plugin and how to avoid my mistake.
Why you should implement error reporting
Have you ever noticed this little red, blinking exclamation mark in the lower–right corner of your IDE’s main window? Well, that’s telling you that a fatal error occurred. If you’re lucky, then your own plugin is not at fault here. But often it is the culprit, isn’t it?
When you click on this red icon, then IntelliJ displays a dialog to report the exception.
Exceptions, which come from JetBrains’s code, are reported to their own servers. If the exception comes from the code of a 3rd–party plugin, then the error report is handled by that plugin. This is only possible if you implement the extension point for this.
If you don’t do that, then users still open the dialog, but can’t send the report because the “Submit” button is disabled. That’s confusing and frustrating.
If you care about your users and the quality of your plugin, then it’s a good idea to implement this. And it’s an easy, automated way to receive error reports with all the data you need.
Extension point “errorHandler”
The extension point we need is called
You need a few things before you can get to the code.
- A way to receive the reports
- You have multiple options here:
- An easy solution is to upload a cgi–script somewhere and to send a HTTP POST request. That’s what I’m doing with the open–source BashSupport plugin. It’s simple and easy to handle for small plugins or single developers.
- The client, whom I mentioned in the introduction, is using Rollbar. Rollbar is okay, but rather expensive if you need more than the basics.
- For BashSupport Pro I’m hosting Sentry on my own server. With Sentry you can either use their cloud-hosting solution or host the same software yourself. This way you get the complete set of features and still are in control of the data. You’ll need a hosting solution which supports Docker, though.
- Or you could do something completely different. For example, you could just open github.com in the browser or show a message to explain what the user should do.
- A privacy statement (optional)
- Users might want to know what’s send. You can provide your own message for the error reporting dialog.
- User identification (optional)
- The dialog provides optional UI to let the user provide identification, e.g. an email address or a login.
This extension isn’t that complicated to implement. At first, I’ll describe how this works. You can find sample code to integrate with Sentry at the end.
Declare your error handler like this in your
You have to create a subclass of com.intellij.openapi.diagnostic.ErrorReportSubmitter. Implement at least these two methods:
public String getReportActionText(). Use it to customize the label of the dialog’s submit button.
public boolean submit(...). This method does all the hard work. The parameters provide the errors to report, an optional note of the user and a callback to tell IntelliJ when the report was send.
How to implement
Here’s the full signature of the method:
The method itself has a
boolean return type. This is to tell the IDE if the report can be send at all. If you can’t send the report, then return
false and you’re done.
true and send the report asynchronously — that’s important.
- This is the list of exceptions, which should be send.
- This is an optional message by the user.
- This might be useful if you want to show UI, e.g. a message box. Ignore this if you’re not interacting with the user.
- The callback. Call
consumer.consume(…)when the report has been send successfully or failed to send. The argument to this method specifies the type of result.
Implementing an asynchronous operation might seem difficult at first. But IntelliJ already provides a bunch of abstractions to handle the most common cases.
IntelliJ’s own, internal error reporter implements this with a
We’ll do this in a very similar manner:
Of course, the logic to send the data to your server isn’t there yet. But we’ll get to that, soon.
Extracting the data to send
Let’s have another look at the dialog and the signature of the
Most of the elements of this dialog can be customized by your implementation. From top to bottom:
- User message
- Here, a user may provide some more details. This message is provided by the parameter
- A list of files, which should be send alongside the report.
- The first one is always the exception itself. Sometimes, IntelliJ adds more items to this list, e.g. the currently edited file. Only user–approved attachments are send. These attachments are made available by
com.intellij.diagnostic.IdeaReportingEvent, which is a subclass of
IdeaLoggingEvent. Logging events are provided by the parameter
- User identification (optional, hidden by default)
- This allows to identify the user. Override
void changeReporterAccount(Component parentComponent)if you want to support this. This was only recently added to the SDK, versions 2019.3 and later come with this feature.
public String getPrivacyNoticeText()if you’d like to show this. Basic HTML tags are allowed Versions 2018.3 and later offer this feature.
- Submit button
- Implement method
String getReportActionText()to customize the label of this button.
Do you still remember the first parameter of the
submit() method? It’s
IdeaLoggingEvent events. We’ll now take a closer look at these events.
As far as I can tell, IntelliJ always passes a single event of type
IdeaReportingEvent wraps the exception and the list of attachments. But this implementation may change at any time. So we’ll handle more than one event and won’t assume that it’s always a
event.getThrowableText() to get the complete stack trace as a string.
event.getThrowable().printStackTrace(…) provides the same value as
But, please, never use the return value of
event.getThrowable() for anything else. For example, don’t use the result of
Here’s why: the event is a
IdeaReportingEvent, and the implementation of IdeaReportingEvent is a bit special. It creates a new exception of type
TextBasedThrowable to wrap the original exception’s stack trace string. But its
getStackTrace() is still returning the stack trace where this wrapper was instantiated. If you use it, then you’ll get the wrong stack trace.
If you need the original exception, then retrieve it like this:
This is what happened for the plugin of my client:
- I implemented the error reporter in 2017. The exception returned by
event.getThrowable()was passed on to the Rollbar library. Everything worked nicely. The Rollbar library used
event.getThrowable().getStackTrace()to get all the frames of the stack. So far, so good.
- Now — in 2018 — this commit refactored the error reporting.
TextBasedThrowablewas introduced. Apparently, this change was made to allow user–editable stack traces.
event.getThrowable().getStackTrace()now returns where
TextBasedThrowablewas created and not were the original exception occurred.
- The error reporting is now messed up. Error reporting of my own plugins worked, because I was just using
event.getThrowableText()and not the throwable itself.
event.getThrowableText()whenever possible. Try to use the user–editable stack trace text.
((AbstractMessage)event.getData()).getThrowable()if you need the original exception with stack trace intact. This is probably the only solution for libraries, which need a
Throwable. And – of course – check
nullwhen you’re using its return value.
As far as I know, this isn’t documented in the public API and this certainly is not an official guideline. Things may break in the future if you access the original throwable as shown above. But — to my knowledge — there’s no other way if you need it.
How to implement error reporting with Sentry
Here we’ll discuss in short how an implementation with Sentry could look like. There’s complete sample code on github.com for your reference. Here, we’re only discussing the general approach.
- Declare a dependency on the Sentry client library to make it available in your project. Add this to your build.gradle file:
- Setup the sentry client. This is usually only needed once. You need a “DSN” for this. Sentry provides it in the settings of your project. SentryDemo is a simple implementation.
- Create a new event with the properties, which you want to send together with the stack trace (sample code).
- Send the event to the Sentry server:
consumer.consume(…)to tell the IDE about the result. You need to do this in the dispatcher thread. It’s usually something like this:
How this is displayed by Sentry
Here’s how such an exceptions shows up in Sentry. The tags
release are especially useful to debug these issues later. The automatic grouping of events is also very helpful.
Code of the Sentry error handler
This is just a copy of the code on GitHub.