airbrake-ruby 2.12.0 → 2.13.0.pre.1

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
  SHA1:
3
- metadata.gz: 3f92ef10b01c37b9374a6662bedba21c1593ef34
4
- data.tar.gz: 957da6a830307c3d3cd9f583f1eaa9bf4a2df96f
3
+ metadata.gz: 3ae033f8b21b79ecf3902d570014f379e76b4c16
4
+ data.tar.gz: 131f71f8a92906a05aadf1ed3dbd4ffc35753162
5
5
  SHA512:
6
- metadata.gz: b0ca6c8f84dfc39758f22203519765820a25c4bddd8e7aae90928d46b1f201ee7ab283679fdc16020043cd222c3a6f3e09def0642d1428d005b3022b8dfc59ee
7
- data.tar.gz: 46f263d46199864c89c70c721ecb1c50f13262a13b9b3603b31260cd0f6ea44536fa2ceb419b3efaee1bd82b7877f53246c754288f433054c43ca3b0af3bba58
6
+ metadata.gz: 62a0b41680b377eefd25a0e9433ada861ce92841d954477de949f3ee16bbb32a198d3133433b0783e7e4f747a7c7bff39e76dcf4744bfbb8da8fffbcd54c4a6d
7
+ data.tar.gz: 6864d904a38696d892dcca249ddeff4b1c973e61735ef792a19518bf1e17c430fc9f1bd09ea59fe4c6d7685d6d1a1e911848a657a6647199bb3208d25183192c
data/lib/airbrake-ruby.rb CHANGED
@@ -4,6 +4,7 @@ require 'json'
4
4
  require 'thread'
5
5
  require 'set'
6
6
  require 'socket'
7
+ require 'time'
7
8
 
8
9
  require 'airbrake-ruby/version'
9
10
  require 'airbrake-ruby/config'
@@ -33,6 +34,7 @@ require 'airbrake-ruby/filter_chain'
33
34
  require 'airbrake-ruby/notifier'
34
35
  require 'airbrake-ruby/code_hunk'
35
36
  require 'airbrake-ruby/file_cache'
37
+ require 'airbrake-ruby/route_sender'
36
38
 
37
39
  # This module defines the Airbrake API. The user is meant to interact with
38
40
  # Airbrake via its public class methods. Before using the library, you must to
@@ -115,6 +117,9 @@ module Airbrake
115
117
 
116
118
  # @macro see_public_api_method
117
119
  def merge_context(_context); end
120
+
121
+ # @macro see_public_api_method
122
+ def inc_request(method, route, status_code, dur, time); end
118
123
  end
119
124
 
120
125
  # A Hash that holds all notifiers. The keys of the Hash are notifier
@@ -343,5 +348,27 @@ module Airbrake
343
348
  def merge_context(context)
344
349
  @notifiers[:default].merge_context(context)
345
350
  end
351
+
352
+ # Increments request count of a certain +route+ that was invoked with
353
+ # +method+, and returned +status_code+ at +time+ and took +dur+
354
+ # milliseconds.
355
+ #
356
+ # After a certain amount of time (n seconds) the aggregated route
357
+ # information will be sent to Airbrake.
358
+ #
359
+ # @example
360
+ # Airbrake.inc_request('POST', '/thing/:id/create', 200, 123, Time.now)
361
+ #
362
+ # @param [String] method The HTTP method that was invoked
363
+ # @param [String] route The route that was invoked
364
+ # @param [Integer] status_code The respose code that the route returned
365
+ # @param [Float] dur How much time the processing of the request took in
366
+ # milliseconds
367
+ # @param [Time] time When the request happened
368
+ # @return [void]
369
+ # @since v2.13.0
370
+ def inc_request(method, route, status_code, dur, time)
371
+ @notifiers[:default].inc_request(method, route, status_code, dur, time)
372
+ end
346
373
  end
347
374
  end
@@ -83,6 +83,12 @@ module Airbrake
83
83
  # @since v2.5.0
84
84
  attr_accessor :code_hunks
85
85
 
86
+ # @return [Integer] how many seconds to wait before sending collected route
87
+ # stats
88
+ # @api public
89
+ # @since v2.13.0
90
+ attr_accessor :route_stats_flush_period
91
+
86
92
  # @param [Hash{Symbol=>Object}] user_config the hash to be used to build the
87
93
  # config
88
94
  def initialize(user_config = {})
@@ -113,6 +119,7 @@ module Airbrake
113
119
  )
114
120
 
115
121
  self.versions = {}
122
+ self.route_stats_flush_period = 15
116
123
 
117
124
  merge(user_config)
118
125
  end
@@ -36,6 +36,8 @@ module Airbrake
36
36
 
37
37
  @async_sender = AsyncSender.new(@config)
38
38
  @sync_sender = SyncSender.new(@config)
39
+
40
+ @route_sender = RouteSender.new(@config)
39
41
  end
40
42
 
41
43
  # @macro see_public_api_method
@@ -95,6 +97,11 @@ module Airbrake
95
97
  @context.merge!(context)
96
98
  end
97
99
 
100
+ # @macro see_public_api_method
101
+ def inc_request(*args)
102
+ @route_sender.inc_request(*args)
103
+ end
104
+
98
105
  private
99
106
 
100
107
  def convert_to_exception(ex)
@@ -16,16 +16,19 @@ module Airbrake
16
16
  # @param [Net::HTTPResponse] response
17
17
  # @param [Logger] logger
18
18
  # @return [Hash{String=>String}] parsed response
19
- # rubocop:disable Metrics/MethodLength
19
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
20
20
  def self.parse(response, logger)
21
21
  code = response.code.to_i
22
22
  body = response.body
23
23
 
24
24
  begin
25
25
  case code
26
+ when 200
27
+ logger.debug("#{LOG_LABEL} #{name} (#{code}): #{body}")
28
+ { response.msg => response.body }
26
29
  when 201
27
30
  parsed_body = JSON.parse(body)
28
- logger.debug("#{LOG_LABEL} #{parsed_body}")
31
+ logger.debug("#{LOG_LABEL} #{name} (#{code}): #{parsed_body}")
29
32
  parsed_body
30
33
  when 400, 401, 403, 420
31
34
  parsed_body = JSON.parse(body)
@@ -47,7 +50,7 @@ module Airbrake
47
50
  { 'error' => ex.inspect }
48
51
  end
49
52
  end
50
- # rubocop:enable Metrics/MethodLength
53
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
51
54
 
52
55
  def self.truncated_body(body)
53
56
  if body.nil?
@@ -0,0 +1,106 @@
1
+ module Airbrake
2
+ # RouteSender aggregates information about requests and periodically sends
3
+ # collected data to Airbrake.
4
+ # @since v2.13.0
5
+ class RouteSender
6
+ # The key that represents a route.
7
+ RouteKey = Struct.new(:method, :route, :statusCode, :time)
8
+
9
+ # RouteStat holds data that describes a route's performance.
10
+ RouteStat = Struct.new(:count, :sum, :sumsq, :min, :max) do
11
+ # @param [Integer] count The number of requests
12
+ # @param [Float] sum The sum of request duration in milliseconds
13
+ # @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)
18
+ end
19
+ end
20
+
21
+ # @param [Airbrake::Config] config
22
+ def initialize(config)
23
+ @config = config
24
+ @flush_period = config.route_stats_flush_period
25
+ @sender = SyncSender.new(config, :put)
26
+ @routes = {}
27
+ @thread = nil
28
+ @mutex = Mutex.new
29
+ end
30
+
31
+ # @macro see_public_api_method
32
+ def inc_request(method, route, status_code, dur, tm)
33
+ route = create_route_key(method, route, status_code, tm)
34
+
35
+ promise = Airbrake::Promise.new
36
+
37
+ @mutex.synchronize do
38
+ @routes[route] ||= RouteStat.new
39
+ increment_stats(@routes[route], dur)
40
+
41
+ if @flush_period > 0
42
+ schedule_flush(promise)
43
+ else
44
+ send(@routes, promise)
45
+ end
46
+ end
47
+
48
+ promise
49
+ end
50
+
51
+ private
52
+
53
+ def create_route_key(method, route, status_code, tm)
54
+ # rubocop:disable Style/DateTime
55
+ time = DateTime.new(
56
+ tm.year, tm.month, tm.day, tm.hour, tm.min, 0, tm.zone || 0
57
+ )
58
+ # rubocop:enable Style/DateTime
59
+ RouteKey.new(method, route, status_code, time.rfc3339)
60
+ end
61
+
62
+ def increment_stats(stat, dur)
63
+ stat.count += 1
64
+
65
+ ms = dur.to_f
66
+ stat.sum += ms
67
+ stat.sumsq += ms * ms
68
+
69
+ stat.min = ms if ms < stat.min || stat.min == 0
70
+ stat.max = ms if ms > stat.max
71
+ end
72
+
73
+ def schedule_flush(promise)
74
+ @thread ||= Thread.new do
75
+ sleep(@flush_period)
76
+
77
+ routes = nil
78
+ @mutex.synchronize do
79
+ routes = @routes
80
+ @routes = {}
81
+ @thread = nil
82
+ end
83
+
84
+ send(routes, promise)
85
+ end
86
+
87
+ # Setting a name is needed to test the timer.
88
+ # Ruby <=2.2 doesn't support Thread#name, so we have this check.
89
+ @thread.name = 'route-stat-thread' if @thread.respond_to?(:name)
90
+ end
91
+
92
+ def send(routes, promise)
93
+ if routes.none?
94
+ raise "#{self.class.name}##{__method__}: routes cannot be empty. Race?"
95
+ end
96
+
97
+ @config.logger.debug("#{LOG_LABEL} RouteStats#send: #{routes}")
98
+
99
+ @sender.send(
100
+ { routes: routes.map { |k, v| k.to_h.merge(v.to_h) } },
101
+ promise,
102
+ URI.join(@config.host, "api/v4/projects/#{@config.project_id}/routes-stats")
103
+ )
104
+ end
105
+ end
106
+ end
@@ -1,5 +1,6 @@
1
1
  module Airbrake
2
- # Responsible for sending notices to Airbrake synchronously. Supports proxies.
2
+ # Responsible for sending data to Airbrake synchronously via PUT or POST
3
+ # methods. Supports proxies.
3
4
  #
4
5
  # @see AsyncSender
5
6
  # @api private
@@ -9,21 +10,22 @@ module Airbrake
9
10
  CONTENT_TYPE = 'application/json'.freeze
10
11
 
11
12
  # @param [Airbrake::Config] config
12
- def initialize(config)
13
+ def initialize(config, method = :post)
13
14
  @config = config
15
+ @method = method
14
16
  @rate_limit_reset = Time.now
15
17
  end
16
18
 
17
- # Sends a POST request to the given +endpoint+ with the +notice+ payload.
19
+ # Sends a POST or PUT request to the given +endpoint+ with the +data+ payload.
18
20
  #
19
- # @param [Airbrake::Notice] notice
20
- # @param [Airbrake::Notice] endpoint
21
+ # @param [#to_json] data
22
+ # @param [URI::HTTPS] endpoint
21
23
  # @return [Hash{String=>String}] the parsed HTTP response
22
- def send(notice, promise, endpoint = @config.endpoint)
24
+ def send(data, promise, endpoint = @config.endpoint)
23
25
  return promise if rate_limited_ip?(promise)
24
26
 
25
27
  response = nil
26
- req = build_post_request(endpoint, notice)
28
+ req = build_request(endpoint, data)
27
29
 
28
30
  return promise if missing_body?(req, promise)
29
31
 
@@ -58,16 +60,27 @@ module Airbrake
58
60
  end
59
61
  end
60
62
 
61
- def build_post_request(uri, notice)
62
- Net::HTTP::Post.new(uri.request_uri).tap do |req|
63
- req.body = notice.to_json
63
+ def build_request(uri, data)
64
+ req =
65
+ if @method == :put
66
+ Net::HTTP::Put.new(uri.request_uri)
67
+ else
68
+ Net::HTTP::Post.new(uri.request_uri)
69
+ end
64
70
 
65
- req['Authorization'] = "Bearer #{@config.project_key}"
66
- req['Content-Type'] = CONTENT_TYPE
67
- req['User-Agent'] =
68
- "#{Airbrake::Notice::NOTIFIER[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION}" \
69
- " Ruby/#{RUBY_VERSION}"
70
- end
71
+ build_request_body(req, data)
72
+ end
73
+
74
+ def build_request_body(req, data)
75
+ req.body = data.to_json
76
+
77
+ req['Authorization'] = "Bearer #{@config.project_key}"
78
+ req['Content-Type'] = CONTENT_TYPE
79
+ req['User-Agent'] =
80
+ "#{Airbrake::Notice::NOTIFIER[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION}" \
81
+ " Ruby/#{RUBY_VERSION}"
82
+
83
+ req
71
84
  end
72
85
 
73
86
  def proxy_params
@@ -87,7 +100,7 @@ module Airbrake
87
100
  missing = req.body.nil?
88
101
 
89
102
  if missing
90
- reason = "#{LOG_LABEL} notice was not sent because of missing body"
103
+ reason = "#{LOG_LABEL} data was not sent because of missing body"
91
104
  @config.logger.error(reason)
92
105
  promise.reject(reason)
93
106
  end
@@ -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.12.0'.freeze
5
+ AIRBRAKE_RUBY_VERSION = '2.13.0.pre.1'.freeze
6
6
  end
@@ -110,4 +110,14 @@ RSpec.describe Airbrake do
110
110
  described_class.merge_context(foo: 'bar')
111
111
  end
112
112
  end
113
+
114
+ describe ".inc_request" do
115
+ it "forwards 'inc_request' to the notifier" do
116
+ t = Time.now
117
+ expect(default_notifier).to receive(:inc_request).with(
118
+ 'GET', '/foo', 200, 1000, t
119
+ )
120
+ described_class.inc_request('GET', '/foo', 200, 1000, t)
121
+ end
122
+ end
113
123
  end
@@ -21,7 +21,7 @@ RSpec.describe Airbrake::AsyncSender do
21
21
  sender.close
22
22
 
23
23
  log = stdout.string.split("\n")
24
- notices_sent = log.grep(/\*\*Airbrake: \{\}/).size
24
+ notices_sent = log.grep(/\*\*Airbrake: Airbrake::Response \(201\): \{\}/).size
25
25
  notices_dropped = log.grep(/\*\*Airbrake:.*not.*delivered/).size
26
26
  expect(notices_sent).to be >= queue_size
27
27
  expect(notices_sent + notices_dropped).to eq(notices_count)
@@ -60,7 +60,7 @@ RSpec.describe Airbrake::AsyncSender do
60
60
 
61
61
  it "prints the correct number of log messages" do
62
62
  log = @stderr.string.split("\n")
63
- notices_sent = log.grep(/\*\*Airbrake: \{\}/).size
63
+ notices_sent = log.grep(/\*\*Airbrake: Airbrake::Response \(201\): \{\}/).size
64
64
  notices_dropped = log.grep(/\*\*Airbrake:.*not.*delivered/).size
65
65
  expect(notices_sent).to be >= @sender.instance_variable_get(:@unsent).max
66
66
  expect(notices_sent + notices_dropped).to eq(300)
data/spec/config_spec.rb CHANGED
@@ -78,6 +78,10 @@ RSpec.describe Airbrake::Config do
78
78
  it "doesn't set default whitelist" do
79
79
  expect(config.whitelist_keys).to be_empty
80
80
  end
81
+
82
+ it "sets the default route_stats_flush_period" do
83
+ expect(config.route_stats_flush_period).to eq(15)
84
+ end
81
85
  end
82
86
  end
83
87
 
@@ -449,5 +449,15 @@ RSpec.describe Airbrake::Notifier do
449
449
  subject.merge_context(apples: 'oranges')
450
450
  end
451
451
  end
452
+
453
+ describe "#inc_request" do
454
+ it "forwards 'inc_request' to RouteSender" do
455
+ t = Time.now
456
+ expect_any_instance_of(Airbrake::RouteSender).to receive(:inc_request).with(
457
+ 'GET', '/foo', 200, 1000, t
458
+ )
459
+ subject.inc_request('GET', '/foo', 200, 1000, t)
460
+ end
461
+ end
452
462
  end
453
463
  # rubocop:enable Layout/DotPosition
@@ -5,10 +5,12 @@ RSpec.describe Airbrake::Response do
5
5
  let(:out) { StringIO.new }
6
6
  let(:logger) { Logger.new(out) }
7
7
 
8
- context "when response code is 201" do
9
- it "logs response body" do
10
- described_class.parse(OpenStruct.new(code: 201, body: '{}'), logger)
11
- expect(out.string).to match(/Airbrake: {}/)
8
+ [200, 201].each do |code|
9
+ context "when response code is #{code}" do
10
+ it "logs response body" do
11
+ described_class.parse(OpenStruct.new(code: code, body: '{}'), logger)
12
+ expect(out.string).to match(/Airbrake: Airbrake::Response \(#{code}\): {}/)
13
+ end
12
14
  end
13
15
  end
14
16
 
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Airbrake::RouteSender do
4
+ let(:endpoint) { 'https://api.airbrake.io/api/v4/projects/1/routes-stats' }
5
+
6
+ let(:config) do
7
+ Airbrake::Config.new(
8
+ project_id: 1,
9
+ project_key: 'banana',
10
+ route_stats_flush_period: 0.1
11
+ )
12
+ end
13
+
14
+ subject { described_class.new(config) }
15
+
16
+ describe "#inc_request" do
17
+ before do
18
+ stub_request(:put, endpoint).to_return(status: 200, body: '')
19
+ end
20
+
21
+ # Let the request finish.
22
+ after { sleep 0.2 }
23
+
24
+ it "rounds time to the floor minute" do
25
+ subject.inc_request('GET', '/foo', 200, 24, Time.new(2018, 1, 1, 0, 0, 20, 0))
26
+ sleep 0.2
27
+ expect(
28
+ a_request(:put, endpoint).with(body: /"time":"2018-01-01T00:00:00\+00:00"/)
29
+ ).to have_been_made
30
+ end
31
+
32
+ it "increments routes with the same key" do
33
+ subject.inc_request('GET', '/foo', 200, 24, Time.new(2018, 1, 1, 0, 0, 20, 0))
34
+ subject.inc_request('GET', '/foo', 200, 24, Time.new(2018, 1, 1, 0, 0, 50, 0))
35
+ sleep 0.2
36
+ expect(
37
+ a_request(:put, endpoint).with(body: /"count":2/)
38
+ ).to have_been_made
39
+ end
40
+
41
+ it "groups routes by time" do
42
+ subject.inc_request('GET', '/foo', 200, 24, Time.new(2018, 1, 1, 0, 0, 20, 0))
43
+ subject.inc_request('GET', '/foo', 200, 10, Time.new(2018, 1, 1, 0, 1, 20, 0))
44
+ sleep 0.2
45
+ expect(
46
+ a_request(:put, endpoint).with(
47
+ body: %r|\A
48
+ {"routes":\[
49
+ {"method":"GET","route":"/foo","statusCode":200,
50
+ "time":"2018-01-01T00:00:00\+00:00","count":1,"sum":24.0,
51
+ "sumsq":576.0,"min":24.0,"max":24.0},
52
+ {"method":"GET","route":"/foo","statusCode":200,
53
+ "time":"2018-01-01T00:01:00\+00:00","count":1,"sum":10.0,
54
+ "sumsq":100.0,"min":10.0,"max":10.0}\]}
55
+ \z|x
56
+ )
57
+ ).to have_been_made
58
+ end
59
+
60
+ it "groups routes by route key" do
61
+ subject.inc_request('GET', '/foo', 200, 24, Time.new(2018, 1, 1, 0, 0, 20, 0))
62
+ subject.inc_request('POST', '/foo', 200, 10, Time.new(2018, 1, 1, 0, 0, 20, 0))
63
+ sleep 0.2
64
+ expect(
65
+ a_request(:put, endpoint).with(
66
+ body: %r|\A
67
+ {"routes":\[
68
+ {"method":"GET","route":"/foo","statusCode":200,
69
+ "time":"2018-01-01T00:00:00\+00:00","count":1,"sum":24.0,
70
+ "sumsq":576.0,"min":24.0,"max":24.0},
71
+ {"method":"POST","route":"/foo","statusCode":200,
72
+ "time":"2018-01-01T00:00:00\+00:00","count":1,"sum":10.0,
73
+ "sumsq":100.0,"min":10.0,"max":10.0}\]}
74
+ \z|x
75
+ )
76
+ ).to have_been_made
77
+ end
78
+
79
+ it "returns a promise" do
80
+ promise = subject.inc_request('GET', '/foo', 200, 24, Time.new)
81
+ sleep 0.2
82
+ expect(promise).to be_an(Airbrake::Promise)
83
+ expect(promise.value).to eq('' => nil)
84
+ end
85
+ end
86
+ end
@@ -30,7 +30,7 @@ RSpec.describe Airbrake::SyncSender do
30
30
  before { stub_request(:post, endpoint).to_return(body: '{}') }
31
31
 
32
32
  it "sets the Content-Type header to JSON" do
33
- sender.send(notice, promise)
33
+ sender.send({}, promise)
34
34
  expect(
35
35
  a_request(:post, endpoint).with(
36
36
  headers: { 'Content-Type' => 'application/json' }
@@ -39,18 +39,18 @@ RSpec.describe Airbrake::SyncSender do
39
39
  end
40
40
 
41
41
  it "sets the User-Agent header to the notifier slug" do
42
- sender.send(notice, promise)
42
+ sender.send({}, promise)
43
43
  expect(
44
44
  a_request(:post, endpoint).with(
45
45
  headers: {
46
- 'User-Agent' => %r{airbrake-ruby/\d+\.\d+\.\d+ Ruby/\d+\.\d+\.\d+}
46
+ 'User-Agent' => %r{airbrake-ruby/\d+\.\d+\.\d+.+ Ruby/\d+\.\d+\.\d+}
47
47
  }
48
48
  )
49
49
  ).to have_been_made.once
50
50
  end
51
51
 
52
52
  it "sets the Authorization header to the project key" do
53
- sender.send(notice, promise)
53
+ sender.send({}, promise)
54
54
  expect(
55
55
  a_request(:post, endpoint).with(
56
56
  headers: { 'Authorization' => 'Bearer banana' }
@@ -62,13 +62,13 @@ RSpec.describe Airbrake::SyncSender do
62
62
  https = double("foo")
63
63
  allow(sender).to receive(:build_https).and_return(https)
64
64
  allow(https).to receive(:request).and_raise(StandardError.new('foo'))
65
- expect(sender.send(notice, promise)).to be_an(Airbrake::Promise)
65
+ expect(sender.send({}, promise)).to be_an(Airbrake::Promise)
66
66
  expect(promise.value).to eq('error' => '**Airbrake: HTTP error: foo')
67
67
  expect(stdout.string).to match(/ERROR -- : .+ HTTP error: foo/)
68
68
  end
69
69
 
70
70
  context "when request body is nil" do
71
- it "doesn't send a notice" do
71
+ it "doesn't send data" do
72
72
  expect_any_instance_of(Airbrake::Truncator).
73
73
  to receive(:reduce_max_size).and_return(0)
74
74
 
@@ -84,8 +84,8 @@ RSpec.describe Airbrake::SyncSender do
84
84
 
85
85
  expect(sender.send(notice, promise)).to be_an(Airbrake::Promise)
86
86
  expect(promise.value).
87
- to match('error' => '**Airbrake: notice was not sent because of missing body')
88
- expect(stdout.string).to match(/ERROR -- : .+ notice was not sent/)
87
+ to match('error' => '**Airbrake: data was not sent because of missing body')
88
+ expect(stdout.string).to match(/ERROR -- : .+ data was not sent/)
89
89
  end
90
90
  end
91
91
 
@@ -102,11 +102,11 @@ RSpec.describe Airbrake::SyncSender do
102
102
 
103
103
  it "returns error" do
104
104
  p1 = Airbrake::Promise.new
105
- sender.send(notice, p1)
105
+ sender.send({}, p1)
106
106
  expect(p1.value).to match('error' => '**Airbrake: IP is rate limited')
107
107
 
108
108
  p2 = Airbrake::Promise.new
109
- sender.send(notice, p2)
109
+ sender.send({}, p2)
110
110
  expect(p2.value).to match('error' => '**Airbrake: IP is rate limited')
111
111
 
112
112
  # Wait for X-RateLimit-Delay and then make a new request to make sure p2
@@ -114,11 +114,29 @@ RSpec.describe Airbrake::SyncSender do
114
114
  sleep 1
115
115
 
116
116
  p3 = Airbrake::Promise.new
117
- sender.send(notice, p3)
117
+ sender.send({}, p3)
118
118
  expect(p3.value).to match('error' => '**Airbrake: IP is rate limited')
119
119
 
120
120
  expect(a_request(:post, endpoint)).to have_been_made.twice
121
121
  end
122
122
  end
123
+
124
+ context "when the provided method is :put" do
125
+ before { stub_request(:put, endpoint).to_return(status: 200, body: '') }
126
+
127
+ it "PUTs the request" do
128
+ sender = described_class.new(config, :put)
129
+ sender.send({}, promise)
130
+ expect(a_request(:put, endpoint)).to have_been_made
131
+ end
132
+ end
133
+
134
+ context "when the provided method is :post" do
135
+ it "POSTs the request" do
136
+ sender = described_class.new(config, :post)
137
+ sender.send({}, promise)
138
+ expect(a_request(:post, endpoint)).to have_been_made
139
+ end
140
+ end
123
141
  end
124
142
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.0
4
+ version: 2.13.0.pre.1
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-11 00:00:00.000000000 Z
11
+ date: 2018-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -141,6 +141,7 @@ files:
141
141
  - lib/airbrake-ruby/notifier.rb
142
142
  - lib/airbrake-ruby/promise.rb
143
143
  - lib/airbrake-ruby/response.rb
144
+ - lib/airbrake-ruby/route_sender.rb
144
145
  - lib/airbrake-ruby/sync_sender.rb
145
146
  - lib/airbrake-ruby/truncator.rb
146
147
  - lib/airbrake-ruby/version.rb
@@ -177,6 +178,7 @@ files:
177
178
  - spec/notifier_spec/options_spec.rb
178
179
  - spec/promise_spec.rb
179
180
  - spec/response_spec.rb
181
+ - spec/route_sender_spec.rb
180
182
  - spec/spec_helper.rb
181
183
  - spec/sync_sender_spec.rb
182
184
  - spec/truncator_spec.rb
@@ -195,9 +197,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
195
197
  version: '2.0'
196
198
  required_rubygems_version: !ruby/object:Gem::Requirement
197
199
  requirements:
198
- - - ">="
200
+ - - ">"
199
201
  - !ruby/object:Gem::Version
200
- version: '0'
202
+ version: 1.3.1
201
203
  requirements: []
202
204
  rubyforge_project:
203
205
  rubygems_version: 2.6.13
@@ -205,6 +207,7 @@ signing_key:
205
207
  specification_version: 4
206
208
  summary: Ruby notifier for https://airbrake.io
207
209
  test_files:
210
+ - spec/route_sender_spec.rb
208
211
  - spec/truncator_spec.rb
209
212
  - spec/helpers.rb
210
213
  - spec/filters/exception_attributes_filter_spec.rb