feat: derive _component from stacktrace for better error grouping#672
feat: derive _component from stacktrace for better error grouping#672
Conversation
For non-web errors (background jobs, GenServers, etc.), Honeybadger's
fingerprinting algorithm uses the component field along with exception
class and backtrace to group errors. Without a component, errors with
similar first stack frames (e.g., both going through Ecto.Repo) could
be incorrectly grouped together even when originating from different
parts of the application.
This adds automatic derivation of _component from the stacktrace when:
- There's no component from plug_env (not a web request)
- The user hasn't explicitly set _component in context
The ComponentDeriver walks through stack frames to find the first module
that belongs to the configured :app and isn't an "infrastructure" module
(Ecto.Repo, Ecto.Changeset, Postgrex, DBConnection, etc.).
Users can customize skipped modules via config:
config :honeybadger,
component_deriver_skip_patterns: [MyApp.CustomInfra, ~r/^Internal/]
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses an error grouping issue where different Ecto.ConstraintError exceptions from different locations were being grouped together in Honeybadger. The solution derives a component name from the stacktrace for non-web errors to provide better fingerprinting differentiation.
Changes:
- Added new
ComponentDerivermodule to derive component names from stacktraces by identifying the first application module that isn't infrastructure (Ecto, Postgrex, etc.) - Integrated component derivation into
Notice.new/3with proper precedence (user-set > plug_env > derived) - Added configuration support for customizing skipped patterns via
:component_deriver_skip_patterns
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| lib/honeybadger/component_deriver.ex | New module that walks stacktraces to find suitable component modules, with configurable skip patterns for infrastructure modules |
| lib/honeybadger/notice.ex | Updated to derive _component from stacktrace when no component exists from plug_env or user context |
| test/honeybadger/component_deriver_test.exs | Comprehensive unit tests (14 tests) covering derivation logic, skip patterns, and edge cases |
| test/honeybadger/notice_test.exs | Integration tests (5 tests) verifying component derivation behavior in notice creation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The documentation incorrectly referenced `component_deriver_skip_modules` but the code reads `component_deriver_skip_patterns`. Updated to match the actual config key and clarified that patterns can be atoms, strings, or regexes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
subzero10
left a comment
There was a problem hiding this comment.
Looks good to me (thank you Claude for all the comments) 😄.
Note: CI is failing because of bad formatting.
Module attributes containing compiled Regex structures cannot be injected into function bodies at compile time because they contain References that Elixir cannot escape. Move the patterns to a private function that returns them at runtime. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
rabidpraxis
left a comment
There was a problem hiding this comment.
Two things I'm wondering about:
I'm pretty sure that app_matches? will filter out all the included default_skip_patterns, as each one of those module's applications should not match against the app name, which makes the pattern match useless (unless the app name is the same as those core libraries, which should be very rare).
Secondly, I don't think this will fix the original issue out of the box. If we are checking for the first user stacktrace, then it would match MyApp.Repo, which in the example would be the same for all of the Ecto.ConstraintError fingerprints, still grouping them together.
What if we remove the current default skip patterns (as they're redundant with app_matches?), but keep the user-configurable skip patterns mechanism. Then automatically add the modules from the :ecto_repos config to the skip list, as that should skip any Ecto plumbing calls within the app.
Problem
Jim reported (in #668) that different
Ecto.ConstraintErrorexceptions from different locations in their application were being grouped together as the same error in Honeybadger. This caused issues with integrations (like Shortcut) where resolved issues would unexpectedly reopen.Root Cause
Honeybadger's fingerprinting algorithm uses three fields to group errors:
"Ecto.ConstraintError")file:method:line)For non-web errors (background jobs, GenServers, Tasks), there's no Phoenix controller, so
componentis empty. When two different constraint errors both flow through the sameEcto.Repomodule, they can end up with identical fingerprints:Even though they originate from completely different parts of the application (e.g.,
MyApp.UsersvsMyApp.Orders).Solution
This PR automatically derives
_componentfrom the stacktrace for non-web errors. TheComponentDerivermodule::app_componentin the contextThe Honeybadger API parser already looks for
_componentin the context (before falling back torequest.component), so this provides differentiated fingerprints:Configuration
Users can customize which modules are skipped:
Workaround (for users on older versions)
Users who can't upgrade immediately can work around this by explicitly setting context before operations that might fail:
Or by implementing a custom fingerprint adapter:
Test plan
ComponentDerivermodule (14 tests)NoticeTest(5 tests)_componentin context is respected🤖 Generated with Claude Code