airbrake-ruby 2.13.0.pre.1 → 3.0.0.rc.2

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
  SHA1:
3
- metadata.gz: 3ae033f8b21b79ecf3902d570014f379e76b4c16
4
- data.tar.gz: 131f71f8a92906a05aadf1ed3dbd4ffc35753162
3
+ metadata.gz: 0aef633a546613b3d46a1669505df654fcc4348b
4
+ data.tar.gz: c35b98d3ffa46c7ee3fe976546585f27ca6d1bd1
5
5
  SHA512:
6
- metadata.gz: 62a0b41680b377eefd25a0e9433ada861ce92841d954477de949f3ee16bbb32a198d3133433b0783e7e4f747a7c7bff39e76dcf4744bfbb8da8fffbcd54c4a6d
7
- data.tar.gz: 6864d904a38696d892dcca249ddeff4b1c973e61735ef792a19518bf1e17c430fc9f1bd09ea59fe4c6d7685d6d1a1e911848a657a6647199bb3208d25183192c
6
+ metadata.gz: 820453ab4d27d72c7f356958af555bc361507ca48cde546e6bc15f77cb32077c2cc1623d6a0afe882c37b9ade81888b58ddaadbd2c7f17b820a6011ab666877d
7
+ data.tar.gz: fc7078da83d4cd3d2e4e4c2602e5441e9ebd6751410cfdf8ffb6c4998f1832790a3ccfb41aa97a39470315b60a965b0b91059bc4914c366f923e986842f8adf1
@@ -76,10 +76,6 @@ module Airbrake
76
76
  # @return [String] the label to be prepended to the log output
77
77
  LOG_LABEL = '**Airbrake:'.freeze
78
78
 
79
- # @return [Boolean] true if current Ruby is Ruby 2.0.*. The result is used
80
- # for special cases where we need to work around older implementations
81
- RUBY_20 = RUBY_VERSION.start_with?('2.0')
82
-
83
79
  # @return [Boolean] true if current Ruby is JRuby. The result is used for
84
80
  # special cases where we need to work around older implementations
85
81
  JRUBY = (RUBY_ENGINE == 'jruby')
@@ -366,7 +362,7 @@ module Airbrake
366
362
  # milliseconds
367
363
  # @param [Time] time When the request happened
368
364
  # @return [void]
369
- # @since v2.13.0
365
+ # @since v3.0.0
370
366
  def inc_request(method, route, status_code, dur, time)
371
367
  @notifiers[:default].inc_request(method, route, status_code, dur, time)
372
368
  end
@@ -83,10 +83,6 @@ module Airbrake
83
83
  #{RUBY}
84
84
  )
85
85
  \z/x
86
-
87
- # @return [Regexp] +EXECJS+ pattern without named captures and
88
- # uncommon frames
89
- EXECJS_SIMPLIFIED = /\A.+ \(.+:\d+:\d+\)\z/
90
86
  end
91
87
 
92
88
  # @return [Integer] how many first frames should include code hunks
@@ -136,24 +132,13 @@ module Airbrake
136
132
  defined?(OCIError) && exception.is_a?(OCIError)
137
133
  end
138
134
 
139
- # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
140
135
  def execjs_exception?(exception)
141
136
  return false unless defined?(ExecJS::RuntimeError)
142
137
  return true if exception.is_a?(ExecJS::RuntimeError)
143
-
144
- if Airbrake::RUBY_20
145
- # Ruby <2.1 doesn't support Exception#cause. We work around this by
146
- # parsing backtraces. It's slow, so we check only a few first frames.
147
- exception.backtrace[0..2].each do |frame|
148
- return true if frame =~ Patterns::EXECJS_SIMPLIFIED
149
- end
150
- elsif exception.cause && exception.cause.is_a?(ExecJS::RuntimeError)
151
- return true
152
- end
138
+ return true if exception.cause && exception.cause.is_a?(ExecJS::RuntimeError)
153
139
 
154
140
  false
155
141
  end
156
- # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
157
142
 
158
143
  def stack_frame(config, regexp, stackframe)
159
144
  if (match = match_frame(regexp, stackframe))
@@ -86,7 +86,7 @@ module Airbrake
86
86
  # @return [Integer] how many seconds to wait before sending collected route
87
87
  # stats
88
88
  # @api public
89
- # @since v2.13.0
89
+ # @since v3.0.0
90
90
  attr_accessor :route_stats_flush_period
91
91
 
92
92
  # @param [Hash{Symbol=>Object}] user_config the hash to be used to build the
@@ -18,9 +18,12 @@ module Airbrake
18
18
  # @return [Integer]
19
19
  DEFAULT_WEIGHT = 0
20
20
 
21
- def initialize
21
+ def initialize(config, context)
22
+ @config = config
23
+ @context = context
22
24
  @filters = []
23
25
  DEFAULT_FILTERS.each { |f| add_filter(f.new) }
26
+ add_default_filters
24
27
  end
25
28
 
26
29
  # Adds a filter to the filter chain. Sorts filters by weight.
@@ -44,5 +47,57 @@ module Airbrake
44
47
  filter.call(notice)
45
48
  end
46
49
  end
50
+
51
+ # @return [String] customized inspect to lessen the amount of clutter
52
+ def inspect
53
+ @filters.map(&:class)
54
+ end
55
+
56
+ # @return [String] {#inspect} for PrettyPrint
57
+ def pretty_print(q)
58
+ q.text('[')
59
+
60
+ # Make nesting of the first element consistent on JRuby and MRI.
61
+ q.nest(2) { q.breakable }
62
+
63
+ q.nest(2) do
64
+ q.seplist(@filters) { |f| q.pp(f.class) }
65
+ end
66
+ q.text(']')
67
+ end
68
+
69
+ private
70
+
71
+ # rubocop:disable Metrics/AbcSize
72
+ def add_default_filters
73
+ if (whitelist_keys = @config.whitelist_keys).any?
74
+ add_filter(
75
+ Airbrake::Filters::KeysWhitelist.new(@config.logger, whitelist_keys)
76
+ )
77
+ end
78
+
79
+ if (blacklist_keys = @config.blacklist_keys).any?
80
+ add_filter(
81
+ Airbrake::Filters::KeysBlacklist.new(@config.logger, blacklist_keys)
82
+ )
83
+ end
84
+
85
+ add_filter(Airbrake::Filters::ContextFilter.new(@context))
86
+ add_filter(Airbrake::Filters::ExceptionAttributesFilter.new(@config.logger))
87
+
88
+ return unless (root_directory = @config.root_directory)
89
+ [
90
+ Airbrake::Filters::RootDirectoryFilter,
91
+ Airbrake::Filters::GitRevisionFilter,
92
+ Airbrake::Filters::GitRepositoryFilter
93
+ ].each do |filter|
94
+ add_filter(filter.new(root_directory))
95
+ end
96
+
97
+ add_filter(
98
+ Airbrake::Filters::GitLastCheckoutFilter.new(@config.logger, root_directory)
99
+ )
100
+ end
101
+ # rubocop:enable Metrics/AbcSize
47
102
  end
48
103
  end
@@ -9,6 +9,12 @@ module Airbrake
9
9
  # @return [String] the label to be prepended to the log output
10
10
  LOG_LABEL = '**Airbrake:'.freeze
11
11
 
12
+ # @return [String] inspect output template
13
+ INSPECT_TEMPLATE =
14
+ "#<#{self}:0x%<id>s project_id=\"%<project_id>s\" " \
15
+ "project_key=\"%<project_key>s\" " \
16
+ "host=\"%<host>s\" filter_chain=%<filter_chain>s>".freeze
17
+
12
18
  # Creates a new Airbrake notifier with the given config options.
13
19
  #
14
20
  # @example Configuring with a Hash
@@ -30,13 +36,9 @@ module Airbrake
30
36
  raise Airbrake::Error, @config.validation_error_message unless @config.valid?
31
37
 
32
38
  @context = {}
33
-
34
- @filter_chain = FilterChain.new
35
- add_default_filters
36
-
39
+ @filter_chain = FilterChain.new(@config, @context)
37
40
  @async_sender = AsyncSender.new(@config)
38
41
  @sync_sender = SyncSender.new(@config)
39
-
40
42
  @route_sender = RouteSender.new(@config)
41
43
  end
42
44
 
@@ -102,6 +104,29 @@ module Airbrake
102
104
  @route_sender.inc_request(*args)
103
105
  end
104
106
 
107
+ # @return [String] customized inspect to lessen the amount of clutter
108
+ def inspect
109
+ format(
110
+ INSPECT_TEMPLATE,
111
+ id: (object_id << 1).to_s(16).rjust(16, '0'),
112
+ project_id: @config.project_id,
113
+ project_key: @config.project_key,
114
+ host: @config.host,
115
+ filter_chain: @filter_chain.inspect
116
+ )
117
+ end
118
+
119
+ # @return [String] {#inspect} for PrettyPrint
120
+ def pretty_print(q)
121
+ q.text("#<#{self.class}:0x#{(object_id << 1).to_s(16).rjust(16, '0')} ")
122
+ q.text(
123
+ "project_id=\"#{@config.project_id}\" project_key=\"#{@config.project_key}\" " \
124
+ "host=\"#{@config.host}\" filter_chain="
125
+ )
126
+ q.pp(@filter_chain)
127
+ q.text('>')
128
+ end
129
+
105
130
  private
106
131
 
107
132
  def convert_to_exception(ex)
@@ -152,39 +177,5 @@ module Airbrake
152
177
  return caller_copy if clean_bt.empty?
153
178
  clean_bt
154
179
  end
155
-
156
- # rubocop:disable Metrics/AbcSize
157
- def add_default_filters
158
- if (whitelist_keys = @config.whitelist_keys).any?
159
- @filter_chain.add_filter(
160
- Airbrake::Filters::KeysWhitelist.new(@config.logger, whitelist_keys)
161
- )
162
- end
163
-
164
- if (blacklist_keys = @config.blacklist_keys).any?
165
- @filter_chain.add_filter(
166
- Airbrake::Filters::KeysBlacklist.new(@config.logger, blacklist_keys)
167
- )
168
- end
169
-
170
- @filter_chain.add_filter(Airbrake::Filters::ContextFilter.new(@context))
171
- @filter_chain.add_filter(
172
- Airbrake::Filters::ExceptionAttributesFilter.new(@config.logger)
173
- )
174
-
175
- return unless (root_directory = @config.root_directory)
176
- [
177
- Airbrake::Filters::RootDirectoryFilter,
178
- Airbrake::Filters::GitRevisionFilter,
179
- Airbrake::Filters::GitRepositoryFilter
180
- ].each do |filter|
181
- @filter_chain.add_filter(filter.new(root_directory))
182
- end
183
-
184
- @filter_chain.add_filter(
185
- Airbrake::Filters::GitLastCheckoutFilter.new(@config.logger, root_directory)
186
- )
187
- end
188
- # rubocop:enable Metrics/AbcSize
189
180
  end
190
181
  end
@@ -23,7 +23,7 @@ module Airbrake
23
23
 
24
24
  begin
25
25
  case code
26
- when 200
26
+ when 200, 204
27
27
  logger.debug("#{LOG_LABEL} #{name} (#{code}): #{body}")
28
28
  { response.msg => response.body }
29
29
  when 201
@@ -1,20 +1,76 @@
1
+ require 'tdigest'
2
+ require 'base64'
3
+
1
4
  module Airbrake
2
5
  # RouteSender aggregates information about requests and periodically sends
3
6
  # collected data to Airbrake.
4
- # @since v2.13.0
7
+ # @since v3.0.0
5
8
  class RouteSender
9
+ # Monkey-patch https://github.com/castle/tdigest to pack with Big Endian
10
+ # (instead of Little Endian) since our backend wants it.
11
+ #
12
+ # @see https://github.com/castle/tdigest/blob/master/lib/tdigest/tdigest.rb
13
+ # @since v3.0.0
14
+ # @api private
15
+ module TDigestBigEndianness
16
+ refine TDigest::TDigest do
17
+ # rubocop:disable Metrics/AbcSize
18
+ def as_small_bytes
19
+ size = @centroids.size
20
+ output = [self.class::SMALL_ENCODING, compression, size]
21
+ x = 0
22
+ # delta encoding allows saving 4-bytes floats
23
+ mean_arr = @centroids.map do |_, c|
24
+ val = c.mean - x
25
+ x = c.mean
26
+ val
27
+ end
28
+ output += mean_arr
29
+ # Variable length encoding of numbers
30
+ c_arr = @centroids.each_with_object([]) do |(_, c), arr|
31
+ k = 0
32
+ n = c.n
33
+ while n < 0 || n > 0x7f
34
+ b = 0x80 | (0x7f & n)
35
+ arr << b
36
+ n = n >> 7
37
+ k += 1
38
+ raise 'Unreasonable large number' if k > 6
39
+ end
40
+ arr << n
41
+ end
42
+ output += c_arr
43
+ output.pack("NGNg#{size}C#{size}")
44
+ end
45
+ # rubocop:enable Metrics/AbcSize
46
+ end
47
+ end
48
+
49
+ using TDigestBigEndianness
50
+
6
51
  # The key that represents a route.
7
52
  RouteKey = Struct.new(:method, :route, :statusCode, :time)
8
53
 
9
54
  # RouteStat holds data that describes a route's performance.
10
- RouteStat = Struct.new(:count, :sum, :sumsq, :min, :max) do
55
+ RouteStat = Struct.new(:count, :sum, :sumsq, :tdigest) do
11
56
  # @param [Integer] count The number of requests
12
57
  # @param [Float] sum The sum of request duration in milliseconds
13
58
  # @param [Float] sumsq The squared sum of request duration in milliseconds
14
- # @param [Float] min The minimal request duration
15
- # @param [Float] max The maximum request duration
16
- def initialize(count: 0, sum: 0.0, sumsq: 0.0, min: 0.0, max: 0.0)
17
- super(count, sum, sumsq, min, max)
59
+ # @param [TDigest::TDigest] tdigest
60
+ def initialize(count: 0, sum: 0.0, sumsq: 0.0, tdigest: TDigest::TDigest.new)
61
+ super(count, sum, sumsq, tdigest)
62
+ end
63
+
64
+ # @return [Hash{String=>Object}] the route stat as a hash with compressed
65
+ # and serialized as binary base64 tdigest
66
+ def to_h
67
+ tdigest.compress!
68
+ {
69
+ 'count' => count,
70
+ 'sum' => sum,
71
+ 'sumsq' => sumsq,
72
+ 'tDigest' => Base64.strict_encode64(tdigest.as_small_bytes)
73
+ }
18
74
  end
19
75
  end
20
76
 
@@ -66,8 +122,7 @@ module Airbrake
66
122
  stat.sum += ms
67
123
  stat.sumsq += ms * ms
68
124
 
69
- stat.min = ms if ms < stat.min || stat.min == 0
70
- stat.max = ms if ms > stat.max
125
+ stat.tdigest.push(ms)
71
126
  end
72
127
 
73
128
  def schedule_flush(promise)
@@ -2,5 +2,5 @@
2
2
  # More information: http://semver.org/
3
3
  module Airbrake
4
4
  # @return [String] the library version
5
- AIRBRAKE_RUBY_VERSION = '2.13.0.pre.1'.freeze
5
+ AIRBRAKE_RUBY_VERSION = '3.0.0.rc.2'.freeze
6
6
  end
@@ -248,6 +248,8 @@ RSpec.describe Airbrake::Backtrace do
248
248
  "/opt/rubies/ruby-2.3.1/lib/ruby/2.3.0/benchmark.rb:308:in `realtime'"]
249
249
  end
250
250
 
251
+ let(:ex) { ExecJS::RuntimeError.new.tap { |e| e.set_backtrace(bt) } }
252
+
251
253
  let(:parsed_backtrace) do
252
254
  [{ file: '(execjs)', line: 6692, function: 'compile' },
253
255
  { file: '<anonymous>', line: 1, function: 'eval' },
@@ -261,31 +263,9 @@ RSpec.describe Airbrake::Backtrace do
261
263
  function: 'realtime' }]
262
264
  end
263
265
 
264
- context "when not on Ruby 2.0" do
265
- let(:ex) { ExecJS::RuntimeError.new.tap { |e| e.set_backtrace(bt) } }
266
-
267
- it "returns a properly formatted array of hashes" do
268
- stub_const('ExecJS::RuntimeError', AirbrakeTestError)
269
- stub_const('Airbrake::RUBY_20', false)
270
-
271
- expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
272
- end
273
- end
274
-
275
- context "when on Ruby 2.0" do
276
- context "and when exception's class isn't ExecJS" do
277
- let(:ex) do
278
- ActionView::Template::Error.new.tap { |e| e.set_backtrace(bt) }
279
- end
280
-
281
- it "returns a properly formatted array of hashes" do
282
- stub_const('ActionView::Template::Error', AirbrakeTestError)
283
- stub_const('ExecJS::RuntimeError', NameError)
284
- stub_const('Airbrake::RUBY_20', true)
285
-
286
- expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
287
- end
288
- end
266
+ it "returns a properly formatted array of hashes" do
267
+ stub_const('ExecJS::RuntimeError', AirbrakeTestError)
268
+ expect(described_class.parse(config, ex)).to eq(parsed_backtrace)
289
269
  end
290
270
  end
291
271
 
@@ -1,8 +1,12 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe Airbrake::FilterChain do
4
+ subject { described_class.new(config, {}) }
5
+
6
+ let(:config) { Airbrake::Config.new }
7
+
4
8
  let(:notice) do
5
- Airbrake::Notice.new(Airbrake::Config.new, AirbrakeTestError.new)
9
+ Airbrake::Notice.new(config, AirbrakeTestError.new)
6
10
  end
7
11
 
8
12
  describe "#refine" do
@@ -459,5 +459,35 @@ RSpec.describe Airbrake::Notifier do
459
459
  subject.inc_request('GET', '/foo', 200, 1000, t)
460
460
  end
461
461
  end
462
+
463
+ describe "#inspect" do
464
+ it "displays object information" do
465
+ expect(subject.inspect).to match(/
466
+ #<Airbrake::Notifier:0x\w+\s
467
+ project_id="\d+"\s
468
+ project_key=".+"\s
469
+ host="http.+"\s
470
+ filter_chain=\[.+\]>
471
+ /x)
472
+ end
473
+ end
474
+
475
+ describe "#pretty_print" do
476
+ it "displays object information in a beautiful way" do
477
+ q = PP.new
478
+
479
+ # Guarding is needed to fix JRuby failure:
480
+ # NoMethodError: undefined method `[]' for nil:NilClass
481
+ q.guard_inspect_key { subject.pretty_print(q) }
482
+
483
+ expect(q.output).to match(/
484
+ #<Airbrake::Notifier:0x\w+\s
485
+ project_id="\d+"\s
486
+ project_key=".+"\s
487
+ host="http.+"\s
488
+ filter_chain=\[\n\s\s
489
+ /x)
490
+ end
491
+ end
462
492
  end
463
493
  # rubocop:enable Layout/DotPosition
@@ -5,7 +5,7 @@ RSpec.describe Airbrake::Response do
5
5
  let(:out) { StringIO.new }
6
6
  let(:logger) { Logger.new(out) }
7
7
 
8
- [200, 201].each do |code|
8
+ [200, 201, 204].each do |code|
9
9
  context "when response code is #{code}" do
10
10
  it "logs response body" do
11
11
  described_class.parse(OpenStruct.new(code: code, body: '{}'), logger)
@@ -48,10 +48,10 @@ RSpec.describe Airbrake::RouteSender do
48
48
  {"routes":\[
49
49
  {"method":"GET","route":"/foo","statusCode":200,
50
50
  "time":"2018-01-01T00:00:00\+00:00","count":1,"sum":24.0,
51
- "sumsq":576.0,"min":24.0,"max":24.0},
51
+ "sumsq":576.0,"tDigest":"AAAAAkBZAAAAAAAAAAAAAUHAAAAB"},
52
52
  {"method":"GET","route":"/foo","statusCode":200,
53
53
  "time":"2018-01-01T00:01:00\+00:00","count":1,"sum":10.0,
54
- "sumsq":100.0,"min":10.0,"max":10.0}\]}
54
+ "sumsq":100.0,"tDigest":"AAAAAkBZAAAAAAAAAAAAAUEgAAAB"}\]}
55
55
  \z|x
56
56
  )
57
57
  ).to have_been_made
@@ -67,10 +67,10 @@ RSpec.describe Airbrake::RouteSender do
67
67
  {"routes":\[
68
68
  {"method":"GET","route":"/foo","statusCode":200,
69
69
  "time":"2018-01-01T00:00:00\+00:00","count":1,"sum":24.0,
70
- "sumsq":576.0,"min":24.0,"max":24.0},
70
+ "sumsq":576.0,"tDigest":"AAAAAkBZAAAAAAAAAAAAAUHAAAAB"},
71
71
  {"method":"POST","route":"/foo","statusCode":200,
72
72
  "time":"2018-01-01T00:00:00\+00:00","count":1,"sum":10.0,
73
- "sumsq":100.0,"min":10.0,"max":10.0}\]}
73
+ "sumsq":100.0,"tDigest":"AAAAAkBZAAAAAAAAAAAAAUEgAAAB"}\]}
74
74
  \z|x
75
75
  )
76
76
  ).to have_been_made
@@ -8,6 +8,7 @@ require 'pathname'
8
8
  require 'webrick'
9
9
  require 'English'
10
10
  require 'base64'
11
+ require 'pp'
11
12
 
12
13
  require 'helpers'
13
14
 
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.13.0.pre.1
4
+ version: 3.0.0.rc.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Airbrake Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-26 00:00:00.000000000 Z
11
+ date: 2018-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tdigest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.1
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rspec
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -104,12 +118,11 @@ description: |
104
118
  Airbrake Ruby is a plain Ruby notifier for Airbrake (https://airbrake.io), the
105
119
  leading exception reporting service. Airbrake Ruby provides minimalist API that
106
120
  enables the ability to send any Ruby exception to the Airbrake dashboard. The
107
- library is extremely lightweight, contains no dependencies and perfectly suits
108
- plain Ruby applications. For apps that are built with Rails, Sinatra or any
109
- other Rack-compliant web framework we offer the airbrake gem
110
- (https://github.com/airbrake/airbrake). It has additional features such as
111
- reporting of any unhandled exceptions automatically, integrations with Resque,
112
- Sidekiq, Delayed Job and many more.
121
+ library is extremely lightweight and it perfectly suits plain Ruby applications.
122
+ For apps that are built with Rails, Sinatra or any other Rack-compliant web
123
+ framework we offer the airbrake gem (https://github.com/airbrake/airbrake). It
124
+ has additional features such as reporting of any unhandled exceptions
125
+ automatically, integrations with Resque, Sidekiq, Delayed Job and many more.
113
126
  email: support@airbrake.io
114
127
  executables: []
115
128
  extensions: []
@@ -194,7 +207,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
194
207
  requirements:
195
208
  - - ">="
196
209
  - !ruby/object:Gem::Version
197
- version: '2.0'
210
+ version: '2.1'
198
211
  required_rubygems_version: !ruby/object:Gem::Requirement
199
212
  requirements:
200
213
  - - ">"