diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index 1f29d1854..0dfd639ea 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -16,6 +16,7 @@ require "sentry/structured_logger" require "sentry/log_event_buffer" require "sentry/metric_event_buffer" +require "sentry/data_collection" module Sentry class Configuration @@ -132,6 +133,7 @@ class Configuration attr_accessor :max_breadcrumbs # Number of lines of code context to capture, or nil for none + # @deprecated Use {#data_collection} and `frame_context_lines` instead. # @return [Integer, nil] attr_accessor :context_lines @@ -167,6 +169,7 @@ class Configuration alias inspect_exception_causes_for_exclusion? inspect_exception_causes_for_exclusion # Whether to capture local variables from the raised exception's frame. Default is false. + # @deprecated Use {#data_collection} and `stack_frame_variables` instead. # @return [Boolean] attr_accessor :include_local_variables @@ -232,9 +235,15 @@ class Configuration # - request body # - query string # will not be sent to Sentry. + # @deprecated Use {#data_collection} instead. # @return [Boolean] attr_accessor :send_default_pii + # Controls which categories of data may be collected. + # Replacement for send_default_pii. + # @return [DataCollection] + attr_accessor :data_collection + # Capture queue time from X-Request-Start header set by reverse proxies. # Works with any Rack app behind Nginx, HAProxy, Heroku router, etc. # Defaults to true. @@ -536,6 +545,7 @@ def initialize self.breadcrumbs_logger = [] self.context_lines = 3 self.include_local_variables = false + self.environment = environment_from_env self.enabled_environments = nil self.exclude_loggers = [] @@ -549,6 +559,7 @@ def initialize self.sample_rate = 1.0 self.send_modules = true self.send_default_pii = false + self.data_collection = DataCollection.new self.skip_rake_integration = false self.send_client_reports = true self.auto_session_tracking = true @@ -595,6 +606,9 @@ def initialize yield(self) if block_given? + log_deprecations + backfill_data_collection + run_callbacks(:after, :configured) end @@ -803,6 +817,35 @@ def csp_report_uri end end + # @api private + def log_deprecations + log_warn("`send_default_pii` is deprecated; use `data_collection` instead.") if self.send_default_pii + log_warn("`include_local_variables` is deprecated; use `data_collection.stack_frame_variables` instead.") if include_local_variables + log_warn("`context_lines` is deprecated; use `data_collection.frame_context_lines` instead.") if context_lines != 3 + end + + # Updates data collection defaults for applications still using send_default_pii. + # This is intentionally called after the user's configuration block has run. + # @api private + def backfill_data_collection + # the new DataCollection defaults are already correct if pii is enabled + return if self.send_default_pii + + # TODO-neel-data map to exact ruby behaviour for backwards compat behavior + self.data_collection.user_info = false + self.data_collection.cookies.mode = :off + self.data_collection.http_headers.request.mode = :off + self.data_collection.http_headers.response.mode = :off + self.data_collection.http_bodies = [] + self.data_collection.url_query_params.mode = :off + self.data_collection.graphql.document = false + self.data_collection.graphql.variables = false + self.data_collection.database_query_data = false + self.data_collection.queues = false + self.data_collection.stack_frame_variables = self.include_local_variables + self.data_collection.frame_context_lines = self.context_lines + end + # @api private def stacktrace_builder @stacktrace_builder ||= StacktraceBuilder.new( diff --git a/sentry-ruby/lib/sentry/data_collection.rb b/sentry-ruby/lib/sentry/data_collection.rb new file mode 100644 index 000000000..60ca15920 --- /dev/null +++ b/sentry-ruby/lib/sentry/data_collection.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +module Sentry + class DataCollection + # Configuration for the categories of data collected by the SDK. + # Replacement for send_default_pii. + # Spec: https://develop.sentry.dev/sdk/foundations/client/data-collection/ + # + # @example Configure data collection + # Sentry.init do |config| + # data_collection = config.data_collection + # data_collection.user_info = true + # data_collection.cookies.mode = :deny_list + # data_collection.cookies.terms = ["session", "token"] + # data_collection.http_headers.request.mode = :deny_list + # data_collection.http_headers.request.terms = nil + # data_collection.http_headers.response.mode = :allow_list + # data_collection.http_headers.response.terms = ["my_special_header"] + # data_collection.http_bodies = [:incoming_request] + # data_collection.url_query_params.mode = :allow_list + # data_collection.url_query_params.terms = ["page", "limit"] + # data_collection.graphql.document = true + # data_collection.graphql.variables = true + # data_collection.database_query_data = true + # data_collection.queues = true + # data_collection.stack_frame_variables = true + # data_collection.frame_context_lines = 5 + # end + MODES = %i[off deny_list allow_list].freeze + BODY_TYPES = %i[ + incoming_request + outgoing_request + incoming_response + outgoing_response + ].freeze + + # Configuration for key-value data collection. + class KeyValueCollection + # `mode` controls whether values are collected: + # - `:off` disables collection. + # - `:deny_list` collects values except those matching `terms`. + # - `:allow_list` collects only values matching `terms`. + # @return [:off, :deny_list, :allow_list] + attr_accessor :mode + + # `terms` contains the keys or patterns used by the selected mode. + # @return [Array, nil] + attr_accessor :terms + + def initialize(mode:, terms:) + @mode = mode + @terms = terms + end + end + + class HttpHeaders + # @return [KeyValueCollection] + attr_accessor :request + + # @return [KeyValueCollection] + attr_accessor :response + + def initialize(request:, response:) + @request = request + @response = response + end + end + + class GraphQL + # @return [Boolean] + attr_accessor :document + + # @return [Boolean] + attr_accessor :variables + + def initialize(document:, variables:) + @document = document + @variables = variables + end + end + + # @return [Boolean] + # @default `true` + attr_accessor :user_info + + # @return [KeyValueCollection] + # @default `mode: :deny_list, terms: nil` + attr_accessor :cookies + + # @return [HttpHeaders] + # @default request and response use `mode: :deny_list, terms: nil` + attr_accessor :http_headers + + # @return [Array] containing values from BODY_TYPES + # @default `nil` (all valid body types) + attr_accessor :http_bodies + + # @return [KeyValueCollection] + # @default `mode: :deny_list, terms: nil` + attr_accessor :url_query_params + + # @return [Boolean] + # @default `true` + attr_accessor :database_query_data + + # @return [GraphQL] + # @default `document: true, variables: true` + attr_accessor :graphql + + # @return [Boolean] + # @default `true` + attr_accessor :queues + + # @return [Boolean] + # @default `false` + attr_accessor :stack_frame_variables + + # @return [Integer] + # @default `3` + attr_accessor :frame_context_lines + + def initialize + @user_info = true + @cookies = KeyValueCollection.new(mode: :deny_list, terms: nil) + @http_headers = HttpHeaders.new( + request: KeyValueCollection.new(mode: :deny_list, terms: nil), + response: KeyValueCollection.new(mode: :deny_list, terms: nil) + ) + @http_bodies = nil + @url_query_params = KeyValueCollection.new(mode: :deny_list, terms: nil) + @database_query_data = true + @graphql = GraphQL.new(document: true, variables: true) + @queues = true + @stack_frame_variables = false + @frame_context_lines = 3 + end + end +end diff --git a/sentry-ruby/spec/sentry/configuration_spec.rb b/sentry-ruby/spec/sentry/configuration_spec.rb index 0b147417d..aeff8b645 100644 --- a/sentry-ruby/spec/sentry/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/configuration_spec.rb @@ -1,6 +1,61 @@ # frozen_string_literal: true RSpec.describe Sentry::Configuration do + describe "#data_collection" do + it "uses the send_default_pii=false defaults" do + expect(subject.data_collection.user_info).to eq(false) + expect(subject.data_collection.cookies.mode).to eq(:off) + expect(subject.data_collection.http_headers.request.mode).to eq(:off) + expect(subject.data_collection.http_headers.response.mode).to eq(:off) + expect(subject.data_collection.http_bodies).to eq([]) + expect(subject.data_collection.url_query_params.mode).to eq(:off) + expect(subject.data_collection.database_query_data).to eq(false) + expect(subject.data_collection.graphql.document).to eq(false) + expect(subject.data_collection.graphql.variables).to eq(false) + expect(subject.data_collection.queues).to eq(false) + expect(subject.data_collection.stack_frame_variables).to eq(false) + expect(subject.data_collection.frame_context_lines).to eq(3) + end + + it "does not backfill data collection when send_default_pii is enabled" do + configuration = Sentry::Configuration.new do |config| + config.send_default_pii = true + config.data_collection.user_info = false + end + + expect(configuration.data_collection.user_info).to eq(false) + end + end + + describe "deprecated configuration warnings" do + it "warns when send_default_pii is enabled" do + allow(subject).to receive(:log_warn) + subject.send_default_pii = true + + subject.send(:log_deprecations) + + expect(subject).to have_received(:log_warn).with("`send_default_pii` is deprecated; use `data_collection` instead.") + end + + it "warns when include_local_variables is enabled" do + allow(subject).to receive(:log_warn) + subject.include_local_variables = true + + subject.send(:log_deprecations) + + expect(subject).to have_received(:log_warn).with("`include_local_variables` is deprecated; use `data_collection.stack_frame_variables` instead.") + end + + it "warns when context_lines is changed" do + allow(subject).to receive(:log_warn) + subject.context_lines = 4 + + subject.send(:log_deprecations) + + expect(subject).to have_received(:log_warn).with("`context_lines` is deprecated; use `data_collection.frame_context_lines` instead.") + 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) diff --git a/sentry-ruby/spec/sentry/data_collection_spec.rb b/sentry-ruby/spec/sentry/data_collection_spec.rb new file mode 100644 index 000000000..6acd55a66 --- /dev/null +++ b/sentry-ruby/spec/sentry/data_collection_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +RSpec.describe Sentry::DataCollection do + subject(:data_collection) { described_class.new } + + describe "defaults" do + it "uses the defaults from the Data Collection specification" do + expect(data_collection.user_info).to eq(true) + expect(data_collection.cookies.mode).to eq(:deny_list) + expect(data_collection.cookies.terms).to be_nil + expect(data_collection.http_headers.request.mode).to eq(:deny_list) + expect(data_collection.http_headers.request.terms).to be_nil + expect(data_collection.http_headers.response.mode).to eq(:deny_list) + expect(data_collection.http_headers.response.terms).to be_nil + expect(data_collection.http_bodies).to be_nil + expect(data_collection.url_query_params.mode).to eq(:deny_list) + expect(data_collection.url_query_params.terms).to be_nil + expect(data_collection.database_query_data).to eq(true) + expect(data_collection.graphql.document).to eq(true) + expect(data_collection.graphql.variables).to eq(true) + expect(data_collection.queues).to eq(true) + expect(data_collection.stack_frame_variables).to eq(false) + expect(data_collection.frame_context_lines).to eq(3) + end + end + + describe "constants" do + it "defines the supported modes" do + expect(described_class::MODES).to eq(%i[off deny_list allow_list]) + end + + it "defines the supported body types" do + expect(described_class::BODY_TYPES).to eq( + %i[incoming_request outgoing_request incoming_response outgoing_response] + ) + end + end + + describe "nested configuration objects" do + it "supports configuring key-value collection modes and terms" do + data_collection.cookies.mode = :allow_list + data_collection.cookies.terms = ["page"] + + expect(data_collection.cookies.mode).to eq(:allow_list) + expect(data_collection.cookies.terms).to eq(["page"]) + end + + it "supports configuring request and response headers independently" do + data_collection.http_headers.request.mode = :off + data_collection.http_headers.response.terms = ["x-request-id"] + + expect(data_collection.http_headers.request.mode).to eq(:off) + expect(data_collection.http_headers.response.terms).to eq(["x-request-id"]) + end + + it "supports configuring GraphQL fields independently" do + data_collection.graphql.document = false + data_collection.graphql.variables = false + + expect(data_collection.graphql.document).to eq(false) + expect(data_collection.graphql.variables).to eq(false) + end + end +end