Skip to content

(Improvement): Capture richer rspec exception details#1085

Merged
trunk-io[bot] merged 2 commits intomainfrom
claude/debug-rspec-nil-error-fm9yD
May 5, 2026
Merged

(Improvement): Capture richer rspec exception details#1085
trunk-io[bot] merged 2 commits intomainfrom
claude/debug-rspec-nil-error-fm9yD

Conversation

@TylerJang27
Copy link
Copy Markdown
Collaborator

@TylerJang27 TylerJang27 commented Apr 30, 2026

Summary

Improving what rspec_trunk_flaky_tests captures for failed examples so the data we report matches what users see in their RSpec console output.

  • failure_message now delegates to RSpec::Core::Formatters::ExceptionPresenter#fully_formatted (with a no-op colorizer). That gives us the full Failure/Error: <source line> header, the exception class and message, and any Caused by: chain — instead of just <class>: <message>. Previously the source line and root-cause chain were silently dropped.
  • backtrace uses RSpec's formatted_backtrace (which applies the configured backtrace cleaner) and walks exception.cause so each cause's backtrace is appended. This recovers the chained backtraces that Ruby exposes via #cause but that we were ignoring.
  • The previous formatting paths are preserved as fallbacks (legacy_format_exception_message / legacy_format_exception_backtrace) and the new code rescues StandardError to fall back if a future RSpec API shift breaks anything, so we still return useful information.
Example

RSpec

Failures:

  1) multiple_exception_error_test with before hook error and test failure test that would also fail
     Failure/Error: raise StandardError, 'Error in before hook'
     
     StandardError:
       Error in before hook
     # ./spec/multiple_exception_spec.rb:7:in `block (3 levels) in <top (required)>'
     # /home/tyler/repos/analytics-cli/rspec-trunk-flaky-tests/lib/trunk_spec_helper.rb:179:in `run'
     # /home/tyler/repos/analytics-cli/rspec-trunk-flaky-tests/lib/trunk_spec_helper.rb:149:in `run_with_trunk'

Before

More often than not, this backtrace is not useful, and rspec itself natively prettifies/hides it. I did a brief audit of some of our customer traces, and I think this rspec native matching will be preferred. More details below on rollout...

"test_output": {
  "text": "/home/tyler/repos/analytics-cli/.github/actions/test_ruby_gem_uploads/spec/multiple_exception_spec.rb:7:in `block (3 levels) in <top (required)>'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:457:in `instance_exec'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:457:in `instance_exec'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:365:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:529:in `block in run_owned_hooks_for'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:528:in `each'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:528:in `run_owned_hooks_for'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:615:in `block in run_example_hooks_for'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:614:in `reverse_each'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:614:in `run_example_hooks_for'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:484:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:505:in `run_before_example'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:261:in `block in run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:511:in `block in with_around_and_singleton_context_hooks'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:468:in `block in with_around_example_hooks'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:486:in `block in run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:626:in `block in run_around_example_hooks_for'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:352:in `call'\n/home/tyler/repos/analytics-cli/rspec-trunk-flaky-tests/lib/trunk_spec_helper.rb:178:in `run'\n/home/tyler/repos/analytics-cli/rspec-trunk-flaky-tests/lib/trunk_spec_helper.rb:148:in `run_with_trunk'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:457:in `instance_exec'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:457:in `instance_exec'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:390:in `execute_with'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:628:in `block (2 levels) in run_around_example_hooks_for'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:352:in `call'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:629:in `run_around_example_hooks_for'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/hooks.rb:486:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:468:in `with_around_example_hooks'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:511:in `with_around_and_singleton_context_hooks'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example.rb:259:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:653:in `block in run_examples'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:649:in `map'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:649:in `run_examples'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:614:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:615:in `block in run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:615:in `map'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/example_group.rb:615:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:121:in `block (3 levels) in run_specs'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:121:in `map'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:121:in `block (2 levels) in run_specs'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/configuration.rb:2097:in `with_suite_hooks'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:116:in `block in run_specs'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/reporter.rb:74:in `report'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:115:in `run_specs'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:89:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:71:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/lib/rspec/core/runner.rb:45:in `invoke'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/rspec-core-3.13.6/exe/rspec:4:in `<top (required)>'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/bin/rspec:25:in `load'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/bin/rspec:25:in `<top (required)>'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/cli/exec.rb:58:in `load'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/cli/exec.rb:58:in `kernel_load'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/cli/exec.rb:23:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/cli.rb:456:in `exec'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/vendor/thor/lib/thor/command.rb:28:in `run'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/vendor/thor/lib/thor.rb:527:in `dispatch'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/cli.rb:35:in `dispatch'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/vendor/thor/lib/thor/base.rb:584:in `start'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/lib/bundler/cli.rb:29:in `start'\n/home/tyler/.cache/trunk/tools/ruby/3.1.4/lib/ruby/gems/3.1.0/gems/bundler-2.5.23/exe/bundle:28:in `block ",
  "message": "Error in before hook",
  "system_out": "",
  "system_err": ""
},

After

"test_output": {
  "text": "./spec/multiple_exception_spec.rb:7:in `block (3 levels) in <top (required)>'\n/home/tyler/repos/analytics-cli/rspec-trunk-flaky-tests/lib/trunk_spec_helper.rb:179:in `run'\n/home/tyler/repos/analytics-cli/rspec-trunk-flaky-tests/lib/trunk_spec_helper.rb:149:in `run_with_trunk'",
  "message": "multiple_exception_error_test with before hook error and test failure test that would also fail\n  Failure/Error: raise StandardError, 'Error in before hook'\n\n  StandardError:\n    Error in before hook\n  # ./spec/multiple_exception_spec.rb:7:in `block (3 levels) in <top (required)>'\n  # /home/tyler/repos/analytics-cli/rspec-trunk-flaky-tests/lib/trunk_spec_helper.rb:179:in `run'\n  # /home/tyler/repos/analytics-cli/rspec-trunk-flaky-tests/lib/trunk_spec_helper.rb:149:in `run_with_trunk'",
  "system_out": "",
  "system_err": ""
},

Rollout

I plan to confirm this approach works with our end user via a beta release, and go from there. I will also proactively follow-up with our other rspec users to make sure this is not a regression for them. Note that embeddings summaries will certainly drift a bit.

Test plan

  • Verify a test that fails with a plain assertion still produces a clean failure_message and backtrace.
  • Verify a test whose failure has a cause chain (e.g. an error raised from inside a rescue) shows the Caused by: block in failure_message and the cause's frames in backtrace.
  • Verify a test that fails with RSpec::Core::MultipleExceptionError (test failure plus an after hook failure) reports all sub-exceptions.

https://claude.ai/code/session_01BHRe4t7dxsGyoMVhLrf3CF


Generated by Claude Code

Use RSpec's ExceptionPresenter for failure_message so customers see what
they're used to seeing in their console output: the Failure/Error source
line, the exception message, and any "Caused by:" chain. Walk
exception.cause for the backtrace field too so root causes aren't
dropped, and truncate the middle (instead of the tail) of oversized
fields so both the throw site and the project-local frames survive.
@trunk-io
Copy link
Copy Markdown

trunk-io Bot commented Apr 30, 2026

😎 Merged directly without going through the merge queue, as the queue was empty and the PR was up to date with the target branch - details.

@trunk-staging-io
Copy link
Copy Markdown

trunk-staging-io Bot commented Apr 30, 2026

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.01%. Comparing base (a5dce4e) to head (8e604bf).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1085      +/-   ##
==========================================
+ Coverage   81.75%   82.01%   +0.26%     
==========================================
  Files          69       69              
  Lines       14917    14917              
==========================================
+ Hits        12195    12234      +39     
+ Misses       2722     2683      -39     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@trunk-io
Copy link
Copy Markdown

trunk-io Bot commented Apr 30, 2026

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@TylerJang27
Copy link
Copy Markdown
Collaborator Author

@claude review

@trunk-io trunk-io Bot merged commit e2604f0 into main May 5, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants