diff --git a/.github/workflows/build_batch_release.yml b/.github/workflows/build_batch_release.yml
index cb0639992..ffc30fcbd 100644
--- a/.github/workflows/build_batch_release.yml
+++ b/.github/workflows/build_batch_release.yml
@@ -13,7 +13,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 2.7
+ ruby-version: 3.3
- name: Build gem source
run: ruby .scripts/batch_build.rb
- name: Archive Artifacts
diff --git a/.github/workflows/sentry_ruby_test.yml b/.github/workflows/sentry_ruby_test.yml
index 2e6f64126..a154df326 100644
--- a/.github/workflows/sentry_ruby_test.yml
+++ b/.github/workflows/sentry_ruby_test.yml
@@ -26,6 +26,7 @@ jobs:
name: Ruby ${{ matrix.ruby_version }} & Rack ${{ matrix.rack_version }}, options - ${{ toJson(matrix.options) }}
runs-on: ubuntu-latest
strategy:
+ fail-fast: false
matrix:
ruby_version: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
rack_version: [2.0, 3.0]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f3c3586d..d3d9f5627 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,99 @@
-## 5.17.3
+## 5.19.0
+
+### Features
+
+- Use `Concurrent.available_processor_count` instead of `Concurrent.usable_processor_count` ([#2358](https://github.com/getsentry/sentry-ruby/pull/2358))
+
+- Support for tracing Faraday requests ([#2345](https://github.com/getsentry/sentry-ruby/pull/2345))
+ - Closes [#1795](https://github.com/getsentry/sentry-ruby/issues/1795)
+ - Please note that the Faraday instrumentation has some limitations in case of async requests: https://github.com/lostisland/faraday/issues/1381
+- Support for attachments ([#2357](https://github.com/getsentry/sentry-ruby/pull/2357))
+
+ Usage:
+
+ ```ruby
+ Sentry.add_attachment(path: '/foo/bar.txt')
+ Sentry.add_attachment(filename: 'payload.json', bytes: '{"value": 42}'))
+ ```
+- Transaction data are now included in the context ([#2365](https://github.com/getsentry/sentry-ruby/pull/2365))
+ - Closes [#2364](https://github.com/getsentry/sentry-ruby/issues/2363)
+
+- Inject Sentry meta tags in the Rails application layout automatically in the generator ([#2369](https://github.com/getsentry/sentry-ruby/pull/2369))
+
+ To turn this behavior off, use
+ ```bash
+ bin/rails generate sentry --inject-meta false
+ ```
+
+### Bug Fixes
+
+- Fix skipping `connect` spans in open-telemetry [#2364](https://github.com/getsentry/sentry-ruby/pull/2364)
+
+## 5.18.2
+
+### Bug Fixes
+
+- Don't overwrite `ip_address` if already set on `user` [#2350](https://github.com/getsentry/sentry-ruby/pull/2350)
+ - Fixes [#2347](https://github.com/getsentry/sentry-ruby/issues/2347)
+- `teardown_sentry_test` helper should clear global even processors too ([#2342](https://github.com/getsentry/sentry-ruby/pull/2342))
+- Suppress the unnecessary “unsupported options notice” ([#2349](https://github.com/getsentry/sentry-ruby/pull/2349))
+
+### Internal
+
+- 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))
+
+## 5.18.1
+
+### Bug Fixes
+
+- Drop `Gem::Specification`'s usage so it doesn't break bundler standalone ([#2335](https://github.com/getsentry/sentry-ruby/pull/2335))
+
+## 5.18.0
### Features
+- Add generator for initializer generation ([#2286](https://github.com/getsentry/sentry-ruby/pull/2286))
+
+ Rails users will be able to use `bin/rails generate sentry` to generate their `config/initializers/sentry.rb` file.
+
+- Notify users when their custom options are discarded ([#2303](https://github.com/getsentry/sentry-ruby/pull/2303))
+- Add a new `:graphql` patch to automatically enable instrumenting GraphQL spans ([#2308](https://github.com/getsentry/sentry-ruby/pull/2308))
+
+ Usage:
+
+ ```rb
+ Sentry.init do |config|
+ # ...
+ config.enabled_patches += [:graphql]
+ end
+ ```
+
+- Add `Sentry.get_trace_propagation_meta` helper for injecting meta tags into views ([#2314](https://github.com/getsentry/sentry-ruby/pull/2314))
+- Add query source support to `sentry-rails` ([#2313](https://github.com/getsentry/sentry-ruby/pull/2313))
+
+ The feature is only activated in apps that use Ruby 3.2+ and Rails 7.1+. By default only queries that take longer than 100ms will have source recorded, which can be adjusted by updating the value of `config.rails.db_query_source_threshold_ms`.
+- Log envelope delivery message with debug instead of info ([#2320](https://github.com/getsentry/sentry-ruby/pull/2320))
+
+### Bug Fixes
+
+- Don't throw error on arbitrary arguments being passed to `capture_event` options [#2301](https://github.com/getsentry/sentry-ruby/pull/2301)
+ - Fixes [#2299](https://github.com/getsentry/sentry-ruby/issues/2299)
+- Decrease the default number of background worker threads by half ([#2305](https://github.com/getsentry/sentry-ruby/pull/2305))
+ - Fixes [#2297](https://github.com/getsentry/sentry-ruby/issues/2297)
+- Don't mutate `enabled_environments` when using `Sentry::TestHelper` ([#2317](https://github.com/getsentry/sentry-ruby/pull/2317))
+- Don't use array for transaction names and sources on scope ([#2324](https://github.com/getsentry/sentry-ruby/pull/2324))
+ - Fixes [#2257](https://github.com/getsentry/sentry-ruby/issues/2257)
+ - **BREAKING** This removes the internal `scope.transaction_names` method, please use `scope.transaction_name` instead
+
+### Internal
+
+- Add `origin` to spans and transactions to track integration sources for instrumentation ([#2319](https://github.com/getsentry/sentry-ruby/pull/2319))
+
+## 5.17.3
+
+### Internal
+
- Update key, unit and tags sanitization logic for metrics [#2292](https://github.com/getsentry/sentry-ruby/pull/2292)
- Consolidate client report and rate limit handling with data categories [#2294](https://github.com/getsentry/sentry-ruby/pull/2294)
- Record `:network_error` client reports for `send_envelope` [#2295](https://github.com/getsentry/sentry-ruby/pull/2295)
@@ -13,7 +105,7 @@
## 5.17.2
-### Features
+### Internal
- Add `Mechanism` interface and default to unhandled for integration exceptions [#2280](https://github.com/getsentry/sentry-ruby/pull/2280)
diff --git a/Gemfile b/Gemfile
index 515440efe..c4344ea66 100644
--- a/Gemfile
+++ b/Gemfile
@@ -9,10 +9,7 @@ ruby_version = Gem::Version.new(RUBY_VERSION)
if ruby_version >= Gem::Version.new("2.7.0")
gem "debug", github: "ruby/debug", platform: :ruby
gem "irb"
-
- if ruby_version >= Gem::Version.new("3.0.0")
- gem "ruby-lsp-rspec"
- end
+ gem "ruby-lsp-rspec" if ruby_version >= Gem::Version.new("3.0.0") && RUBY_PLATFORM != "java"
end
# For RSpec
diff --git a/sentry-delayed_job/Gemfile b/sentry-delayed_job/Gemfile
index c1b2a698b..936bec48a 100644
--- a/sentry-delayed_job/Gemfile
+++ b/sentry-delayed_job/Gemfile
@@ -20,7 +20,10 @@ platform :jruby do
gem "jdbc-sqlite3"
end
-# 1.7.0 dropped support for ruby < 3.0, remove later after upgrading craft setup
-gem "sqlite3", "1.6.9", platform: :ruby
+if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5.0")
+ gem "sqlite3", "~> 1.3.0", platform: :ruby
+else
+ gem "sqlite3", "~> 1.6.9", platform: :ruby
+end
eval_gemfile File.expand_path("../Gemfile", __dir__)
diff --git a/sentry-delayed_job/lib/sentry/delayed_job/plugin.rb b/sentry-delayed_job/lib/sentry/delayed_job/plugin.rb
index 29e1d9e4e..66093f340 100644
--- a/sentry-delayed_job/lib/sentry/delayed_job/plugin.rb
+++ b/sentry-delayed_job/lib/sentry/delayed_job/plugin.rb
@@ -8,7 +8,8 @@ class Plugin < ::Delayed::Plugin
# need to symbolize strings as keyword arguments in Ruby 2.4~2.6
DELAYED_JOB_CONTEXT_KEY = :"Delayed-Job"
ACTIVE_JOB_CONTEXT_KEY = :"Active-Job"
- OP_NAME = "queue.delayed_job".freeze
+ OP_NAME = "queue.delayed_job"
+ SPAN_ORIGIN = "auto.queue.delayed_job"
callbacks do |lifecycle|
lifecycle.before(:enqueue) do |job, *args, &block|
@@ -93,7 +94,13 @@ def self.report?(job)
end
def self.start_transaction(scope, env, contexts)
- options = { name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME }
+ options = {
+ name: scope.transaction_name,
+ source: scope.transaction_source,
+ op: OP_NAME,
+ origin: SPAN_ORIGIN
+ }
+
transaction = Sentry.continue_trace(env, **options)
Sentry.start_transaction(transaction: transaction, custom_sampling_context: contexts, **options)
end
diff --git a/sentry-delayed_job/lib/sentry/delayed_job/version.rb b/sentry-delayed_job/lib/sentry/delayed_job/version.rb
index daf287304..95d3630b0 100644
--- a/sentry-delayed_job/lib/sentry/delayed_job/version.rb
+++ b/sentry-delayed_job/lib/sentry/delayed_job/version.rb
@@ -1,5 +1,5 @@
module Sentry
module DelayedJob
- VERSION = "5.17.3"
+ VERSION = "5.19.0"
end
end
diff --git a/sentry-delayed_job/sentry-delayed_job.gemspec b/sentry-delayed_job/sentry-delayed_job.gemspec
index ce952668f..e97096bcb 100644
--- a/sentry-delayed_job/sentry-delayed_job.gemspec
+++ b/sentry-delayed_job/sentry-delayed_job.gemspec
@@ -7,21 +7,27 @@ Gem::Specification.new do |spec|
spec.description = spec.summary = "A gem that provides DelayedJob integration for the Sentry error logger"
spec.email = "accounts@sentry.io"
spec.license = 'MIT'
- spec.homepage = "https://github.com/getsentry/sentry-ruby"
spec.platform = Gem::Platform::RUBY
spec.required_ruby_version = '>= 2.4'
spec.extra_rdoc_files = ["README.md", "LICENSE.txt"]
spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n")
- spec.metadata["homepage_uri"] = spec.homepage
- spec.metadata["source_code_uri"] = spec.homepage
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
+ github_root_uri = 'https://github.com/getsentry/sentry-ruby'
+ spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}"
+
+ spec.metadata = {
+ "homepage_uri" => spec.homepage,
+ "source_code_uri" => spec.homepage,
+ "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md",
+ "bug_tracker_uri" => "#{github_root_uri}/issues",
+ "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}"
+ }
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
- spec.add_dependency "sentry-ruby", "~> 5.17.3"
+ spec.add_dependency "sentry-ruby", "~> 5.19.0"
spec.add_dependency "delayed_job", ">= 4.0"
end
diff --git a/sentry-delayed_job/spec/sentry/delayed_job_spec.rb b/sentry-delayed_job/spec/sentry/delayed_job_spec.rb
index 7c7440452..ac2d31fb1 100644
--- a/sentry-delayed_job/spec/sentry/delayed_job_spec.rb
+++ b/sentry-delayed_job/spec/sentry/delayed_job_spec.rb
@@ -383,6 +383,7 @@ def perform
expect(transaction.contexts.dig(:trace, :span_id)).to be_a(String)
expect(transaction.contexts.dig(:trace, :status)).to eq("ok")
expect(transaction.contexts.dig(:trace, :op)).to eq("queue.delayed_job")
+ expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.queue.delayed_job")
end
it "records transaction with exception" do
diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb
index d35bb7025..71152ad29 100644
--- a/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb
+++ b/sentry-opentelemetry/lib/sentry/opentelemetry/span_processor.rb
@@ -11,6 +11,7 @@ class SpanProcessor < ::OpenTelemetry::SDK::Trace::SpanProcessor
SEMANTIC_CONVENTIONS = ::OpenTelemetry::SemanticConventions::Trace
INTERNAL_SPAN_KINDS = %i[client internal]
+ SPAN_ORIGIN = "auto.otel"
# The mapping from otel span ids to sentry spans
# @return [Hash]
@@ -34,7 +35,8 @@ def on_start(otel_span, parent_context)
sentry_parent_span.start_child(
span_id: trace_data.span_id,
description: otel_span.name,
- start_timestamp: otel_span.start_timestamp / 1e9
+ start_timestamp: otel_span.start_timestamp / 1e9,
+ origin: SPAN_ORIGIN
)
else
options = {
@@ -45,7 +47,8 @@ def on_start(otel_span, parent_context)
parent_span_id: trace_data.parent_span_id,
parent_sampled: trace_data.parent_sampled,
baggage: trace_data.baggage,
- start_timestamp: otel_span.start_timestamp / 1e9
+ start_timestamp: otel_span.start_timestamp / 1e9,
+ origin: SPAN_ORIGIN
}
Sentry.start_transaction(**options)
@@ -80,7 +83,7 @@ def from_sentry_sdk?(otel_span)
dsn = Sentry.configuration.dsn
return false unless dsn
- if otel_span.name.start_with?("HTTP")
+ if otel_span.name.start_with?("HTTP") || otel_span.name == "connect"
# only check client requests, connects are sometimes internal
return false unless INTERNAL_SPAN_KINDS.include?(otel_span.kind)
diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb
index ec7f7366e..ddc0cbe67 100644
--- a/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb
+++ b/sentry-opentelemetry/lib/sentry/opentelemetry/version.rb
@@ -2,6 +2,6 @@
module Sentry
module OpenTelemetry
- VERSION = "5.17.3"
+ VERSION = "5.19.0"
end
end
diff --git a/sentry-opentelemetry/sentry-opentelemetry.gemspec b/sentry-opentelemetry/sentry-opentelemetry.gemspec
index 5ff363e99..aaee606be 100644
--- a/sentry-opentelemetry/sentry-opentelemetry.gemspec
+++ b/sentry-opentelemetry/sentry-opentelemetry.gemspec
@@ -9,21 +9,27 @@ Gem::Specification.new do |spec|
spec.description = spec.summary = "A gem that provides OpenTelemetry integration for the Sentry error logger"
spec.email = "accounts@sentry.io"
spec.license = 'MIT'
- spec.homepage = "https://github.com/getsentry/sentry-ruby"
spec.platform = Gem::Platform::RUBY
spec.required_ruby_version = '>= 2.4'
spec.extra_rdoc_files = ["README.md", "LICENSE.txt"]
spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n")
- spec.metadata["homepage_uri"] = spec.homepage
- spec.metadata["source_code_uri"] = spec.homepage
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
+ github_root_uri = 'https://github.com/getsentry/sentry-ruby'
+ spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}"
+
+ spec.metadata = {
+ "homepage_uri" => spec.homepage,
+ "source_code_uri" => spec.homepage,
+ "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md",
+ "bug_tracker_uri" => "#{github_root_uri}/issues",
+ "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}"
+ }
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
- spec.add_dependency "sentry-ruby", "~> 5.17.3"
+ spec.add_dependency "sentry-ruby", "~> 5.19.0"
spec.add_dependency "opentelemetry-sdk", "~> 1.0"
end
diff --git a/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb
index e90179498..0d5626730 100644
--- a/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb
+++ b/sentry-opentelemetry/spec/sentry/opentelemetry/span_processor_spec.rb
@@ -64,6 +64,18 @@
tracer.start_span('HTTP POST', with_parent: root_parent_context, attributes: attributes, kind: :client)
end
+ let(:child_internal_span_connect) do
+ attributes = {
+ 'http.method' => 'POST',
+ 'http.scheme' => 'https',
+ 'http.target' => '/api/5434472/envelope/',
+ 'net.peer.name' => 'sentry.localdomain',
+ 'net.peer.port' => 443
+ }
+
+ tracer.start_span('connect', with_parent: root_parent_context, attributes: attributes, kind: :internal)
+ end
+
before do
perform_basic_setup
perform_otel_setup
@@ -92,6 +104,7 @@
expect(event.contexts).to include(:trace)
expect(event.contexts[:trace][:trace_id]).to eq(root_span.context.hex_trace_id)
expect(event.contexts[:trace][:span_id]).to eq(root_span.context.hex_span_id)
+ expect(event.contexts[:trace][:origin]).to eq('auto.otel')
end
end
end
@@ -134,6 +147,7 @@
expect(transaction.span_id).to eq(span_id)
expect(transaction.trace_id).to eq(trace_id)
expect(transaction.start_timestamp).to eq(root_span.start_timestamp / 1e9)
+ expect(transaction.origin).to eq('auto.otel')
expect(transaction.parent_span_id).to eq(nil)
expect(transaction.parent_sampled).to eq(nil)
@@ -151,6 +165,11 @@
subject.on_start(child_internal_span, root_parent_context)
end
+ it 'noops on `connect` requests' do
+ expect(transaction).not_to receive(:start_child)
+ subject.on_start(child_internal_span_connect, root_parent_context)
+ end
+
it 'starts a sentry child span on otel child span' do
expect(transaction).to receive(:start_child).and_call_original
subject.on_start(child_db_span, root_parent_context)
@@ -168,6 +187,7 @@
expect(sentry_span.trace_id).to eq(trace_id)
expect(sentry_span.description).to eq(child_db_span.name)
expect(sentry_span.start_timestamp).to eq(child_db_span.start_timestamp / 1e9)
+ expect(sentry_span.origin).to eq('auto.otel')
end
end
end
@@ -215,6 +235,7 @@
subject.on_finish(finished_db_span)
expect(sentry_span.op).to eq('db')
+ expect(sentry_span.origin).to eq('auto.otel')
expect(sentry_span.description).to eq(finished_db_span.attributes['db.statement'])
expect(sentry_span.data).to include(finished_db_span.attributes)
expect(sentry_span.data).to include({ 'otel.kind' => finished_db_span.kind })
@@ -235,6 +256,7 @@
subject.on_finish(finished_http_span)
expect(sentry_span.op).to eq('http.client')
+ expect(sentry_span.origin).to eq('auto.otel')
expect(sentry_span.description).to eq('GET www.google.com/search')
expect(sentry_span.data).to include(finished_http_span.attributes)
expect(sentry_span.data).to include({ 'otel.kind' => finished_http_span.kind })
@@ -259,6 +281,7 @@
subject.on_finish(finished_root_span)
expect(transaction.op).to eq('http.server')
+ expect(transaction.origin).to eq('auto.otel')
expect(transaction.name).to eq(finished_root_span.name)
expect(transaction.status).to eq('ok')
expect(transaction.contexts[:otel]).to eq({
diff --git a/sentry-rails/Gemfile b/sentry-rails/Gemfile
index fd2ce76cb..8449f342a 100644
--- a/sentry-rails/Gemfile
+++ b/sentry-rails/Gemfile
@@ -17,8 +17,11 @@ rails_version = Gem::Version.new(rails_version)
if rails_version < Gem::Version.new("6.0.0")
gem "sqlite3", "~> 1.3.0", platform: :ruby
else
- # 1.7.0 dropped support for ruby < 3.0, remove later after upgrading craft setup
- gem "sqlite3", "1.6.9", platform: :ruby
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0")
+ gem "sqlite3", "~> 1.7.3", platform: :ruby
+ else
+ gem "sqlite3", "~> 1.6.9", platform: :ruby
+ end
end
if rails_version >= Gem::Version.new("7.2.0.alpha")
diff --git a/sentry-rails/lib/generators/sentry_generator.rb b/sentry-rails/lib/generators/sentry_generator.rb
new file mode 100644
index 000000000..de694e40d
--- /dev/null
+++ b/sentry-rails/lib/generators/sentry_generator.rb
@@ -0,0 +1,29 @@
+require "rails/generators/base"
+
+class SentryGenerator < ::Rails::Generators::Base
+ class_option :dsn, type: :string, desc: "Sentry DSN"
+
+ class_option :inject_meta, type: :boolean, default: true, desc: "Inject meta tag into layout"
+
+ def copy_initializer_file
+ dsn = options[:dsn] ? "'#{options[:dsn]}'" : "ENV['SENTRY_DSN']"
+
+ create_file "config/initializers/sentry.rb", <<~RUBY
+ # frozen_string_literal: true
+
+ Sentry.init do |config|
+ config.breadcrumbs_logger = [:active_support_logger]
+ config.dsn = #{dsn}
+ config.enable_tracing = true
+ end
+ RUBY
+ end
+
+ def inject_code_into_layout
+ return unless options[:inject_meta]
+
+ inject_into_file "app/views/layouts/application.html.erb", before: "\n" do
+ " <%= Sentry.get_trace_propagation_meta.html_safe %>\n "
+ end
+ end
+end
diff --git a/sentry-rails/lib/sentry/rails/action_cable.rb b/sentry-rails/lib/sentry/rails/action_cable.rb
index 155377466..06833a68b 100644
--- a/sentry-rails/lib/sentry/rails/action_cable.rb
+++ b/sentry-rails/lib/sentry/rails/action_cable.rb
@@ -3,6 +3,7 @@ module Rails
module ActionCableExtensions
class ErrorHandler
OP_NAME = "websocket.server".freeze
+ SPAN_ORIGIN = "auto.http.rails.actioncable"
class << self
def capture(connection, transaction_name:, extra_context: nil, &block)
@@ -33,7 +34,13 @@ def capture(connection, transaction_name:, extra_context: nil, &block)
end
def start_transaction(env, scope)
- options = { name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME }
+ options = {
+ name: scope.transaction_name,
+ source: scope.transaction_source,
+ op: OP_NAME,
+ origin: SPAN_ORIGIN
+ }
+
transaction = Sentry.continue_trace(env, **options)
Sentry.start_transaction(transaction: transaction, **options)
end
diff --git a/sentry-rails/lib/sentry/rails/active_job.rb b/sentry-rails/lib/sentry/rails/active_job.rb
index cf0c7ca1a..afdf87cd7 100644
--- a/sentry-rails/lib/sentry/rails/active_job.rb
+++ b/sentry-rails/lib/sentry/rails/active_job.rb
@@ -17,6 +17,7 @@ def already_supported_by_sentry_integration?
class SentryReporter
OP_NAME = "queue.active_job".freeze
+ SPAN_ORIGIN = "auto.queue.active_job".freeze
class << self
def record(job, &block)
@@ -27,7 +28,12 @@ def record(job, &block)
if job.is_a?(::Sentry::SendEventJob)
nil
else
- Sentry.start_transaction(name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME)
+ Sentry.start_transaction(
+ name: scope.transaction_name,
+ source: scope.transaction_source,
+ op: OP_NAME,
+ origin: SPAN_ORIGIN
+ )
end
scope.set_span(transaction) if transaction
diff --git a/sentry-rails/lib/sentry/rails/capture_exceptions.rb b/sentry-rails/lib/sentry/rails/capture_exceptions.rb
index 8d93784ed..d52456081 100644
--- a/sentry-rails/lib/sentry/rails/capture_exceptions.rb
+++ b/sentry-rails/lib/sentry/rails/capture_exceptions.rb
@@ -2,6 +2,7 @@ module Sentry
module Rails
class CaptureExceptions < Sentry::Rack::CaptureExceptions
RAILS_7_1 = Gem::Version.new(::Rails.version) >= Gem::Version.new("7.1.0.alpha")
+ SPAN_ORIGIN = 'auto.http.rails'.freeze
def initialize(_)
super
@@ -32,7 +33,12 @@ def capture_exception(exception, env)
end
def start_transaction(env, scope)
- options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op }
+ options = {
+ name: scope.transaction_name,
+ source: scope.transaction_source,
+ op: transaction_op,
+ origin: SPAN_ORIGIN
+ }
if @assets_regexp && scope.transaction_name.match?(@assets_regexp)
options.merge!(sampled: false)
diff --git a/sentry-rails/lib/sentry/rails/configuration.rb b/sentry-rails/lib/sentry/rails/configuration.rb
index ce385408c..883700142 100644
--- a/sentry-rails/lib/sentry/rails/configuration.rb
+++ b/sentry-rails/lib/sentry/rails/configuration.rb
@@ -126,6 +126,14 @@ class Configuration
attr_accessor :tracing_subscribers
+ # When the ActiveRecordSubscriber is enabled, capture the source location of the query in the span data.
+ # This is enabled by default, but can be disabled by setting this to false.
+ attr_accessor :enable_db_query_source
+
+ # The threshold in milliseconds for the ActiveRecordSubscriber to capture the source location of the query
+ # in the span data. Default is 100ms.
+ attr_accessor :db_query_source_threshold_ms
+
# sentry-rails by default skips asset request' transactions by checking if the path matches
#
# ```rb
@@ -157,6 +165,8 @@ def initialize
Sentry::Rails::Tracing::ActiveRecordSubscriber,
Sentry::Rails::Tracing::ActiveStorageSubscriber
])
+ @enable_db_query_source = true
+ @db_query_source_threshold_ms = 100
@active_support_logger_subscription_items = Sentry::Rails::ACTIVE_SUPPORT_LOGGER_SUBSCRIPTION_ITEMS_DEFAULT.dup
end
end
diff --git a/sentry-rails/lib/sentry/rails/controller_transaction.rb b/sentry-rails/lib/sentry/rails/controller_transaction.rb
index 72f31e6c1..b85506a3d 100644
--- a/sentry-rails/lib/sentry/rails/controller_transaction.rb
+++ b/sentry-rails/lib/sentry/rails/controller_transaction.rb
@@ -1,6 +1,8 @@
module Sentry
module Rails
module ControllerTransaction
+ SPAN_ORIGIN = 'auto.view.rails'.freeze
+
def self.included(base)
base.prepend_around_action(:sentry_around_action)
end
@@ -11,7 +13,7 @@ def sentry_around_action
if Sentry.initialized?
transaction_name = "#{self.class}##{action_name}"
Sentry.get_current_scope.set_transaction_name(transaction_name, source: :view)
- Sentry.with_child_span(op: "view.process_action.action_controller", description: transaction_name) do |child_span|
+ Sentry.with_child_span(op: "view.process_action.action_controller", description: transaction_name, origin: SPAN_ORIGIN) do |child_span|
if child_span
begin
result = yield
diff --git a/sentry-rails/lib/sentry/rails/railtie.rb b/sentry-rails/lib/sentry/rails/railtie.rb
index ef09851c5..44dd450dc 100644
--- a/sentry-rails/lib/sentry/rails/railtie.rb
+++ b/sentry-rails/lib/sentry/rails/railtie.rb
@@ -12,7 +12,7 @@ class Railtie < ::Rails::Railtie
app.config.middleware.insert_after ActionDispatch::DebugExceptions, Sentry::Rails::RescuedExceptionInterceptor
end
- # because the extension works by registering the around_perform callcack, it should always be ran
+ # because the extension works by registering the around_perform callback, it should always be run
# before the application is eager-loaded (before user's jobs register their own callbacks)
# See https://github.com/getsentry/sentry-ruby/issues/1249#issuecomment-853871871 for the detail explanation
initializer "sentry.extend_active_job", before: :eager_load! do |app|
diff --git a/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb
index 8c3ec5ddf..11e9eadf2 100644
--- a/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb
+++ b/sentry-rails/lib/sentry/rails/tracing/action_controller_subscriber.rb
@@ -9,6 +9,7 @@ class ActionControllerSubscriber < AbstractSubscriber
EVENT_NAMES = ["process_action.action_controller"].freeze
OP_NAME = "view.process_action.action_controller".freeze
+ SPAN_ORIGIN = "auto.view.rails".freeze
def self.subscribe!
Sentry.logger.warn <<~MSG
@@ -22,6 +23,7 @@ def self.subscribe!
record_on_current_span(
op: OP_NAME,
+ origin: SPAN_ORIGIN,
start_timestamp: payload[START_TIMESTAMP_NAME],
description: "#{controller}##{action}",
duration: duration
diff --git a/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb
index 588af1374..baed2c7e5 100644
--- a/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb
+++ b/sentry-rails/lib/sentry/rails/tracing/action_view_subscriber.rb
@@ -6,10 +6,17 @@ module Tracing
class ActionViewSubscriber < AbstractSubscriber
EVENT_NAMES = ["render_template.action_view"].freeze
SPAN_PREFIX = "template.".freeze
+ SPAN_ORIGIN = "auto.template.rails".freeze
def self.subscribe!
subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload|
- record_on_current_span(op: SPAN_PREFIX + event_name, start_timestamp: payload[START_TIMESTAMP_NAME], description: payload[:identifier], duration: duration)
+ record_on_current_span(
+ op: SPAN_PREFIX + event_name,
+ origin: SPAN_ORIGIN,
+ start_timestamp: payload[START_TIMESTAMP_NAME],
+ description: payload[:identifier],
+ duration: duration
+ )
end
end
end
diff --git a/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb
index ab25273a9..480e7011a 100644
--- a/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb
+++ b/sentry-rails/lib/sentry/rails/tracing/active_record_subscriber.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "sentry/rails/tracing/abstract_subscriber"
module Sentry
@@ -5,41 +7,95 @@ module Rails
module Tracing
class ActiveRecordSubscriber < AbstractSubscriber
EVENT_NAMES = ["sql.active_record"].freeze
- SPAN_PREFIX = "db.".freeze
+ SPAN_PREFIX = "db."
+ SPAN_ORIGIN = "auto.db.rails"
EXCLUDED_EVENTS = ["SCHEMA", "TRANSACTION"].freeze
- def self.subscribe!
- subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload|
- next if EXCLUDED_EVENTS.include? payload[:name]
+ SUPPORT_SOURCE_LOCATION = ActiveSupport::BacktraceCleaner.method_defined?(:clean_frame)
- record_on_current_span(op: SPAN_PREFIX + event_name, start_timestamp: payload[START_TIMESTAMP_NAME], description: payload[:sql], duration: duration) do |span|
- span.set_tag(:cached, true) if payload.fetch(:cached, false) # cached key is only set for hits in the QueryCache, from Rails 5.1
+ if SUPPORT_SOURCE_LOCATION
+ class_attribute :backtrace_cleaner, default: (ActiveSupport::BacktraceCleaner.new.tap do |cleaner|
+ cleaner.add_silencer { |line| line.include?("sentry-ruby/lib") || line.include?("sentry-rails/lib") }
+ end)
+ end
- connection = payload[:connection]
+ class << self
+ def subscribe!
+ record_query_source = SUPPORT_SOURCE_LOCATION && Sentry.configuration.rails.enable_db_query_source
+ query_source_threshold = Sentry.configuration.rails.db_query_source_threshold_ms
- if payload[:connection_id]
- span.set_data(:connection_id, payload[:connection_id])
+ subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload|
+ next if EXCLUDED_EVENTS.include? payload[:name]
- # we fallback to the base connection on rails < 6.0.0 since the payload doesn't have it
- connection ||= ActiveRecord::Base.connection_pool.connections.find { |conn| conn.object_id == payload[:connection_id] }
- end
+ record_on_current_span(
+ op: SPAN_PREFIX + event_name,
+ origin: SPAN_ORIGIN,
+ start_timestamp: payload[START_TIMESTAMP_NAME],
+ description: payload[:sql],
+ duration: duration
+ ) do |span|
+ span.set_tag(:cached, true) if payload.fetch(:cached, false) # cached key is only set for hits in the QueryCache, from Rails 5.1
- next unless connection
+ connection = payload[:connection]
- db_config =
- if connection.pool.respond_to?(:db_config)
- connection.pool.db_config.configuration_hash
- elsif connection.pool.respond_to?(:spec)
- connection.pool.spec.config
+ if payload[:connection_id]
+ span.set_data(:connection_id, payload[:connection_id])
+
+ # we fallback to the base connection on rails < 6.0.0 since the payload doesn't have it
+ connection ||= ActiveRecord::Base.connection_pool.connections.find { |conn| conn.object_id == payload[:connection_id] }
end
- next unless db_config
+ next unless connection
+
+ db_config =
+ if connection.pool.respond_to?(:db_config)
+ connection.pool.db_config.configuration_hash
+ elsif connection.pool.respond_to?(:spec)
+ connection.pool.spec.config
+ end
+
+ next unless db_config
+
+ span.set_data(Span::DataConventions::DB_SYSTEM, db_config[:adapter]) if db_config[:adapter]
+ span.set_data(Span::DataConventions::DB_NAME, db_config[:database]) if db_config[:database]
+ span.set_data(Span::DataConventions::SERVER_ADDRESS, db_config[:host]) if db_config[:host]
+ span.set_data(Span::DataConventions::SERVER_PORT, db_config[:port]) if db_config[:port]
+ span.set_data(Span::DataConventions::SERVER_SOCKET_ADDRESS, db_config[:socket]) if db_config[:socket]
+
+ next unless record_query_source
+
+ # both duration and query_source_threshold are in ms
+ next unless duration >= query_source_threshold
- span.set_data(Span::DataConventions::DB_SYSTEM, db_config[:adapter]) if db_config[:adapter]
- span.set_data(Span::DataConventions::DB_NAME, db_config[:database]) if db_config[:database]
- span.set_data(Span::DataConventions::SERVER_ADDRESS, db_config[:host]) if db_config[:host]
- span.set_data(Span::DataConventions::SERVER_PORT, db_config[:port]) if db_config[:port]
- span.set_data(Span::DataConventions::SERVER_SOCKET_ADDRESS, db_config[:socket]) if db_config[:socket]
+ source_location = query_source_location
+
+ if source_location
+ backtrace_line = Sentry::Backtrace::Line.parse(source_location)
+ span.set_data(Span::DataConventions::FILEPATH, backtrace_line.file) if backtrace_line.file
+ span.set_data(Span::DataConventions::LINENO, backtrace_line.number) if backtrace_line.number
+ span.set_data(Span::DataConventions::FUNCTION, backtrace_line.method) if backtrace_line.method
+ # Only JRuby has namespace in the backtrace
+ span.set_data(Span::DataConventions::NAMESPACE, backtrace_line.module_name) if backtrace_line.module_name
+ end
+ end
+ end
+ end
+
+ # Thread.each_caller_location is an API added in Ruby 3.2 that doesn't always collect the entire stack like
+ # Kernel#caller or #caller_locations do. See https://github.com/rails/rails/pull/49095 for more context.
+ if SUPPORT_SOURCE_LOCATION && Thread.respond_to?(:each_caller_location)
+ def query_source_location
+ Thread.each_caller_location do |location|
+ frame = backtrace_cleaner.clean_frame(location)
+ return frame if frame
+ end
+ nil
+ end
+ else
+ # Since Sentry is mostly used in production, we don't want to fallback to the slower implementation
+ # and adds potentially big overhead to the application.
+ def query_source_location
+ nil
end
end
end
diff --git a/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb b/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb
index 4a8f8a306..5d62bad22 100644
--- a/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb
+++ b/sentry-rails/lib/sentry/rails/tracing/active_storage_subscriber.rb
@@ -19,9 +19,17 @@ class ActiveStorageSubscriber < AbstractSubscriber
analyze.active_storage
].freeze
+ SPAN_ORIGIN = "auto.file.rails".freeze
+
def self.subscribe!
subscribe_to_event(EVENT_NAMES) do |event_name, duration, payload|
- record_on_current_span(op: "file.#{event_name}".freeze, start_timestamp: payload[START_TIMESTAMP_NAME], description: payload[:service], duration: duration) do |span|
+ record_on_current_span(
+ op: "file.#{event_name}".freeze,
+ origin: SPAN_ORIGIN,
+ start_timestamp: payload[START_TIMESTAMP_NAME],
+ description: payload[:service],
+ duration: duration
+ ) do |span|
payload.each do |key, value|
span.set_data(key, value) unless key == START_TIMESTAMP_NAME
end
diff --git a/sentry-rails/lib/sentry/rails/version.rb b/sentry-rails/lib/sentry/rails/version.rb
index 5cc93512e..ac6bcb23b 100644
--- a/sentry-rails/lib/sentry/rails/version.rb
+++ b/sentry-rails/lib/sentry/rails/version.rb
@@ -1,5 +1,5 @@
module Sentry
module Rails
- VERSION = "5.17.3"
+ VERSION = "5.19.0"
end
end
diff --git a/sentry-rails/sentry-rails.gemspec b/sentry-rails/sentry-rails.gemspec
index f7d61c958..7cfdd0e70 100644
--- a/sentry-rails/sentry-rails.gemspec
+++ b/sentry-rails/sentry-rails.gemspec
@@ -7,21 +7,27 @@ Gem::Specification.new do |spec|
spec.description = spec.summary = "A gem that provides Rails integration for the Sentry error logger"
spec.email = "accounts@sentry.io"
spec.license = 'MIT'
- spec.homepage = "https://github.com/getsentry/sentry-ruby"
spec.platform = Gem::Platform::RUBY
spec.required_ruby_version = '>= 2.4'
spec.extra_rdoc_files = ["README.md", "LICENSE.txt"]
spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n")
- spec.metadata["homepage_uri"] = spec.homepage
- spec.metadata["source_code_uri"] = spec.homepage
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
+ github_root_uri = 'https://github.com/getsentry/sentry-ruby'
+ spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}"
+
+ spec.metadata = {
+ "homepage_uri" => spec.homepage,
+ "source_code_uri" => spec.homepage,
+ "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md",
+ "bug_tracker_uri" => "#{github_root_uri}/issues",
+ "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}"
+ }
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
spec.add_dependency "railties", ">= 5.0"
- spec.add_dependency "sentry-ruby", "~> 5.17.3"
+ spec.add_dependency "sentry-ruby", "~> 5.19.0"
end
diff --git a/sentry-rails/spec/sentry/generator_spec.rb b/sentry-rails/spec/sentry/generator_spec.rb
new file mode 100644
index 000000000..7da9e12a7
--- /dev/null
+++ b/sentry-rails/spec/sentry/generator_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require "fileutils"
+require "rails/generators/test_case"
+require "generators/sentry_generator"
+
+RSpec.describe SentryGenerator do
+ include ::Rails::Generators::Testing::Behaviour
+ include FileUtils
+ self.destination File.expand_path('../../tmp', __dir__)
+ self.generator_class = described_class
+
+ let(:layout_file) do
+ File.join(destination_root, "app/views/layouts/application.html.erb")
+ end
+
+ before do
+ prepare_destination
+
+ FileUtils.mkdir_p(File.dirname(layout_file))
+
+ File.write(layout_file, <<~STR)
+
+
+
+ SentryTesting
+
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
+ <%= javascript_importmap_tags %>
+
+
+
+ <%= yield %>
+
+
+ STR
+ end
+
+ it "creates a initializer file" do
+ run_generator
+
+ file = File.join(destination_root, "config/initializers/sentry.rb")
+ expect(File).to exist(file)
+ content = File.read(file)
+ expect(content).to include(<<~RUBY)
+ Sentry.init do |config|
+ config.breadcrumbs_logger = [:active_support_logger]
+ config.dsn = ENV['SENTRY_DSN']
+ config.enable_tracing = true
+ end
+ RUBY
+ end
+
+ it "injects meta tag into the layout" do
+ run_generator
+
+ content = File.read(layout_file)
+
+ expect(content).to include("Sentry.get_trace_propagation_meta.html_safe")
+ end
+
+ it "doesn't inject meta tag when it's disabled" do
+ run_generator %w[--inject-meta false]
+
+ content = File.read(layout_file)
+
+ expect(content).not_to include("Sentry.get_trace_propagation_meta.html_safe")
+ end
+
+ context "with a DSN option" do
+ it "creates a initializer file with the DSN" do
+ run_generator %w[--dsn foobarbaz]
+
+ file = File.join(destination_root, "config/initializers/sentry.rb")
+ expect(File).to exist(file)
+ content = File.read(file)
+ expect(content).to include(<<~RUBY)
+ Sentry.init do |config|
+ config.breadcrumbs_logger = [:active_support_logger]
+ config.dsn = 'foobarbaz'
+ config.enable_tracing = true
+ end
+ RUBY
+ end
+ end
+end
diff --git a/sentry-rails/spec/sentry/rails/action_cable_spec.rb b/sentry-rails/spec/sentry/rails/action_cable_spec.rb
index 40db25c74..4b353adff 100644
--- a/sentry-rails/spec/sentry/rails/action_cable_spec.rb
+++ b/sentry-rails/spec/sentry/rails/action_cable_spec.rb
@@ -204,7 +204,8 @@ def disconnect
expect(transaction["contexts"]).to include(
"trace" => hash_including(
"op" => "websocket.server",
- "status" => "internal_error"
+ "status" => "internal_error",
+ "origin" => "auto.http.rails.actioncable"
)
)
end
@@ -230,7 +231,8 @@ def disconnect
expect(subscription_transaction["contexts"]).to include(
"trace" => hash_including(
"op" => "websocket.server",
- "status" => "ok"
+ "status" => "ok",
+ "origin" => "auto.http.rails.actioncable"
)
)
@@ -259,7 +261,8 @@ def disconnect
expect(action_transaction["contexts"]).to include(
"trace" => hash_including(
"op" => "websocket.server",
- "status" => "internal_error"
+ "status" => "internal_error",
+ "origin" => "auto.http.rails.actioncable"
)
)
end
@@ -281,7 +284,8 @@ def disconnect
expect(subscription_transaction["contexts"]).to include(
"trace" => hash_including(
"op" => "websocket.server",
- "status" => "ok"
+ "status" => "ok",
+ "origin" => "auto.http.rails.actioncable"
)
)
@@ -308,7 +312,8 @@ def disconnect
expect(transaction["contexts"]).to include(
"trace" => hash_including(
"op" => "websocket.server",
- "status" => "internal_error"
+ "status" => "internal_error",
+ "origin" => "auto.http.rails.actioncable"
)
)
end
diff --git a/sentry-rails/spec/sentry/rails/activejob_spec.rb b/sentry-rails/spec/sentry/rails/activejob_spec.rb
index f4d1550be..f5892d944 100644
--- a/sentry-rails/spec/sentry/rails/activejob_spec.rb
+++ b/sentry-rails/spec/sentry/rails/activejob_spec.rb
@@ -182,6 +182,7 @@ def post.to_global_id
expect(transaction.contexts.dig(:trace, :span_id)).to be_present
expect(transaction.contexts.dig(:trace, :status)).to eq("ok")
expect(transaction.contexts.dig(:trace, :op)).to eq("queue.active_job")
+ expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.queue.active_job")
expect(transaction.spans.count).to eq(1)
expect(transaction.spans.first[:op]).to eq("db.sql.active_record")
@@ -199,6 +200,7 @@ def post.to_global_id
expect(transaction.contexts.dig(:trace, :trace_id)).to be_present
expect(transaction.contexts.dig(:trace, :span_id)).to be_present
expect(transaction.contexts.dig(:trace, :status)).to eq("internal_error")
+ expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.queue.active_job")
event = transport.events.last
expect(event.transaction).to eq("FailedWithExtraJob")
diff --git a/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb
index 58852df52..f147e1b8e 100644
--- a/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb
+++ b/sentry-rails/spec/sentry/rails/tracing/action_controller_subscriber_spec.rb
@@ -36,6 +36,7 @@
span = transaction[:spans][0]
expect(span[:op]).to eq("view.process_action.action_controller")
+ expect(span[:origin]).to eq("auto.view.rails")
expect(span[:description]).to eq("HelloController#world")
expect(span[:trace_id]).to eq(transaction.dig(:contexts, :trace, :trace_id))
expect(span[:data].keys).to match_array(["http.response.status_code", :format, :method, :path, :params])
diff --git a/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb
index f6d3ff31f..aa2473312 100644
--- a/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb
+++ b/sentry-rails/spec/sentry/rails/tracing/action_view_subscriber_spec.rb
@@ -25,6 +25,7 @@
# ignore the first span, which is for controller action
span = transaction[:spans][1]
expect(span[:op]).to eq("template.render_template.action_view")
+ expect(span[:origin]).to eq("auto.template.rails")
expect(span[:description]).to match(/test_template\.html\.erb/)
expect(span[:trace_id]).to eq(transaction.dig(:contexts, :trace, :trace_id))
end
diff --git a/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb
index 4e385a374..aa29b82d2 100644
--- a/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb
+++ b/sentry-rails/spec/sentry/rails/tracing/active_record_subscriber_spec.rb
@@ -6,10 +6,15 @@
end
context "when transaction is sampled" do
+ let(:enable_db_query_source) { true }
+ let(:db_query_source_threshold_ms) { 0 }
+
before do
make_basic_app do |config|
config.traces_sample_rate = 1.0
config.rails.tracing_subscribers = [described_class]
+ config.rails.enable_db_query_source = enable_db_query_source
+ config.rails.db_query_source_threshold_ms = db_query_source_threshold_ms
end
end
@@ -29,6 +34,7 @@
span = transaction[:spans][0]
expect(span[:op]).to eq("db.sql.active_record")
+ expect(span[:origin]).to eq("auto.db.rails")
expect(span[:description]).to eq("SELECT \"posts\".* FROM \"posts\"")
expect(span[:tags].key?(:cached)).to eq(false)
expect(span[:trace_id]).to eq(transaction.dig(:contexts, :trace, :trace_id))
@@ -38,6 +44,78 @@
expect(data["db.system"]).to eq("sqlite3")
end
+ context "when query source location is avaialble", skip: RUBY_VERSION.to_f < 3.2 || Rails.version.to_f < 7.1 do
+ def foo
+ Post.all.to_a
+ end
+ query_line = __LINE__ - 2
+
+ before do
+ transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
+ Sentry.get_current_scope.set_span(transaction)
+
+ foo
+
+ transaction.finish
+ end
+
+ context "when config.rails.enable_db_query_source is false" do
+ let(:enable_db_query_source) { false }
+
+ it "doesn't record query's source location" do
+ expect(transport.events.count).to eq(1)
+
+ transaction = transport.events.first.to_hash
+ expect(transaction[:type]).to eq("transaction")
+ expect(transaction[:spans].count).to eq(1)
+
+ span = transaction[:spans][0]
+ data = span[:data]
+ expect(data["db.name"]).to include("db")
+ expect(data["code.filepath"]).to eq(nil)
+ expect(data["code.lineno"]).to eq(nil)
+ expect(data["code.function"]).to eq(nil)
+ end
+ end
+
+ context "when the query takes longer than the threshold" do
+ let(:db_query_source_threshold_ms) { 0 }
+
+ it "records query's source location" do
+ expect(transport.events.count).to eq(1)
+
+ transaction = transport.events.first.to_hash
+ expect(transaction[:type]).to eq("transaction")
+ expect(transaction[:spans].count).to eq(1)
+
+ span = transaction[:spans][0]
+ data = span[:data]
+ expect(data["code.filepath"]).to eq(__FILE__)
+ expect(data["code.lineno"]).to eq(query_line)
+ expect(data["code.function"]).to eq("foo")
+ end
+ end
+
+ context "when the query takes shorter than the threshold" do
+ let(:db_query_source_threshold_ms) { 1000 }
+
+ it "doesn't record query's source location" do
+ expect(transport.events.count).to eq(1)
+
+ transaction = transport.events.first.to_hash
+ expect(transaction[:type]).to eq("transaction")
+ expect(transaction[:spans].count).to eq(1)
+
+ span = transaction[:spans][0]
+ data = span[:data]
+ expect(data["db.name"]).to include("db")
+ expect(data["code.filepath"]).to eq(nil)
+ expect(data["code.lineno"]).to eq(nil)
+ expect(data["code.function"]).to eq(nil)
+ end
+ end
+ end
+
it "records database cached query events", skip: Rails.version.to_f < 5.1 do
transaction = Sentry::Transaction.new(sampled: true, hub: Sentry.get_current_hub)
Sentry.get_current_scope.set_span(transaction)
@@ -57,6 +135,7 @@
cached_query_span = transaction[:spans][1]
expect(cached_query_span[:op]).to eq("db.sql.active_record")
+ expect(cached_query_span[:origin]).to eq("auto.db.rails")
expect(cached_query_span[:description]).to eq("SELECT \"posts\".* FROM \"posts\"")
expect(cached_query_span[:tags]).to include({ cached: true })
diff --git a/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb b/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb
index bfacfe433..5d34db1e3 100644
--- a/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb
+++ b/sentry-rails/spec/sentry/rails/tracing/active_storage_subscriber_spec.rb
@@ -29,10 +29,13 @@
if Rails.version.to_f > 6.1
expect(analysis_transaction[:spans].count).to eq(2)
expect(analysis_transaction[:spans][0][:op]).to eq("file.service_streaming_download.active_storage")
+ expect(analysis_transaction[:spans][0][:origin]).to eq("auto.file.rails")
expect(analysis_transaction[:spans][1][:op]).to eq("file.analyze.active_storage")
+ expect(analysis_transaction[:spans][1][:origin]).to eq("auto.file.rails")
else
expect(analysis_transaction[:spans].count).to eq(1)
expect(analysis_transaction[:spans][0][:op]).to eq("file.service_streaming_download.active_storage")
+ expect(analysis_transaction[:spans][0][:origin]).to eq("auto.file.rails")
end
request_transaction = transport.events.last.to_hash
@@ -41,6 +44,7 @@
span = request_transaction[:spans][1]
expect(span[:op]).to eq("file.service_upload.active_storage")
+ expect(span[:origin]).to eq("auto.file.rails")
expect(span[:description]).to eq("Disk")
expect(span.dig(:data, :key)).to eq(p.cover.key)
expect(span[:trace_id]).to eq(request_transaction.dig(:contexts, :trace, :trace_id))
diff --git a/sentry-rails/spec/sentry/rails/tracing_spec.rb b/sentry-rails/spec/sentry/rails/tracing_spec.rb
index 719ab1794..287bd8acc 100644
--- a/sentry-rails/spec/sentry/rails/tracing_spec.rb
+++ b/sentry-rails/spec/sentry/rails/tracing_spec.rb
@@ -32,11 +32,13 @@
expect(transaction[:type]).to eq("transaction")
expect(transaction.dig(:contexts, :trace, :op)).to eq("http.server")
+ expect(transaction.dig(:contexts, :trace, :origin)).to eq("auto.http.rails")
parent_span_id = transaction.dig(:contexts, :trace, :span_id)
expect(transaction[:spans].count).to eq(2)
first_span = transaction[:spans][0]
expect(first_span[:op]).to eq("view.process_action.action_controller")
+ expect(first_span[:origin]).to eq("auto.view.rails")
expect(first_span[:description]).to eq("PostsController#index")
expect(first_span[:parent_span_id]).to eq(parent_span_id)
expect(first_span[:status]).to eq("internal_error")
@@ -44,6 +46,7 @@
second_span = transaction[:spans][1]
expect(second_span[:op]).to eq("db.sql.active_record")
+ expect(second_span[:origin]).to eq("auto.db.rails")
expect(second_span[:description]).to eq("SELECT \"posts\".* FROM \"posts\"")
expect(second_span[:parent_span_id]).to eq(first_span[:span_id])
@@ -63,19 +66,21 @@
expect(transaction[:type]).to eq("transaction")
expect(transaction.dig(:contexts, :trace, :op)).to eq("http.server")
+ expect(transaction.dig(:contexts, :trace, :origin)).to eq("auto.http.rails")
parent_span_id = transaction.dig(:contexts, :trace, :span_id)
expect(transaction[:spans].count).to eq(3)
first_span = transaction[:spans][0]
expect(first_span[:data].keys).to match_array(["http.response.status_code", :format, :method, :path, :params])
expect(first_span[:op]).to eq("view.process_action.action_controller")
+ expect(first_span[:origin]).to eq("auto.view.rails")
expect(first_span[:description]).to eq("PostsController#show")
expect(first_span[:parent_span_id]).to eq(parent_span_id)
expect(first_span[:status]).to eq("ok")
-
second_span = transaction[:spans][1]
expect(second_span[:op]).to eq("db.sql.active_record")
+ expect(second_span[:origin]).to eq("auto.db.rails")
expect(second_span[:description].squeeze("\s")).to eq(
'SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?'
)
@@ -86,6 +91,7 @@
third_span = transaction[:spans][2]
expect(third_span[:op]).to eq("template.render_template.action_view")
+ expect(third_span[:origin]).to eq("auto.template.rails")
expect(third_span[:description].squeeze("\s")).to eq("text template")
expect(third_span[:parent_span_id]).to eq(first_span[:span_id])
end
@@ -239,6 +245,7 @@
expect(transaction.timestamp).not_to be_nil
expect(transaction.contexts.dig(:trace, :status)).to eq("ok")
expect(transaction.contexts.dig(:trace, :op)).to eq("http.server")
+ expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.http.rails")
expect(transaction.spans.count).to eq(3)
# should inherit information from the external_transaction
diff --git a/sentry-resque/lib/sentry/resque.rb b/sentry-resque/lib/sentry/resque.rb
index f204e5236..84907d84f 100644
--- a/sentry-resque/lib/sentry/resque.rb
+++ b/sentry-resque/lib/sentry/resque.rb
@@ -15,6 +15,8 @@ def perform
end
class SentryReporter
+ SPAN_ORIGIN = "auto.queue.resque"
+
class << self
def record(queue, worker, payload, &block)
Sentry.with_scope do |scope|
@@ -25,7 +27,13 @@ def record(queue, worker, payload, &block)
name = contexts.dig(:"Active-Job", :job_class) || contexts.dig(:"Resque", :job_class)
scope.set_transaction_name(name, source: :task)
- transaction = Sentry.start_transaction(name: scope.transaction_name, source: scope.transaction_source, op: "queue.resque")
+ transaction = Sentry.start_transaction(
+ name: scope.transaction_name,
+ source: scope.transaction_source,
+ op: "queue.resque",
+ origin: SPAN_ORIGIN
+ )
+
scope.set_span(transaction) if transaction
yield
diff --git a/sentry-resque/lib/sentry/resque/version.rb b/sentry-resque/lib/sentry/resque/version.rb
index 0575cfb47..9e6a6ec4f 100644
--- a/sentry-resque/lib/sentry/resque/version.rb
+++ b/sentry-resque/lib/sentry/resque/version.rb
@@ -1,5 +1,5 @@
module Sentry
module Resque
- VERSION = "5.17.3"
+ VERSION = "5.19.0"
end
end
diff --git a/sentry-resque/sentry-resque.gemspec b/sentry-resque/sentry-resque.gemspec
index 6a51a680a..36d777b00 100644
--- a/sentry-resque/sentry-resque.gemspec
+++ b/sentry-resque/sentry-resque.gemspec
@@ -7,21 +7,27 @@ Gem::Specification.new do |spec|
spec.description = spec.summary = "A gem that provides Resque integration for the Sentry error logger"
spec.email = "accounts@sentry.io"
spec.license = 'MIT'
- spec.homepage = "https://github.com/getsentry/sentry-ruby"
spec.platform = Gem::Platform::RUBY
spec.required_ruby_version = '>= 2.4'
spec.extra_rdoc_files = ["README.md", "LICENSE.txt"]
spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n")
- spec.metadata["homepage_uri"] = spec.homepage
- spec.metadata["source_code_uri"] = spec.homepage
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
+ github_root_uri = 'https://github.com/getsentry/sentry-ruby'
+ spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}"
+
+ spec.metadata = {
+ "homepage_uri" => spec.homepage,
+ "source_code_uri" => spec.homepage,
+ "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md",
+ "bug_tracker_uri" => "#{github_root_uri}/issues",
+ "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}"
+ }
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
- spec.add_dependency "sentry-ruby", "~> 5.17.3"
+ spec.add_dependency "sentry-ruby", "~> 5.19.0"
spec.add_dependency "resque", ">= 1.24"
end
diff --git a/sentry-resque/spec/sentry/tracing_spec.rb b/sentry-resque/spec/sentry/tracing_spec.rb
index 01605b950..e77b84f4e 100644
--- a/sentry-resque/spec/sentry/tracing_spec.rb
+++ b/sentry-resque/spec/sentry/tracing_spec.rb
@@ -42,6 +42,7 @@ def self.perform(msg)
expect(tracing_event[:type]).to eq("transaction")
expect(tracing_event.dig(:contexts, :trace, :status)).to eq("ok")
expect(tracing_event.dig(:contexts, :trace, :op)).to eq("queue.resque")
+ expect(tracing_event.dig(:contexts, :trace, :origin)).to eq("auto.queue.resque")
end
it "records tracing events with exceptions" do
@@ -59,6 +60,7 @@ def self.perform(msg)
expect(tracing_event[:type]).to eq("transaction")
expect(tracing_event.dig(:contexts, :trace, :status)).to eq("internal_error")
expect(tracing_event.dig(:contexts, :trace, :op)).to eq("queue.resque")
+ expect(tracing_event.dig(:contexts, :trace, :origin)).to eq("auto.queue.resque")
end
context "with instrumenter :otel" do
diff --git a/sentry-ruby/Gemfile b/sentry-ruby/Gemfile
index be196db7e..c6e1db011 100644
--- a/sentry-ruby/Gemfile
+++ b/sentry-ruby/Gemfile
@@ -15,6 +15,8 @@ gem "puma"
gem "timecop"
gem "stackprof" unless RUBY_PLATFORM == "java"
+gem "graphql", ">= 2.2.6" if RUBY_VERSION.to_f >= 2.7
+
gem "benchmark-ips"
gem "benchmark_driver"
gem "benchmark-ipsa"
@@ -22,5 +24,6 @@ gem "benchmark-memory"
gem "yard", github: "lsegal/yard"
gem "webrick"
+gem "faraday"
eval_gemfile File.expand_path("../Gemfile", __dir__)
diff --git a/sentry-ruby/README.md b/sentry-ruby/README.md
index 1c82fa467..c98757a32 100644
--- a/sentry-ruby/README.md
+++ b/sentry-ruby/README.md
@@ -13,14 +13,14 @@ _Bad software is everywhere, and we're tired of it. Sentry is on a mission to he
Sentry SDK for Ruby
===========
-| current version | build | coverage | downloads |
-| --- | ----- | -------- | --------- |
-| [](https://rubygems.org/gems/sentry-ruby) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_ruby_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://rubygems.org/gems/sentry-ruby/) |
-| [](https://rubygems.org/gems/sentry-rails) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_rails_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://rubygems.org/gems/sentry-rails/) |
-| [](https://rubygems.org/gems/sentry-sidekiq) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_sidekiq_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://rubygems.org/gems/sentry-sidekiq/) |
-| [](https://rubygems.org/gems/sentry-delayed_job) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_delayed_job_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://rubygems.org/gems/sentry-delayed_job/) |
-| [](https://rubygems.org/gems/sentry-resque) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_resque_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://rubygems.org/gems/sentry-resque/) |
-| [](https://rubygems.org/gems/sentry-opentelemetry) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_opentelemetry_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://rubygems.org/gems/sentry-opentelemetry/) |
+| Current version | Build | Coverage | API doc |
+| ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
+| [](https://rubygems.org/gems/sentry-ruby) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_ruby_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://www.rubydoc.info/gems/sentry-ruby) |
+| [](https://rubygems.org/gems/sentry-rails) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_rails_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://www.rubydoc.info/gems/sentry-rails) |
+| [](https://rubygems.org/gems/sentry-sidekiq) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_sidekiq_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://www.rubydoc.info/gems/sentry-sidekiq) |
+| [](https://rubygems.org/gems/sentry-delayed_job) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_delayed_job_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://www.rubydoc.info/gems/sentry-delayed_job) |
+| [](https://rubygems.org/gems/sentry-resque) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_resque_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://www.rubydoc.info/gems/sentry-resque) |
+| [](https://rubygems.org/gems/sentry-opentelemetry) | [](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_opentelemetry_test.yml) | [](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [](https://www.rubydoc.info/gems/sentry-opentelemetry) |
@@ -103,6 +103,16 @@ To learn more about sampling transactions, please visit the [official documentat
* [](https://docs.sentry.io/platforms/ruby/)
* [](https://forum.sentry.io/c/sdks)
-* [](https://discord.gg/PXa5Apfe7K)
+* [](https://discord.gg/PXa5Apfe7K)
* [](https://stackoverflow.com/questions/tagged/sentry)
* [](https://twitter.com/intent/follow?screen_name=getsentry)
+
+## Contributing to the SDK
+
+Please make sure to read the [CONTRIBUTING.md](https://github.com/getsentry/sentry-ruby/blob/master/CONTRIBUTING.md) before making a pull request.
+
+Thanks to everyone who has contributed to this project so far.
+
+
+
+
diff --git a/sentry-ruby/bin/console b/sentry-ruby/bin/console
index 81e352ff7..57a532604 100755
--- a/sentry-ruby/bin/console
+++ b/sentry-ruby/bin/console
@@ -1,6 +1,7 @@
#!/usr/bin/env ruby
require "bundler/setup"
+require "debug"
require "sentry-ruby"
# You can add fixtures and/or initialization code here to make experimenting
diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb
index 439d83ce5..358a7c329 100644
--- a/sentry-ruby/lib/sentry-ruby.rb
+++ b/sentry-ruby/lib/sentry-ruby.rb
@@ -20,6 +20,7 @@
require "sentry/transaction"
require "sentry/hub"
require "sentry/background_worker"
+require "sentry/threaded_periodic_worker"
require "sentry/session_flusher"
require "sentry/backpressure_monitor"
require "sentry/cron/monitor_check_ins"
@@ -210,6 +211,13 @@ def set_context(*args)
get_current_scope.set_context(*args)
end
+ # @!method add_attachment
+ # @!macro add_attachment
+ def add_attachment(**opts)
+ return unless initialized?
+ get_current_scope.add_attachment(**opts)
+ end
+
##### Main APIs #####
# Initializes the SDK with given configuration.
@@ -550,6 +558,15 @@ def get_trace_propagation_headers
get_current_hub.get_trace_propagation_headers
end
+ # Returns the a Hash containing sentry-trace and baggage.
+ # Can be either from the currently active span or the propagation context.
+ #
+ # @return [String]
+ def get_trace_propagation_meta
+ return '' unless initialized?
+ get_current_hub.get_trace_propagation_meta
+ end
+
# Continue an incoming trace from a rack env like hash.
#
# @param env [Hash]
@@ -590,3 +607,5 @@ def utc_now
require "sentry/net/http"
require "sentry/redis"
require "sentry/puma"
+require "sentry/graphql"
+require "sentry/faraday"
diff --git a/sentry-ruby/lib/sentry/attachment.rb b/sentry-ruby/lib/sentry/attachment.rb
new file mode 100644
index 000000000..f40971599
--- /dev/null
+++ b/sentry-ruby/lib/sentry/attachment.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Sentry
+ class Attachment
+ PathNotFoundError = Class.new(StandardError)
+
+ attr_reader :bytes, :filename, :path, :content_type
+
+ def initialize(bytes: nil, filename: nil, content_type: nil, path: nil)
+ @bytes = bytes
+ @filename = infer_filename(filename, path)
+ @path = path
+ @content_type = content_type
+ end
+
+ def to_envelope_headers
+ { type: 'attachment', filename: filename, content_type: content_type, length: payload.bytesize }
+ end
+
+ def payload
+ @payload ||= if bytes
+ bytes
+ else
+ File.binread(path)
+ end
+ rescue Errno::ENOENT
+ raise PathNotFoundError, "Failed to read attachment file, file not found: #{path}"
+ end
+
+ private
+
+ def infer_filename(filename, path)
+ return filename if filename
+
+ if path
+ File.basename(path)
+ else
+ raise ArgumentError, "filename or path is required"
+ end
+ end
+ end
+end
diff --git a/sentry-ruby/lib/sentry/backpressure_monitor.rb b/sentry-ruby/lib/sentry/backpressure_monitor.rb
index 4b8695a5a..b3f5652c7 100644
--- a/sentry-ruby/lib/sentry/backpressure_monitor.rb
+++ b/sentry-ruby/lib/sentry/backpressure_monitor.rb
@@ -1,19 +1,13 @@
# frozen_string_literal: true
module Sentry
- class BackpressureMonitor
- include LoggingHelper
-
+ class BackpressureMonitor < ThreadedPeriodicWorker
DEFAULT_INTERVAL = 10
MAX_DOWNSAMPLE_FACTOR = 10
def initialize(configuration, client, interval: DEFAULT_INTERVAL)
- @interval = interval
+ super(configuration.logger, interval)
@client = client
- @logger = configuration.logger
-
- @thread = nil
- @exited = false
@healthy = true
@downsample_factor = 0
@@ -47,29 +41,5 @@ def set_downsample_factor
log_debug("[BackpressureMonitor] health check negative, downsampling with a factor of #{@downsample_factor}")
end
end
-
- def kill
- log_debug("[BackpressureMonitor] killing monitor")
-
- @exited = true
- @thread&.kill
- end
-
- private
-
- def ensure_thread
- return if @exited
- return if @thread&.alive?
-
- @thread = Thread.new do
- loop do
- sleep(@interval)
- run
- end
- end
- rescue ThreadError
- log_debug("[BackpressureMonitor] Thread creation failed")
- @exited = true
- end
end
end
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/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb
index a44fa21a1..480172a88 100644
--- a/sentry-ruby/lib/sentry/configuration.rb
+++ b/sentry-ruby/lib/sentry/configuration.rb
@@ -351,7 +351,7 @@ def add_post_initialization_callback(&block)
def initialize
self.app_dirs_pattern = nil
self.debug = false
- self.background_worker_threads = Concurrent.processor_count
+ self.background_worker_threads = (processor_count / 2.0).ceil
self.background_worker_max_queue = BackgroundWorker::DEFAULT_MAX_QUEUE
self.backtrace_cleanup_callback = nil
self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
@@ -654,5 +654,10 @@ def run_post_initialization_callbacks
instance_eval(&hook)
end
end
+
+ def processor_count
+ available_processor_count = Concurrent.available_processor_count if Concurrent.respond_to?(:available_processor_count)
+ available_processor_count || Concurrent.processor_count
+ end
end
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/event.rb b/sentry-ruby/lib/sentry/event.rb
index 97777a1c9..7fbd6172c 100644
--- a/sentry-ruby/lib/sentry/event.rb
+++ b/sentry-ruby/lib/sentry/event.rb
@@ -42,6 +42,9 @@ class Event
# @return [Hash, nil]
attr_accessor :dynamic_sampling_context
+ # @return [Array]
+ attr_accessor :attachments
+
# @param configuration [Configuration]
# @param integration_meta [Hash, nil]
# @param message [String, nil]
@@ -57,6 +60,7 @@ def initialize(configuration:, integration_meta: nil, message: nil)
@extra = {}
@contexts = {}
@tags = {}
+ @attachments = []
@fingerprint = []
@dynamic_sampling_context = nil
@@ -104,9 +108,7 @@ def rack_env=(env)
unless request || env.empty?
add_request_interface(env)
- if @send_default_pii
- user[:ip_address] = calculate_real_ip_from_rack(env)
- end
+ user[:ip_address] ||= calculate_real_ip_from_rack(env) if @send_default_pii
if request_id = Utils::RequestId.read_from(env)
tags[:request_id] = request_id
diff --git a/sentry-ruby/lib/sentry/faraday.rb b/sentry-ruby/lib/sentry/faraday.rb
new file mode 100644
index 000000000..05d5f45bd
--- /dev/null
+++ b/sentry-ruby/lib/sentry/faraday.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Sentry
+ module Faraday
+ OP_NAME = "http.client"
+
+ module Connection
+ # Since there's no way to preconfigure Faraday connections and add our instrumentation
+ # by default, we need to extend the connection constructor and do it there
+ #
+ # @see https://lostisland.github.io/faraday/#/customization/index?id=configuration
+ def initialize(url = nil, options = nil)
+ super
+
+ # Ensure that we attach instrumentation only if the adapter is not net/http
+ # because if is is, then the net/http instrumentation will take care of it
+ if builder.adapter.name != "Faraday::Adapter::NetHttp"
+ # Make sure that it's going to be the first middleware so that it can capture
+ # the entire request processing involving other middlewares
+ builder.insert(0, ::Faraday::Request::Instrumentation, name: OP_NAME, instrumenter: Instrumenter.new)
+ end
+ end
+ end
+
+ class Instrumenter
+ SPAN_ORIGIN = "auto.http.faraday"
+ BREADCRUMB_CATEGORY = "http"
+
+ include Utils::HttpTracing
+
+ def instrument(op_name, env, &block)
+ return block.call unless Sentry.initialized?
+
+ Sentry.with_child_span(op: op_name, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span|
+ request_info = extract_request_info(env)
+
+ if propagate_trace?(request_info[:url])
+ set_propagation_headers(env[:request_headers])
+ end
+
+ res = block.call
+ response_status = res.status
+
+ if record_sentry_breadcrumb?
+ record_sentry_breadcrumb(request_info, response_status)
+ end
+
+ if sentry_span
+ set_span_info(sentry_span, request_info, response_status)
+ end
+
+ res
+ end
+ end
+
+ private
+
+ def extract_request_info(env)
+ url = env[:url].scheme + "://" + env[:url].host + env[:url].path
+ result = { method: env[:method].to_s.upcase, url: url }
+
+ if Sentry.configuration.send_default_pii
+ result[:query] = env[:url].query
+ result[:body] = env[:body]
+ end
+
+ result
+ end
+ end
+ end
+end
+
+Sentry.register_patch(:faraday) do
+ if defined?(::Faraday)
+ ::Faraday::Connection.prepend(Sentry::Faraday::Connection)
+ end
+end
diff --git a/sentry-ruby/lib/sentry/graphql.rb b/sentry-ruby/lib/sentry/graphql.rb
new file mode 100644
index 000000000..17bb792c6
--- /dev/null
+++ b/sentry-ruby/lib/sentry/graphql.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+Sentry.register_patch(:graphql) do |config|
+ if defined?(::GraphQL::Schema) && defined?(::GraphQL::Tracing::SentryTrace) && ::GraphQL::Schema.respond_to?(:trace_with)
+ ::GraphQL::Schema.trace_with(::GraphQL::Tracing::SentryTrace, set_transaction_name: true)
+ else
+ config.logger.warn(Sentry::LOGGER_PROGNAME) { 'You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile.' }
+ end
+end
diff --git a/sentry-ruby/lib/sentry/hub.rb b/sentry-ruby/lib/sentry/hub.rb
index a2e82445f..66d0b379a 100644
--- a/sentry-ruby/lib/sentry/hub.rb
+++ b/sentry-ruby/lib/sentry/hub.rb
@@ -193,7 +193,14 @@ def capture_event(event, **options, &block)
elsif custom_scope = options[:scope]
scope.update_from_scope(custom_scope)
elsif !options.empty?
- scope.update_from_options(**options)
+ unsupported_option_keys = scope.update_from_options(**options)
+
+ unless unsupported_option_keys.empty?
+ configuration.log_debug <<~MSG
+ Options #{unsupported_option_keys} are not supported and will not be applied to the event.
+ You may want to set them under the `extra` option.
+ MSG
+ end
end
event = current_client.capture_event(event, scope, hint)
@@ -279,6 +286,12 @@ def get_trace_propagation_headers
headers
end
+ def get_trace_propagation_meta
+ get_trace_propagation_headers.map do |k, v|
+ ""
+ end.join("\n")
+ end
+
def continue_trace(env, **options)
configure_scope { |s| s.generate_propagation_context(env) }
diff --git a/sentry-ruby/lib/sentry/metrics.rb b/sentry-ruby/lib/sentry/metrics.rb
index e67fc769f..99bd9f7f1 100644
--- a/sentry-ruby/lib/sentry/metrics.rb
+++ b/sentry-ruby/lib/sentry/metrics.rb
@@ -15,6 +15,7 @@ module Metrics
FRACTIONAL_UNITS = %w[ratio percent]
OP_NAME = 'metric.timing'
+ SPAN_ORIGIN = 'auto.metric.timing'
class << self
def increment(key, value = 1.0, unit: 'none', tags: {}, timestamp: nil)
@@ -37,7 +38,7 @@ def timing(key, unit: 'second', tags: {}, timestamp: nil, &block)
return unless block_given?
return yield unless DURATION_UNITS.include?(unit)
- result, value = Sentry.with_child_span(op: OP_NAME, description: key) do |span|
+ result, value = Sentry.with_child_span(op: OP_NAME, description: key, origin: SPAN_ORIGIN) do |span|
tags.each { |k, v| span.set_tag(k, v.is_a?(Array) ? v.join(', ') : v.to_s) } if span
start = Timing.send(unit.to_sym)
diff --git a/sentry-ruby/lib/sentry/metrics/aggregator.rb b/sentry-ruby/lib/sentry/metrics/aggregator.rb
index e02def2c0..45ffbd687 100644
--- a/sentry-ruby/lib/sentry/metrics/aggregator.rb
+++ b/sentry-ruby/lib/sentry/metrics/aggregator.rb
@@ -2,9 +2,7 @@
module Sentry
module Metrics
- class Aggregator
- include LoggingHelper
-
+ class Aggregator < ThreadedPeriodicWorker
FLUSH_INTERVAL = 5
ROLLUP_IN_SECONDS = 10
@@ -36,8 +34,8 @@ class Aggregator
attr_reader :client, :thread, :buckets, :flush_shift, :code_locations
def initialize(configuration, client)
+ super(configuration.logger, FLUSH_INTERVAL)
@client = client
- @logger = configuration.logger
@before_emit = configuration.metrics.before_emit
@enable_code_locations = configuration.metrics.enable_code_locations
@stacktrace_builder = configuration.stacktrace_builder
@@ -46,8 +44,6 @@ def initialize(configuration, client)
@default_tags['release'] = configuration.release if configuration.release
@default_tags['environment'] = configuration.environment if configuration.environment
- @thread = nil
- @exited = false
@mutex = Mutex.new
# a nested hash of timestamp -> bucket keys -> Metric instance
@@ -120,34 +116,10 @@ def flush(force: false)
@client.capture_envelope(envelope)
end
- def kill
- log_debug('[Metrics::Aggregator] killing thread')
-
- @exited = true
- @thread&.kill
- end
+ alias_method :run, :flush
private
- def ensure_thread
- return false if @exited
- return true if @thread&.alive?
-
- @thread = Thread.new do
- loop do
- # TODO-neel-metrics use event for force flush later
- sleep(FLUSH_INTERVAL)
- flush
- end
- end
-
- true
- rescue ThreadError
- log_debug('[Metrics::Aggregator] thread creation failed')
- @exited = true
- false
- end
-
# important to sort for key consistency
def serialize_tags(tags)
tags.flat_map do |k, v|
diff --git a/sentry-ruby/lib/sentry/net/http.rb b/sentry-ruby/lib/sentry/net/http.rb
index 9480ec081..c769b4c3d 100644
--- a/sentry-ruby/lib/sentry/net/http.rb
+++ b/sentry-ruby/lib/sentry/net/http.rb
@@ -2,12 +2,16 @@
require "net/http"
require "resolv"
+require "sentry/utils/http_tracing"
module Sentry
# @api private
module Net
module HTTP
+ include Utils::HttpTracing
+
OP_NAME = "http.client"
+ SPAN_ORIGIN = "auto.http.net_http"
BREADCRUMB_CATEGORY = "net.http"
# To explain how the entire thing works, we need to know how the original Net::HTTP#request works
@@ -20,8 +24,7 @@ module HTTP
# req['connection'] ||= 'close'
# return request(req, body, &block) # <- request will be called for the second time from the first call
# }
- # end
- # # .....
+ # end # .....
# end
# ```
#
@@ -30,47 +33,29 @@ def request(req, body = nil, &block)
return super unless started? && Sentry.initialized?
return super if from_sentry_sdk?
- Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |sentry_span|
+ Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span|
request_info = extract_request_info(req)
- if propagate_trace?(request_info[:url], Sentry.configuration)
+ if propagate_trace?(request_info[:url])
set_propagation_headers(req)
end
- super.tap do |res|
- record_sentry_breadcrumb(request_info, res)
+ res = super
+ response_status = res.code.to_i
- if sentry_span
- sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
- sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
- sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
- sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
- sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, res.code.to_i)
- end
+ if record_sentry_breadcrumb?
+ record_sentry_breadcrumb(request_info, response_status)
end
- end
- end
- private
+ if sentry_span
+ set_span_info(sentry_span, request_info, response_status)
+ end
- def set_propagation_headers(req)
- Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
+ res
+ end
end
- def record_sentry_breadcrumb(request_info, res)
- return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
-
- crumb = Sentry::Breadcrumb.new(
- level: :info,
- category: BREADCRUMB_CATEGORY,
- type: :info,
- data: {
- status: res.code.to_i,
- **request_info
- }
- )
- Sentry.add_breadcrumb(crumb)
- end
+ private
def from_sentry_sdk?
dsn = Sentry.configuration.dsn
@@ -93,12 +78,6 @@ def extract_request_info(req)
result
end
-
- def propagate_trace?(url, configuration)
- url &&
- configuration.propagate_traces &&
- configuration.trace_propagation_targets.any? { |target| url.match?(target) }
- end
end
end
end
diff --git a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb
index 99a84a0fe..653188e10 100644
--- a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb
+++ b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb
@@ -5,6 +5,7 @@ module Rack
class CaptureExceptions
ERROR_EVENT_ID_KEY = "sentry.error_event_id"
MECHANISM_TYPE = "rack"
+ SPAN_ORIGIN = "auto.http.rack"
def initialize(app)
@app = app
@@ -63,7 +64,13 @@ def capture_exception(exception, env)
end
def start_transaction(env, scope)
- options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op }
+ options = {
+ name: scope.transaction_name,
+ source: scope.transaction_source,
+ op: transaction_op,
+ origin: SPAN_ORIGIN
+ }
+
transaction = Sentry.continue_trace(env, **options)
Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
end
diff --git a/sentry-ruby/lib/sentry/redis.rb b/sentry-ruby/lib/sentry/redis.rb
index 3c3832321..050b85346 100644
--- a/sentry-ruby/lib/sentry/redis.rb
+++ b/sentry-ruby/lib/sentry/redis.rb
@@ -4,6 +4,7 @@ module Sentry
# @api private
class Redis
OP_NAME = "db.redis"
+ SPAN_ORIGIN = "auto.db.redis"
LOGGER_NAME = :redis_logger
def initialize(commands, host, port, db)
@@ -13,7 +14,7 @@ def initialize(commands, host, port, db)
def instrument
return yield unless Sentry.initialized?
- Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |span|
+ Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |span|
yield.tap do
record_breadcrumb
diff --git a/sentry-ruby/lib/sentry/scope.rb b/sentry-ruby/lib/sentry/scope.rb
index 6e78c6e04..73e873f43 100644
--- a/sentry-ruby/lib/sentry/scope.rb
+++ b/sentry-ruby/lib/sentry/scope.rb
@@ -2,6 +2,7 @@
require "sentry/breadcrumb_buffer"
require "sentry/propagation_context"
+require "sentry/attachment"
require "etc"
module Sentry
@@ -9,8 +10,8 @@ class Scope
include ArgumentCheckingHelper
ATTRIBUTES = [
- :transaction_names,
- :transaction_sources,
+ :transaction_name,
+ :transaction_source,
:contexts,
:extra,
:tags,
@@ -22,6 +23,7 @@ class Scope
:rack_env,
:span,
:session,
+ :attachments,
:propagation_context
]
@@ -55,6 +57,7 @@ def apply_to_event(event, hint = nil)
event.level = level
event.breadcrumbs = breadcrumbs
event.rack_env = rack_env if rack_env
+ event.attachments = attachments
end
if span
@@ -96,12 +99,13 @@ def dup
copy.extra = extra.deep_dup
copy.tags = tags.deep_dup
copy.user = user.deep_dup
- copy.transaction_names = transaction_names.dup
- copy.transaction_sources = transaction_sources.dup
+ copy.transaction_name = transaction_name.dup
+ copy.transaction_source = transaction_source.dup
copy.fingerprint = fingerprint.deep_dup
copy.span = span.deep_dup
copy.session = session.deep_dup
copy.propagation_context = propagation_context.deep_dup
+ copy.attachments = attachments.dup
copy
end
@@ -114,11 +118,12 @@ def update_from_scope(scope)
self.extra = scope.extra
self.tags = scope.tags
self.user = scope.user
- self.transaction_names = scope.transaction_names
- self.transaction_sources = scope.transaction_sources
+ self.transaction_name = scope.transaction_name
+ self.transaction_source = scope.transaction_source
self.fingerprint = scope.fingerprint
self.span = scope.span
self.propagation_context = scope.propagation_context
+ self.attachments = scope.attachments
end
# Updates the scope's data from the given options.
@@ -128,14 +133,17 @@ def update_from_scope(scope)
# @param user [Hash]
# @param level [String, Symbol]
# @param fingerprint [Array]
- # @return [void]
+ # @param attachments [Array]
+ # @return [Array]
def update_from_options(
contexts: nil,
extra: nil,
tags: nil,
user: nil,
level: nil,
- fingerprint: nil
+ fingerprint: nil,
+ attachments: nil,
+ **options
)
self.contexts.merge!(contexts) if contexts
self.extra.merge!(extra) if extra
@@ -143,6 +151,9 @@ def update_from_options(
self.user = user if user
self.level = level if level
self.fingerprint = fingerprint if fingerprint
+
+ # Returns unsupported option keys so we can notify users.
+ options.keys
end
# Sets the scope's rack_env attribute.
@@ -227,8 +238,8 @@ def set_level(level)
# @param transaction_name [String]
# @return [void]
def set_transaction_name(transaction_name, source: :custom)
- @transaction_names << transaction_name
- @transaction_sources << source
+ @transaction_name = transaction_name
+ @transaction_source = source
end
# Sets the currently active session on the scope.
@@ -238,20 +249,6 @@ def set_session(session)
@session = session
end
- # Returns current transaction name.
- # The "transaction" here does not refer to `Transaction` objects.
- # @return [String, nil]
- def transaction_name
- @transaction_names.last
- end
-
- # Returns current transaction source.
- # The "transaction" here does not refer to `Transaction` objects.
- # @return [String, nil]
- def transaction_source
- @transaction_sources.last
- end
-
# These are high cardinality and thus bad.
# @return [Boolean]
def transaction_source_low_quality?
@@ -293,6 +290,12 @@ def generate_propagation_context(env = nil)
@propagation_context = PropagationContext.new(self, env)
end
+ # Add a new attachment to the scope.
+ def add_attachment(**opts)
+ attachments << (attachment = Attachment.new(**opts))
+ attachment
+ end
+
protected
# for duplicating scopes internally
@@ -307,12 +310,13 @@ def set_default_value
@user = {}
@level = :error
@fingerprint = []
- @transaction_names = []
- @transaction_sources = []
+ @transaction_name = nil
+ @transaction_source = nil
@event_processors = []
@rack_env = {}
@span = nil
@session = nil
+ @attachments = []
generate_propagation_context
set_new_breadcrumb_buffer
end
diff --git a/sentry-ruby/lib/sentry/session_flusher.rb b/sentry-ruby/lib/sentry/session_flusher.rb
index 256320ca7..5971cfc59 100644
--- a/sentry-ruby/lib/sentry/session_flusher.rb
+++ b/sentry-ruby/lib/sentry/session_flusher.rb
@@ -1,19 +1,15 @@
# frozen_string_literal: true
module Sentry
- class SessionFlusher
- include LoggingHelper
-
+ class SessionFlusher < ThreadedPeriodicWorker
FLUSH_INTERVAL = 60
def initialize(configuration, client)
- @thread = nil
- @exited = false
+ super(configuration.logger, FLUSH_INTERVAL)
@client = client
@pending_aggregates = {}
@release = configuration.release
@environment = configuration.environment
- @logger = configuration.logger
log_debug("[Sessions] Sessions won't be captured without a valid release") unless @release
end
@@ -25,30 +21,18 @@ def flush
@pending_aggregates = {}
end
+ alias_method :run, :flush
+
def add_session(session)
- return if @exited
return unless @release
- begin
- ensure_thread
- rescue ThreadError
- log_debug("Session flusher thread creation failed")
- @exited = true
- return
- end
+ return unless ensure_thread
return unless Session::AGGREGATE_STATUSES.include?(session.status)
@pending_aggregates[session.aggregation_key] ||= init_aggregates(session.aggregation_key)
@pending_aggregates[session.aggregation_key][session.status] += 1
end
- def kill
- log_debug("Killing session flusher")
-
- @exited = true
- @thread&.kill
- end
-
private
def init_aggregates(aggregation_key)
@@ -70,16 +54,5 @@ def pending_envelope
def attrs
{ release: @release, environment: @environment }
end
-
- def ensure_thread
- return if @thread&.alive?
-
- @thread = Thread.new do
- loop do
- sleep(FLUSH_INTERVAL)
- flush
- end
- end
- end
end
end
diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb
index 69374b496..da7232d9a 100644
--- a/sentry-ruby/lib/sentry/span.rb
+++ b/sentry-ruby/lib/sentry/span.rb
@@ -39,6 +39,11 @@ module DataConventions
# Recommended: If different than server.port.
# Example: 16456
SERVER_SOCKET_PORT = "server.socket.port"
+
+ FILEPATH = "code.filepath"
+ LINENO = "code.lineno"
+ FUNCTION = "code.function"
+ NAMESPACE = "code.namespace"
end
STATUS_MAP = {
@@ -55,6 +60,8 @@ module DataConventions
504 => "deadline_exceeded"
}
+ DEFAULT_SPAN_ORIGIN = "manual"
+
# An uuid that can be used to identify a trace.
# @return [String]
attr_reader :trace_id
@@ -88,6 +95,9 @@ module DataConventions
# Span data
# @return [Hash]
attr_reader :data
+ # Span origin that tracks what kind of instrumentation created a span
+ # @return [String]
+ attr_reader :origin
# The SpanRecorder the current span belongs to.
# SpanRecorder holds all spans under the same Transaction object (including the Transaction itself).
@@ -109,7 +119,8 @@ def initialize(
parent_span_id: nil,
sampled: nil,
start_timestamp: nil,
- timestamp: nil
+ timestamp: nil,
+ origin: nil
)
@trace_id = trace_id || SecureRandom.uuid.delete("-")
@span_id = span_id || SecureRandom.uuid.delete("-").slice(0, 16)
@@ -123,6 +134,7 @@ def initialize(
@status = status
@data = {}
@tags = {}
+ @origin = origin || DEFAULT_SPAN_ORIGIN
end
# Finishes the span by adding a timestamp.
@@ -160,7 +172,8 @@ def to_hash
op: @op,
status: @status,
tags: @tags,
- data: @data
+ data: @data,
+ origin: @origin
}
summary = metrics_summary
@@ -178,7 +191,9 @@ def get_trace_context
parent_span_id: @parent_span_id,
description: @description,
op: @op,
- status: @status
+ status: @status,
+ origin: @origin,
+ data: @data
}
end
@@ -275,6 +290,12 @@ def set_tag(key, value)
@tags[key] = value
end
+ # Sets the origin of the span.
+ # @param origin [String]
+ def set_origin(origin)
+ @origin = origin
+ end
+
# Collects gauge metrics on the span for metric summaries.
def metrics_local_aggregator
@metrics_local_aggregator ||= Sentry::Metrics::LocalAggregator.new
diff --git a/sentry-ruby/lib/sentry/test_helper.rb b/sentry-ruby/lib/sentry/test_helper.rb
index 75a6f29bd..7db34cbd2 100644
--- a/sentry-ruby/lib/sentry/test_helper.rb
+++ b/sentry-ruby/lib/sentry/test_helper.rb
@@ -20,7 +20,7 @@ def setup_sentry_test(&block)
# set transport to DummyTransport, so we can easily intercept the captured events
dummy_config.transport.transport_class = Sentry::DummyTransport
# make sure SDK allows sending under the current environment
- dummy_config.enabled_environments << dummy_config.environment unless dummy_config.enabled_environments.include?(dummy_config.environment)
+ dummy_config.enabled_environments += [dummy_config.environment] unless dummy_config.enabled_environments.include?(dummy_config.environment)
# disble async event sending
dummy_config.background_worker_threads = 0
@@ -50,6 +50,7 @@ def teardown_sentry_test
if Sentry.get_current_hub.instance_variable_get(:@stack).size > 1
Sentry.get_current_hub.pop_scope
end
+ Sentry::Scope.global_event_processors.clear
end
# @return [Transport]
diff --git a/sentry-ruby/lib/sentry/threaded_periodic_worker.rb b/sentry-ruby/lib/sentry/threaded_periodic_worker.rb
new file mode 100644
index 000000000..cf1272083
--- /dev/null
+++ b/sentry-ruby/lib/sentry/threaded_periodic_worker.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Sentry
+ class ThreadedPeriodicWorker
+ include LoggingHelper
+
+ def initialize(logger, internal)
+ @thread = nil
+ @exited = false
+ @interval = internal
+ @logger = logger
+ end
+
+ def ensure_thread
+ return false if @exited
+ return true if @thread&.alive?
+
+ @thread = Thread.new do
+ loop do
+ sleep(@interval)
+ run
+ end
+ end
+
+ true
+ rescue ThreadError
+ log_debug("[#{self.class.name}] thread creation failed")
+ @exited = true
+ false
+ end
+
+ def kill
+ log_debug("[#{self.class.name}] thread killed")
+
+ @exited = true
+ @thread&.kill
+ end
+ end
+end
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 326dcbf35..39a993fe5 100644
--- a/sentry-ruby/lib/sentry/transport.rb
+++ b/sentry-ruby/lib/sentry/transport.rb
@@ -61,7 +61,7 @@ def send_envelope(envelope)
data, serialized_items = serialize_envelope(envelope)
if data
- log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
+ log_debug("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
send_data(data)
end
end
@@ -145,17 +145,23 @@ def envelope_from_event(event)
)
end
+ if event.is_a?(Event) && event.attachments.any?
+ event.attachments.each do |attachment|
+ envelope.add_item(attachment.to_envelope_headers, attachment.payload)
+ end
+ end
+
client_report_headers, client_report_payload = fetch_pending_client_report
envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
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/lib/sentry/utils/http_tracing.rb b/sentry-ruby/lib/sentry/utils/http_tracing.rb
new file mode 100644
index 000000000..abf6a9141
--- /dev/null
+++ b/sentry-ruby/lib/sentry/utils/http_tracing.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Sentry
+ module Utils
+ module HttpTracing
+ def set_span_info(sentry_span, request_info, response_status)
+ sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
+ sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
+ sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
+ sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
+ sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, response_status)
+ end
+
+ def set_propagation_headers(req)
+ Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
+ end
+
+ def record_sentry_breadcrumb(request_info, response_status)
+ crumb = Sentry::Breadcrumb.new(
+ level: :info,
+ category: self.class::BREADCRUMB_CATEGORY,
+ type: :info,
+ data: { status: response_status, **request_info }
+ )
+
+ Sentry.add_breadcrumb(crumb)
+ end
+
+ def record_sentry_breadcrumb?
+ Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
+ end
+
+ def propagate_trace?(url)
+ url &&
+ Sentry.initialized? &&
+ Sentry.configuration.propagate_traces &&
+ Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) }
+ end
+ end
+ end
+end
diff --git a/sentry-ruby/lib/sentry/utils/logging_helper.rb b/sentry-ruby/lib/sentry/utils/logging_helper.rb
index 8c3166892..34a589e8f 100644
--- a/sentry-ruby/lib/sentry/utils/logging_helper.rb
+++ b/sentry-ruby/lib/sentry/utils/logging_helper.rb
@@ -11,10 +11,6 @@ def log_error(message, exception, debug: false)
end
end
- def log_info(message)
- @logger.info(LOGGER_PROGNAME) { message }
- end
-
def log_debug(message)
@logger.debug(LOGGER_PROGNAME) { message }
end
diff --git a/sentry-ruby/lib/sentry/version.rb b/sentry-ruby/lib/sentry/version.rb
index 6829dc652..a06f8cebc 100644
--- a/sentry-ruby/lib/sentry/version.rb
+++ b/sentry-ruby/lib/sentry/version.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Sentry
- VERSION = "5.17.3"
+ VERSION = "5.19.0"
end
diff --git a/sentry-ruby/sentry-ruby.gemspec b/sentry-ruby/sentry-ruby.gemspec
index 77e9711ec..52cbcc180 100644
--- a/sentry-ruby/sentry-ruby.gemspec
+++ b/sentry-ruby/sentry-ruby.gemspec
@@ -7,19 +7,25 @@ Gem::Specification.new do |spec|
spec.description = spec.summary = "A gem that provides a client interface for the Sentry error logger"
spec.email = "accounts@sentry.io"
spec.license = 'MIT'
- spec.homepage = "https://github.com/getsentry/sentry-ruby"
spec.platform = Gem::Platform::RUBY
spec.required_ruby_version = '>= 2.4'
spec.extra_rdoc_files = ["README.md", "LICENSE.txt"]
spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n")
- spec.metadata["homepage_uri"] = spec.homepage
- spec.metadata["source_code_uri"] = spec.homepage
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
+ github_root_uri = 'https://github.com/getsentry/sentry-ruby'
+ spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}"
+
+ spec.metadata = {
+ "homepage_uri" => spec.homepage,
+ "source_code_uri" => spec.homepage,
+ "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md",
+ "bug_tracker_uri" => "#{github_root_uri}/issues",
+ "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}"
+ }
spec.require_paths = ["lib"]
- spec.add_dependency "concurrent-ruby", '~> 1.0', '>= 1.0.2'
+ spec.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
spec.add_dependency "bigdecimal"
end
diff --git a/sentry-ruby/spec/fixtures/attachment.txt b/sentry-ruby/spec/fixtures/attachment.txt
new file mode 100644
index 000000000..3b18e512d
--- /dev/null
+++ b/sentry-ruby/spec/fixtures/attachment.txt
@@ -0,0 +1 @@
+hello world
diff --git a/sentry-ruby/spec/sentry/background_worker_spec.rb b/sentry-ruby/spec/sentry/background_worker_spec.rb
index cefbc398b..e1b900094 100644
--- a/sentry-ruby/spec/sentry/background_worker_spec.rb
+++ b/sentry-ruby/spec/sentry/background_worker_spec.rb
@@ -28,10 +28,11 @@
context "when config.background_worker_threads is set" do
it "initializes a background worker with correct number of threads and queue size" do
+ configuration.background_worker_threads = 4
worker = described_class.new(configuration)
expect(worker.max_queue).to eq(30)
- expect(worker.number_of_threads).to eq(Concurrent.processor_count)
+ expect(worker.number_of_threads).to eq(4)
end
end
diff --git a/sentry-ruby/spec/sentry/backpressure_monitor_spec.rb b/sentry-ruby/spec/sentry/backpressure_monitor_spec.rb
index 6f18d605f..1419cffdb 100644
--- a/sentry-ruby/spec/sentry/backpressure_monitor_spec.rb
+++ b/sentry-ruby/spec/sentry/backpressure_monitor_spec.rb
@@ -55,7 +55,7 @@
it 'logs error' do
subject.healthy?
- expect(string_io.string).to match(/\[BackpressureMonitor\] Thread creation failed/)
+ expect(string_io.string).to include("[#{described_class.name}] thread creation failed")
end
end
@@ -111,7 +111,7 @@
subject.healthy?
expect(subject.instance_variable_get(:@thread)).to receive(:kill)
subject.kill
- expect(string_io.string).to match(/\[BackpressureMonitor\] killing monitor/)
+ expect(string_io.string).to include("[#{described_class.name}] thread killed")
end
end
end
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/configuration_spec.rb b/sentry-ruby/spec/sentry/configuration_spec.rb
index 4ab21a101..92926836a 100644
--- a/sentry-ruby/spec/sentry/configuration_spec.rb
+++ b/sentry-ruby/spec/sentry/configuration_spec.rb
@@ -24,6 +24,18 @@
end
end
+ describe "#background_worker_threads" do
+ it "sets to have of the processors count" do
+ allow_any_instance_of(Sentry::Configuration).to receive(:processor_count).and_return(8)
+ expect(subject.background_worker_threads).to eq(4)
+ end
+
+ it "sets to 1 with only 1 processor" do
+ allow_any_instance_of(Sentry::Configuration).to receive(:processor_count).and_return(1)
+ expect(subject.background_worker_threads).to eq(1)
+ end
+ end
+
describe "#csp_report_uri" do
it "returns nil if the dsn is not present" do
expect(subject.csp_report_uri).to eq(nil)
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/event_spec.rb b/sentry-ruby/spec/sentry/event_spec.rb
index a5ca457e7..5eeb232f9 100644
--- a/sentry-ruby/spec/sentry/event_spec.rb
+++ b/sentry-ruby/spec/sentry/event_spec.rb
@@ -141,6 +141,12 @@
expect(event.to_hash[:user][:ip_address]).to eq("2.2.2.2")
end
+ it "doesn't overwrite already set ip address" do
+ Sentry.set_user({ ip_address: "3.3.3.3" })
+ Sentry.get_current_scope.apply_to_event(event)
+ expect(event.to_hash[:user][:ip_address]).to eq("3.3.3.3")
+ end
+
context "with config.trusted_proxies = [\"2.2.2.2\"]" do
before do
Sentry.configuration.trusted_proxies = ["2.2.2.2"]
diff --git a/sentry-ruby/spec/sentry/faraday_spec.rb b/sentry-ruby/spec/sentry/faraday_spec.rb
new file mode 100644
index 000000000..f4632032f
--- /dev/null
+++ b/sentry-ruby/spec/sentry/faraday_spec.rb
@@ -0,0 +1,279 @@
+require "faraday"
+require_relative "../spec_helper"
+
+RSpec.describe Sentry::Faraday do
+ before(:all) do
+ perform_basic_setup do |config|
+ config.enabled_patches << :faraday
+ config.traces_sample_rate = 1.0
+ config.logger = ::Logger.new(StringIO.new)
+ end
+ end
+
+ after(:all) do
+ Sentry.configuration.enabled_patches = Sentry::Configuration::DEFAULT_PATCHES
+ end
+
+ context "with tracing enabled" do
+ let(:http) do
+ Faraday.new(url) do |f|
+ f.request :json
+
+ f.adapter Faraday::Adapter::Test do |stub|
+ stub.get("/test") do
+ [200, { "Content-Type" => "text/html" }, "hello world
"]
+ end
+ end
+ end
+ end
+
+ let(:url) { "http://example.com/" }
+
+ it "records the request's span" do
+ transaction = Sentry.start_transaction
+ Sentry.get_current_scope.set_span(transaction)
+
+ _response = http.get("/test")
+
+ request_span = transaction.span_recorder.spans.last
+
+ expect(request_span.op).to eq("http.client")
+ expect(request_span.origin).to eq("auto.http.faraday")
+ expect(request_span.start_timestamp).not_to be_nil
+ expect(request_span.timestamp).not_to be_nil
+ expect(request_span.start_timestamp).not_to eq(request_span.timestamp)
+ expect(request_span.description).to eq("GET http://example.com/test")
+
+ expect(request_span.data).to eq({
+ "http.response.status_code" => 200,
+ "url" => "http://example.com/test",
+ "http.request.method" => "GET"
+ })
+ end
+ end
+
+ context "with config.send_default_pii = true" do
+ let(:http) do
+ Faraday.new(url) do |f|
+ f.adapter Faraday::Adapter::Test do |stub|
+ stub.get("/test") do
+ [200, { "Content-Type" => "text/html" }, "hello world
"]
+ end
+
+ stub.post("/test") do
+ [200, { "Content-Type" => "application/json" }, { hello: "world" }.to_json]
+ end
+ end
+ end
+ end
+
+ let(:url) { "http://example.com/" }
+
+ before do
+ Sentry.configuration.send_default_pii = true
+ Sentry.configuration.breadcrumbs_logger = [:http_logger]
+ end
+
+ it "records the request's span with query string in data" do
+ transaction = Sentry.start_transaction
+ Sentry.get_current_scope.set_span(transaction)
+
+ _response = http.get("/test?foo=bar")
+
+ request_span = transaction.span_recorder.spans.last
+
+ expect(request_span.description).to eq("GET http://example.com/test")
+
+ expect(request_span.data).to eq({
+ "http.response.status_code" => 200,
+ "url" => "http://example.com/test",
+ "http.request.method" => "GET",
+ "http.query" => "foo=bar"
+ })
+ end
+
+ it "records breadcrumbs" do
+ transaction = Sentry.start_transaction
+ Sentry.get_current_scope.set_span(transaction)
+
+ _response = http.get("/test?foo=bar")
+
+ transaction.span_recorder.spans.last
+
+ crumb = Sentry.get_current_scope.breadcrumbs.peek
+
+ expect(crumb.category).to eq("http")
+ expect(crumb.data[:status]).to eq(200)
+ expect(crumb.data[:method]).to eq("GET")
+ expect(crumb.data[:url]).to eq("http://example.com/test")
+ expect(crumb.data[:query]).to eq("foo=bar")
+ expect(crumb.data[:body]).to be(nil)
+ end
+
+ it "records POST request body" do
+ transaction = Sentry.start_transaction
+ Sentry.get_current_scope.set_span(transaction)
+
+ body = { foo: "bar" }.to_json
+ _response = http.post("/test?foo=bar", body, "Content-Type" => "application/json")
+
+ request_span = transaction.span_recorder.spans.last
+
+ expect(request_span.description).to eq("POST http://example.com/test")
+
+ expect(request_span.data).to eq({
+ "http.response.status_code" => 200,
+ "url" => "http://example.com/test",
+ "http.request.method" => "POST",
+ "http.query" => "foo=bar"
+ })
+
+ crumb = Sentry.get_current_scope.breadcrumbs.peek
+
+ expect(crumb.data[:body]).to eq(body)
+ end
+
+ context "with custom trace_propagation_targets" do
+ let(:http) do
+ Faraday.new(url) do |f|
+ f.adapter Faraday::Adapter::Test do |stub|
+ stub.get("/test") do
+ [200, { "Content-Type" => "text/html" }, "hello world
"]
+ end
+ end
+ end
+ end
+
+ before do
+ Sentry.configuration.trace_propagation_targets = ["example.com", /foobar.org\/api\/v2/]
+ end
+
+ context "when the request is not to the same target" do
+ let(:url) { "http://another.site/" }
+
+ it "doesn't add sentry headers to outgoing requests to different target" do
+ transaction = Sentry.start_transaction
+ Sentry.get_current_scope.set_span(transaction)
+
+ response = http.get("/test")
+
+ request_span = transaction.span_recorder.spans.last
+
+ expect(request_span.description).to eq("GET #{url}/test")
+
+ expect(request_span.data).to eq({
+ "http.response.status_code" => 200,
+ "url" => "#{url}/test",
+ "http.request.method" => "GET"
+ })
+
+ expect(response.headers.key?("sentry-trace")).to eq(false)
+ expect(response.headers.key?("baggage")).to eq(false)
+ end
+ end
+
+ context "when the request is to the same target" do
+ let(:url) { "http://example.com/" }
+
+ before do
+ Sentry.configuration.trace_propagation_targets = ["example.com"]
+ end
+
+ it "adds sentry headers to outgoing requests" do
+ transaction = Sentry.start_transaction
+ Sentry.get_current_scope.set_span(transaction)
+
+ response = http.get("/test")
+
+ request_span = transaction.span_recorder.spans.last
+
+ expect(request_span.description).to eq("GET #{url}/test")
+
+ expect(request_span.data).to eq({
+ "http.response.status_code" => 200,
+ "url" => "#{url}/test",
+ "http.request.method" => "GET"
+ })
+
+ expect(response.env.request_headers.key?("sentry-trace")).to eq(true)
+ expect(response.env.request_headers.key?("baggage")).to eq(true)
+ end
+ end
+
+ context "when the request's url configured target regexp" do
+ let(:url) { "http://example.com/" }
+
+ before do
+ Sentry.configuration.trace_propagation_targets = [/example/]
+ end
+
+ it "adds sentry headers to outgoing requests" do
+ transaction = Sentry.start_transaction
+ Sentry.get_current_scope.set_span(transaction)
+
+ response = http.get("/test")
+
+ request_span = transaction.span_recorder.spans.last
+
+ expect(request_span.description).to eq("GET #{url}/test")
+
+ expect(request_span.data).to eq({
+ "http.response.status_code" => 200,
+ "url" => "#{url}/test",
+ "http.request.method" => "GET"
+ })
+
+ expect(response.env.request_headers.key?("sentry-trace")).to eq(true)
+ expect(response.env.request_headers.key?("baggage")).to eq(true)
+ end
+ end
+ end
+ end
+
+ context "when adapter is net/http" do
+ let(:http) do
+ Faraday.new(url) do |f|
+ f.request :json
+ f.adapter :net_http
+ end
+ end
+
+ let(:url) { "http://example.com/" }
+
+ it "skips instrumentation" do
+ transaction = Sentry.start_transaction
+ Sentry.get_current_scope.set_span(transaction)
+
+ _response = http.get("/test")
+
+ request_span = transaction.span_recorder.spans.last
+
+ expect(request_span.op).to eq("http.client")
+ expect(request_span.origin).to eq("auto.http.net_http")
+
+ expect(transaction.span_recorder.spans.map(&:origin)).not_to include("auto.http.faraday")
+ end
+ end
+
+ context "when Sentry is not initialized" do
+ let(:http) do
+ Faraday.new(url) do |f|
+ f.adapter Faraday::Adapter::Test do |stub|
+ stub.get("/test") do
+ [200, { "Content-Type" => "text/html" }, "hello world
"]
+ end
+ end
+ end
+ end
+
+ let(:url) { "http://example.com/" }
+
+ it "skips instrumentation" do
+ allow(Sentry).to receive(:initialized?).and_return(false)
+
+ response = http.get("/test")
+
+ expect(response.status).to eq(200)
+ end
+ end
+end
diff --git a/sentry-ruby/spec/sentry/graphql_spec.rb b/sentry-ruby/spec/sentry/graphql_spec.rb
new file mode 100644
index 000000000..5820b73d2
--- /dev/null
+++ b/sentry-ruby/spec/sentry/graphql_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+with_graphql = begin
+ require 'graphql'
+ true
+ rescue LoadError
+ false
+ end
+
+RSpec.describe 'GraphQL' do
+ it 'adds the graphql patch to registered patches' do
+ expect(Sentry.registered_patches.keys).to include(:graphql)
+ end
+
+ context 'when patch enabled' do
+ if with_graphql
+ describe 'with graphql gem' do
+ class Thing < GraphQL::Schema::Object
+ field :str, String
+ def str; 'blah'; end
+ end
+
+ class Query < GraphQL::Schema::Object
+ field :int, Integer, null: false
+ def int; 1; end
+
+ field :thing, Thing
+ def thing; :thing; end
+ end
+
+ class MySchema < GraphQL::Schema
+ query(Query)
+ end
+
+ before do
+ perform_basic_setup do |config|
+ config.traces_sample_rate = 1.0
+ config.enabled_patches << :graphql
+ end
+ end
+
+ it 'enables the sentry tracer' do
+ expect(MySchema.trace_modules_for(:default)).to include(::GraphQL::Tracing::SentryTrace)
+ end
+
+ it 'adds graphql spans to the transaction' do
+ transaction = Sentry.start_transaction
+ Sentry.get_current_scope.set_span(transaction)
+ MySchema.execute('query foobar { int thing { str } }')
+ transaction.finish
+
+ expect(last_sentry_event.transaction).to eq('GraphQL/query.foobar')
+
+ execute_span = last_sentry_event.spans.find { |s| s[:op] == 'graphql.execute' }
+ expect(execute_span[:description]).to eq('query foobar')
+ expect(execute_span[:data]).to eq({
+ 'graphql.document'=>'query foobar { int thing { str } }',
+ 'graphql.operation.name'=>'foobar',
+ 'graphql.operation.type'=>'query'
+ })
+ end
+ end
+ else
+ describe 'without graphql gem' do
+ it 'logs warning' do
+ string_io = StringIO.new
+
+ perform_basic_setup do |config|
+ config.enabled_patches << :graphql
+ config.logger = Logger.new(string_io)
+ end
+
+ expect(string_io.string).to include('WARN -- sentry: You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile.')
+ end
+ end
+ end
+ end
+end
diff --git a/sentry-ruby/spec/sentry/hub_spec.rb b/sentry-ruby/spec/sentry/hub_spec.rb
index bc942ca25..6d06595aa 100644
--- a/sentry-ruby/spec/sentry/hub_spec.rb
+++ b/sentry-ruby/spec/sentry/hub_spec.rb
@@ -342,6 +342,22 @@
end
end
+ it "reminds users about unsupported options" do
+ expect do
+ subject.capture_event(event, unsupported: true)
+ end.not_to raise_error
+
+ expect(string_io.string).to include("Options [:unsupported] are not supported and will not be applied to the event.")
+ end
+
+ it "does not warn about unsupported options if all passed options are supported" do
+ expect do
+ subject.capture_event(event, level: 'DEBUG')
+ end.not_to raise_error
+
+ expect(string_io.string).not_to include("Options [] are not supported and will not be applied to the event.")
+ end
+
context "when event is a transaction" do
it "transaction.set_context merges and takes precedence over scope.set_context" do
scope.set_context(:foo, { val: 42 })
diff --git a/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb b/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb
index e9570f520..580a610d3 100644
--- a/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb
+++ b/sentry-ruby/spec/sentry/interfaces/request_interface_spec.rb
@@ -1,7 +1,7 @@
-return unless defined?(Rack)
-
require 'spec_helper'
+return unless defined?(Rack)
+
RSpec.describe Sentry::RequestInterface do
let(:env) { Rack::MockRequest.env_for("/test") }
let(:send_default_pii) { false }
@@ -44,14 +44,16 @@
let(:env) { Rack::MockRequest.env_for("/test", additional_headers) }
it 'transforms headers to conform with the interface' do
- expect(subject.headers).to eq("Content-Length" => "0", "Version" => "HTTP/1.1", "X-Request-Id" => "12345678")
+ expect(subject.headers).to include("Version" => "HTTP/1.1", "X-Request-Id" => "12345678")
+ expect(subject.headers).not_to include("Cookie")
end
context 'from Rails middleware' do
let(:additional_headers) { { "action_dispatch.request_id" => "12345678" } }
it 'transforms headers to conform with the interface' do
- expect(subject.headers).to eq("Content-Length" => "0", "X-Request-Id" => "12345678")
+ expect(subject.headers).to include("X-Request-Id" => "12345678")
+ expect(subject.headers).not_to include("Cookie")
end
end
@@ -61,7 +63,7 @@
it "doesn't cause any issue" do
json = JSON.generate(subject.to_hash)
- expect(JSON.parse(json)["headers"]).to eq({ "Content-Length"=>"0", "Foo"=>"Tekirda�" })
+ expect(JSON.parse(json)["headers"]).to include("Foo"=>"Tekirda�")
end
end
diff --git a/sentry-ruby/spec/sentry/metrics/aggregator_spec.rb b/sentry-ruby/spec/sentry/metrics/aggregator_spec.rb
index f397d07d5..dcd78ab86 100644
--- a/sentry-ruby/spec/sentry/metrics/aggregator_spec.rb
+++ b/sentry-ruby/spec/sentry/metrics/aggregator_spec.rb
@@ -47,7 +47,7 @@
it 'logs error' do
subject.add(:c, 'incr', 1)
- expect(string_io.string).to match(/\[Metrics::Aggregator\] thread creation failed/)
+ expect(string_io.string).to include("[#{described_class.name}] thread creation failed")
end
end
@@ -479,7 +479,7 @@
it 'logs message when killing the thread' do
expect(subject.thread).to receive(:kill)
subject.kill
- expect(string_io.string).to match(/\[Metrics::Aggregator\] killing thread/)
+ expect(string_io.string).to include("[#{described_class.name}] thread killed")
end
end
end
diff --git a/sentry-ruby/spec/sentry/metrics_spec.rb b/sentry-ruby/spec/sentry/metrics_spec.rb
index 42d9ad754..335c31e5a 100644
--- a/sentry-ruby/spec/sentry/metrics_spec.rb
+++ b/sentry-ruby/spec/sentry/metrics_spec.rb
@@ -122,7 +122,7 @@
end
it 'starts a span' do
- expect(Sentry).to receive(:with_child_span).with(op: Sentry::Metrics::OP_NAME, description: 'foo').and_call_original
+ expect(Sentry).to receive(:with_child_span).with(op: Sentry::Metrics::OP_NAME, description: 'foo', origin: Sentry::Metrics::SPAN_ORIGIN).and_call_original
described_class.timing('foo') { sleep(0.1) }
end
diff --git a/sentry-ruby/spec/sentry/net/http_spec.rb b/sentry-ruby/spec/sentry/net/http_spec.rb
index 4718b5e1f..064c4f36c 100644
--- a/sentry-ruby/spec/sentry/net/http_spec.rb
+++ b/sentry-ruby/spec/sentry/net/http_spec.rb
@@ -62,6 +62,7 @@
request_span = transaction.span_recorder.spans.last
expect(request_span.op).to eq("http.client")
+ expect(request_span.origin).to eq("auto.http.net_http")
expect(request_span.start_timestamp).not_to be_nil
expect(request_span.timestamp).not_to be_nil
expect(request_span.start_timestamp).not_to eq(request_span.timestamp)
@@ -93,6 +94,7 @@
request_span = transaction.span_recorder.spans.last
expect(request_span.op).to eq("http.client")
+ expect(request_span.origin).to eq("auto.http.net_http")
expect(request_span.start_timestamp).not_to be_nil
expect(request_span.timestamp).not_to be_nil
expect(request_span.start_timestamp).not_to eq(request_span.timestamp)
@@ -320,6 +322,7 @@ def verify_spans(transaction)
request_span = transaction.span_recorder.spans[1]
expect(request_span.op).to eq("http.client")
+ expect(request_span.origin).to eq("auto.http.net_http")
expect(request_span.start_timestamp).not_to be_nil
expect(request_span.timestamp).not_to be_nil
expect(request_span.start_timestamp).not_to eq(request_span.timestamp)
@@ -332,6 +335,7 @@ def verify_spans(transaction)
request_span = transaction.span_recorder.spans[2]
expect(request_span.op).to eq("http.client")
+ expect(request_span.origin).to eq("auto.http.net_http")
expect(request_span.start_timestamp).not_to be_nil
expect(request_span.timestamp).not_to be_nil
expect(request_span.start_timestamp).not_to eq(request_span.timestamp)
diff --git a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb
index c5751bba0..e03984cdf 100644
--- a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb
+++ b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb
@@ -79,7 +79,7 @@
event = last_sentry_event
expect(event.transaction).to eq("/test")
expect(event.to_hash.dig(:request, :url)).to eq("http://example.org/test")
- expect(Sentry.get_current_scope.transaction_names).to be_empty
+ expect(Sentry.get_current_scope.transaction_name).to be_nil
expect(Sentry.get_current_scope.rack_env).to eq({})
end
@@ -264,6 +264,7 @@ def verify_transaction_attributes(transaction)
expect(transaction.timestamp).not_to be_nil
expect(transaction.contexts.dig(:trace, :status)).to eq("ok")
expect(transaction.contexts.dig(:trace, :op)).to eq("http.server")
+ expect(transaction.contexts.dig(:trace, :origin)).to eq("auto.http.rack")
expect(transaction.spans.count).to eq(0)
end
diff --git a/sentry-ruby/spec/sentry/rake_spec.rb b/sentry-ruby/spec/sentry/rake_spec.rb
index bb77a3ee9..a1eb22998 100644
--- a/sentry-ruby/spec/sentry/rake_spec.rb
+++ b/sentry-ruby/spec/sentry/rake_spec.rb
@@ -22,7 +22,7 @@
message = `cd spec/support && bundle exec rake raise_exception_without_rake_integration 2>&1`
end.join
- expect(message).not_to match(/Sentry/)
+ expect(message).not_to match(/Sending envelope with items/)
end
it "run rake task with original arguments" do
diff --git a/sentry-ruby/spec/sentry/redis_spec.rb b/sentry-ruby/spec/sentry/redis_spec.rb
index ef2c4ce79..2722e0498 100644
--- a/sentry-ruby/spec/sentry/redis_spec.rb
+++ b/sentry-ruby/spec/sentry/redis_spec.rb
@@ -23,6 +23,7 @@
expect(result).to eq("OK")
request_span = transaction.span_recorder.spans.last
expect(request_span.op).to eq("db.redis")
+ expect(request_span.origin).to eq("auto.db.redis")
expect(request_span.start_timestamp).not_to be_nil
expect(request_span.timestamp).not_to be_nil
expect(request_span.start_timestamp).not_to eq(request_span.timestamp)
diff --git a/sentry-ruby/spec/sentry/scope/setters_spec.rb b/sentry-ruby/spec/sentry/scope/setters_spec.rb
index 47d915980..3ca240488 100644
--- a/sentry-ruby/spec/sentry/scope/setters_spec.rb
+++ b/sentry-ruby/spec/sentry/scope/setters_spec.rb
@@ -206,7 +206,7 @@
end
describe "#set_transaction_name" do
- it "pushes the transaction_name to transaction_names stack" do
+ it "sets the transaction name" do
subject.set_transaction_name("WelcomeController#home")
expect(subject.transaction_name).to eq("WelcomeController#home")
diff --git a/sentry-ruby/spec/sentry/scope_spec.rb b/sentry-ruby/spec/sentry/scope_spec.rb
index c3799602a..5de4d8d6d 100644
--- a/sentry-ruby/spec/sentry/scope_spec.rb
+++ b/sentry-ruby/spec/sentry/scope_spec.rb
@@ -22,8 +22,8 @@
expect(subject.tags).to eq({})
expect(subject.user).to eq({})
expect(subject.fingerprint).to eq([])
- expect(subject.transaction_names).to eq([])
- expect(subject.transaction_sources).to eq([])
+ expect(subject.transaction_name).to eq(nil)
+ expect(subject.transaction_source).to eq(nil)
expect(subject.propagation_context).to be_a(Sentry::PropagationContext)
end
@@ -42,8 +42,7 @@
copy.extra.merge!(foo: "bar")
copy.tags.merge!(foo: "bar")
copy.user.merge!(foo: "bar")
- copy.transaction_names << "foo"
- copy.transaction_sources << :url
+ copy.set_transaction_name("foo", source: :url)
copy.fingerprint << "bar"
expect(subject.breadcrumbs.to_hash).to eq({ values: [] })
@@ -53,8 +52,8 @@
expect(subject.tags).to eq({})
expect(subject.user).to eq({})
expect(subject.fingerprint).to eq([])
- expect(subject.transaction_names).to eq([])
- expect(subject.transaction_sources).to eq([])
+ expect(subject.transaction_name).to eq(nil)
+ expect(subject.transaction_source).to eq(nil)
expect(subject.span).to eq(nil)
end
@@ -143,8 +142,8 @@
expect(subject.tags).to eq({})
expect(subject.user).to eq({})
expect(subject.fingerprint).to eq([])
- expect(subject.transaction_names).to eq([])
- expect(subject.transaction_sources).to eq([])
+ expect(subject.transaction_name).to eq(nil)
+ expect(subject.transaction_source).to eq(nil)
expect(subject.span).to eq(nil)
end
end
@@ -202,6 +201,7 @@
scope.set_user({ id: 1 })
scope.set_transaction_name("WelcomeController#index", source: :view)
scope.set_fingerprint(["foo"])
+ scope.add_attachment(bytes: "file-data", filename: "test.txt")
scope
end
@@ -220,6 +220,10 @@
expect(event.contexts).to include(:trace)
expect(event.contexts[:os].keys).to match_array([:name, :version, :build, :kernel_version, :machine])
expect(event.contexts.dig(:runtime, :version)).to match(/ruby/)
+
+ attachment = event.attachments.first
+ expect(attachment.filename).to eql("test.txt")
+ expect(attachment.bytes).to eql("file-data")
end
it "does not apply the contextual data to a check-in event" do
@@ -336,4 +340,49 @@
subject.generate_propagation_context(env)
end
end
+
+ describe '#update_from_options' do
+ it 'updates data from arguments' do
+ result = subject.update_from_options(
+ contexts: { context: 1 },
+ extra: { foo: 42 },
+ tags: { tag: 2 },
+ user: { name: 'jane' },
+ level: :info,
+ fingerprint: 'ABCD'
+ )
+
+ expect(subject.contexts).to include(context: 1)
+ expect(subject.extra).to eq({ foo: 42 })
+ expect(subject.tags).to eq({ tag: 2 })
+ expect(subject.user).to eq({ name: 'jane' })
+ expect(subject.level).to eq(:info)
+ expect(subject.fingerprint).to eq('ABCD')
+ expect(result).to eq([])
+ end
+
+ it 'returns unsupported option keys' do
+ result = subject.update_from_options(foo: 42, bar: 43)
+ expect(result).to eq([:foo, :bar])
+ end
+ end
+
+ describe "#add_attachment" do
+ before { perform_basic_setup }
+
+ let(:opts) do
+ { bytes: "file-data", filename: "test.txt" }
+ end
+
+ subject do
+ described_class.new
+ end
+
+ it "adds a new attachment" do
+ attachment = subject.add_attachment(**opts)
+
+ expect(attachment.bytes).to eq("file-data")
+ expect(attachment.filename).to eq("test.txt")
+ end
+ end
end
diff --git a/sentry-ruby/spec/sentry/session_flusher_spec.rb b/sentry-ruby/spec/sentry/session_flusher_spec.rb
index 5f9f53cdf..0c0fbb4eb 100644
--- a/sentry-ruby/spec/sentry/session_flusher_spec.rb
+++ b/sentry-ruby/spec/sentry/session_flusher_spec.rb
@@ -146,7 +146,7 @@
it "logs error" do
subject.add_session(session)
- expect(string_io.string).to match(/Session flusher thread creation failed/)
+ expect(string_io.string).to include("[#{described_class.name}] thread creation failed")
end
end
@@ -173,7 +173,7 @@
describe "#kill" do
it "logs message when killing the thread" do
subject.kill
- expect(string_io.string).to match(/Killing session flusher/)
+ expect(string_io.string).to include("[#{described_class.name}] thread killed")
end
end
end
diff --git a/sentry-ruby/spec/sentry/span_spec.rb b/sentry-ruby/spec/sentry/span_spec.rb
index 3e61c8058..d890dfa25 100644
--- a/sentry-ruby/spec/sentry/span_spec.rb
+++ b/sentry-ruby/spec/sentry/span_spec.rb
@@ -28,11 +28,15 @@
it "returns correct context data" do
context = subject.get_trace_context
+ subject.set_data(:foo, "bar")
+
expect(context[:op]).to eq("sql.query")
expect(context[:description]).to eq("SELECT * FROM users;")
expect(context[:status]).to eq("ok")
expect(context[:trace_id].length).to eq(32)
expect(context[:span_id].length).to eq(16)
+ expect(context[:origin]).to eq('manual')
+ expect(context[:data]).to eq(foo: "bar")
end
end
@@ -69,6 +73,7 @@
expect(hash[:tags]).to eq({ "foo" => "bar" })
expect(hash[:trace_id].length).to eq(32)
expect(hash[:span_id].length).to eq(16)
+ expect(hash[:origin]).to eq('manual')
end
it 'has metric summary if present' do
@@ -296,4 +301,12 @@
expect(subject.tags).to eq({ foo: "bar" })
end
end
+
+ describe "#set_origin" do
+ it "sets origin" do
+ subject.set_origin('auto.http')
+
+ expect(subject.origin).to eq('auto.http')
+ end
+ end
end
diff --git a/sentry-ruby/spec/sentry/test_helper_spec.rb b/sentry-ruby/spec/sentry/test_helper_spec.rb
index 200356661..2753b9b64 100644
--- a/sentry-ruby/spec/sentry/test_helper_spec.rb
+++ b/sentry-ruby/spec/sentry/test_helper_spec.rb
@@ -127,6 +127,12 @@
expect(Sentry.get_current_scope.tags).to eq({})
end
+ it "clears global processors" do
+ Sentry.add_global_event_processor { |event| event }
+ teardown_sentry_test
+ expect(Sentry::Scope.global_event_processors).to eq([])
+ end
+
context "when the configuration is mutated" do
it "rolls back client changes" do
Sentry.configuration.environment = "quack"
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 7c90f4be7..b78dc7878 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
)
@@ -433,6 +435,24 @@
end
end
end
+
+ context "event with attachments" do
+ let(:event) { client.event_from_exception(ZeroDivisionError.new("divided by 0")) }
+ let(:envelope) { subject.envelope_from_event(event) }
+
+ before do
+ event.attachments << Sentry::Attachment.new(filename: "test-1.txt", bytes: "test")
+ event.attachments << Sentry::Attachment.new(path: fixture_path("attachment.txt"))
+ end
+
+ it "sends the event and logs the action" do
+ expect(subject).to receive(:send_data)
+
+ subject.send_envelope(envelope)
+
+ expect(io.string).to match(/Sending envelope with items \[event, attachment, attachment\]/)
+ end
+ end
end
describe "#send_event" do
@@ -460,7 +480,7 @@
expect(subject.send_event(event)).to eq(event)
expect(io.string).to match(
- /INFO -- sentry: \[Transport\] Sending envelope with items \[event\] #{event.event_id} to Sentry/
+ /DEBUG -- sentry: \[Transport\] Sending envelope with items \[event\] #{event.event_id} to Sentry/
)
end
end
diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb
index c3f12777a..dd3f85c93 100644
--- a/sentry-ruby/spec/sentry_spec.rb
+++ b/sentry-ruby/spec/sentry_spec.rb
@@ -713,6 +713,57 @@
end
end
+ describe ".add_attachment" do
+ it "adds a new attachment to the current scope with provided filename and bytes" do
+ described_class.add_attachment(filename: "test.txt", bytes: "test")
+
+ expect(described_class.get_current_scope.attachments.size).to be(1)
+
+ attachment = described_class.get_current_scope.attachments.first
+ expect(attachment.filename).to eq("test.txt")
+ expect(attachment.bytes).to eq("test")
+ end
+
+ it "adds a new attachment to the current scope with provided path to a file" do
+ described_class.add_attachment(path: fixture_path("attachment.txt"))
+
+ expect(described_class.get_current_scope.attachments.size).to be(1)
+
+ attachment = described_class.get_current_scope.attachments.first
+ expect(attachment.filename).to eq("attachment.txt")
+ expect(attachment.payload).to eq("hello world\n")
+ end
+
+ it "adds a new attachment to the current scope favoring bytes over path" do
+ described_class.add_attachment(path: fixture_path("attachment.txt"), bytes: "test", content_type: "text/plain")
+
+ expect(described_class.get_current_scope.attachments.size).to be(1)
+
+ attachment = described_class.get_current_scope.attachments.first
+ expect(attachment.filename).to eq("attachment.txt")
+ expect(attachment.content_type).to eq("text/plain")
+ expect(attachment.payload).to eq("test")
+ end
+
+ it "raises meaningful error when path is invalid" do
+ described_class.add_attachment(path: "/not-here/oops")
+
+ expect(described_class.get_current_scope.attachments.size).to be(1)
+
+ attachment = described_class.get_current_scope.attachments.first
+
+ expect { attachment.payload }
+ .to raise_error(
+ Sentry::Attachment::PathNotFoundError,
+ "Failed to read attachment file, file not found: /not-here/oops"
+ )
+ end
+
+ it "requires either filename or path" do
+ expect { described_class.add_attachment(bytes: "test") }.to raise_error(ArgumentError, "filename or path is required")
+ end
+ end
+
describe ".csp_report_uri" do
it "returns the csp_report_uri generated from the main Configuration" do
expect(Sentry.configuration).to receive(:csp_report_uri).and_call_original
@@ -782,6 +833,17 @@
end
end
+ describe ".get_trace_propagation_meta" do
+ it "returns meta tags for sentry-trace and baggage" do
+ meta = <<~META
+
+
+ META
+
+ expect(described_class.get_trace_propagation_meta).to eq(meta.chomp)
+ end
+ end
+
describe ".continue_trace" do
context "without incoming sentry trace" do
let(:env) { { "HTTP_FOO" => "bar" } }
diff --git a/sentry-ruby/spec/spec_helper.rb b/sentry-ruby/spec/spec_helper.rb
index 1d4a6ff15..611205163 100644
--- a/sentry-ruby/spec/spec_helper.rb
+++ b/sentry-ruby/spec/spec_helper.rb
@@ -50,13 +50,21 @@
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
+def fixtures_root
+ @fixtures_root ||= Pathname(__dir__).join("fixtures")
+end
+
+def fixture_path(name)
+ fixtures_root.join(name).realpath
+end
+
def build_exception_with_cause(cause = "exception a")
begin
raise cause
diff --git a/sentry-ruby/spec/support/Rakefile.rb b/sentry-ruby/spec/support/Rakefile.rb
index f7627051f..2304ba3a4 100644
--- a/sentry-ruby/spec/support/Rakefile.rb
+++ b/sentry-ruby/spec/support/Rakefile.rb
@@ -4,6 +4,7 @@
Sentry.init do |config|
config.dsn = 'http://12345:67890@sentry.localdomain/sentry/42'
config.background_worker_threads = 0
+ config.logger.level = Logger::DEBUG
end
task :raise_exception do
diff --git a/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb b/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb
index b6cde1ea5..748ca0155 100644
--- a/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb
+++ b/sentry-sidekiq/lib/sentry/sidekiq/sentry_context_middleware.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
require 'sentry/sidekiq/context_filter'
module Sentry
module Sidekiq
class SentryContextServerMiddleware
- OP_NAME = "queue.sidekiq".freeze
+ OP_NAME = "queue.sidekiq"
+ SPAN_ORIGIN = "auto.queue.sidekiq"
def call(_worker, job, queue)
return yield unless Sentry.initialized?
@@ -40,7 +43,13 @@ def build_tags(tags)
end
def start_transaction(scope, env)
- options = { name: scope.transaction_name, source: scope.transaction_source, op: OP_NAME }
+ options = {
+ name: scope.transaction_name,
+ source: scope.transaction_source,
+ op: OP_NAME,
+ origin: SPAN_ORIGIN
+ }
+
transaction = Sentry.continue_trace(env, **options)
Sentry.start_transaction(transaction: transaction, **options)
end
diff --git a/sentry-sidekiq/lib/sentry/sidekiq/version.rb b/sentry-sidekiq/lib/sentry/sidekiq/version.rb
index 7b62c57ec..b127d83ba 100644
--- a/sentry-sidekiq/lib/sentry/sidekiq/version.rb
+++ b/sentry-sidekiq/lib/sentry/sidekiq/version.rb
@@ -1,5 +1,5 @@
module Sentry
module Sidekiq
- VERSION = "5.17.3"
+ VERSION = "5.19.0"
end
end
diff --git a/sentry-sidekiq/sentry-sidekiq.gemspec b/sentry-sidekiq/sentry-sidekiq.gemspec
index 56c43a71c..56f737887 100644
--- a/sentry-sidekiq/sentry-sidekiq.gemspec
+++ b/sentry-sidekiq/sentry-sidekiq.gemspec
@@ -7,21 +7,27 @@ Gem::Specification.new do |spec|
spec.description = spec.summary = "A gem that provides Sidekiq integration for the Sentry error logger"
spec.email = "accounts@sentry.io"
spec.license = 'MIT'
- spec.homepage = "https://github.com/getsentry/sentry-ruby"
spec.platform = Gem::Platform::RUBY
spec.required_ruby_version = '>= 2.4'
spec.extra_rdoc_files = ["README.md", "LICENSE.txt"]
spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples)'`.split("\n")
- spec.metadata["homepage_uri"] = spec.homepage
- spec.metadata["source_code_uri"] = spec.homepage
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
+ github_root_uri = 'https://github.com/getsentry/sentry-ruby'
+ spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}"
+
+ spec.metadata = {
+ "homepage_uri" => spec.homepage,
+ "source_code_uri" => spec.homepage,
+ "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md",
+ "bug_tracker_uri" => "#{github_root_uri}/issues",
+ "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}"
+ }
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
- spec.add_dependency "sentry-ruby", "~> 5.17.3"
+ spec.add_dependency "sentry-ruby", "~> 5.19.0"
spec.add_dependency "sidekiq", ">= 3.0"
end
diff --git a/sentry-sidekiq/spec/sentry/rails_spec.rb b/sentry-sidekiq/spec/sentry/rails_spec.rb
index 903f8ab38..87518ea06 100644
--- a/sentry-sidekiq/spec/sentry/rails_spec.rb
+++ b/sentry-sidekiq/spec/sentry/rails_spec.rb
@@ -4,6 +4,8 @@
require "sentry-rails"
require "spec_helper"
+require "action_controller/railtie"
+
class TestApp < Rails::Application
end
diff --git a/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb b/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb
index f7c1a21a3..8d28577e7 100644
--- a/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb
+++ b/sentry-sidekiq/spec/sentry/sidekiq/sentry_context_middleware_spec.rb
@@ -57,6 +57,12 @@
expect(event.tags.keys).to include(:"sidekiq.marvel", :"sidekiq.dc")
end
+ it "has the correct origin" do
+ execute_worker(processor, TagsWorker)
+ transaction = transport.events.last
+ expect(transaction.contexts.dig(:trace, :origin)).to eq('auto.queue.sidekiq')
+ end
+
context "with trace_propagation_headers" do
let(:parent_transaction) { Sentry.start_transaction(op: "sidekiq") }