bugsnag 6.10.0 → 6.11.0

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +4 -0
  3. data/CHANGELOG.md +20 -0
  4. data/Gemfile +1 -0
  5. data/README.md +1 -0
  6. data/VERSION +1 -1
  7. data/features/fixtures/docker-compose.yml +13 -0
  8. data/features/fixtures/rails3/app/app/controllers/breadcrumbs_controller.rb +19 -0
  9. data/features/fixtures/rails3/app/app/controllers/session_tracking_controller.rb +10 -6
  10. data/features/fixtures/rails3/app/config/initializers/bugsnag.rb +8 -2
  11. data/features/fixtures/rails3/app/config/routes.rb +1 -0
  12. data/features/fixtures/rails4/app/Gemfile +5 -1
  13. data/features/fixtures/rails4/app/app/controllers/breadcrumbs_controller.rb +26 -0
  14. data/features/fixtures/rails4/app/app/controllers/mongo_controller.rb +23 -0
  15. data/features/fixtures/rails4/app/app/controllers/session_tracking_controller.rb +9 -5
  16. data/features/fixtures/rails4/app/app/jobs/application_job.rb +2 -0
  17. data/features/fixtures/rails4/app/app/jobs/notify_job.rb +5 -0
  18. data/features/fixtures/rails4/app/app/models/mongo_model.rb +6 -0
  19. data/features/fixtures/rails4/app/config/initializers/bugsnag.rb +7 -1
  20. data/features/fixtures/rails4/app/config/mongoid.yml +22 -0
  21. data/features/fixtures/rails4/app/config/routes.rb +2 -0
  22. data/features/fixtures/rails5/app/Gemfile +4 -0
  23. data/features/fixtures/rails5/app/app/controllers/breadcrumbs_controller.rb +24 -0
  24. data/features/fixtures/rails5/app/app/controllers/mongo_controller.rb +22 -0
  25. data/features/fixtures/rails5/app/app/controllers/session_tracking_controller.rb +9 -5
  26. data/features/fixtures/rails5/app/app/jobs/notify_job.rb +5 -0
  27. data/features/fixtures/rails5/app/app/models/mongo_model.rb +6 -0
  28. data/features/fixtures/rails5/app/config/initializers/bugsnag.rb +7 -1
  29. data/features/fixtures/rails5/app/config/mongoid.yml +23 -0
  30. data/features/fixtures/rails5/app/config/routes.rb +11 -1
  31. data/features/rails_features/auto_capture_sessions.feature +55 -5
  32. data/features/rails_features/breadcrumbs.feature +135 -0
  33. data/features/rails_features/mongo_breadcrumbs.feature +100 -0
  34. data/features/steps/ruby_notifier_steps.rb +6 -0
  35. data/lib/bugsnag.rb +59 -3
  36. data/lib/bugsnag/breadcrumbs/breadcrumb.rb +76 -0
  37. data/lib/bugsnag/breadcrumbs/breadcrumbs.rb +14 -0
  38. data/lib/bugsnag/breadcrumbs/validator.rb +59 -0
  39. data/lib/bugsnag/configuration.rb +103 -6
  40. data/lib/bugsnag/integrations/mongo.rb +132 -0
  41. data/lib/bugsnag/integrations/rails/rails_breadcrumbs.rb +118 -0
  42. data/lib/bugsnag/integrations/railtie.rb +28 -1
  43. data/lib/bugsnag/middleware/breadcrumbs.rb +21 -0
  44. data/lib/bugsnag/report.rb +30 -1
  45. data/lib/bugsnag/session_tracker.rb +1 -0
  46. data/lib/bugsnag/utility/circular_buffer.rb +62 -0
  47. data/spec/breadcrumbs/breadcrumb_spec.rb +93 -0
  48. data/spec/breadcrumbs/validator_spec.rb +200 -0
  49. data/spec/bugsnag_spec.rb +230 -0
  50. data/spec/configuration_spec.rb +176 -2
  51. data/spec/integrations/mongo_spec.rb +262 -0
  52. data/spec/report_spec.rb +149 -0
  53. data/spec/session_tracker_spec.rb +24 -2
  54. data/spec/utility/circular_buffer_spec.rb +98 -0
  55. metadata +27 -2
@@ -1,12 +1,14 @@
1
1
  # Rails 3.x hooks
2
2
 
3
+ require "json"
3
4
  require "rails"
4
5
  require "bugsnag"
5
6
  require "bugsnag/middleware/rails3_request"
6
7
  require "bugsnag/middleware/rack_request"
8
+ require "bugsnag/integrations/rails/rails_breadcrumbs"
7
9
 
8
10
  module Bugsnag
9
- class Railtie < Rails::Railtie
11
+ class Railtie < ::Rails::Railtie
10
12
 
11
13
  FRAMEWORK_ATTRIBUTES = {
12
14
  :framework => "Rails"
@@ -38,6 +40,8 @@ module Bugsnag
38
40
  include Bugsnag::Rails::ActiveRecordRescue
39
41
  end
40
42
 
43
+ Bugsnag::Rails::DEFAULT_RAILS_BREADCRUMBS.each { |event| event_subscription(event) }
44
+
41
45
  Bugsnag.configuration.app_type = "rails"
42
46
  end
43
47
 
@@ -63,5 +67,28 @@ module Bugsnag
63
67
  app.config.middleware.use Bugsnag::Rack
64
68
  end
65
69
  end
70
+
71
+ ##
72
+ # Subscribes to an ActiveSupport event, leaving a breadcrumb when it triggers
73
+ #
74
+ # @api private
75
+ # @param event [Hash] details of the event to subscribe to
76
+ def event_subscription(event)
77
+ ActiveSupport::Notifications.subscribe(event[:id]) do |*, event_id, data|
78
+ filtered_data = data.slice(*event[:allowed_data])
79
+ filtered_data[:event_name] = event[:id]
80
+ filtered_data[:event_id] = event_id
81
+ if event[:id] == "sql.active_record"
82
+ binds = data[:binds].each_with_object({}) { |bind, output| output[bind.name] = '?' if defined?(bind.name) }
83
+ filtered_data[:binds] = JSON.dump(binds) unless binds.empty?
84
+ end
85
+ Bugsnag.leave_breadcrumb(
86
+ event[:message],
87
+ filtered_data,
88
+ event[:type],
89
+ :auto
90
+ )
91
+ end
92
+ end
66
93
  end
67
94
  end
@@ -0,0 +1,21 @@
1
+ module Bugsnag::Middleware
2
+ ##
3
+ # Adds breadcrumbs to the report
4
+ class Breadcrumbs
5
+ ##
6
+ # @param next_callable [#call] the next callable middleware
7
+ def initialize(next_callable)
8
+ @next = next_callable
9
+ end
10
+
11
+ ##
12
+ # Execute this middleware
13
+ #
14
+ # @param report [Bugsnag::Report] the report being iterated over
15
+ def call(report)
16
+ breadcrumbs = report.configuration.breadcrumbs.to_a
17
+ report.breadcrumbs = breadcrumbs unless breadcrumbs.empty?
18
+ @next.call(report)
19
+ end
20
+ end
21
+ end
@@ -23,6 +23,7 @@ module Bugsnag
23
23
  attr_accessor :api_key
24
24
  attr_accessor :app_type
25
25
  attr_accessor :app_version
26
+ attr_accessor :breadcrumbs
26
27
  attr_accessor :configuration
27
28
  attr_accessor :context
28
29
  attr_accessor :delivery_method
@@ -51,6 +52,7 @@ module Bugsnag
51
52
  self.api_key = configuration.api_key
52
53
  self.app_type = configuration.app_type
53
54
  self.app_version = configuration.app_version
55
+ self.breadcrumbs = []
54
56
  self.delivery_method = configuration.delivery_method
55
57
  self.hostname = configuration.hostname
56
58
  self.meta_data = {}
@@ -110,7 +112,14 @@ module Bugsnag
110
112
  payload_event = Bugsnag::Cleaner.clean_object_encoding(payload_event)
111
113
 
112
114
  # filter out sensitive values in (and cleanup encodings) metaData
113
- payload_event[:metaData] = Bugsnag::Cleaner.new(configuration.meta_data_filters).clean_object(meta_data)
115
+ filter_cleaner = Bugsnag::Cleaner.new(configuration.meta_data_filters)
116
+ payload_event[:metaData] = filter_cleaner.clean_object(meta_data)
117
+ payload_event[:breadcrumbs] = breadcrumbs.map do |breadcrumb|
118
+ breadcrumb_hash = breadcrumb.to_h
119
+ breadcrumb_hash[:metaData] = filter_cleaner.clean_object(breadcrumb_hash[:metaData])
120
+ breadcrumb_hash
121
+ end
122
+
114
123
  payload_event.reject! {|k,v| v.nil? }
115
124
 
116
125
  # return the payload hash
@@ -153,6 +162,26 @@ module Bugsnag
153
162
  @should_ignore = true
154
163
  end
155
164
 
165
+ ##
166
+ # Generates a summary to be attached as a breadcrumb
167
+ #
168
+ # @return [Hash] a Hash containing the report's error class, error message, and severity
169
+ def summary
170
+ # Guard against the exceptions array being removed/changed or emptied here
171
+ if exceptions.respond_to?(:first) && exceptions.first
172
+ {
173
+ :error_class => exceptions.first[:errorClass],
174
+ :message => exceptions.first[:message],
175
+ :severity => severity
176
+ }
177
+ else
178
+ {
179
+ :error_class => "Unknown",
180
+ :severity => severity
181
+ }
182
+ end
183
+ end
184
+
156
185
  private
157
186
 
158
187
  def generate_exception_list
@@ -35,6 +35,7 @@ module Bugsnag
35
35
  #
36
36
  # This allows Bugsnag to track error rates for a release.
37
37
  def start_session
38
+ return unless Bugsnag.configuration.enable_sessions
38
39
  start_delivery_thread
39
40
  start_time = Time.now().utc().strftime('%Y-%m-%dT%H:%M:00')
40
41
  new_session = {
@@ -0,0 +1,62 @@
1
+ module Bugsnag::Utility
2
+ ##
3
+ # A container class with a maximum size, that removes oldest items as required.
4
+ #
5
+ # @api private
6
+ class CircularBuffer
7
+ include Enumerable
8
+
9
+ # @return [Integer] the current maximum allowable number of items
10
+ attr_reader :max_items
11
+
12
+ ##
13
+ # @param max_items [Integer] the initial maximum number of items
14
+ def initialize(max_items = 25)
15
+ @max_items = max_items
16
+ @buffer = []
17
+ end
18
+
19
+ ##
20
+ # Adds an item to the circular buffer
21
+ #
22
+ # If this causes the buffer to exceed its maximum items, the oldest item will be removed
23
+ #
24
+ # @param item [Object] the item to add to the buffer
25
+ # @return [self] returns itself to allow method chaining
26
+ def <<(item)
27
+ @buffer << item
28
+ trim_buffer
29
+ self
30
+ end
31
+
32
+ ##
33
+ # Iterates over the buffer
34
+ #
35
+ # @yield [Object] sequentially gives stored items to the block
36
+ def each(&block)
37
+ @buffer.each(&block)
38
+ end
39
+
40
+ ##
41
+ # Sets the maximum allowable number of items
42
+ #
43
+ # If the current number of items exceeds the new maximum, oldest items will be removed
44
+ # until this is no longer the case
45
+ #
46
+ # @param new_max_items [Integer] the new allowed item maximum
47
+ def max_items=(new_max_items)
48
+ @max_items = new_max_items
49
+ trim_buffer
50
+ end
51
+
52
+ private
53
+
54
+ ##
55
+ # Trims the buffer down to the current maximum allowable item number
56
+ def trim_buffer
57
+ trim_size = @buffer.size - @max_items
58
+ trim_size = 0 if trim_size < 0
59
+ @buffer.shift(trim_size)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ require 'bugsnag/breadcrumbs/breadcrumb'
6
+
7
+ RSpec.describe Bugsnag::Breadcrumbs::Breadcrumb do
8
+ describe "#name" do
9
+ it "is assigned in #initialize" do
10
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new("my message", nil, nil, nil)
11
+
12
+ expect(breadcrumb.name).to eq("my message")
13
+ end
14
+ end
15
+
16
+ describe "#type" do
17
+ it "is assigned in #initialize" do
18
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new(nil, "test type", nil, nil)
19
+
20
+ expect(breadcrumb.type).to eq("test type")
21
+ end
22
+ end
23
+
24
+ describe "#meta_data" do
25
+ it "is assigned in #initialize" do
26
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new(nil, nil, {:a => 1, :b => 2}, nil)
27
+
28
+ expect(breadcrumb.meta_data).to eq({:a => 1, :b => 2})
29
+ end
30
+ end
31
+
32
+ describe "#auto" do
33
+ it "defaults to false" do
34
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new(nil, nil, nil, nil)
35
+
36
+ expect(breadcrumb.auto).to eq(false)
37
+ end
38
+
39
+ it "is true if auto argument == :auto" do
40
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new(nil, nil, nil, :auto)
41
+
42
+ expect(breadcrumb.auto).to eq(true)
43
+ end
44
+
45
+ it "is false if auto argument is anything else" do
46
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new(nil, nil, nil, :manual)
47
+
48
+ expect(breadcrumb.auto).to eq(false)
49
+ end
50
+ end
51
+
52
+ describe "#timestamp" do
53
+ it "is stored as a timestamp" do
54
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new(nil, nil, nil, nil)
55
+
56
+ expect(breadcrumb.timestamp).to be_within(0.5).of Time.now.utc
57
+ end
58
+ end
59
+
60
+ describe "#ignore?" do
61
+ it "is not true by default" do
62
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new("my message", "test type", {:a => 1, :b => 2}, :manual)
63
+
64
+ expect(breadcrumb.ignore?).to eq(false)
65
+ end
66
+
67
+ it "is able to be set" do
68
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new("my message", "test type", {:a => 1, :b => 2}, :manual)
69
+ breadcrumb.ignore!
70
+
71
+ expect(breadcrumb.ignore?).to eq(true)
72
+ end
73
+ end
74
+
75
+ describe "#to_h" do
76
+ it "outputs as a hash" do
77
+ breadcrumb = Bugsnag::Breadcrumbs::Breadcrumb.new("my message", "test type", {:a => 1, :b => 2}, :manual)
78
+ output = breadcrumb.to_h
79
+
80
+ timestamp_regex = /^\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:[\d\.]+Z$/
81
+
82
+ expect(output).to match(
83
+ :name => "my message",
84
+ :type => "test type",
85
+ :metaData => {
86
+ :a => 1,
87
+ :b => 2
88
+ },
89
+ :timestamp => eq(breadcrumb.timestamp.iso8601)
90
+ )
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,200 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ require 'bugsnag/breadcrumbs/breadcrumb'
5
+ require 'bugsnag/breadcrumbs/validator'
6
+
7
+ RSpec.describe Bugsnag::Breadcrumbs::Validator do
8
+ let(:enabled_automatic_breadcrumb_types) { Bugsnag::Breadcrumbs::VALID_BREADCRUMB_TYPES }
9
+ let(:auto) { false }
10
+ let(:name) { "Valid message" }
11
+ let(:type) { Bugsnag::Breadcrumbs::ERROR_BREADCRUMB_TYPE }
12
+ let(:meta_data) { {} }
13
+
14
+ describe "#validate" do
15
+ it "does not 'ignore!' a valid breadcrumb" do
16
+ config = instance_double(Bugsnag::Configuration)
17
+ allow(config).to receive(:enabled_automatic_breadcrumb_types).and_return(enabled_automatic_breadcrumb_types)
18
+ validator = Bugsnag::Breadcrumbs::Validator.new(config)
19
+
20
+ breadcrumb = instance_double(Bugsnag::Breadcrumbs::Breadcrumb, {
21
+ :auto => auto,
22
+ :name => name,
23
+ :type => type,
24
+ :meta_data => meta_data,
25
+ :meta_data= => nil
26
+ })
27
+
28
+ expect(breadcrumb).to_not receive(:ignore!)
29
+ expect(config).to_not receive(:warn)
30
+
31
+ validator.validate(breadcrumb)
32
+ end
33
+
34
+ it "trims long messages to length and warns" do
35
+ config = instance_double(Bugsnag::Configuration)
36
+ allow(config).to receive(:enabled_automatic_breadcrumb_types).and_return(enabled_automatic_breadcrumb_types)
37
+ validator = Bugsnag::Breadcrumbs::Validator.new(config)
38
+
39
+ name = "1234567890123456789012345678901234567890"
40
+
41
+ breadcrumb = instance_double(Bugsnag::Breadcrumbs::Breadcrumb, {
42
+ :auto => auto,
43
+ :name => name,
44
+ :type => type,
45
+ :meta_data => meta_data,
46
+ :meta_data= => nil
47
+ })
48
+
49
+ expect(breadcrumb).to_not receive(:ignore!)
50
+ expect(breadcrumb).to receive(:name=).with("123456789012345678901234567890")
51
+ expected_string = "Breadcrumb name trimmed to length 30. Original name: #{name}"
52
+ expect(config).to receive(:warn).with(expected_string)
53
+
54
+ validator.validate(breadcrumb)
55
+ # Check the original message has not been modified
56
+ expect(name).to eq("1234567890123456789012345678901234567890")
57
+ end
58
+
59
+ describe "tests meta_data types" do
60
+ it "accepts Strings, Numerics, Booleans, & nil" do
61
+ config = instance_double(Bugsnag::Configuration)
62
+ allow(config).to receive(:enabled_automatic_breadcrumb_types).and_return(enabled_automatic_breadcrumb_types)
63
+ validator = Bugsnag::Breadcrumbs::Validator.new(config)
64
+
65
+ meta_data = {
66
+ :string => "This is a string",
67
+ :integer => 12345,
68
+ :float => 12345.6789,
69
+ :false => false,
70
+ :true => true,
71
+ :nil => nil
72
+ }
73
+
74
+ breadcrumb = instance_double(Bugsnag::Breadcrumbs::Breadcrumb, {
75
+ :auto => auto,
76
+ :name => name,
77
+ :type => type,
78
+ :meta_data => meta_data,
79
+ :meta_data= => nil
80
+ })
81
+
82
+ expect(breadcrumb).to_not receive(:ignore!)
83
+ expect(config).to_not receive(:warn)
84
+
85
+ validator.validate(breadcrumb)
86
+ end
87
+
88
+ it "rejects Arrays, Hashes, and non-primitive objects" do
89
+ config = instance_double(Bugsnag::Configuration)
90
+ allow(config).to receive(:enabled_automatic_breadcrumb_types).and_return(enabled_automatic_breadcrumb_types)
91
+ validator = Bugsnag::Breadcrumbs::Validator.new(config)
92
+
93
+ class TestClass
94
+ end
95
+
96
+ meta_data = {
97
+ :fine => 1,
98
+ :array => [1, 2, 3],
99
+ :hash => {
100
+ :a => 1
101
+ },
102
+ :object => TestClass.new
103
+ }
104
+
105
+ breadcrumb = instance_double(Bugsnag::Breadcrumbs::Breadcrumb, {
106
+ :auto => auto,
107
+ :name => name,
108
+ :type => type,
109
+ :meta_data => meta_data
110
+ })
111
+
112
+ expect(breadcrumb).to_not receive(:ignore!)
113
+ expected_string_1 = "Breadcrumb #{breadcrumb.name} meta_data array:#{meta_data[:array]} has been dropped for having an invalid data type"
114
+ expected_string_2 = "Breadcrumb #{breadcrumb.name} meta_data hash:#{meta_data[:hash]} has been dropped for having an invalid data type"
115
+ expected_string_3 = "Breadcrumb #{breadcrumb.name} meta_data object:#{ meta_data[:object]} has been dropped for having an invalid data type"
116
+ expect(config).to receive(:warn).with(expected_string_1)
117
+ expect(config).to receive(:warn).with(expected_string_2)
118
+ expect(config).to receive(:warn).with(expected_string_3)
119
+
120
+ # Confirms that the meta_data is being filtered
121
+ expect(breadcrumb).to receive(:meta_data=).with({
122
+ :fine => 1
123
+ })
124
+
125
+ validator.validate(breadcrumb)
126
+ end
127
+ end
128
+
129
+ it "tests type, defaulting to 'manual' if invalid" do
130
+ config = instance_double(Bugsnag::Configuration)
131
+ allow(config).to receive(:enabled_automatic_breadcrumb_types).and_return(enabled_automatic_breadcrumb_types)
132
+ validator = Bugsnag::Breadcrumbs::Validator.new(config)
133
+
134
+ type = "Not a valid type"
135
+
136
+ breadcrumb = instance_double(Bugsnag::Breadcrumbs::Breadcrumb, {
137
+ :auto => auto,
138
+ :name => name,
139
+ :type => type,
140
+ :meta_data => meta_data,
141
+ :meta_data= => nil
142
+ })
143
+
144
+ expect(breadcrumb).to receive(:type=).with(Bugsnag::Breadcrumbs::MANUAL_BREADCRUMB_TYPE)
145
+ expect(breadcrumb).to_not receive(:ignore!)
146
+ expected_string = "Invalid type: #{type} for breadcrumb: #{breadcrumb.name}, defaulting to #{Bugsnag::Breadcrumbs::MANUAL_BREADCRUMB_TYPE}"
147
+ expect(config).to receive(:warn).with(expected_string)
148
+
149
+ validator.validate(breadcrumb)
150
+ end
151
+
152
+ describe "with enabled_automatic_breadcrumb_types set" do
153
+ it "rejects automatic breadcrumbs with rejected types" do
154
+ config = instance_double(Bugsnag::Configuration)
155
+ allowed_breadcrumb_types = []
156
+ allow(config).to receive(:enabled_automatic_breadcrumb_types).and_return(allowed_breadcrumb_types)
157
+ validator = Bugsnag::Breadcrumbs::Validator.new(config)
158
+
159
+ auto = true
160
+ type = Bugsnag::Breadcrumbs::ERROR_BREADCRUMB_TYPE
161
+
162
+ breadcrumb = instance_double(Bugsnag::Breadcrumbs::Breadcrumb, {
163
+ :auto => auto,
164
+ :name => name,
165
+ :type => type,
166
+ :meta_data => meta_data,
167
+ :meta_data= => nil
168
+ })
169
+
170
+ expect(breadcrumb).to receive(:ignore!)
171
+ expected_string = "Automatic breadcrumb of type #{Bugsnag::Breadcrumbs::ERROR_BREADCRUMB_TYPE} ignored: #{breadcrumb.name}"
172
+ expect(config).to receive(:warn).with(expected_string)
173
+
174
+ validator.validate(breadcrumb)
175
+ end
176
+
177
+ it "does not reject manual breadcrumbs with rejected types" do
178
+ config = instance_double(Bugsnag::Configuration)
179
+ allowed_breadcrumb_types = []
180
+ allow(config).to receive(:enabled_automatic_breadcrumb_types).and_return(allowed_breadcrumb_types)
181
+ validator = Bugsnag::Breadcrumbs::Validator.new(config)
182
+
183
+ type = Bugsnag::Breadcrumbs::ERROR_BREADCRUMB_TYPE
184
+
185
+ breadcrumb = instance_double(Bugsnag::Breadcrumbs::Breadcrumb, {
186
+ :auto => auto,
187
+ :name => name,
188
+ :type => type,
189
+ :meta_data => meta_data,
190
+ :meta_data= => nil
191
+ })
192
+
193
+ expect(breadcrumb).to_not receive(:ignore!)
194
+ expect(config).to_not receive(:warn)
195
+
196
+ validator.validate(breadcrumb)
197
+ end
198
+ end
199
+ end
200
+ end