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
46 changes: 46 additions & 0 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,29 @@ defmodule Sentry.Config do
]
]
],
org_id: [
type: {:or, [:string, nil]},
default: nil,
type_doc: "`t:String.t/0` or `nil`",
doc: """
An explicit organization ID for trace continuation validation. If not set, the SDK
will extract it from the DSN host (e.g., `o1234` from `o1234.ingest.sentry.io` gives `"1234"`).
This is useful for self-hosted Sentry or Relay setups where the org ID cannot be extracted
from the DSN. *Available since 12.1.0*.
"""
],
strict_trace_continuation: [
type: :boolean,
default: false,
doc: """
When `true`, both the SDK's org ID and the incoming baggage `sentry-org_id` must be present
and match for a trace to be continued. Traces with a missing org ID on either side are rejected
and a new trace is started. When `false` (the default), only a mismatch between two present
org IDs will cause a new trace to be started. See the
[SDK spec](https://develop.sentry.dev/sdk/foundations/trace-propagation/#strict-trace-continuation)
for the full decision matrix. *Available since 12.1.0*.
"""
],
telemetry_processor_categories: [
type: {:list, {:in, [:error, :check_in, :transaction, :log]}},
default: [:log],
Expand Down Expand Up @@ -926,6 +949,29 @@ defmodule Sentry.Config do
@spec transport_capacity() :: pos_integer()
def transport_capacity, do: fetch!(:transport_capacity)

@spec org_id() :: String.t() | nil
def org_id, do: get(:org_id)

@spec strict_trace_continuation?() :: boolean()
def strict_trace_continuation?, do: fetch!(:strict_trace_continuation)

@doc """
Returns the effective org ID, preferring the explicit `:org_id` config over the DSN-derived value.
"""
@spec effective_org_id() :: String.t() | nil
def effective_org_id do
case org_id() do
nil ->
case dsn() do
%Sentry.DSN{org_id: org_id} -> org_id
_ -> nil
end

explicit ->
explicit
end
end

@spec telemetry_processor_categories() :: [atom()]
def telemetry_processor_categories, do: fetch!(:telemetry_processor_categories)

Expand Down
19 changes: 16 additions & 3 deletions lib/sentry/dsn.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ defmodule Sentry.DSN do
original_dsn: String.t(),
endpoint_uri: String.t(),
public_key: String.t(),
secret_key: String.t() | nil
secret_key: String.t() | nil,
org_id: String.t() | nil
}

defstruct [
:original_dsn,
:endpoint_uri,
:public_key,
:secret_key
:secret_key,
:org_id
]

# {PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}{PATH}/{PROJECT_ID}
Expand Down Expand Up @@ -65,7 +67,8 @@ defmodule Sentry.DSN do
endpoint_uri: URI.to_string(endpoint_uri),
public_key: public_key,
secret_key: secret_key,
original_dsn: dsn
original_dsn: dsn,
org_id: extract_org_id(uri.host)
}

{:ok, parsed_dsn}
Expand All @@ -80,6 +83,16 @@ defmodule Sentry.DSN do

## Helpers

# Extract org ID from host (e.g., "o123.ingest.sentry.io" -> "123")
defp extract_org_id(host) when is_binary(host) do
case Regex.run(~r/^o(\d+)\./, host) do
[_, org_id] -> org_id
_ -> nil
end
end

defp extract_org_id(_host), do: nil

defp pop_project_id(uri_path) do
path = String.split(uri_path, "/")
{project_id, path} = List.pop_at(path, -1)
Expand Down
89 changes: 79 additions & 10 deletions lib/sentry/opentelemetry/propagator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
carrier = setter.(@sentry_trace_key, sentry_trace_header, carrier)

baggage_value = :otel_ctx.get_value(ctx, @sentry_baggage_ctx_key, :not_found)
baggage_value = ensure_org_id_in_baggage(baggage_value)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just try to extract it already, instead of checking if the string is present and then parsing/extracting.


if is_binary(baggage_value) and baggage_value != :not_found do
setter.(@sentry_baggage_key, baggage_value, carrier)
Expand All @@ -56,19 +57,27 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
header when is_binary(header) ->
case decode_sentry_trace(header) do
{:ok, {trace_hex, span_hex, sampled}} ->
ctx =
ctx
|> :otel_ctx.set_value(@sentry_trace_ctx_key, {trace_hex, span_hex, sampled})
|> maybe_set_baggage(getter.(@sentry_baggage_key, carrier))
raw_baggage = getter.(@sentry_baggage_key, carrier)

if should_continue_trace?(raw_baggage) do
ctx =
ctx
|> :otel_ctx.set_value(@sentry_trace_ctx_key, {trace_hex, span_hex, sampled})
|> maybe_set_baggage(raw_baggage)

trace_id = hex_to_int(trace_hex)
span_id = hex_to_int(span_hex)
trace_id = hex_to_int(trace_hex)
span_id = hex_to_int(span_hex)

# Create a remote, sampled parent span in the OTEL context.
# We will set to "always sample" because Sentry will decide real sampling
remote_span_ctx = :otel_tracer.from_remote_span(trace_id, span_id, 1)
# Create a remote, sampled parent span in the OTEL context.
# We will set to "always sample" because Sentry will decide real sampling
remote_span_ctx = :otel_tracer.from_remote_span(trace_id, span_id, 1)

Tracer.set_current_span(ctx, remote_span_ctx)
Tracer.set_current_span(ctx, remote_span_ctx)
else
require Logger
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must be in the module module at the top, not inside the function.

Logger.debug("[Sentry] Not continuing trace due to org ID mismatch")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Logger.debug("[Sentry] Not continuing trace due to org ID mismatch")
Logger.warning("[Sentry] Not continuing trace due to org ID mismatch")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably including the other org id could be useful?

ctx
end

{:error, _reason} ->
ctx
Expand Down Expand Up @@ -131,5 +140,65 @@ if Sentry.OpenTelemetry.VersionChecker.tracing_compatible?() do
missing = total_bytes - byte_size(bin)
if missing > 0, do: :binary.copy(<<0>>, missing) <> bin, else: bin
end

# Ensure sentry-org_id is present in the baggage string
defp ensure_org_id_in_baggage(baggage) when is_binary(baggage) do
org_id = Sentry.Config.effective_org_id()

if org_id != nil and not String.contains?(baggage, "sentry-org_id=") do
baggage <> ",sentry-org_id=" <> org_id
else
baggage
end
end

defp ensure_org_id_in_baggage(_baggage) do
case Sentry.Config.effective_org_id() do
nil -> :not_found
org_id -> "sentry-org_id=" <> org_id
end
end

# Extract sentry-org_id from a baggage header string
defp extract_baggage_org_id(baggage) when is_binary(baggage) do
baggage
|> String.split(",")
|> Enum.find_value(fn entry ->
case String.split(String.trim(entry), "=", parts: 2) do
["sentry-org_id", value] ->
trimmed = String.trim(value)
if trimmed == "", do: nil, else: trimmed

_ ->
nil
end
end)
end

defp extract_baggage_org_id(_), do: nil

# Determine whether to continue an incoming trace based on org_id validation
@doc false
def should_continue_trace?(raw_baggage) do
sdk_org_id = Sentry.Config.effective_org_id()
baggage_org_id = extract_baggage_org_id(raw_baggage)
strict = Sentry.Config.strict_trace_continuation?()

cond do
# Mismatched org IDs always reject
sdk_org_id != nil and baggage_org_id != nil and sdk_org_id != baggage_org_id ->
false

# In strict mode, both must be present and match (unless both are missing)
strict and sdk_org_id == nil and baggage_org_id == nil ->
true

strict ->
sdk_org_id != nil and sdk_org_id == baggage_org_id

true ->
true
end
end
end
end
Loading
Loading