enhanced_errors 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42e0ffde5da4be326f8b1d721652eed031f7bfb20a07d31a256d69c588a383b7
4
- data.tar.gz: 5ef8303d0e6dd089253643b2abbf4aeef9a195831c946b2c92396390685e0a5b
3
+ metadata.gz: 1a4579802affa3f4ee55675cadce0a0782e239e95cacdeb3eac12c6a716c913e
4
+ data.tar.gz: 81e803f5ae6f247f6f38b116e92d3d07befed02028927cdb07dad3b7f0b87d28
5
5
  SHA512:
6
- metadata.gz: cb553e2a7f127a2d7efbf77c25867a9706d500ffb70e6e48060cd42390cdd903f140d60a899ed61f0d3340697b3ec4458a704545aa4380dad928d64a76d9b158
7
- data.tar.gz: 467e3f82cb5a170081b43f417dfa0b53c12df77a6a2614d13377d47227a00df0623d515a92707b52fd5cc8461f3862c16ae83c3aa264beff2ee63f88072299ce
6
+ metadata.gz: a110f026595e2dc79a65a311844860311f6454d593d5231604c368658526d2830d231c25417338e4b43f18febfc06158c3fa57c607ec47e1cf8dc4d4929077ab
7
+ data.tar.gz: ea199eaedd965fdcae33b99c3709e0ba1b0b492505506a2f9b0cb00daf1773225526670fb0537ade59bf300343e5d8cc32fd76c5906037d5e0522c2b88315e4c
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- **EnhancedErrors** is a pure Ruby gem that enhances exception messages by capturing and appending variables and their values from the scope where the error was raised.
5
+ **EnhancedErrors** is a pure Ruby gem that enhances exceptions by capturing variables and their values from the scope where the error was raised.
6
6
 
7
7
  **EnhancedErrors** leverages Ruby's built-in [TracePoint](https://ruby-doc.org/core-3.1.0/TracePoint.html) feature to provide detailed context for exceptions, making debugging easier without significant performance overhead.
8
8
 
@@ -16,7 +16,10 @@ When an exception is raised, EnhancedErrors captures the surrounding context. I
16
16
  require 'enhanced_errors'
17
17
  require 'awesome_print' # Optional, for better output
18
18
 
19
- EnhancedErrors.enhance!
19
+ # Enable capturing of variables at exception at raise-time. The .captured_variables method
20
+ # is added to all Exceptions and gets populated with in-scope variables and values on `raise`
21
+
22
+ EnhancedErrors.enhance_exceptions!
20
23
 
21
24
  def foo
22
25
  begin
@@ -24,18 +27,19 @@ def foo
24
27
  @myinstance = 10
25
28
  foo = @myinstance / myvar
26
29
  rescue => e
27
- puts e.message
30
+ puts e.captured_variables
28
31
  end
29
32
  end
30
33
 
31
34
  foo
32
-
33
35
  ```
34
36
 
35
37
  ##### Output:
36
38
 
37
39
  <img src="./doc/images/enhanced-error.png" style="height: 215px; width: 429px;"></img>
38
40
  <br>
41
+
42
+
39
43
  #### Enhanced Exception In Specs:
40
44
 
41
45
  ```ruby
@@ -60,6 +64,67 @@ end
60
64
 
61
65
  <img src="./doc/images/enhanced-spec.png" style="height: 369px; width: 712px;"></img>
62
66
 
67
+ # RSpec Setup
68
+
69
+ The simplest way to get started with EnhancedErrors is to use it for RSpec
70
+ exception capturing. To get variable output into RSpec, the approach below
71
+ enables capturing, but also gives nice output by formatting the failure message
72
+ with the variable capture.
73
+
74
+ The advantage of this approach is that it is only active for your spec runs.
75
+ This approach is ideal for CI and local testing because it doesn't make
76
+ any changes that should bleed through to production--it doesn't enhance
77
+ exceptions except those that pass by during the RSpec run.
78
+
79
+ ```ruby
80
+
81
+ RSpec.configure do |config|
82
+
83
+ # add these config changes to your RSpec config to get variable messages
84
+ config.before(:suite) do
85
+ RSpec::Core::Example.prepend(Enhanced::Integrations::RSpecErrorFailureMessage)
86
+ end
87
+
88
+ config.before(:example) do |_example|
89
+ EnhancedErrors.start_rspec_binding_capture
90
+ end
91
+
92
+ config.after(:example) do |example|
93
+ example.metadata[:expect_binding] = EnhancedErrors.stop_rspec_binding_capture
94
+ end
95
+
96
+ end
97
+
98
+ ```
99
+
100
+ ## Minitest
101
+
102
+ Untested as of yet, but enhance_exceptions!(override_messages: true) is likely to work.
103
+
104
+ If anyone wants to look into an integration implementation like RSpec, it would
105
+ be welcomed. With a more targeted approach like the RSpec one, exceptions could be captured and
106
+ modified only during test time, like the RSpec approach. This would be advantageous as
107
+ it wouldn't modify the exception message itself, but still makes the variable output available in
108
+ test messages.
109
+
110
+ ## Enhancing .message
111
+
112
+ EnhancedErrors can also append the captured variable description into the Exception's
113
+ .message method output if the override_messages argument is true.
114
+
115
+ This can be very convenient as it lets you capture and diagnose
116
+ the context of totally unanticipated exceptions without modifying all your error handlers.
117
+
118
+ The downside to this approach is that if you have expectations in your tests/specs
119
+ around exception messages, those may break. Also, if you are doing something with the error messages,
120
+ like storing them in a database, they could be *much* longer and that may pose an issue.
121
+
122
+ Ideally, use exception.captured_variables instead.
123
+
124
+ ```ruby
125
+ EnhancedErrors.enhance_exceptions!(override_messages: true)
126
+ ```
127
+
63
128
 
64
129
  ## Features
65
130
 
@@ -116,28 +181,35 @@ $ gem install enhanced_errors
116
181
 
117
182
  ## Basic Usage
118
183
 
119
- To enable EnhancedErrors, call the `enhance!` method:
184
+ To enable EnhancedErrors, call the `enhance_exceptions!` method:
120
185
 
121
186
  ```ruby
122
- # For a rails app, put this in an initializer, or spec_helper.rb
123
- # ex: config/initializers/enhanced_errors.rb
124
-
187
+ # For a rails app, you may put this in an initializer, or spec_helper.rb
188
+ # ex: config/initializers/enhanced.rb
189
+ # you should immediately see nice errors with variables in your logs
190
+
125
191
  require 'awesome_print' # Optional, for better output
126
- EnhancedErrors.enhance!
192
+ EnhancedErrors.enhance_exceptions!(override_messages: true)
127
193
 
128
- # -> now your error messages will have variables and their values appended to them.
129
194
 
130
195
  ```
131
196
 
132
- This activates the TracePoint to start capturing exceptions and their surrounding context.
197
+ The approach above activates the TracePoint to start capturing exceptions and their surrounding context.
198
+ It also overrides the .message to have the variables.
133
199
 
200
+ If modifying your exception handlers is an option, it is better *not* to use
201
+ override_messages: true, but instead just use the exception.captured_variables, which is
202
+ a string describing what was found, that is available regardless.
203
+
204
+ Note that a minimalistic approach is taken to generating the string--if no qualifying variables were present, you won't see any message!
134
205
 
135
206
  ### Configuration Options
136
207
 
137
- You can pass configuration options to `enhance!`:
208
+ You can pass configuration options to `enhance_exceptions!`:
138
209
 
139
210
  ```ruby
140
- EnhancedErrors.enhance!(enabled: true, max_length: 2000) do
211
+
212
+ EnhancedErrors.enhance_exceptions!(enabled: true, max_length: 2000) do
141
213
  # Additional configuration here
142
214
  add_to_skip_list :@instance_variable_to_skip, :local_to_skip
143
215
  end
@@ -145,7 +217,7 @@ end
145
217
  ```
146
218
  - `add_to_skip_list`: Variables to ignore, as symbols. ex: :@instance_variable_to_skip, :local_to_skip`
147
219
  - `enabled`: Enables or disables the enhancement (default: `true`).
148
- - `max_length`: Sets the maximum length of the enhanced message (default: `2500`).
220
+ - `max_length`: Sets the maximum length of the captured_variables string (default: `2500`).
149
221
 
150
222
  Currently, the first `raise` exception binding is presented.
151
223
  This may be changed in the future to allow more binding data to be presented.
@@ -236,7 +308,7 @@ end
236
308
 
237
309
  #### Using `on_format`
238
310
 
239
- `on_format` is the last stop for the message string that will be appended to `exception.message`.
311
+ `on_format` is the last stop for the message string that will be `exception.captured_variables`.
240
312
 
241
313
  Here it can be encrypted, rewritten, or otherwise modified.
242
314
 
@@ -257,7 +329,7 @@ You can add additional variables to the skip list as needed:
257
329
 
258
330
  ```ruby
259
331
 
260
- EnhancedErrors.enhance! do
332
+ EnhancedErrors.enhance_exceptions! do
261
333
  add_to_skip_list :@variable_to_skip
262
334
  end
263
335
 
@@ -271,15 +343,11 @@ The skip list is pre-populated with common variables to exclude and can be exten
271
343
  These exceptions are always ignored:
272
344
 
273
345
  ```ruby
274
- SystemExit,
275
- NoMemoryError,
276
- SignalException,
277
- Interrupt,
278
- ScriptError,
279
- LoadError,
280
- NotImplementedError,
281
- SyntaxError,
282
- SystemStackError
346
+ SystemExit NoMemoryError SignalException Interrupt
347
+ ScriptError LoadError NotImplementedError SyntaxError
348
+ RSpec::Expectations::ExpectationNotMetError
349
+ RSpec::Matchers::BuiltIn::RaiseError
350
+ SystemStackError Psych::BadAlias
283
351
  ```
284
352
 
285
353
  While this is close to "Things that don't descend from StandardError", it's not exactly that.
@@ -299,7 +367,6 @@ EnhancedErrors supports different capture levels to control the verbosity of the
299
367
  The info mode is recommended.
300
368
 
301
369
 
302
-
303
370
  ### Capture Types
304
371
 
305
372
  EnhancedErrors differentiates between two types of capture events:
@@ -307,8 +374,9 @@ EnhancedErrors differentiates between two types of capture events:
307
374
  - **`raise`**: Captures the context when an exception is initially raised.
308
375
  - **`rescue`**: Captures the context when an exception is last rescued.
309
376
 
310
- **Default Behavior**: By default, EnhancedErrors returns the first `raise` and the last `rescue` event for each exception.
377
+ **Default Behavior**: By default, EnhancedErrors starts with rescue capture off.
311
378
  The `rescue` exception is only available in Ruby 3.2+ as it was added to TracePoint events in Ruby 3.2.
379
+ If enabled, it returns the first `raise` and the last `rescue` event for each exception.
312
380
 
313
381
 
314
382
  ### Example: Redacting Sensitive Information
@@ -338,8 +406,13 @@ When an exception is raised or rescued, it captures:
338
406
 
339
407
  The captured data includes a `capture_event` field indicating whether the data was captured during a `raise` or `rescue` event. By default, EnhancedErrors returns the first `raise` and the last `rescue` event for each exception, providing a clear trace of the exception lifecycle.
340
408
 
341
- The captured data is then appended to the exception's message, providing rich context for debugging.
409
+ The captured data is available in .captured_variables, to provide context for debugging.
342
410
 
411
+ * EnhancedErrors does not persist captured data--it only keep it in memory for the lifetime of the exception.
412
+ * There are benchmarks around Tracepoint in the benchmark folder. Targeted tracepoints
413
+ seem to be very cheap--as in, you can hit them ten thousand+ times a second
414
+ without heavy overhead.
415
+ *
343
416
 
344
417
  ## Awesome Print
345
418
 
@@ -353,6 +426,7 @@ if you want to use it.
353
426
  gem 'awesome_print'
354
427
  ```
355
428
 
429
+
356
430
  ## Alternatives
357
431
 
358
432
  Why not use:
@@ -384,8 +458,7 @@ Ruby's TracePoint binding capture very narrowly with no other C API or dependenc
384
458
 
385
459
  - **TBD**: Memory considerations. This does capture data when an exception happens. EnhancedErrors hides under the bed when it sees **NoMemoryError**.
386
460
 
387
- - **Goal: Production Safety**: The gem is designed to, once vetted, be safe for production use, giving you valuable insights without compromising performance.
388
- I suggest letting it get well-vetted before making the leap and testing it for both performance and memory under load internally, as well.
461
+ - **Goal: Production Safety**: The gem is designed to, eventually, be made safe for production use, giving you valuable insights without compromising performance.
389
462
  I would not enable it in production *yet*.
390
463
 
391
464
  ## Contributing
@@ -1,5 +1,5 @@
1
1
  require 'benchmark'
2
- require_relative '../lib/enhanced_errors' # Adjust the path if necessary
2
+ require_relative '../lib/enhanced/enhanced' # Adjust the path if necessary
3
3
 
4
4
  # Define the number of iterations
5
5
  ITERATIONS = 10_000
@@ -13,7 +13,7 @@ def calculate_cost(time_in_seconds)
13
13
  end
14
14
 
15
15
  def with_enhanced_errors
16
- EnhancedErrors.enhance!(debug: false)
16
+ EnhancedErrors.enhance_exceptions!(debug: false)
17
17
  ITERATIONS.times do
18
18
  begin
19
19
  foo = 'bar'
@@ -38,7 +38,7 @@ def without_enhanced_errors
38
38
  end
39
39
 
40
40
  def when_capture_only_regexp_matched
41
- EnhancedErrors.enhance!(debug: false) do
41
+ EnhancedErrors.enhance_exceptions!(debug: false) do
42
42
  eligible_for_capture { |exception| !!/Boo/.match(exception.class.to_s) }
43
43
  end
44
44
 
@@ -54,7 +54,7 @@ def when_capture_only_regexp_matched
54
54
  end
55
55
 
56
56
  def when_capture_only_regexp_did_not_match
57
- EnhancedErrors.enhance!(debug: false) do
57
+ EnhancedErrors.enhance_exceptions!(debug: false) do
58
58
  eligible_for_capture { |exception| !!/Baz/.match(exception.class.to_s) }
59
59
  end
60
60
 
@@ -1,10 +1,10 @@
1
1
  require 'stackprof'
2
- require_relative '../lib/enhanced_errors' # Adjust the path if necessary
2
+ require_relative '../lib/enhanced/enhanced' # Adjust the path if necessary
3
3
 
4
4
  # gem install stackprof
5
5
 
6
6
  # adjust path as needed
7
- # ruby ./lib/core_ext/enhanced_errors/benchmark/stackprofile.rb
7
+ # ruby ./lib/core_ext/enhanced/benchmark/stackprofile.rb
8
8
  # dumps to current folder. read the stackprof dump:
9
9
  # stackprof stackprof.dump
10
10
 
@@ -12,7 +12,7 @@ require_relative '../lib/enhanced_errors' # Adjust the path if necessary
12
12
  ITERATIONS = 10_000
13
13
 
14
14
  def run_with_enhanced_errors
15
- EnhancedErrors.enhance!(debug: false)
15
+ EnhancedErrors.enhance_exceptions!(debug: false, override_messages: true)
16
16
  ITERATIONS.times do
17
17
  begin
18
18
  raise 'Test exception with EnhancedErrors'
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "enhanced_errors"
3
- spec.version = "1.0.0"
3
+ spec.version = "2.0.1"
4
4
  spec.authors = ["Eric Beland"]
5
5
 
6
6
  spec.summary = "Automatically enhance your errors with messages containing variable values from the moment they were raised."
@@ -23,5 +23,5 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_development_dependency "awesome_print", "~> 1.0"
25
25
  spec.add_development_dependency "rspec", "~> 3.0"
26
- spec.add_development_dependency "yard", "> 0.9"
26
+ spec.add_development_dependency 'yard', '~> 0.9'
27
27
  end
@@ -0,0 +1,32 @@
1
+ # spec/enhanced_errors_spec.rb
2
+
3
+ require_relative '../lib/enhanced_errors'
4
+ require_relative '../spec/spec_helper'
5
+
6
+ RSpec.describe 'Demo' do
7
+ let(:baz) { 'bee'}
8
+
9
+ before do
10
+ @yo = 'sup'
11
+ end
12
+
13
+ it 'does something' do
14
+ foo = 'bar'
15
+ baz
16
+ expect(false).to eq(true)
17
+ end
18
+
19
+ it 'does something else' do
20
+ something = 'else'
21
+ expect(false).to eq(true)
22
+ end
23
+
24
+ it 'passes fine' do
25
+ expect(true).to eq(true)
26
+ end
27
+
28
+ it 'works if it raises an errors' do
29
+ hi = 'there'
30
+ raise StandardError.new('crud')
31
+ end
32
+ end
@@ -1,7 +1,7 @@
1
1
  require './lib/enhanced_errors'
2
2
  require 'awesome_print' # Optional, for better output
3
3
 
4
- EnhancedErrors.enhance!
4
+ EnhancedErrors.enhance_exceptions!(override_messages: true, capture_events: [:raise, :rescue])
5
5
 
6
6
  def foo
7
7
  begin
@@ -20,7 +20,7 @@ end
20
20
  def boo
21
21
  seeme = 'youshould'
22
22
  baz
23
- rescue => e
23
+ rescue Exception => e
24
24
  puts e.message
25
25
  end
26
26
 
@@ -7,7 +7,7 @@ require_relative '../lib/enhanced_errors'
7
7
 
8
8
  RSpec.describe 'Neo' do
9
9
  before(:each) do
10
- EnhancedErrors.enhance!
10
+ EnhancedErrors.enhance_exceptions!(override_messages: true)
11
11
  end
12
12
 
13
13
  describe 'sees through' do
@@ -25,3 +25,23 @@ RSpec.describe 'Neo' do
25
25
  end
26
26
  end
27
27
  end
28
+
29
+ # Note:
30
+ # The approach above is unlikely to work in large codebases where there are many
31
+ # exception-based specs that verify exception messages.
32
+ #
33
+ # Instead, take this (recommended) approach:
34
+ #
35
+ # RSpec.configure do |config|
36
+ # config.before(:suite) do
37
+ # RSpec::Core::Example.prepend(Enhanced::Integrations::RSpecErrorFailureMessage)
38
+ # end
39
+ #
40
+ # config.before(:example) do |_example|
41
+ # EnhancedErrors.start_rspec_binding_capture
42
+ # end
43
+ #
44
+ # config.after(:example) do |example|
45
+ # example.metadata[:expect_binding] = EnhancedErrors.stop_rspec_binding_capture
46
+ # end
47
+ # end
@@ -0,0 +1,30 @@
1
+ module Enhanced
2
+ class Colors
3
+ COLORS = { red: 31, green: 32, yellow: 33, blue: 34, purple: 35, cyan: 36, white: 0 }
4
+
5
+ class << self
6
+ def enabled?
7
+ @enabled
8
+ end
9
+
10
+ def enabled=(value)
11
+ @enabled = value
12
+ end
13
+
14
+ def color(num, string)
15
+ @enabled ? "#{code(num)}#{string}#{code(0)}" : string
16
+ end
17
+
18
+ def code(num)
19
+ "\e[#{num}m"
20
+ end
21
+
22
+ COLORS.each do |color, code|
23
+ define_method(color) do |str|
24
+ color(COLORS[color], str)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,45 @@
1
+ # exception.rb
2
+
3
+ class Exception
4
+ def captured_variables
5
+ if binding_infos.any?
6
+ bindings_of_interest = select_binding_infos
7
+ EnhancedErrors.format(bindings_of_interest)
8
+ else
9
+ ''
10
+ end
11
+ rescue
12
+ ''
13
+ end
14
+
15
+ def binding_infos
16
+ @binding_infos ||= []
17
+ end
18
+
19
+ private
20
+
21
+ def select_binding_infos
22
+ # Preference:
23
+ # 1. First 'raise' binding that isn't from a library (gem).
24
+ # 2. If none, the first binding.
25
+ # 3. The last 'rescue' binding if available.
26
+
27
+ bindings_of_interest = []
28
+
29
+ first_app_raise = binding_infos.find do |info|
30
+ info[:capture_event] == 'raise' && !info[:library]
31
+ end
32
+ bindings_of_interest << first_app_raise if first_app_raise
33
+
34
+ if bindings_of_interest.empty? && binding_infos.first
35
+ bindings_of_interest << binding_infos.first
36
+ end
37
+
38
+ last_rescue = binding_infos.reverse.find do |info|
39
+ info[:capture_event] == 'rescue'
40
+ end
41
+ bindings_of_interest << last_rescue if last_rescue
42
+
43
+ bindings_of_interest.compact
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ module Enhanced
2
+ module Integrations
3
+ module RSpecErrorFailureMessage
4
+ def execution_result
5
+ result = super
6
+ if result.exception
7
+ EnhancedErrors.override_exception_message(result.exception, self.metadata[:expect_binding])
8
+ end
9
+ result
10
+ end
11
+ end
12
+ end
13
+ end