Skip to content

feat: derive _component from stacktrace for better error grouping#672

Open
stympy wants to merge 4 commits intomasterfrom
better-component-reporting
Open

feat: derive _component from stacktrace for better error grouping#672
stympy wants to merge 4 commits intomasterfrom
better-component-reporting

Conversation

@stympy
Copy link
Copy Markdown
Member

@stympy stympy commented Jan 20, 2026

Problem

Jim reported (in #668) that different Ecto.ConstraintError exceptions 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:

  • Exception class (e.g., "Ecto.ConstraintError")
  • Component (e.g., controller name)
  • First application backtrace frame (file:method:line)

For non-web errors (background jobs, GenServers, Tasks), there's no Phoenix controller, so component is empty. When two different constraint errors both flow through the same Ecto.Repo module, they can end up with identical fingerprints:

hexdigest("Ecto.ConstraintError", "", "lib/my_app/repo.ex:insert:100")

Even though they originate from completely different parts of the application (e.g., MyApp.Users vs MyApp.Orders).

Solution

This PR automatically derives _component from the stacktrace for non-web errors. The ComponentDeriver module:

  1. Walks through stack frames looking for modules belonging to the configured :app
  2. Skips "infrastructure" modules (Ecto.Repo, Ecto.Changeset, Postgrex, DBConnection, etc.) that don't indicate the error's true origin
  3. Sets the first suitable module as _component in the context

The Honeybadger API parser already looks for _component in the context (before falling back to request.component), so this provides differentiated fingerprints:

hexdigest("Ecto.ConstraintError", "MyApp.Users", "lib/my_app/repo.ex:insert:100")
hexdigest("Ecto.ConstraintError", "MyApp.Orders", "lib/my_app/repo.ex:insert:100")

Configuration

Users can customize which modules are skipped:

config :honeybadger,
  component_deriver_skip_patterns: [
    MyApp.CustomInfraModule,      # atom
    ~r/^MyApp\.Internal/,         # regex
    "MyApp.Utilities"             # string
  ]

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:

Honeybadger.context(_component: "MyApp.Users", _action: "create")

Or by implementing a custom fingerprint adapter:

config :honeybadger, fingerprint_adapter: MyApp.FingerprintAdapter

Test plan

  • Added unit tests for ComponentDeriver module (14 tests)
  • Added integration tests in NoticeTest (5 tests)
  • Verified existing tests still pass
  • Tested that component is derived correctly from stacktraces
  • Tested that existing plug_env components are not overridden
  • Tested that user-set _component in context is respected

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings January 20, 2026 21:25
@stympy stympy linked an issue Jan 20, 2026 that may be closed by this pull request
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ComponentDeriver module 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/3 with 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.

Comment thread lib/honeybadger/component_deriver.ex Outdated
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>
Copy link
Copy Markdown
Member

@subzero10 subzero10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me (thank you Claude for all the comments) 😄.

Note: CI is failing because of bad formatting.

stympy and others added 2 commits January 22, 2026 03:30
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>
Copy link
Copy Markdown
Contributor

@rabidpraxis rabidpraxis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ecto.ConstraintError reports are grouped too tightly

4 participants