From 4b0f92d845ab97d55ccbd419f1ad5858ba48fc6d Mon Sep 17 00:00:00 2001 From: st0012 Date: Thu, 11 Mar 2021 21:16:18 +0800 Subject: [PATCH 01/11] Support parsing x-sentry-rate-limits header --- .../rails-6.0/config/initializers/sentry.rb | 1 + sentry-ruby/Gemfile | 1 + .../lib/sentry/transport/http_transport.rb | 52 +++++++++++++++++-- sentry-ruby/spec/sentry/configuration_spec.rb | 2 +- .../sentry/transport/http_transport_spec.rb | 50 ++++++++++++++++++ sentry-ruby/spec/spec_helper.rb | 1 + 6 files changed, 103 insertions(+), 4 deletions(-) diff --git a/sentry-rails/examples/rails-6.0/config/initializers/sentry.rb b/sentry-rails/examples/rails-6.0/config/initializers/sentry.rb index 571083b5f..46caa8205 100644 --- a/sentry-rails/examples/rails-6.0/config/initializers/sentry.rb +++ b/sentry-rails/examples/rails-6.0/config/initializers/sentry.rb @@ -1,5 +1,6 @@ Sentry.init do |config| config.breadcrumbs_logger = [:active_support_logger] + config.background_worker_threads = 0 config.send_default_pii = true config.traces_sample_rate = 1.0 # set a float between 0.0 and 1.0 to enable performance monitoring config.dsn = 'https://2fb45f003d054a7ea47feb45898f7649@o447951.ingest.sentry.io/5434472' diff --git a/sentry-ruby/Gemfile b/sentry-ruby/Gemfile index 44fb7ef87..546b2b38e 100644 --- a/sentry-ruby/Gemfile +++ b/sentry-ruby/Gemfile @@ -9,6 +9,7 @@ gem "i18n", "<= 1.8.7" gem "rake", "~> 12.0" gem "rspec", "~> 3.0" gem "rspec-retry" +gem "timecop" gem "codecov", "0.2.12" gem "pry" diff --git a/sentry-ruby/lib/sentry/transport/http_transport.rb b/sentry-ruby/lib/sentry/transport/http_transport.rb index 99dfcd590..677488135 100644 --- a/sentry-ruby/lib/sentry/transport/http_transport.rb +++ b/sentry-ruby/lib/sentry/transport/http_transport.rb @@ -6,14 +6,17 @@ class HTTPTransport < Transport GZIP_ENCODING = "gzip" GZIP_THRESHOLD = 1024 * 30 CONTENT_TYPE = 'application/x-sentry-envelope' + RETRY_AFTER_HEADER = "retry-after" + RATE_LIMIT_HEADER = "x-sentry-rate-limits" - attr_reader :conn, :adapter + attr_reader :conn, :adapter, :rate_limits def initialize(*args) super @adapter = @transport_configuration.http_adapter || Faraday.default_adapter @conn = set_conn @endpoint = @dsn.envelope_endpoint + @rate_limits = {} end def send_data(data) @@ -34,8 +37,12 @@ def send_data(data) error_info = e.message if e.response - error_info += "\nbody: #{e.response[:body]}" - error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error'] + if has_rate_limited_header?(e.response) + handle_sentry_response(e.response) + else + error_info += "\nbody: #{e.response[:body]}" + error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error'] + end end raise Sentry::ExternalError, error_info @@ -43,6 +50,45 @@ def send_data(data) private + def has_rate_limited_header?(response) + response.dig(:headers, RETRY_AFTER_HEADER) || response.dig(:headers, RATE_LIMIT_HEADER) + end + + def handle_sentry_response(response) + rate_limits = + if rate_limit_header = response.dig(:headers, RATE_LIMIT_HEADER) + parse_rate_limit_header(rate_limit_header) + end + + @rate_limits.merge!(rate_limits) + end + + def parse_rate_limit_header(rate_limit_header) + time = Time.now + + result = {} + + limits = rate_limit_header.split(",") + limits.each do |limit| + begin + retry_after, categories = limit.strip.split(":").first(2) + retry_after = time + retry_after.to_i + categories = categories.split(";") + + if categories.empty? + result[nil] = retry_after + else + categories.each do |category| + result[category] = retry_after + end + end + rescue StandardError + end + end + + result + end + def should_compress?(data) @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD end diff --git a/sentry-ruby/spec/sentry/configuration_spec.rb b/sentry-ruby/spec/sentry/configuration_spec.rb index b867a898e..f20e8d579 100644 --- a/sentry-ruby/spec/sentry/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/configuration_spec.rb @@ -17,7 +17,7 @@ context "when traces_sample_rate == 0.0" do it "returns false" do - subject.traces_sample_rate = 0.0 + subject.traces_sample_rate = 0 expect(subject.tracing_enabled?).to eq(false) end diff --git a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb index 641bb408e..16c855f85 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb @@ -111,6 +111,56 @@ end end + context "receive 429 response" do + let(:stubs) do + Faraday::Adapter::Test::Stubs.new do |stub| + stub.post('sentry/api/42/envelope/') do + [ + 429, headers, "{\"detail\":\"event rejected due to rate limit\"}" + ] + end + end + end + context "with x-sentry-rate-limits header" do + NOW = Time.now + + [ + { + header: "", expected_limits: {} + }, + { + header: "invalid", expected_limits: {} + }, + { + header: ",,foo,", expected_limits: {} + }, + { + header: "42::organization, invalid, 4711:foobar;transaction;security:project", + expected_limits: { + nil => NOW + 42, + "transaction" => NOW + 4711, + "foobar" => NOW + 4711, + "security" => NOW + 4711 + } + } + ].each do |pair| + context "with header value: '#{pair[:header]}'" do + let(:headers) do + { status: 429, "x-sentry-rate-limits" => pair[:header] } + end + + it "parses the header into correct limits" do + Timecop.freeze(NOW) do + expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + end + + expect(subject.rate_limits).to eq(pair[:expected_limits]) + end + end + end + end + end + context "receive 5xx responses" do let(:stubs) do Faraday::Adapter::Test::Stubs.new do |stub| diff --git a/sentry-ruby/spec/spec_helper.rb b/sentry-ruby/spec/spec_helper.rb index 8376ef27a..154d6d9d9 100644 --- a/sentry-ruby/spec/spec_helper.rb +++ b/sentry-ruby/spec/spec_helper.rb @@ -1,5 +1,6 @@ require "bundler/setup" require "pry" +require "timecop" require 'simplecov' require 'rspec/retry' From 5762b8b87bc60abeca707aa11c4f305db30cb2e6 Mon Sep 17 00:00:00 2001 From: st0012 Date: Fri, 12 Mar 2021 19:38:50 +0800 Subject: [PATCH 02/11] Support parsing retry-after header --- .../lib/sentry/transport/http_transport.rb | 17 +++++--- .../sentry/transport/http_transport_spec.rb | 43 ++++++++++++++++--- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/sentry-ruby/lib/sentry/transport/http_transport.rb b/sentry-ruby/lib/sentry/transport/http_transport.rb index 677488135..66f9945a7 100644 --- a/sentry-ruby/lib/sentry/transport/http_transport.rb +++ b/sentry-ruby/lib/sentry/transport/http_transport.rb @@ -6,6 +6,8 @@ class HTTPTransport < Transport GZIP_ENCODING = "gzip" GZIP_THRESHOLD = 1024 * 30 CONTENT_TYPE = 'application/x-sentry-envelope' + + DEFAULT_DELAY = 60 RETRY_AFTER_HEADER = "retry-after" RATE_LIMIT_HEADER = "x-sentry-rate-limits" @@ -37,8 +39,8 @@ def send_data(data) error_info = e.message if e.response - if has_rate_limited_header?(e.response) - handle_sentry_response(e.response) + if e.response[:status] == 429 + handle_rate_limited_response(e.response) else error_info += "\nbody: #{e.response[:body]}" error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error'] @@ -54,10 +56,15 @@ def has_rate_limited_header?(response) response.dig(:headers, RETRY_AFTER_HEADER) || response.dig(:headers, RATE_LIMIT_HEADER) end - def handle_sentry_response(response) + def handle_rate_limited_response(response) rate_limits = - if rate_limit_header = response.dig(:headers, RATE_LIMIT_HEADER) - parse_rate_limit_header(rate_limit_header) + if rate_limits = response.dig(:headers, RATE_LIMIT_HEADER) + parse_rate_limit_header(rate_limits) + elsif retry_after = response.dig(:headers, RETRY_AFTER_HEADER) + retry_after = retry_after.to_i + retry_after = DEFAULT_DELAY if retry_after == 0 + + { nil => Time.now + retry_after } end @rate_limits.merge!(rate_limits) diff --git a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb index 16c855f85..0e2728f08 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb @@ -121,8 +121,9 @@ end end end + context "with x-sentry-rate-limits header" do - NOW = Time.now + now = Time.now [ { @@ -137,10 +138,10 @@ { header: "42::organization, invalid, 4711:foobar;transaction;security:project", expected_limits: { - nil => NOW + 42, - "transaction" => NOW + 4711, - "foobar" => NOW + 4711, - "security" => NOW + 4711 + nil => now + 42, + "transaction" => now + 4711, + "foobar" => now + 4711, + "security" => now + 4711 } } ].each do |pair| @@ -150,7 +151,37 @@ end it "parses the header into correct limits" do - Timecop.freeze(NOW) do + Timecop.freeze(now) do + expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + end + + expect(subject.rate_limits).to eq(pair[:expected_limits]) + end + end + end + end + + context "with retry-after header" do + now = Time.now + + [ + { + header: "48", expected_limits: { nil => now + 48 } + }, + { + header: "invalid", expected_limits: { nil => now + 60} + }, + { + header: "", expected_limits: { nil => now + 60} + }, + ].each do |pair| + context "with header value: '#{pair[:header]}'" do + let(:headers) do + { status: 429, "retry-after" => pair[:header] } + end + + it "parses the header into correct limits" do + Timecop.freeze(now) do expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) end From 911ec139c15f7dfd80c1ca51c25ceee486a17f2e Mon Sep 17 00:00:00 2001 From: st0012 Date: Fri, 12 Mar 2021 19:42:34 +0800 Subject: [PATCH 03/11] Hanlde all 429 response cases --- .../lib/sentry/transport/http_transport.rb | 2 ++ .../sentry/transport/http_transport_spec.rb | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/sentry-ruby/lib/sentry/transport/http_transport.rb b/sentry-ruby/lib/sentry/transport/http_transport.rb index 66f9945a7..ac391f4b4 100644 --- a/sentry-ruby/lib/sentry/transport/http_transport.rb +++ b/sentry-ruby/lib/sentry/transport/http_transport.rb @@ -65,6 +65,8 @@ def handle_rate_limited_response(response) retry_after = DEFAULT_DELAY if retry_after == 0 { nil => Time.now + retry_after } + else + { nil => Time.now + DEFAULT_DELAY } end @rate_limits.merge!(rate_limits) diff --git a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb index 0e2728f08..bdc322415 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb @@ -190,6 +190,38 @@ end end end + + context "with both x-sentry-rate-limits and retry-after headers" do + let(:headers) do + { status: 429, "x-sentry-rate-limits" => "42:error:organization", "retry-after" => "42" } + end + + it "parses x-sentry-rate-limits first" do + now = Time.now + + Timecop.freeze(now) do + expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + end + + expect(subject.rate_limits).to eq({ "error" => now + 42 }) + end + end + + context "with no rate limiting headers" do + let(:headers) do + { status: 429 } + end + + it "adds default limits" do + now = Time.now + + Timecop.freeze(now) do + expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + end + + expect(subject.rate_limits).to eq({ nil => now + 60 }) + end + end end context "receive 5xx responses" do From 4170b2f06be797f5e97074bb23468ae3860f7e27 Mon Sep 17 00:00:00 2001 From: st0012 Date: Fri, 12 Mar 2021 22:00:42 +0800 Subject: [PATCH 04/11] Handle rate limit headers of 200 responses If Sentry's 200 responses also contain rate limit related headers, we should store and update the SDK's rate limits like we see them in 429 responses. The only difference is that when receiving 429 responses the SDK should add default 60s delay to all event categories, which must not happen to 200 responses. --- .../lib/sentry/transport/http_transport.rb | 18 +- .../http_transport_rate_limiting_spec.rb | 168 ++++++++++++++++++ .../sentry/transport/http_transport_spec.rb | 113 ------------ 3 files changed, 179 insertions(+), 120 deletions(-) create mode 100644 sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb diff --git a/sentry-ruby/lib/sentry/transport/http_transport.rb b/sentry-ruby/lib/sentry/transport/http_transport.rb index ac391f4b4..19f3d10eb 100644 --- a/sentry-ruby/lib/sentry/transport/http_transport.rb +++ b/sentry-ruby/lib/sentry/transport/http_transport.rb @@ -29,18 +29,22 @@ def send_data(data) encoding = GZIP_ENCODING end - conn.post @endpoint do |req| + response = conn.post @endpoint do |req| req.headers['Content-Type'] = CONTENT_TYPE req.headers['Content-Encoding'] = encoding req.headers['X-Sentry-Auth'] = generate_auth_header req.body = data end + + if has_rate_limited_header?(response.headers) + handle_rate_limited_response(response.headers) + end rescue Faraday::Error => e error_info = e.message if e.response if e.response[:status] == 429 - handle_rate_limited_response(e.response) + handle_rate_limited_response(e.response[:headers]) else error_info += "\nbody: #{e.response[:body]}" error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error'] @@ -52,15 +56,15 @@ def send_data(data) private - def has_rate_limited_header?(response) - response.dig(:headers, RETRY_AFTER_HEADER) || response.dig(:headers, RATE_LIMIT_HEADER) + def has_rate_limited_header?(headers) + headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER] end - def handle_rate_limited_response(response) + def handle_rate_limited_response(headers) rate_limits = - if rate_limits = response.dig(:headers, RATE_LIMIT_HEADER) + if rate_limits = headers[RATE_LIMIT_HEADER] parse_rate_limit_header(rate_limits) - elsif retry_after = response.dig(:headers, RETRY_AFTER_HEADER) + elsif retry_after = headers[RETRY_AFTER_HEADER] retry_after = retry_after.to_i retry_after = DEFAULT_DELAY if retry_after == 0 diff --git a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb new file mode 100644 index 000000000..91328fbd7 --- /dev/null +++ b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' + +RSpec.describe "rate limiting" do + let(:configuration) do + Sentry::Configuration.new.tap do |config| + config.dsn = 'http://12345@sentry.localdomain/sentry/42' + end + end + let(:client) { Sentry::Client.new(configuration) } + let(:event) { client.event_from_message("foobarbaz") } + let(:data) do + subject.encode(event.to_hash) + end + + subject { Sentry::HTTPTransport.new(configuration) } + + before do + configuration.transport.http_adapter = [:test, stubs] + end + + shared_examples "rate limiting headers handling" do + context "with x-sentry-rate-limits header" do + now = Time.now + + [ + { + header: "", expected_limits: {} + }, + { + header: "invalid", expected_limits: {} + }, + { + header: ",,foo,", expected_limits: {} + }, + { + header: "42::organization, invalid, 4711:foobar;transaction;security:project", + expected_limits: { + nil => now + 42, + "transaction" => now + 4711, + "foobar" => now + 4711, + "security" => now + 4711 + } + } + ].each do |pair| + context "with header value: '#{pair[:header]}'" do + let(:headers) do + { "x-sentry-rate-limits" => pair[:header] } + end + + it "parses the header into correct limits" do + send_data_and_verify_response(now) + expect(subject.rate_limits).to eq(pair[:expected_limits]) + end + end + end + end + + context "with retry-after header" do + now = Time.now + + [ + { + header: "48", expected_limits: { nil => now + 48 } + }, + { + header: "invalid", expected_limits: { nil => now + 60} + }, + { + header: "", expected_limits: { nil => now + 60} + }, + ].each do |pair| + context "with header value: '#{pair[:header]}'" do + let(:headers) do + { "retry-after" => pair[:header] } + end + + it "parses the header into correct limits" do + send_data_and_verify_response(now) + expect(subject.rate_limits).to eq(pair[:expected_limits]) + end + end + end + end + + context "with both x-sentry-rate-limits and retry-after headers" do + let(:headers) do + { "x-sentry-rate-limits" => "42:error:organization", "retry-after" => "42" } + end + + it "parses x-sentry-rate-limits first" do + now = Time.now + + send_data_and_verify_response(now) + expect(subject.rate_limits).to eq({ "error" => now + 42 }) + end + end + end + + context "received 200 response" do + let(:stubs) do + Faraday::Adapter::Test::Stubs.new do |stub| + stub.post('sentry/api/42/envelope/') do + [ + 200, headers, "" + ] + end + end + end + + it_behaves_like "rate limiting headers handling" do + def send_data_and_verify_response(time) + Timecop.freeze(time) do + subject.send_data(data) + end + end + end + + context "with no rate limiting headers" do + let(:headers) do + {} + end + + it "doesn't add any rate limites" do + now = Time.now + + Timecop.freeze(now) do + subject.send_data(data) + end + expect(subject.rate_limits).to eq({}) + end + end + end + + context "received 429 response" do + let(:stubs) do + Faraday::Adapter::Test::Stubs.new do |stub| + stub.post('sentry/api/42/envelope/') do + [ + 429, headers, "{\"detail\":\"event rejected due to rate limit\"}" + ] + end + end + end + + it_behaves_like "rate limiting headers handling" do + def send_data_and_verify_response(time) + Timecop.freeze(time) do + expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + end + end + end + + context "with no rate limiting headers" do + let(:headers) do + {} + end + + it "adds default limits" do + now = Time.now + + Timecop.freeze(now) do + expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + end + expect(subject.rate_limits).to eq({ nil => now + 60 }) + end + end + end +end diff --git a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb index bdc322415..641bb408e 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_spec.rb @@ -111,119 +111,6 @@ end end - context "receive 429 response" do - let(:stubs) do - Faraday::Adapter::Test::Stubs.new do |stub| - stub.post('sentry/api/42/envelope/') do - [ - 429, headers, "{\"detail\":\"event rejected due to rate limit\"}" - ] - end - end - end - - context "with x-sentry-rate-limits header" do - now = Time.now - - [ - { - header: "", expected_limits: {} - }, - { - header: "invalid", expected_limits: {} - }, - { - header: ",,foo,", expected_limits: {} - }, - { - header: "42::organization, invalid, 4711:foobar;transaction;security:project", - expected_limits: { - nil => now + 42, - "transaction" => now + 4711, - "foobar" => now + 4711, - "security" => now + 4711 - } - } - ].each do |pair| - context "with header value: '#{pair[:header]}'" do - let(:headers) do - { status: 429, "x-sentry-rate-limits" => pair[:header] } - end - - it "parses the header into correct limits" do - Timecop.freeze(now) do - expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) - end - - expect(subject.rate_limits).to eq(pair[:expected_limits]) - end - end - end - end - - context "with retry-after header" do - now = Time.now - - [ - { - header: "48", expected_limits: { nil => now + 48 } - }, - { - header: "invalid", expected_limits: { nil => now + 60} - }, - { - header: "", expected_limits: { nil => now + 60} - }, - ].each do |pair| - context "with header value: '#{pair[:header]}'" do - let(:headers) do - { status: 429, "retry-after" => pair[:header] } - end - - it "parses the header into correct limits" do - Timecop.freeze(now) do - expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) - end - - expect(subject.rate_limits).to eq(pair[:expected_limits]) - end - end - end - end - - context "with both x-sentry-rate-limits and retry-after headers" do - let(:headers) do - { status: 429, "x-sentry-rate-limits" => "42:error:organization", "retry-after" => "42" } - end - - it "parses x-sentry-rate-limits first" do - now = Time.now - - Timecop.freeze(now) do - expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) - end - - expect(subject.rate_limits).to eq({ "error" => now + 42 }) - end - end - - context "with no rate limiting headers" do - let(:headers) do - { status: 429 } - end - - it "adds default limits" do - now = Time.now - - Timecop.freeze(now) do - expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) - end - - expect(subject.rate_limits).to eq({ nil => now + 60 }) - end - end - end - context "receive 5xx responses" do let(:stubs) do Faraday::Adapter::Test::Stubs.new do |stub| From 2fa55051963af5fb22a5ef00592f52e62f19d36a Mon Sep 17 00:00:00 2001 From: st0012 Date: Sat, 13 Mar 2021 17:03:43 +0800 Subject: [PATCH 05/11] Check rate limits before sending an event --- sentry-ruby/lib/sentry/transport.rb | 23 ++ .../lib/sentry/transport/http_transport.rb | 1 - .../http_transport_rate_limiting_spec.rb | 272 +++++++++++------- 3 files changed, 185 insertions(+), 111 deletions(-) diff --git a/sentry-ruby/lib/sentry/transport.rb b/sentry-ruby/lib/sentry/transport.rb index 486c1bf88..e7c272e0d 100644 --- a/sentry-ruby/lib/sentry/transport.rb +++ b/sentry-ruby/lib/sentry/transport.rb @@ -16,6 +16,7 @@ def initialize(configuration) @logger = configuration.logger @transport_configuration = configuration.transport @dsn = configuration.dsn + @rate_limits = {} end def send_data(data, options = {}) @@ -28,6 +29,8 @@ def send_event(event) return end + return if is_rate_limited?(event) + encoded_data = encode(event) return nil unless encoded_data @@ -37,6 +40,26 @@ def send_event(event) event end + def is_rate_limited?(event) + event_hash = event.to_hash + event_type = event_hash[:type] || event_hash['type'] + + # check category-specific limit + delay = + case event_type + when "event" + # confusing mapping, but it's decided by Sentry + @rate_limits["error"] + when "transaction" + @rate_limits["transaction"] + end + + # check universal limit if not category limit + delay ||= @rate_limits[nil] + + !!delay && delay > Time.now + end + def generate_auth_header now = Sentry.utc_now.to_i fields = { diff --git a/sentry-ruby/lib/sentry/transport/http_transport.rb b/sentry-ruby/lib/sentry/transport/http_transport.rb index 19f3d10eb..028595773 100644 --- a/sentry-ruby/lib/sentry/transport/http_transport.rb +++ b/sentry-ruby/lib/sentry/transport/http_transport.rb @@ -18,7 +18,6 @@ def initialize(*args) @adapter = @transport_configuration.http_adapter || Faraday.default_adapter @conn = set_conn @endpoint = @dsn.envelope_endpoint - @rate_limits = {} end def send_data(data) diff --git a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb index 91328fbd7..92a0f8a2f 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb @@ -14,154 +14,206 @@ subject { Sentry::HTTPTransport.new(configuration) } - before do - configuration.transport.http_adapter = [:test, stubs] - end + describe "#is_rate_limited?" do + let(:transaction_event) do + client.event_from_transaction(Sentry::Transaction.new) + end - shared_examples "rate limiting headers handling" do - context "with x-sentry-rate-limits header" do - now = Time.now - - [ - { - header: "", expected_limits: {} - }, - { - header: "invalid", expected_limits: {} - }, - { - header: ",,foo,", expected_limits: {} - }, - { - header: "42::organization, invalid, 4711:foobar;transaction;security:project", - expected_limits: { - nil => now + 42, - "transaction" => now + 4711, - "foobar" => now + 4711, - "security" => now + 4711 - } - } - ].each do |pair| - context "with header value: '#{pair[:header]}'" do - let(:headers) do - { "x-sentry-rate-limits" => pair[:header] } - end + context "with only category limits" do + it "returns true for still limited category" do + subject.rate_limits.merge!("error" => Time.now + 60) - it "parses the header into correct limits" do - send_data_and_verify_response(now) - expect(subject.rate_limits).to eq(pair[:expected_limits]) - end - end + expect(subject.is_rate_limited?(event.to_hash)).to eq(true) end - end - context "with retry-after header" do - now = Time.now - - [ - { - header: "48", expected_limits: { nil => now + 48 } - }, - { - header: "invalid", expected_limits: { nil => now + 60} - }, - { - header: "", expected_limits: { nil => now + 60} - }, - ].each do |pair| - context "with header value: '#{pair[:header]}'" do - let(:headers) do - { "retry-after" => pair[:header] } - end + it "returns false for passed limited category" do + subject.rate_limits.merge!("error" => Time.now - 10) - it "parses the header into correct limits" do - send_data_and_verify_response(now) - expect(subject.rate_limits).to eq(pair[:expected_limits]) - end - end + expect(subject.is_rate_limited?(event.to_hash)).to eq(false) + end + + it "returns false for not listed category" do + subject.rate_limits.merge!("transaction" => Time.now + 10) + + expect(subject.is_rate_limited?(event.to_hash)).to eq(false) end end - context "with both x-sentry-rate-limits and retry-after headers" do - let(:headers) do - { "x-sentry-rate-limits" => "42:error:organization", "retry-after" => "42" } + context "with only universal limits" do + it "returns true when still limited" do + subject.rate_limits.merge!(nil => Time.now + 60) + + expect(subject.is_rate_limited?(event.to_hash)).to eq(true) end - it "parses x-sentry-rate-limits first" do - now = Time.now + it "returns false when passed limit" do + subject.rate_limits.merge!(nil => Time.now - 10) - send_data_and_verify_response(now) - expect(subject.rate_limits).to eq({ "error" => now + 42 }) + expect(subject.is_rate_limited?(event.to_hash)).to eq(false) end end - end - context "received 200 response" do - let(:stubs) do - Faraday::Adapter::Test::Stubs.new do |stub| - stub.post('sentry/api/42/envelope/') do - [ - 200, headers, "" - ] - end + context "with both category-based and universal limits" do + it "prioritizes category limits" do + subject.rate_limits.merge!( + "error" => Time.now + 60, + nil => Time.now - 10 + ) + + expect(subject.is_rate_limited?(event.to_hash)).to eq(true) end end + end - it_behaves_like "rate limiting headers handling" do - def send_data_and_verify_response(time) - Timecop.freeze(time) do - subject.send_data(data) - end - end + describe "rate limit header processing" do + before do + configuration.transport.http_adapter = [:test, stubs] end - context "with no rate limiting headers" do - let(:headers) do - {} + shared_examples "rate limiting headers handling" do + context "with x-sentry-rate-limits header" do + now = Time.now + + [ + { + header: "", expected_limits: {} + }, + { + header: "invalid", expected_limits: {} + }, + { + header: ",,foo,", expected_limits: {} + }, + { + header: "42::organization, invalid, 4711:foobar;transaction;security:project", + expected_limits: { + nil => now + 42, + "transaction" => now + 4711, + "foobar" => now + 4711, "security" => now + 4711 + } + } + ].each do |pair| + context "with header value: '#{pair[:header]}'" do + let(:headers) do + { "x-sentry-rate-limits" => pair[:header] } + end + + it "parses the header into correct limits" do + send_data_and_verify_response(now) + expect(subject.rate_limits).to eq(pair[:expected_limits]) + end + end + end end - it "doesn't add any rate limites" do + context "with retry-after header" do now = Time.now - Timecop.freeze(now) do - subject.send_data(data) + [ + { + header: "48", expected_limits: { nil => now + 48 } + }, + { + header: "invalid", expected_limits: { nil => now + 60} + }, + { + header: "", expected_limits: { nil => now + 60} + }, + ].each do |pair| + context "with header value: '#{pair[:header]}'" do + let(:headers) do + { "retry-after" => pair[:header] } + end + + it "parses the header into correct limits" do + send_data_and_verify_response(now) + expect(subject.rate_limits).to eq(pair[:expected_limits]) + end + end + end + end + + context "with both x-sentry-rate-limits and retry-after headers" do + let(:headers) do + { "x-sentry-rate-limits" => "42:error:organization", "retry-after" => "42" } + end + + it "parses x-sentry-rate-limits first" do + now = Time.now + + send_data_and_verify_response(now) + expect(subject.rate_limits).to eq({ "error" => now + 42 }) end - expect(subject.rate_limits).to eq({}) end end - end - context "received 429 response" do - let(:stubs) do - Faraday::Adapter::Test::Stubs.new do |stub| - stub.post('sentry/api/42/envelope/') do - [ - 429, headers, "{\"detail\":\"event rejected due to rate limit\"}" - ] + context "received 200 response" do + let(:stubs) do + Faraday::Adapter::Test::Stubs.new do |stub| + stub.post('sentry/api/42/envelope/') do + [ + 200, headers, "" + ] + end end end - end - it_behaves_like "rate limiting headers handling" do - def send_data_and_verify_response(time) - Timecop.freeze(time) do - expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + it_behaves_like "rate limiting headers handling" do + def send_data_and_verify_response(time) + Timecop.freeze(time) do + subject.send_data(data) + end + end + end + + context "with no rate limiting headers" do + let(:headers) do + {} + end + + it "doesn't add any rate limites" do + now = Time.now + + Timecop.freeze(now) do + subject.send_data(data) + end + expect(subject.rate_limits).to eq({}) end end end - context "with no rate limiting headers" do - let(:headers) do - {} + context "received 429 response" do + let(:stubs) do + Faraday::Adapter::Test::Stubs.new do |stub| + stub.post('sentry/api/42/envelope/') do + [ + 429, headers, "{\"detail\":\"event rejected due to rate limit\"}" + ] + end + end end - it "adds default limits" do - now = Time.now + it_behaves_like "rate limiting headers handling" do + def send_data_and_verify_response(time) + Timecop.freeze(time) do + expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + end + end + end + + context "with no rate limiting headers" do + let(:headers) do + {} + end + + it "adds default limits" do + now = Time.now - Timecop.freeze(now) do - expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + Timecop.freeze(now) do + expect { subject.send_data(data) }.to raise_error(Sentry::ExternalError, /the server responded with status 429/) + end + expect(subject.rate_limits).to eq({ nil => now + 60 }) end - expect(subject.rate_limits).to eq({ nil => now + 60 }) end end end From 4bee05902dc5d3ff983ca32aba612402e6e065ab Mon Sep 17 00:00:00 2001 From: st0012 Date: Sat, 13 Mar 2021 17:18:31 +0800 Subject: [PATCH 06/11] Only keeps greater limit value --- .../lib/sentry/transport/http_transport.rb | 10 ++++++- .../http_transport_rate_limiting_spec.rb | 30 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/sentry-ruby/lib/sentry/transport/http_transport.rb b/sentry-ruby/lib/sentry/transport/http_transport.rb index 028595773..3c8d85fe3 100644 --- a/sentry-ruby/lib/sentry/transport/http_transport.rb +++ b/sentry-ruby/lib/sentry/transport/http_transport.rb @@ -72,7 +72,15 @@ def handle_rate_limited_response(headers) { nil => Time.now + DEFAULT_DELAY } end - @rate_limits.merge!(rate_limits) + rate_limits.each do |category, limit| + if current_limit = @rate_limits[category] + if current_limit < limit + @rate_limits[category] = limit + end + else + @rate_limits[category] = limit + end + end end def parse_rate_limit_header(rate_limit_header) diff --git a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb index 92a0f8a2f..7ec033ba9 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb @@ -104,6 +104,36 @@ end end end + + context "when receiving a greater value for a present category" do + let(:headers) do + { "x-sentry-rate-limits" => "120:error:organization" } + end + + before do + subject.rate_limits.merge!("error" => now + 10) + end + + it "overrides the current limit" do + send_data_and_verify_response(now) + expect(subject.rate_limits).to eq({ "error" => now + 120 }) + end + end + + context "when receiving a smaller value for a present category" do + let(:headers) do + { "x-sentry-rate-limits" => "10:error:organization" } + end + + before do + subject.rate_limits.merge!("error" => now + 120) + end + + it "keeps the current limit" do + send_data_and_verify_response(now) + expect(subject.rate_limits).to eq({ "error" => now + 120 }) + end + end end context "with retry-after header" do From 1f17941e556615803d6d814849a8a5eac4803383 Mon Sep 17 00:00:00 2001 From: st0012 Date: Sat, 13 Mar 2021 17:49:36 +0800 Subject: [PATCH 07/11] Log message when events are not sent due to rate limiting --- sentry-ruby/lib/sentry/transport.rb | 34 ++++++++++++------- .../lib/sentry/transport/http_transport.rb | 2 +- sentry-ruby/spec/sentry/hub_spec.rb | 1 + .../http_transport_rate_limiting_spec.rb | 18 +++++----- sentry-ruby/spec/sentry/transport_spec.rb | 2 +- sentry-ruby/spec/sentry_spec.rb | 18 ++++++++++ 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/sentry-ruby/lib/sentry/transport.rb b/sentry-ruby/lib/sentry/transport.rb index e7c272e0d..fdd6ebbbc 100644 --- a/sentry-ruby/lib/sentry/transport.rb +++ b/sentry-ruby/lib/sentry/transport.rb @@ -9,7 +9,7 @@ class Transport include LoggingHelper attr_accessor :configuration - attr_reader :logger + attr_reader :logger, :rate_limits def initialize(configuration) @configuration = configuration @@ -24,12 +24,20 @@ def send_data(data, options = {}) end def send_event(event) + event_hash = event.to_hash + item_type = get_item_type(event_hash) + unless configuration.sending_allowed? - log_debug("Event not sent: #{configuration.error_messages}") + log_debug("Envelope [#{item_type}] not sent: #{configuration.error_messages}") + return end - return if is_rate_limited?(event) + if is_rate_limited?(item_type) + log_info("Envelope [#{item_type}] not sent: rate limiting") + + return + end encoded_data = encode(event) @@ -40,18 +48,14 @@ def send_event(event) event end - def is_rate_limited?(event) - event_hash = event.to_hash - event_type = event_hash[:type] || event_hash['type'] - + def is_rate_limited?(item_type) # check category-specific limit delay = - case event_type - when "event" - # confusing mapping, but it's decided by Sentry - @rate_limits["error"] + case item_type when "transaction" @rate_limits["transaction"] + else + @rate_limits["error"] end # check universal limit if not category limit @@ -77,7 +81,7 @@ def encode(event) event_hash = event.to_hash event_id = event_hash[:event_id] || event_hash["event_id"] - item_type = event_hash[:type] || event_hash["type"] || "event" + item_type = get_item_type(event_hash) envelope = <<~ENVELOPE {"event_id":"#{event_id}","dsn":"#{configuration.dsn.to_s}","sdk":#{Sentry.sdk_meta.to_json},"sent_at":"#{Sentry.utc_now.iso8601}"} @@ -89,6 +93,12 @@ def encode(event) envelope end + + private + + def get_item_type(event_hash) + event_hash[:type] || event_hash["type"] || "event" + end end end diff --git a/sentry-ruby/lib/sentry/transport/http_transport.rb b/sentry-ruby/lib/sentry/transport/http_transport.rb index 3c8d85fe3..be55d7aa1 100644 --- a/sentry-ruby/lib/sentry/transport/http_transport.rb +++ b/sentry-ruby/lib/sentry/transport/http_transport.rb @@ -11,7 +11,7 @@ class HTTPTransport < Transport RETRY_AFTER_HEADER = "retry-after" RATE_LIMIT_HEADER = "x-sentry-rate-limits" - attr_reader :conn, :adapter, :rate_limits + attr_reader :conn, :adapter def initialize(*args) super diff --git a/sentry-ruby/spec/sentry/hub_spec.rb b/sentry-ruby/spec/sentry/hub_spec.rb index 2f868a867..2f0498245 100644 --- a/sentry-ruby/spec/sentry/hub_spec.rb +++ b/sentry-ruby/spec/sentry/hub_spec.rb @@ -119,6 +119,7 @@ end end end + describe '#capture_message' do let(:message) { "Test message" } diff --git a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb index 7ec033ba9..feb7a3659 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb @@ -21,21 +21,23 @@ context "with only category limits" do it "returns true for still limited category" do - subject.rate_limits.merge!("error" => Time.now + 60) + subject.rate_limits.merge!("error" => Time.now + 60, "transaction" => Time.now + 60) - expect(subject.is_rate_limited?(event.to_hash)).to eq(true) + expect(subject.is_rate_limited?("event")).to eq(true) + expect(subject.is_rate_limited?("transaction")).to eq(true) end it "returns false for passed limited category" do - subject.rate_limits.merge!("error" => Time.now - 10) + subject.rate_limits.merge!("error" => Time.now - 10, "transaction" => Time.now - 10) - expect(subject.is_rate_limited?(event.to_hash)).to eq(false) + expect(subject.is_rate_limited?("event")).to eq(false) + expect(subject.is_rate_limited?("transaction")).to eq(false) end it "returns false for not listed category" do subject.rate_limits.merge!("transaction" => Time.now + 10) - expect(subject.is_rate_limited?(event.to_hash)).to eq(false) + expect(subject.is_rate_limited?("event")).to eq(false) end end @@ -43,13 +45,13 @@ it "returns true when still limited" do subject.rate_limits.merge!(nil => Time.now + 60) - expect(subject.is_rate_limited?(event.to_hash)).to eq(true) + expect(subject.is_rate_limited?("event")).to eq(true) end it "returns false when passed limit" do subject.rate_limits.merge!(nil => Time.now - 10) - expect(subject.is_rate_limited?(event.to_hash)).to eq(false) + expect(subject.is_rate_limited?("event")).to eq(false) end end @@ -60,7 +62,7 @@ nil => Time.now - 10 ) - expect(subject.is_rate_limited?(event.to_hash)).to eq(true) + expect(subject.is_rate_limited?("event")).to eq(true) end end end diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index e72608682..d1da14293 100644 --- a/sentry-ruby/spec/sentry/transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport_spec.rb @@ -90,7 +90,7 @@ subject.send_event(event) logs = string_io.string - expect(logs).to match(/Event not sent: Excluded by random sample/) + expect(logs).to match(/Envelope \[event\] not sent: Excluded by random sample/) end end diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb index 4069ff4f2..32bbf9534 100644 --- a/sentry-ruby/spec/sentry_spec.rb +++ b/sentry-ruby/spec/sentry_spec.rb @@ -111,6 +111,24 @@ expect(subject.last_event_id).to eq(nil) end end + + context "when rate limited" do + let(:string_io) { StringIO.new } + before do + perform_basic_setup do |config| + config.logger = Logger.new(string_io) + config.transport.transport_class = Sentry::HTTPTransport + end + + Sentry.get_current_client.transport.rate_limits.merge!("error" => Time.now + 100) + end + + it "stops the event and logs correct message" do + described_class.send(capture_helper, capture_subject) + + expect(string_io.string).to match(/Envelope \[event\] not sent: rate limiting/) + end + end end describe ".send_event" do From 359605e2785a48714edf45f6fac419307aadd071 Mon Sep 17 00:00:00 2001 From: st0012 Date: Fri, 26 Mar 2021 19:00:29 +0800 Subject: [PATCH 08/11] Fix delay picking logic --- sentry-ruby/lib/sentry/transport.rb | 17 +++++++++++++++-- .../http_transport_rate_limiting_spec.rb | 9 ++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/sentry-ruby/lib/sentry/transport.rb b/sentry-ruby/lib/sentry/transport.rb index fdd6ebbbc..05d481b61 100644 --- a/sentry-ruby/lib/sentry/transport.rb +++ b/sentry-ruby/lib/sentry/transport.rb @@ -50,7 +50,7 @@ def send_event(event) def is_rate_limited?(item_type) # check category-specific limit - delay = + category_delay = case item_type when "transaction" @rate_limits["transaction"] @@ -59,7 +59,20 @@ def is_rate_limited?(item_type) end # check universal limit if not category limit - delay ||= @rate_limits[nil] + universal_delay = @rate_limits[nil] + + delay = + if category_delay && universal_delay + if category_delay > universal_delay + category_delay + else + universal_delay + end + elsif category_delay + category_delay + else + universal_delay + end !!delay && delay > Time.now end diff --git a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb index feb7a3659..bf573dc51 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb @@ -56,13 +56,20 @@ end context "with both category-based and universal limits" do - it "prioritizes category limits" do + it "checks both limits and picks the greater value" do subject.rate_limits.merge!( "error" => Time.now + 60, nil => Time.now - 10 ) expect(subject.is_rate_limited?("event")).to eq(true) + + subject.rate_limits.merge!( + "error" => Time.now - 60, + nil => Time.now + 10 + ) + + expect(subject.is_rate_limited?("event")).to eq(true) end end end From 517d9686cab42d44931dd0fe233333193226e0d9 Mon Sep 17 00:00:00 2001 From: st0012 Date: Fri, 26 Mar 2021 19:09:10 +0800 Subject: [PATCH 09/11] Address edge cases --- sentry-ruby/lib/sentry/transport/http_transport.rb | 4 ++++ .../sentry/transport/http_transport_rate_limiting_spec.rb | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/sentry-ruby/lib/sentry/transport/http_transport.rb b/sentry-ruby/lib/sentry/transport/http_transport.rb index be55d7aa1..764ef24bc 100644 --- a/sentry-ruby/lib/sentry/transport/http_transport.rb +++ b/sentry-ruby/lib/sentry/transport/http_transport.rb @@ -64,6 +64,8 @@ def handle_rate_limited_response(headers) if rate_limits = headers[RATE_LIMIT_HEADER] parse_rate_limit_header(rate_limits) elsif retry_after = headers[RETRY_AFTER_HEADER] + # although Sentry doesn't send a date string back + # based on HTTP specification, this could be a date string (instead of an integer) retry_after = retry_after.to_i retry_after = DEFAULT_DELAY if retry_after == 0 @@ -90,6 +92,8 @@ def parse_rate_limit_header(rate_limit_header) limits = rate_limit_header.split(",") limits.each do |limit| + next if limit.nil? || limit.empty? + begin retry_after, categories = limit.strip.split(":").first(2) retry_after = time + retry_after.to_i diff --git a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb index bf573dc51..04d71fd72 100644 --- a/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb +++ b/sentry-ruby/spec/sentry/transport/http_transport_rate_limiting_spec.rb @@ -87,6 +87,12 @@ { header: "", expected_limits: {} }, + { + header: " ", expected_limits: {} + }, + { + header: " , ", expected_limits: {} + }, { header: "invalid", expected_limits: {} }, From c97d87f7a9762049db0b77ea4663fff51e76596f Mon Sep 17 00:00:00 2001 From: st0012 Date: Sat, 10 Apr 2021 15:45:53 +0800 Subject: [PATCH 10/11] Update changelog --- sentry-ruby/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry-ruby/CHANGELOG.md b/sentry-ruby/CHANGELOG.md index 717e5cc7c..c2966e74a 100644 --- a/sentry-ruby/CHANGELOG.md +++ b/sentry-ruby/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Support category-based rate limiting [#1336](https://github.com/getsentry/sentry-ruby/pull/1336) + ### Refactorings - Let Transaction constructor take an optional hub argument [#1384](https://github.com/getsentry/sentry-ruby/pull/1384) From c7e97669381a1e41f3f8ab678570f332e3e7d9a5 Mon Sep 17 00:00:00 2001 From: st0012 Date: Sat, 10 Apr 2021 15:49:30 +0800 Subject: [PATCH 11/11] Fix transaction spec --- sentry-ruby/spec/sentry/transaction_spec.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/sentry-ruby/spec/sentry/transaction_spec.rb b/sentry-ruby/spec/sentry/transaction_spec.rb index ee2c3486f..23cec7619 100644 --- a/sentry-ruby/spec/sentry/transaction_spec.rb +++ b/sentry-ruby/spec/sentry/transaction_spec.rb @@ -1,6 +1,10 @@ require "spec_helper" RSpec.describe Sentry::Transaction do + before do + perform_basic_setup + end + subject do described_class.new( op: "sql.query", @@ -15,10 +19,6 @@ describe ".from_sentry_trace" do let(:sentry_trace) { subject.to_sentry_trace } - before do - perform_basic_setup - end - let(:configuration) do Sentry.configuration end @@ -302,10 +302,6 @@ end describe "#finish" do - before do - perform_basic_setup - end - let(:events) do Sentry.get_current_client.transport.events end