From c785d0444ed77c9e8ffe675184e2a2442a526ca4 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Tue, 23 Jul 2024 15:19:04 +0200 Subject: [PATCH] Add client reports for span drop counts (#2346) --- CHANGELOG.md | 3 +- sentry-ruby/lib/sentry/client.rb | 23 +- sentry-ruby/lib/sentry/envelope.rb | 2 +- sentry-ruby/lib/sentry/transaction.rb | 1 + sentry-ruby/lib/sentry/transport.rb | 4 +- .../spec/sentry/client/event_sending_spec.rb | 223 ++++++++++++------ sentry-ruby/spec/sentry/envelope_spec.rb | 1 + sentry-ruby/spec/sentry/transaction_spec.rb | 2 + sentry-ruby/spec/sentry/transport_spec.rb | 4 +- sentry-ruby/spec/spec_helper.rb | 4 +- 10 files changed, 182 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b5cf6465..3bd7bc977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ ### Internal -- Use Concurrent.usable_processor_count when it is available ([#2339](https://github.com/getsentry/sentry-ruby/pull/2339)) +- Use `Concurrent.usable_processor_count` when it is available ([#2339](https://github.com/getsentry/sentry-ruby/pull/2339)) +- Report dropped spans in Client Reports ([#2346](https://github.com/getsentry/sentry-ruby/pull/2346)) ### Bug Fixes diff --git a/sentry-ruby/lib/sentry/client.rb b/sentry-ruby/lib/sentry/client.rb index 608c4e1d5..6176508b7 100644 --- a/sentry-ruby/lib/sentry/client.rb +++ b/sentry-ruby/lib/sentry/client.rb @@ -55,19 +55,29 @@ def capture_event(event, scope, hint = {}) event_type = event.is_a?(Event) ? event.type : event["type"] data_category = Envelope::Item.data_category(event_type) + + is_transaction = event.is_a?(TransactionEvent) + spans_before = is_transaction ? event.spans.size : 0 + event = scope.apply_to_event(event, hint) if event.nil? log_debug("Discarded event because one of the event processors returned nil") transport.record_lost_event(:event_processor, data_category) + transport.record_lost_event(:event_processor, 'span', num: spans_before + 1) if is_transaction return + elsif is_transaction + spans_delta = spans_before - event.spans.size + transport.record_lost_event(:event_processor, 'span', num: spans_delta) if spans_delta > 0 end if async_block = configuration.async dispatch_async_event(async_block, event, hint) elsif configuration.background_worker_threads != 0 && hint.fetch(:background, true) - queued = dispatch_background_event(event, hint) - transport.record_lost_event(:queue_overflow, data_category) unless queued + unless dispatch_background_event(event, hint) + transport.record_lost_event(:queue_overflow, data_category) + transport.record_lost_event(:queue_overflow, 'span', num: spans_before + 1) if is_transaction + end else send_event(event, hint) end @@ -168,6 +178,7 @@ def event_from_transaction(transaction) def send_event(event, hint = nil) event_type = event.is_a?(Event) ? event.type : event["type"] data_category = Envelope::Item.data_category(event_type) + spans_before = event.is_a?(TransactionEvent) ? event.spans.size : 0 if event_type != TransactionEvent::TYPE && configuration.before_send event = configuration.before_send.call(event, hint) @@ -184,8 +195,13 @@ def send_event(event, hint = nil) if event.nil? log_debug("Discarded event because before_send_transaction returned nil") - transport.record_lost_event(:before_send, data_category) + transport.record_lost_event(:before_send, 'transaction') + transport.record_lost_event(:before_send, 'span', num: spans_before + 1) return + else + spans_after = event.is_a?(TransactionEvent) ? event.spans.size : 0 + spans_delta = spans_before - spans_after + transport.record_lost_event(:before_send, 'span', num: spans_delta) if spans_delta > 0 end end @@ -196,6 +212,7 @@ def send_event(event, hint = nil) rescue => e log_error("Event sending failed", e, debug: configuration.debug) transport.record_lost_event(:network_error, data_category) + transport.record_lost_event(:network_error, 'span', num: spans_before + 1) if event.is_a?(TransactionEvent) raise end diff --git a/sentry-ruby/lib/sentry/envelope.rb b/sentry-ruby/lib/sentry/envelope.rb index 96bb83beb..51220a062 100644 --- a/sentry-ruby/lib/sentry/envelope.rb +++ b/sentry-ruby/lib/sentry/envelope.rb @@ -21,7 +21,7 @@ def type # rate limits and client reports use the data_category rather than envelope item type def self.data_category(type) case type - when 'session', 'attachment', 'transaction', 'profile' then type + when 'session', 'attachment', 'transaction', 'profile', 'span' then type when 'sessions' then 'session' when 'check_in' then 'monitor' when 'statsd', 'metric_meta' then 'metric_bucket' diff --git a/sentry-ruby/lib/sentry/transaction.rb b/sentry-ruby/lib/sentry/transaction.rb index 456b49e74..89d169512 100644 --- a/sentry-ruby/lib/sentry/transaction.rb +++ b/sentry-ruby/lib/sentry/transaction.rb @@ -266,6 +266,7 @@ def finish(hub: nil, end_timestamp: nil) is_backpressure = Sentry.backpressure_monitor&.downsample_factor&.positive? reason = is_backpressure ? :backpressure : :sample_rate hub.current_client.transport.record_lost_event(reason, 'transaction') + hub.current_client.transport.record_lost_event(reason, 'span') end end diff --git a/sentry-ruby/lib/sentry/transport.rb b/sentry-ruby/lib/sentry/transport.rb index 564734791..acef83de0 100644 --- a/sentry-ruby/lib/sentry/transport.rb +++ b/sentry-ruby/lib/sentry/transport.rb @@ -151,11 +151,11 @@ def envelope_from_event(event) envelope end - def record_lost_event(reason, data_category) + def record_lost_event(reason, data_category, num: 1) return unless @send_client_reports return unless CLIENT_REPORT_REASONS.include?(reason) - @discarded_events[[reason, data_category]] += 1 + @discarded_events[[reason, data_category]] += num end def flush diff --git a/sentry-ruby/spec/sentry/client/event_sending_spec.rb b/sentry-ruby/spec/sentry/client/event_sending_spec.rb index 86098a469..b1b1a041c 100644 --- a/sentry-ruby/spec/sentry/client/event_sending_spec.rb +++ b/sentry-ruby/spec/sentry/client/event_sending_spec.rb @@ -8,16 +8,23 @@ config.transport.transport_class = Sentry::DummyTransport end end - subject { Sentry::Client.new(configuration) } + subject(:client) { Sentry::Client.new(configuration) } let(:hub) do - Sentry::Hub.new(subject, Sentry::Scope.new) + Sentry::Hub.new(client, Sentry::Scope.new) end + let(:transaction) do + transaction = Sentry::Transaction.new(name: "test transaction", op: "rack.request", hub: hub) + 5.times { |i| transaction.with_child_span(description: "span_#{i}") { } } + transaction + end + let(:transaction_event) { client.event_from_transaction(transaction) } + describe "#capture_event" do let(:message) { "Test message" } let(:scope) { Sentry::Scope.new } - let(:event) { subject.event_from_message(message) } + let(:event) { client.event_from_message(message) } context "with sample_rate set" do before do @@ -28,26 +35,23 @@ context "with Event" do it "sends the event when it's sampled" do allow(Random).to receive(:rand).and_return(0.49) - subject.capture_event(event, scope) - expect(subject.transport.events.count).to eq(1) + client.capture_event(event, scope) + expect(client.transport.events.count).to eq(1) end it "doesn't send the event when it's not sampled" do allow(Random).to receive(:rand).and_return(0.51) - subject.capture_event(event, scope) - expect(subject.transport).to have_recorded_lost_event(:sample_rate, 'error') - expect(subject.transport.events.count).to eq(0) + client.capture_event(event, scope) + expect(client.transport).to have_recorded_lost_event(:sample_rate, 'error') + expect(client.transport.events.count).to eq(0) end end context "with TransactionEvent" do it "ignores the sampling" do - transaction_event = subject.event_from_transaction(Sentry::Transaction.new(hub: hub)) allow(Random).to receive(:rand).and_return(0.51) - - subject.capture_event(transaction_event, scope) - - expect(subject.transport.events.count).to eq(1) + client.capture_event(transaction_event, scope) + expect(client.transport.events.count).to eq(1) end end end @@ -55,7 +59,7 @@ context 'with config.async set' do let(:async_block) do lambda do |event| - subject.send_event(event) + client.send_event(event) end end @@ -69,10 +73,10 @@ it "executes the given block" do expect(async_block).to receive(:call).and_call_original - returned = subject.capture_event(event, scope) + returned = client.capture_event(event, scope) expect(returned).to be_a(Sentry::ErrorEvent) - expect(subject.transport.events.first).to eq(event.to_json_compatible) + expect(client.transport.events.first).to eq(event.to_json_compatible) end it "doesn't call the async block if not allow sending events" do @@ -80,7 +84,7 @@ expect(async_block).not_to receive(:call) - returned = subject.capture_event(event, scope) + returned = client.capture_event(event, scope) expect(returned).to eq(nil) end @@ -88,12 +92,12 @@ context "with to json conversion failed" do let(:logger) { ::Logger.new(string_io) } let(:string_io) { StringIO.new } - let(:event) { subject.event_from_message("Bad data '\x80\xF8'") } + let(:event) { client.event_from_message("Bad data '\x80\xF8'") } it "does not mask the exception" do configuration.logger = logger - subject.capture_event(event, scope) + client.capture_event(event, scope) expect(string_io.string).to include("Converting event (#{event.event_id}) to JSON compatible hash failed: source sequence is illegal/malformed utf-8") end @@ -103,10 +107,10 @@ let(:async_block) { nil } it "doesn't cause any issue" do - returned = subject.capture_event(event, scope, { background: false }) + returned = client.capture_event(event, scope, { background: false }) expect(returned).to be_a(Sentry::ErrorEvent) - expect(subject.transport.events.first).to eq(event) + expect(client.transport.events.first).to eq(event) end end @@ -114,17 +118,17 @@ let(:async_block) do lambda do |event, hint| event["tags"]["hint"] = hint - subject.send_event(event) + client.send_event(event) end end it "serializes hint and supplies it as the second argument" do expect(configuration.async).to receive(:call).and_call_original - returned = subject.capture_event(event, scope, { foo: "bar" }) + returned = client.capture_event(event, scope, { foo: "bar" }) expect(returned).to be_a(Sentry::ErrorEvent) - event = subject.transport.events.first + event = client.transport.events.first expect(event.dig("tags", "hint")).to eq({ "foo" => "bar" }) end end @@ -140,20 +144,20 @@ end it "sends events asynchronously" do - subject.capture_event(event, scope) + client.capture_event(event, scope) - expect(subject.transport.events.count).to eq(0) + expect(client.transport.events.count).to eq(0) sleep(0.2) - expect(subject.transport.events.count).to eq(1) + expect(client.transport.events.count).to eq(1) end context "with hint: { background: false }" do it "sends the event immediately" do - subject.capture_event(event, scope, { background: false }) + client.capture_event(event, scope, { background: false }) - expect(subject.transport.events.count).to eq(1) + expect(client.transport.events.count).to eq(1) end end @@ -161,48 +165,57 @@ it "sends the event immediately" do configuration.background_worker_threads = 0 - subject.capture_event(event, scope) + client.capture_event(event, scope) - expect(subject.transport.events.count).to eq(1) + expect(client.transport.events.count).to eq(1) end end - it "records queue overflow" do + it "records queue overflow for error event" do + allow(Sentry.background_worker).to receive(:perform).and_return(false) + + client.capture_event(event, scope) + expect(client.transport).to have_recorded_lost_event(:queue_overflow, 'error') + + expect(client.transport.events.count).to eq(0) + sleep(0.2) + expect(client.transport.events.count).to eq(0) + end + + it "records queue overflow for transaction event with span counts" do allow(Sentry.background_worker).to receive(:perform).and_return(false) - subject.capture_event(event, scope) - expect(subject.transport).to have_recorded_lost_event(:queue_overflow, 'error') + client.capture_event(transaction_event, scope) + expect(client.transport).to have_recorded_lost_event(:queue_overflow, 'transaction') + expect(client.transport).to have_recorded_lost_event(:queue_overflow, 'span', num: 6) - expect(subject.transport.events.count).to eq(0) + expect(client.transport.events.count).to eq(0) sleep(0.2) - expect(subject.transport.events.count).to eq(0) + expect(client.transport.events.count).to eq(0) end end end describe "#send_event" do let(:event_object) do - subject.event_from_exception(ZeroDivisionError.new("divided by 0")) - end - let(:transaction_event_object) do - subject.event_from_transaction(Sentry::Transaction.new(hub: hub)) + client.event_from_exception(ZeroDivisionError.new("divided by 0")) end shared_examples "Event in send_event" do context "when there's an exception" do before do - expect(subject.transport).to receive(:send_event).and_raise(Sentry::ExternalError.new("networking error")) + expect(client.transport).to receive(:send_event).and_raise(Sentry::ExternalError.new("networking error")) end it "raises the error" do expect do - subject.send_event(event) + client.send_event(event) end.to raise_error(Sentry::ExternalError, "networking error") end end it "sends data through the transport" do - expect(subject.transport).to receive(:send_event).with(event) - subject.send_event(event) + expect(client.transport).to receive(:send_event).with(event) + client.send_event(event) end it "applies before_send callback before sending the event" do @@ -216,7 +229,7 @@ event end - subject.send_event(event) + client.send_event(event) if event.is_a?(Sentry::Event) expect(event.tags[:called]).to eq(true) @@ -231,7 +244,7 @@ configuration.before_send_transaction = dbl expect(dbl).not_to receive(:call) - subject.send_event(event) + client.send_event(event) end end @@ -245,7 +258,7 @@ shared_examples "TransactionEvent in send_event" do it "sends data through the transport" do - subject.send_event(event) + client.send_event(event) end it "doesn't apply before_send to TransactionEvent" do @@ -253,7 +266,7 @@ raise "shouldn't trigger me" end - subject.send_event(event) + client.send_event(event) end it "applies before_send_transaction callback before sending the event" do @@ -267,7 +280,7 @@ event end - subject.send_event(event) + client.send_event(event) if event.is_a?(Sentry::Event) expect(event.tags[:called]).to eq(true) @@ -278,11 +291,11 @@ end it_behaves_like "TransactionEvent in send_event" do - let(:event) { transaction_event_object } + let(:event) { transaction_event } end it_behaves_like "TransactionEvent in send_event" do - let(:event) { transaction_event_object.to_json_compatible } + let(:event) { transaction_event.to_json_compatible } end end @@ -300,7 +313,7 @@ let(:message) { "Test message" } let(:scope) { Sentry::Scope.new } - let(:event) { subject.event_from_message(message) } + let(:event) { client.event_from_message(message) } describe "#capture_event" do around do |example| @@ -317,11 +330,35 @@ end it "discards the event and logs a info" do - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil - expect(subject.transport).to have_recorded_lost_event(:event_processor, 'error') expect(string_io.string).to match(/Discarded event because one of the event processors returned nil/) end + + it "records correct client report for error event" do + client.capture_event(event, scope) + expect(client.transport).to have_recorded_lost_event(:event_processor, 'error') + end + + it "records correct transaction and span client reports for transaction event" do + client.capture_event(transaction_event, scope) + expect(client.transport).to have_recorded_lost_event(:event_processor, 'transaction') + expect(client.transport).to have_recorded_lost_event(:event_processor, 'span', num: 6) + end + end + + context "when scope.apply_to_event modifies spans" do + before do + scope.add_event_processor do |event, hint| + 2.times { event.spans.pop } + event + end + end + + it "records correct span delta client report for transaction event" do + client.capture_event(transaction_event, scope) + expect(client.transport).to have_recorded_lost_event(:event_processor, 'span', num: 2) + end end context "when scope.apply_to_event fails" do @@ -332,7 +369,7 @@ end it "swallows the event and logs the failure" do - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil expect(string_io.string).to match(/Event capturing failed: TypeError/) expect(string_io.string).not_to match(__FILE__) @@ -343,7 +380,7 @@ configuration.debug = true end it "logs the error with backtrace" do - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil expect(string_io.string).to match(/Event capturing failed: TypeError/) expect(string_io.string).to match(__FILE__) @@ -358,9 +395,8 @@ end it "swallows and logs Sentry::ExternalError (caused by transport's networking error)" do - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil - expect(subject.transport).to have_recorded_lost_event(:network_error, 'error') expect(string_io.string).to match(/Event sending failed: Failed to open TCP connection/) expect(string_io.string).to match(/Event capturing failed: Failed to open TCP connection/) end @@ -368,10 +404,21 @@ it "swallows and logs errors caused by the user (like in before_send)" do configuration.before_send = ->(_, _) { raise TypeError } - expect(subject.capture_event(event, scope)).to be_nil + expect(client.capture_event(event, scope)).to be_nil expect(string_io.string).to match(/Event sending failed: TypeError/) end + + it "captures client report for error event" do + client.capture_event(event, scope) + expect(client.transport).to have_recorded_lost_event(:network_error, 'error') + end + + it "captures client report for transaction event with span counts" do + client.capture_event(transaction_event, scope) + expect(client.transport).to have_recorded_lost_event(:network_error, 'transaction') + expect(client.transport).to have_recorded_lost_event(:network_error, 'span', num: 6) + end end context "when sending events in background causes error", retry: 3 do @@ -380,32 +427,44 @@ end it "swallows and logs Sentry::ExternalError (caused by transport's networking error)" do - expect(subject.capture_event(event, scope)).to be_a(Sentry::ErrorEvent) + expect(client.capture_event(event, scope)).to be_a(Sentry::ErrorEvent) sleep(0.2) - expect(subject.transport).to have_recorded_lost_event(:network_error, 'error') expect(string_io.string).to match(/Event sending failed: Failed to open TCP connection/) end it "swallows and logs errors caused by the user (like in before_send)" do configuration.before_send = ->(_, _) { raise TypeError } - expect(subject.capture_event(event, scope)).to be_a(Sentry::ErrorEvent) + expect(client.capture_event(event, scope)).to be_a(Sentry::ErrorEvent) sleep(0.2) expect(string_io.string).to match(/Event sending failed: TypeError/) end + + it "captures client report for error event" do + client.capture_event(event, scope) + sleep(0.2) + expect(client.transport).to have_recorded_lost_event(:network_error, 'error') + end + + it "captures client report for transaction event with span counts" do + client.capture_event(transaction_event, scope) + sleep(0.2) + expect(client.transport).to have_recorded_lost_event(:network_error, 'transaction') + expect(client.transport).to have_recorded_lost_event(:network_error, 'span', num: 6) + end end context "when config.async causes error" do before do - expect(subject).to receive(:send_event) + expect(client).to receive(:send_event) end it "swallows Redis related error and send the event synchronizely" do configuration.async = ->(_, _) { raise Redis::ConnectionError } - subject.capture_event(event, scope) + client.capture_event(event, scope) expect(string_io.string).to match(/Async event sending failed: Redis::ConnectionError/) end @@ -413,7 +472,7 @@ it "swallows and logs the exception" do configuration.async = ->(_, _) { raise TypeError } - subject.capture_event(event, scope) + client.capture_event(event, scope) expect(string_io.string).to match(/Async event sending failed: TypeError/) end @@ -424,7 +483,7 @@ context "error happens when sending the event" do it "raises the error" do expect do - subject.send_event(event) + client.send_event(event) end.to raise_error(Sentry::ExternalError) expect(string_io.string).to match(/Event sending failed: Failed to open TCP connection/) @@ -440,7 +499,7 @@ it "raises the error" do expect do - subject.send_event(event) + client.send_event(event) end.to raise_error(TypeError) expect(string_io.string).to match(/Event sending failed: TypeError/) @@ -453,7 +512,7 @@ it "logs the error with backtrace" do expect do - subject.send_event(event) + client.send_event(event) end.to raise_error(TypeError) expect(string_io.string).to match(/Event sending failed: TypeError/) @@ -469,9 +528,9 @@ end end - it "records lost event" do - subject.send_event(event) - expect(subject.transport).to have_recorded_lost_event(:before_send, 'error') + it "records lost error event" do + client.send_event(event) + expect(client.transport).to have_recorded_lost_event(:before_send, 'error') end end @@ -482,10 +541,24 @@ end end - it "records lost event" do - transaction_event = subject.event_from_transaction(Sentry::Transaction.new(hub: hub)) - subject.send_event(transaction_event) - expect(subject.transport).to have_recorded_lost_event(:before_send, 'transaction') + it "records lost transaction with span counts client reports" do + client.send_event(transaction_event) + expect(client.transport).to have_recorded_lost_event(:before_send, 'transaction') + expect(client.transport).to have_recorded_lost_event(:before_send, 'span', num: 6) + end + end + + context "before_send_transaction modifies spans" do + before do + configuration.before_send_transaction = lambda do |event, _hint| + 2.times { event.spans.pop } + event + end + end + + it "records lost span delta client reports" do + expect { client.send_event(transaction_event) }.to raise_error(Sentry::ExternalError) + expect(client.transport).to have_recorded_lost_event(:before_send, 'span', num: 2) end end end diff --git a/sentry-ruby/spec/sentry/envelope_spec.rb b/sentry-ruby/spec/sentry/envelope_spec.rb index c12ee3052..8936c7707 100644 --- a/sentry-ruby/spec/sentry/envelope_spec.rb +++ b/sentry-ruby/spec/sentry/envelope_spec.rb @@ -7,6 +7,7 @@ ['sessions', 'session'], ['attachment', 'attachment'], ['transaction', 'transaction'], + ['span', 'span'], ['profile', 'profile'], ['check_in', 'monitor'], ['statsd', 'metric_bucket'], diff --git a/sentry-ruby/spec/sentry/transaction_spec.rb b/sentry-ruby/spec/sentry/transaction_spec.rb index 6dafc1371..effb249f1 100644 --- a/sentry-ruby/spec/sentry/transaction_spec.rb +++ b/sentry-ruby/spec/sentry/transaction_spec.rb @@ -494,6 +494,7 @@ it "records lost event with reason sample_rate" do subject.finish expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:sample_rate, 'transaction') + expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:sample_rate, 'span') end end @@ -514,6 +515,7 @@ subject.finish expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:backpressure, 'transaction') + expect(Sentry.get_current_client.transport).to have_recorded_lost_event(:backpressure, 'span') end end diff --git a/sentry-ruby/spec/sentry/transport_spec.rb b/sentry-ruby/spec/sentry/transport_spec.rb index 35ebed86a..73b22d91d 100644 --- a/sentry-ruby/spec/sentry/transport_spec.rb +++ b/sentry-ruby/spec/sentry/transport_spec.rb @@ -153,6 +153,7 @@ before do 5.times { subject.record_lost_event(:ratelimit_backoff, 'error') } 3.times { subject.record_lost_event(:queue_overflow, 'transaction') } + 2.times { subject.record_lost_event(:network_error, 'span', num: 5) } end it "incudes client report in envelope" do @@ -170,7 +171,8 @@ timestamp: Time.now.utc.iso8601, discarded_events: [ { reason: :ratelimit_backoff, category: 'error', quantity: 5 }, - { reason: :queue_overflow, category: 'transaction', quantity: 3 } + { reason: :queue_overflow, category: 'transaction', quantity: 3 }, + { reason: :network_error, category: 'span', quantity: 10 } ] }.to_json ) diff --git a/sentry-ruby/spec/spec_helper.rb b/sentry-ruby/spec/spec_helper.rb index 1d4a6ff15..6ba0ccee0 100644 --- a/sentry-ruby/spec/spec_helper.rb +++ b/sentry-ruby/spec/spec_helper.rb @@ -50,9 +50,9 @@ skip("skip rack related tests") unless defined?(Rack) end - RSpec::Matchers.define :have_recorded_lost_event do |reason, data_category| + RSpec::Matchers.define :have_recorded_lost_event do |reason, data_category, num: 1| match do |transport| - expect(transport.discarded_events[[reason, data_category]]).to be > 0 + expect(transport.discarded_events[[reason, data_category]]).to eq(num) end end end