airbrake-ruby 3.2.6-java → 4.0.0-java

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/airbrake-ruby.rb +31 -138
  3. data/lib/airbrake-ruby/async_sender.rb +20 -8
  4. data/lib/airbrake-ruby/backtrace.rb +15 -13
  5. data/lib/airbrake-ruby/code_hunk.rb +2 -4
  6. data/lib/airbrake-ruby/config.rb +8 -38
  7. data/lib/airbrake-ruby/deploy_notifier.rb +4 -17
  8. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +5 -4
  9. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +6 -4
  10. data/lib/airbrake-ruby/filters/keys_blacklist.rb +0 -1
  11. data/lib/airbrake-ruby/filters/keys_filter.rb +4 -4
  12. data/lib/airbrake-ruby/filters/keys_whitelist.rb +0 -1
  13. data/lib/airbrake-ruby/loggable.rb +31 -0
  14. data/lib/airbrake-ruby/nested_exception.rb +2 -3
  15. data/lib/airbrake-ruby/notice.rb +6 -6
  16. data/lib/airbrake-ruby/notice_notifier.rb +11 -47
  17. data/lib/airbrake-ruby/performance_notifier.rb +6 -18
  18. data/lib/airbrake-ruby/response.rb +5 -2
  19. data/lib/airbrake-ruby/sync_sender.rb +8 -6
  20. data/lib/airbrake-ruby/version.rb +1 -1
  21. data/spec/airbrake_spec.rb +1 -143
  22. data/spec/async_sender_spec.rb +83 -90
  23. data/spec/backtrace_spec.rb +36 -47
  24. data/spec/code_hunk_spec.rb +12 -15
  25. data/spec/config_spec.rb +79 -96
  26. data/spec/deploy_notifier_spec.rb +3 -7
  27. data/spec/filter_chain_spec.rb +1 -3
  28. data/spec/filters/context_filter_spec.rb +1 -3
  29. data/spec/filters/dependency_filter_spec.rb +1 -3
  30. data/spec/filters/exception_attributes_filter_spec.rb +1 -14
  31. data/spec/filters/gem_root_filter_spec.rb +1 -4
  32. data/spec/filters/git_last_checkout_filter_spec.rb +3 -5
  33. data/spec/filters/git_revision_filter_spec.rb +1 -3
  34. data/spec/filters/keys_blacklist_spec.rb +14 -25
  35. data/spec/filters/keys_whitelist_spec.rb +14 -25
  36. data/spec/filters/root_directory_filter_spec.rb +1 -4
  37. data/spec/filters/system_exit_filter_spec.rb +2 -2
  38. data/spec/filters/thread_filter_spec.rb +1 -3
  39. data/spec/nested_exception_spec.rb +3 -5
  40. data/spec/notice_notifier_spec.rb +23 -20
  41. data/spec/notice_notifier_spec/options_spec.rb +20 -25
  42. data/spec/notice_spec.rb +13 -12
  43. data/spec/performance_notifier_spec.rb +19 -31
  44. data/spec/response_spec.rb +23 -17
  45. data/spec/sync_sender_spec.rb +26 -33
  46. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 64e5df55b7870cdf2b1cefcdf718e491d2816a5f
4
- data.tar.gz: f0c09f8d665390398e8226e624d5fb407927ab09
3
+ metadata.gz: 8c1c00a0d9dbcec58dcd2bd22cae10de9b4c5a3a
4
+ data.tar.gz: aab5dbba3ec59829fb95b4b3d6a24c67a38023d7
5
5
  SHA512:
6
- metadata.gz: 97a3ad072f176de590f2be02e6222632507f27ee0820669b79254c95a26aea8bbe3a8ed1f148f2a3b3c12de4889acad351040f3d3ec3a44a1061686f12852714
7
- data.tar.gz: 380596bb7dc0836185ac4a812fd309d5c3c5913b72177e4df660c620d9ae29520c9659bdcb01ccb52879e4946e37687231205b72a6191dbd1c177f2b3ad591aa
6
+ metadata.gz: 4c5bb3838dcfeade40d47a359f64df0cdc5b016af2c6a117cb907d5dc4a7d8e8d03c0bf1398d4d58b709854d0e16ed6c4efc2cbb95b44c3975259d4f9094bb63
7
+ data.tar.gz: 01d60cea9501d88ca04b45c57a19d568e42a3bfb07e09ffbe23fb9a67e1dab02fc60c2bd1ab564aad164a40e28635a6fb47ec2d1baf5c8c2e13c4196e64eba99
data/lib/airbrake-ruby.rb CHANGED
@@ -7,6 +7,7 @@ require 'socket'
7
7
  require 'time'
8
8
 
9
9
  require 'airbrake-ruby/version'
10
+ require 'airbrake-ruby/loggable'
10
11
  require 'airbrake-ruby/config'
11
12
  require 'airbrake-ruby/config/validator'
12
13
  require 'airbrake-ruby/promise'
@@ -46,40 +47,22 @@ require 'airbrake-ruby/tdigest'
46
47
  require 'airbrake-ruby/query'
47
48
  require 'airbrake-ruby/request'
48
49
 
49
- # This module defines the Airbrake API. The user is meant to interact with
50
- # Airbrake via its public class methods. Before using the library, you must to
51
- # {configure} the default notifier.
50
+ # Airbrake is a thin wrapper around instances of the notifier classes (such as
51
+ # notice, performance & deploy notifiers). It creates a way to access them via a
52
+ # consolidated global interface.
52
53
  #
53
- # The module supports multiple notifiers, each of which can be configured
54
- # differently. By default, every method is invoked in context of the default
55
- # notifier. To use a different notifier, you need to {configure} it first and
56
- # pass the notifier's name as the last argument of the method you're calling.
54
+ # Prior to using it, you must {configure} it.
57
55
  #
58
- # You can have as many notifiers as you want, but they must have unique names.
59
- #
60
- # @example Configuring multiple notifiers and using them
61
- # # Configure the default notifier.
56
+ # @example
62
57
  # Airbrake.configure do |c|
63
58
  # c.project_id = 113743
64
59
  # c.project_key = 'fd04e13d806a90f96614ad8e529b2822'
65
60
  # end
66
61
  #
67
- # # Configure a named notifier.
68
- # Airbrake.configure(:my_other_project) do |c|
69
- # c.project_id = 224854
70
- # c.project_key = '91ac5e4a37496026c6837f63276ed2b6'
71
- # end
72
- #
73
- # # Send an exception via the default notifier.
74
62
  # Airbrake.notify('Oops!')
75
63
  #
76
- # # Send an exception via other configured notifier.
77
- # params = {}
78
- # Airbrake[:my_other_project].notify('Oops', params)
79
- #
80
- # @see Airbrake::NoticeNotifier
81
64
  # @since v1.0.0
82
- # rubocop:disable Metrics/ModuleLength
65
+ # @api public
83
66
  module Airbrake
84
67
  # The general error that this library uses when it wants to raise.
85
68
  Error = Class.new(StandardError)
@@ -127,14 +110,6 @@ module Airbrake
127
110
  def merge_context(_context); end
128
111
  end
129
112
 
130
- # @deprecated Use {Airbrake::NoticeNotifier} instead
131
- Notifier = NoticeNotifier
132
- deprecate_constant(:Notifier) if respond_to?(:deprecate_constant)
133
-
134
- # @deprecated Use {Airbrake::NilNoticeNotifier} instead
135
- NilNotifier = NilNoticeNotifier
136
- deprecate_constant(:NilNotifier) if respond_to?(:deprecate_constant)
137
-
138
113
  # NilPerformanceNotifier is a no-op notifier, which mimics
139
114
  # {Airbrake::PerformanceNotifier} and serves only the purpose of making the
140
115
  # library API easier to use.
@@ -160,75 +135,23 @@ module Airbrake
160
135
  #
161
136
  # @since v3.2.0
162
137
  class NilDeployNotifier
163
- # @see Airbrake.create_deploy
138
+ # @see Airbrake.notify_deploy
164
139
  def notify(_deploy_info); end
165
140
  end
166
141
 
167
- # A Hash that holds all notice notifiers. The keys of the Hash are notifier
168
- # names, the values are {Airbrake::NoticeNotifier} instances. If a notifier is
169
- # not assigned to the hash, then it returns a null object (NilNoticeNotifier).
170
- @notice_notifiers = Hash.new(NilNoticeNotifier.new)
171
-
172
- # A Hash that holds all performance notifiers. The keys of the Hash are
173
- # notifier names, the values are {Airbrake::PerformanceNotifier} instances. If
174
- # a notifier is not assigned to the hash, then it returns a null object
175
- # (NilPerformanceNotifier).
176
- @performance_notifiers = Hash.new(NilPerformanceNotifier.new)
177
-
178
- # A Hash that holds all deploy notifiers. The keys of the Hash are notifier
179
- # names, the values are {Airbrake::DeployNotifier} instances. If a deploy
180
- # notifier is not assigned to the hash, then it returns a null object
181
- # (NilDeployNotifier).
182
- @deploy_notifiers = Hash.new(NilDeployNotifier.new)
142
+ @notice_notifier = NilNoticeNotifier.new
143
+ @performance_notifier = NilPerformanceNotifier.new
144
+ @deploy_notifier = NilDeployNotifier.new
183
145
 
184
146
  class << self
185
- # Retrieves configured notifiers.
186
- #
187
- # @example
188
- # Airbrake[:my_notifier].notify('oops')
189
- #
190
- # @param [Symbol] notifier_name the name of the notice notifier you want to
191
- # use
192
- # @return [Airbrake::NoticeNotifier, NilClass]
193
- # @since v1.8.0
194
- def [](notifier_name)
195
- loc = caller_locations(1..1).first
196
- signature = "#{self}##{__method__}"
197
- warn(
198
- "#{loc.path}:#{loc.lineno}: warning: #{signature} is deprecated. It " \
199
- "will be removed from airbrake-ruby v4 altogether."
200
- )
201
-
202
- @notice_notifiers[notifier_name]
203
- end
204
-
205
- # @return [Hash{Symbol=>Array<Object>}] a Hash with all configured notifiers
206
- # (notice, performance, deploy)
207
- # @since v3.2.0
208
- def notifiers
209
- loc = caller_locations(1..1).first
210
- signature = "#{self}##{__method__}"
211
- warn(
212
- "#{loc.path}:#{loc.lineno}: warning: #{signature} is deprecated. It " \
213
- "will be removed from airbrake-ruby v4 altogether."
214
- )
215
-
216
- {
217
- notice: @notice_notifiers,
218
- performance: @performance_notifiers,
219
- deploy: @deploy_notifiers
220
- }
221
- end
222
-
223
147
  # Configures the Airbrake notifier.
224
148
  #
225
- # @example Configuring the default notifier
149
+ # @example
226
150
  # Airbrake.configure do |c|
227
151
  # c.project_id = 113743
228
152
  # c.project_key = 'fd04e13d806a90f96614ad8e529b2822'
229
153
  # end
230
154
  #
231
- # @param [Symbol] notifier_name the name to be associated with the notifier
232
155
  # @yield [config] The configuration object
233
156
  # @yieldparam config [Airbrake::Config]
234
157
  # @return [void]
@@ -236,41 +159,23 @@ module Airbrake
236
159
  # existing notifier
237
160
  # @raise [Airbrake::Error] when either +project_id+ or +project_key+
238
161
  # is missing (or both)
239
- # @note There's no way to reconfigure a notifier
240
162
  # @note There's no way to read config values outside of this library
241
- def configure(notifier_name = :default)
242
- unless notifier_name == :default
243
- loc = caller_locations(1..1).first
244
- warn(
245
- "#{loc.path}:#{loc.lineno}: warning: configuring a notifier with a " \
246
- "custom name is deprecated. This feature will be removed from " \
247
- "airbrake-ruby v4 altogether."
248
- )
249
- end
250
-
251
- yield config = Airbrake::Config.new
252
-
253
- if @notice_notifiers.key?(notifier_name)
254
- raise Airbrake::Error,
255
- "the '#{notifier_name}' notifier was already configured"
256
- end
163
+ def configure
164
+ yield config = Airbrake::Config.instance
257
165
 
258
166
  raise Airbrake::Error, config.validation_error_message unless config.valid?
259
167
 
260
- # TODO: Kludge to avoid
261
- # https://github.com/airbrake/airbrake-ruby/issues/406
262
- # Stop passing perf_notifier to NoticeNotifier as soon as possible.
263
- perf_notifier = PerformanceNotifier.new(config)
264
- @performance_notifiers[notifier_name] = perf_notifier
265
- @notice_notifiers[notifier_name] = NoticeNotifier.new(config, perf_notifier)
168
+ Airbrake::Loggable.instance = Airbrake::Config.instance
266
169
 
267
- @deploy_notifiers[notifier_name] = DeployNotifier.new(config)
170
+ @performance_notifier = PerformanceNotifier.new
171
+ @notice_notifier = NoticeNotifier.new
172
+ @deploy_notifier = DeployNotifier.new
268
173
  end
269
174
 
270
175
  # @return [Boolean] true if the notifier was configured, false otherwise
271
176
  # @since v2.3.0
272
177
  def configured?
273
- @notice_notifiers[:default].configured?
178
+ @notice_notifier.configured?
274
179
  end
275
180
 
276
181
  # Sends an exception to Airbrake asynchronously.
@@ -295,7 +200,7 @@ module Airbrake
295
200
  # @return [Airbrake::Promise]
296
201
  # @see .notify_sync
297
202
  def notify(exception, params = {}, &block)
298
- @notice_notifiers[:default].notify(exception, params, &block)
203
+ @notice_notifier.notify(exception, params, &block)
299
204
  end
300
205
 
301
206
  # Sends an exception to Airbrake synchronously.
@@ -315,7 +220,7 @@ module Airbrake
315
220
  # @return [Hash{String=>String}] the reponse from the server
316
221
  # @see .notify
317
222
  def notify_sync(exception, params = {}, &block)
318
- @notice_notifiers[:default].notify_sync(exception, params, &block)
223
+ @notice_notifier.notify_sync(exception, params, &block)
319
224
  end
320
225
 
321
226
  # Runs a callback before {.notify} or {.notify_sync} kicks in. This is
@@ -343,7 +248,7 @@ module Airbrake
343
248
  # @yieldreturn [void]
344
249
  # @return [void]
345
250
  def add_filter(filter = nil, &block)
346
- @notice_notifiers[:default].add_filter(filter, &block)
251
+ @notice_notifier.add_filter(filter, &block)
347
252
  end
348
253
 
349
254
  # Deletes a filter added via {Airbrake#add_filter}.
@@ -360,7 +265,7 @@ module Airbrake
360
265
  # @since v3.1.0
361
266
  # @note This method cannot delete filters assigned via the Proc form.
362
267
  def delete_filter(filter_class)
363
- @notice_notifiers[:default].delete_filter(filter_class)
268
+ @notice_notifier.delete_filter(filter_class)
364
269
  end
365
270
 
366
271
  # Builds an Airbrake notice. This is useful, if you want to add or modify a
@@ -378,7 +283,7 @@ module Airbrake
378
283
  # @return [Airbrake::Notice] the notice built with help of the given
379
284
  # arguments
380
285
  def build_notice(exception, params = {})
381
- @notice_notifiers[:default].build_notice(exception, params)
286
+ @notice_notifier.build_notice(exception, params)
382
287
  end
383
288
 
384
289
  # Makes the notice notifier a no-op, which means you cannot use the
@@ -391,7 +296,7 @@ module Airbrake
391
296
  #
392
297
  # @return [void]
393
298
  def close
394
- @notice_notifiers[:default].close
299
+ @notice_notifier.close
395
300
  end
396
301
 
397
302
  # Pings the Airbrake Deploy API endpoint about the occurred deploy.
@@ -404,18 +309,7 @@ module Airbrake
404
309
  # @option deploy_info [Symbol] :version
405
310
  # @return [void]
406
311
  def notify_deploy(deploy_info)
407
- @deploy_notifiers[:default].notify(deploy_info)
408
- end
409
-
410
- # @see notify_deploy
411
- def create_deploy(deploy_info)
412
- loc = caller_locations(1..1).first
413
- signature = "#{self}##{__method__}"
414
- warn(
415
- "#{loc.path}:#{loc.lineno}: warning: #{signature} is deprecated. Call " \
416
- "#{self}#notify_deploy instead"
417
- )
418
- notify_deploy(deploy_info)
312
+ @deploy_notifier.notify(deploy_info)
419
313
  end
420
314
 
421
315
  # Merges +context+ with the current context.
@@ -463,7 +357,7 @@ module Airbrake
463
357
  # @param [Hash{Symbol=>Object}] context
464
358
  # @return [void]
465
359
  def merge_context(context)
466
- @notice_notifiers[:default].merge_context(context)
360
+ @notice_notifier.merge_context(context)
467
361
  end
468
362
 
469
363
  # Increments request statistics of a certain +route+ that was invoked on
@@ -502,7 +396,7 @@ module Airbrake
502
396
  # @since v3.0.0
503
397
  # @see Airbrake::PerformanceNotifier#notify
504
398
  def notify_request(request_info)
505
- @performance_notifiers[:default].notify(Request.new(request_info))
399
+ @performance_notifier.notify(Request.new(request_info))
506
400
  end
507
401
 
508
402
  # Increments SQL statistics of a certain +query+ that was invoked on
@@ -534,7 +428,7 @@ module Airbrake
534
428
  # @since v3.2.0
535
429
  # @see Airbrake::PerformanceNotifier#notify
536
430
  def notify_query(query_info)
537
- @performance_notifiers[:default].notify(Query.new(query_info))
431
+ @performance_notifier.notify(Query.new(query_info))
538
432
  end
539
433
 
540
434
  # Runs a callback before {.notify_request} or {.notify_query} kicks in. This
@@ -569,7 +463,7 @@ module Airbrake
569
463
  # @since v3.2.0
570
464
  # @see Airbrake::PerformanceNotifier#add_filter
571
465
  def add_performance_filter(filter = nil, &block)
572
- @performance_notifiers[:default].add_filter(filter, &block)
466
+ @performance_notifier.add_filter(filter, &block)
573
467
  end
574
468
 
575
469
  # Deletes a filter added via {Airbrake#add_performance_filter}.
@@ -587,8 +481,7 @@ module Airbrake
587
481
  # @note This method cannot delete filters assigned via the Proc form.
588
482
  # @see Airbrake::PerformanceNotifier#delete_filter
589
483
  def delete_performance_filter(filter_class)
590
- @performance_notifiers[:default].delete_filter(filter_class)
484
+ @performance_notifier.delete_filter(filter_class)
591
485
  end
592
486
  end
593
487
  end
594
- # rubocop:enable Metrics/ModuleLength
@@ -7,11 +7,23 @@ module Airbrake
7
7
  # @api private
8
8
  # @since v1.0.0
9
9
  class AsyncSender
10
- # @param [Airbrake::Config] config
11
- def initialize(config)
12
- @config = config
13
- @unsent = SizedQueue.new(config.queue_size)
14
- @sender = SyncSender.new(config)
10
+ include Loggable
11
+
12
+ # @return [ThreadGroup] the list of workers
13
+ # @note This is exposed for eaiser unit testing
14
+ # @since v4.0.0
15
+ attr_reader :workers
16
+
17
+ # @return [Array<[Airbrake::Notice,Airbrake::Promise]>] the list of unsent
18
+ # payload
19
+ # @note This is exposed for eaiser unit testing
20
+ # @since v4.0.0
21
+ attr_reader :unsent
22
+
23
+ def initialize
24
+ @config = Airbrake::Config.instance
25
+ @unsent = SizedQueue.new(Airbrake::Config.instance.queue_size)
26
+ @sender = SyncSender.new
15
27
  @closed = false
16
28
  @workers = ThreadGroup.new
17
29
  @mutex = Mutex.new
@@ -41,7 +53,7 @@ module Airbrake
41
53
 
42
54
  unless @unsent.empty?
43
55
  msg = "#{LOG_LABEL} waiting to send #{@unsent.size} unsent notice(s)..."
44
- @config.logger.debug(msg + ' (Ctrl-C to abort)')
56
+ logger.debug(msg + ' (Ctrl-C to abort)')
45
57
  end
46
58
 
47
59
  @config.workers.times { @unsent << [:stop, Airbrake::Promise.new] }
@@ -50,7 +62,7 @@ module Airbrake
50
62
  end
51
63
 
52
64
  threads.each(&:join)
53
- @config.logger.debug("#{LOG_LABEL} closed")
65
+ logger.debug("#{LOG_LABEL} closed")
54
66
  end
55
67
 
56
68
  # Checks whether the sender is closed and thus usable.
@@ -107,7 +119,7 @@ module Airbrake
107
119
  backtrace = notice[:errors][0][:backtrace].map do |line|
108
120
  "#{line[:file]}:#{line[:line]} in `#{line[:function]}'"
109
121
  end
110
- @config.logger.error(
122
+ logger.error(
111
123
  "#{LOG_LABEL} AsyncSender has reached its capacity of " \
112
124
  "#{@unsent.max} and the following notice will not be delivered " \
113
125
  "Error: #{notice[:errors][0][:type]} - #{notice[:errors][0][:message]}\n" \
@@ -7,7 +7,7 @@ module Airbrake
7
7
  # begin
8
8
  # raise 'Oops!'
9
9
  # rescue
10
- # Backtrace.parse($!, Logger.new(STDOUT))
10
+ # Backtrace.parse($!)
11
11
  # end
12
12
  #
13
13
  # @api private
@@ -93,9 +93,9 @@ module Airbrake
93
93
  # @param [Exception] exception The exception, which contains a backtrace to
94
94
  # parse
95
95
  # @return [Array<Hash{Symbol=>String,Integer}>] the parsed backtrace
96
- def self.parse(config, exception)
96
+ def self.parse(exception)
97
97
  return [] if exception.backtrace.nil? || exception.backtrace.none?
98
- parse_backtrace(config, exception)
98
+ parse_backtrace(exception)
99
99
  end
100
100
 
101
101
  # Checks whether the given exception was generated by JRuby's VM.
@@ -114,6 +114,8 @@ module Airbrake
114
114
  end
115
115
 
116
116
  class << self
117
+ include Loggable
118
+
117
119
  private
118
120
 
119
121
  def best_regexp_for(exception)
@@ -140,7 +142,7 @@ module Airbrake
140
142
  false
141
143
  end
142
144
 
143
- def stack_frame(config, regexp, stackframe)
145
+ def stack_frame(regexp, stackframe)
144
146
  if (match = match_frame(regexp, stackframe))
145
147
  return {
146
148
  file: match[:file],
@@ -149,7 +151,7 @@ module Airbrake
149
151
  }
150
152
  end
151
153
 
152
- config.logger.error(
154
+ logger.error(
153
155
  "can't parse '#{stackframe}' (please file an issue so we can fix " \
154
156
  "it: https://github.com/airbrake/airbrake-ruby/issues/new)"
155
157
  )
@@ -163,26 +165,26 @@ module Airbrake
163
165
  Patterns::GENERIC.match(stackframe)
164
166
  end
165
167
 
166
- def parse_backtrace(config, exception)
168
+ def parse_backtrace(exception)
167
169
  regexp = best_regexp_for(exception)
168
- root_directory = config.root_directory.to_s
170
+ root_directory = Airbrake::Config.instance.root_directory.to_s
169
171
 
170
172
  exception.backtrace.map.with_index do |stackframe, i|
171
- frame = stack_frame(config, regexp, stackframe)
172
- next(frame) if !config.code_hunks || frame[:file].nil?
173
+ frame = stack_frame(regexp, stackframe)
174
+ next(frame) if !Airbrake::Config.instance.code_hunks || frame[:file].nil?
173
175
 
174
176
  if !root_directory.empty?
175
- populate_code(config, frame) if frame_in_root?(frame, root_directory)
177
+ populate_code(frame) if frame_in_root?(frame, root_directory)
176
178
  elsif i < CODE_FRAME_LIMIT
177
- populate_code(config, frame)
179
+ populate_code(frame)
178
180
  end
179
181
 
180
182
  frame
181
183
  end
182
184
  end
183
185
 
184
- def populate_code(config, frame)
185
- code = Airbrake::CodeHunk.new(config).get(frame[:file], frame[:line])
186
+ def populate_code(frame)
187
+ code = Airbrake::CodeHunk.new.get(frame[:file], frame[:line])
186
188
  frame[:code] = code if code
187
189
  end
188
190