Skip to content
Merged
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
2 changes: 1 addition & 1 deletion sentry-ruby/lib/sentry/baggage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def initialize(items, mutable: true)
# The presence of a Sentry item makes the baggage object immutable.
#
# @param header [String] The incoming Baggage header string.
# @return [Baggage, nil]
# @return [Baggage]
def self.from_incoming_header(header)
items = {}
mutable = true
Expand Down
30 changes: 30 additions & 0 deletions sentry-ruby/lib/sentry/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,24 @@ class Configuration
# @return [Proc, nil]
attr_reader :std_lib_logger_filter

# An optional organization ID. The SDK will try to extract it from the DSN in most cases
# but you can provide it explicitly for self-hosted and Relay setups.
# This value is used for trace propagation and for features like strict_trace_continuation.
# @return [String, nil]
attr_reader :org_id

# If set to true, the SDK will only continue a trace if the org_id of the incoming trace found in the
# baggage header matches the org_id of the current Sentry client and only if BOTH are present.
#
# If set to false, consistency of org_id will only be enforced if both are present.
# If either are missing, the trace will be continued.
#
# The client's organization ID is extracted from the DSN or can be set with the org_id option.
# If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one.
# This is useful to prevent traces of unknown third-party services from being continued in your application.
# @return [Boolean]
attr_accessor :strict_trace_continuation

# these are not config options
# @!visibility private
attr_reader :errors, :gem_specs
Expand Down Expand Up @@ -520,6 +538,8 @@ def initialize
self.trusted_proxies = []
self.dsn = ENV["SENTRY_DSN"]
self.capture_queue_time = true
self.org_id = nil
self.strict_trace_continuation = false

spotlight_env = ENV["SENTRY_SPOTLIGHT"]
spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true)
Expand Down Expand Up @@ -673,6 +693,16 @@ def profiler_class=(profiler_class)
@profiler_class = profiler_class
end

def org_id=(value)
@org_id = value&.to_s
end

# Returns the effective org ID, preferring the explicit config option over the DSN-parsed value.
# @return [String, nil]
def effective_org_id
org_id || dsn&.org_id
end

def sending_allowed?
spotlight || sending_to_dsn_allowed?
end
Expand Down
14 changes: 13 additions & 1 deletion sentry-ruby/lib/sentry/dsn.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ class DSN
REQUIRED_ATTRIBUTES = %w[host path public_key project_id].freeze
LOCALHOST_NAMES = %w[localhost 127.0.0.1 ::1 [::1]].freeze
LOCALHOST_PATTERN = /\.local(host|domain)?$/i
ORG_ID_REGEX = /\Ao(\d+)\./

attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES
attr_reader :scheme, :secret_key, :port, :org_id, *REQUIRED_ATTRIBUTES

def initialize(dsn_string)
@raw_value = dsn_string
Expand All @@ -31,6 +32,8 @@ def initialize(dsn_string)
@host = uri.host
@port = uri.port if uri.port
@path = uri_path.join("/")

@org_id = extract_org_id_from_host
end

def valid?
Expand Down Expand Up @@ -101,5 +104,14 @@ def generate_auth_header(client: nil)

"Sentry " + fields.map { |key, value| "#{key}=#{value}" }.join(", ")
end

private

def extract_org_id_from_host
return nil unless @host

match = ORG_ID_REGEX.match(@host)
match ? match[1] : nil
end
end
end
56 changes: 48 additions & 8 deletions sentry-ruby/lib/sentry/propagation_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,44 @@ def self.extract_sentry_trace(sentry_trace)
[trace_id, parent_span_id, parent_sampled]
end

# Determines whether we should continue an incoming trace based on org_id matching
# and the strict_trace_continuation configuration option.
#
# @param incoming_baggage [Baggage] the baggage from the incoming request
# @return [Boolean]
def self.should_continue_trace?(incoming_baggage)
return true unless Sentry.initialized?

configuration = Sentry.configuration
sdk_org_id = configuration.effective_org_id
baggage_org_id = incoming_baggage.items["org_id"]

# Mismatched org IDs always start a new trace regardless of strict mode
if sdk_org_id && baggage_org_id && sdk_org_id != baggage_org_id
Sentry.sdk_logger.debug(LOGGER_PROGNAME) do
"Starting a new trace because org IDs don't match (incoming baggage org_id: #{baggage_org_id}, SDK org_id: #{sdk_org_id})"
end

return false
end

return true unless configuration.strict_trace_continuation

# In strict mode, both must be present and match (unless both are missing)
if sdk_org_id.nil? && baggage_org_id.nil?
true
elsif sdk_org_id.nil? || baggage_org_id.nil?
Sentry.sdk_logger.debug(LOGGER_PROGNAME) do
"Starting a new trace because strict trace continuation is enabled and one org ID is missing " \
"(incoming baggage org_id: #{baggage_org_id.inspect}, SDK org_id: #{sdk_org_id.inspect})"
end

false
else
true
end
end

def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
return unless baggage&.items

Expand Down Expand Up @@ -96,9 +134,7 @@ def initialize(scope, env = nil)
sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)

if sentry_trace_data
@trace_id, @parent_span_id, @parent_sampled = sentry_trace_data

@baggage =
incoming_baggage =
if baggage_header && !baggage_header.empty?
Baggage.from_incoming_header(baggage_header)
else
Expand All @@ -108,10 +144,13 @@ def initialize(scope, env = nil)
Baggage.new({})
end

@sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)

@baggage.freeze!
@incoming_trace = true
if self.class.should_continue_trace?(incoming_baggage)
@trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
@baggage = incoming_baggage
@sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)
@baggage.freeze!
@incoming_trace = true
end
end
end
end
Expand Down Expand Up @@ -162,7 +201,8 @@ def populate_head_baggage
"sample_rand" => Utils::SampleRand.format(@sample_rand),
"environment" => configuration.environment,
"release" => configuration.release,
"public_key" => configuration.dsn&.public_key
"public_key" => configuration.dsn&.public_key,
"org_id" => configuration.effective_org_id
}

items.compact!
Expand Down
3 changes: 2 additions & 1 deletion sentry-ruby/lib/sentry/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ def populate_head_baggage
"sampled" => sampled&.to_s,
"environment" => configuration&.environment,
"release" => configuration&.release,
"public_key" => configuration&.dsn&.public_key
"public_key" => configuration&.dsn&.public_key,
"org_id" => configuration&.effective_org_id
}

items["transaction"] = name unless source_low_quality?
Expand Down
22 changes: 22 additions & 0 deletions sentry-ruby/spec/sentry/dsn_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@
end
end

describe "#org_id" do
it "extracts org_id from DSN host with org prefix" do
dsn = described_class.new("https://key@o1234.ingest.sentry.io/42")
expect(dsn.org_id).to eq("1234")
end

it "extracts single digit org_id" do
dsn = described_class.new("https://key@o1.ingest.us.sentry.io/42")
expect(dsn.org_id).to eq("1")
end

it "returns nil when host does not have org prefix" do
dsn = described_class.new("http://12345:67890@sentry.localdomain:3000/sentry/42")
expect(dsn.org_id).to be_nil
end

it "returns nil for non-standard host without o prefix" do
dsn = described_class.new("https://key@not_org_id.ingest.sentry.io/42")
expect(dsn.org_id).to be_nil
end
end

describe "#local?" do
it "returns true for localhost" do
expect(described_class.new("http://12345:67890@localhost/sentry/42").local?).to eq(true)
Expand Down
3 changes: 2 additions & 1 deletion sentry-ruby/spec/sentry/net/http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@
"sentry-sample_rand=#{Sentry::Utils::SampleRand.format(transaction.sample_rand)},"\
"sentry-sampled=true,"\
"sentry-environment=development,"\
"sentry-public_key=foobarbaz"
"sentry-public_key=foobarbaz,"\
"sentry-org_id=447951"
)
end

Expand Down
Loading
Loading