Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions sentry-ruby/lib/sentry/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 = []
Expand All @@ -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
Expand Down Expand Up @@ -595,6 +606,9 @@ def initialize

yield(self) if block_given?

log_deprecations
backfill_data_collection

run_callbacks(:after, :configured)
end

Expand Down Expand Up @@ -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(
Expand Down
138 changes: 138 additions & 0 deletions sentry-ruby/lib/sentry/data_collection.rb
Original file line number Diff line number Diff line change
@@ -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<String>, 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<Symbol>] 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
55 changes: 55 additions & 0 deletions sentry-ruby/spec/sentry/configuration_spec.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
64 changes: 64 additions & 0 deletions sentry-ruby/spec/sentry/data_collection_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading