enhanced_errors 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42e0ffde5da4be326f8b1d721652eed031f7bfb20a07d31a256d69c588a383b7
4
- data.tar.gz: 5ef8303d0e6dd089253643b2abbf4aeef9a195831c946b2c92396390685e0a5b
3
+ metadata.gz: f9b5672dcc4f5583ab60b46799b58676f6ded0dbdec03a6dcd4637d53c5f9f77
4
+ data.tar.gz: c6a1faec38495fa64ce018c14bd10248ac38278dd41840137c7dbc6edc083661
5
5
  SHA512:
6
- metadata.gz: cb553e2a7f127a2d7efbf77c25867a9706d500ffb70e6e48060cd42390cdd903f140d60a899ed61f0d3340697b3ec4458a704545aa4380dad928d64a76d9b158
7
- data.tar.gz: 467e3f82cb5a170081b43f417dfa0b53c12df77a6a2614d13377d47227a00df0623d515a92707b52fd5cc8461f3862c16ae83c3aa264beff2ee63f88072299ce
6
+ metadata.gz: 00dde7064adf86a435200f8944139f7f6f042e3c797e3a97f38592e4c274b1131b54dbf6964069fa61a36aac5f7560cdeb6a62da3f0996d4f68416727de638d2
7
+ data.tar.gz: 17e332764e329e84a749621121eeb281e8c7e69b7b2c66dcb7248bfce81af6f9952d7ef5976e8bea0d72adcba483e5c9a564674256386b807a81a6629eec085c
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.0"
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