Skip to content
9 changes: 1 addition & 8 deletions lib/sentry/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,12 @@ defmodule Sentry.Client do
@spec send_transaction(Transaction.t(), keyword()) ::
{:ok, transaction_id :: String.t()}
| {:error, ClientError.t()}
| :unsampled
| :excluded
def send_transaction(%Transaction{} = transaction, opts \\ []) do
opts = NimbleOptions.validate!(opts, Options.send_transaction_schema())

result_type = Keyword.get_lazy(opts, :result, &Config.send_result/0)
client = Keyword.get_lazy(opts, :client, &Config.client/0)
sample_rate = Keyword.get_lazy(opts, :sample_rate, &Config.traces_sample_rate/0)
before_send = Keyword.get_lazy(opts, :before_send, &Config.before_send/0)
after_send_event = Keyword.get_lazy(opts, :after_send_event, &Config.after_send_event/0)

Expand All @@ -126,16 +124,11 @@ defmodule Sentry.Client do
Application.get_env(:sentry, :request_retries, Transport.default_retries())
end)

with :ok <- sample_event(sample_rate),
{:ok, %Transaction{} = transaction} <- maybe_call_before_send(transaction, before_send) do
with {:ok, %Transaction{} = transaction} <- maybe_call_before_send(transaction, before_send) do
send_result = encode_and_send(transaction, result_type, client, request_retries)
_ignored = maybe_call_after_send(transaction, send_result, after_send_event)
send_result
else
:unsampled ->
ClientReport.Sender.record_discarded_events(:sample_rate, [transaction])
:unsampled

:excluded ->
:excluded
end
Expand Down
11 changes: 9 additions & 2 deletions lib/sentry/client_report/sender.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@ defmodule Sentry.ClientReport.Sender do
GenServer.start_link(__MODULE__, nil, name: Keyword.get(opts, :name, __MODULE__))
end

def record_discarded_events(reason, info, genserver \\ __MODULE__)

@spec record_discarded_events(atom(), String.t(), GenServer.server()) :: :ok
def record_discarded_events(reason, data_category, genserver)
when is_binary(data_category) do
GenServer.cast(genserver, {:record_discarded_events, reason, data_category})
end

@spec record_discarded_events(atom(), [item], GenServer.server()) :: :ok
when item:
Sentry.Attachment.t()
| Sentry.CheckIn.t()
| ClientReport.t()
| Sentry.Event.t()
| Sentry.Transaction.t()
def record_discarded_events(reason, event_items, genserver \\ __MODULE__)
def record_discarded_events(reason, event_items, genserver)
when is_list(event_items) do
# We silently ignore events whose reasons aren't valid because we have to add it to the allowlist in Snuba
# https://develop.sentry.dev/sdk/client-reports/
Expand Down
19 changes: 10 additions & 9 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,13 @@ defmodule Sentry.Config do
],
traces_sample_rate: [
type: {:custom, __MODULE__, :__validate_traces_sample_rate__, []},
default: 0.0,
default: nil,
doc: """
The sample rate for transaction events. A value between `0.0` and `1.0` (inclusive).
A value of `0.0` means no transactions will be sampled, while `1.0` means all transactions
will be sampled. This value is also used to determine if tracing is enabled: if it's
greater than `0`, tracing is enabled.
will be sampled.

This value is also used to determine if tracing is enabled: if it's not `nil`, tracing is enabled.

Tracing requires OpenTelemetry packages to work. See [the
OpenTelemetry setup documentation](https://opentelemetry.io/docs/languages/erlang/getting-started/)
Expand Down Expand Up @@ -621,7 +622,7 @@ defmodule Sentry.Config do
@spec sample_rate() :: float()
def sample_rate, do: fetch!(:sample_rate)

@spec traces_sample_rate() :: float()
@spec traces_sample_rate() :: nil | float()
def traces_sample_rate, do: fetch!(:traces_sample_rate)

@spec hackney_opts() :: keyword()
Expand Down Expand Up @@ -662,7 +663,7 @@ defmodule Sentry.Config do
def integrations, do: fetch!(:integrations)

@spec tracing?() :: boolean()
def tracing?, do: fetch!(:traces_sample_rate) > 0.0
def tracing?, do: not is_nil(fetch!(:traces_sample_rate))

@spec put_config(atom(), term()) :: :ok
def put_config(key, value) when is_atom(key) do
Expand Down Expand Up @@ -763,12 +764,12 @@ defmodule Sentry.Config do
end
end

def __validate_traces_sample_rate__(float) do
if is_float(float) and float >= 0.0 and float <= 1.0 do
{:ok, float}
def __validate_traces_sample_rate__(value) do
if is_nil(value) or (is_float(value) and value >= 0.0 and value <= 1.0) do
{:ok, value}
else
{:error,
"expected :traces_sample_rate to be a float between 0.0 and 1.0 (included), got: #{inspect(float)}"}
"expected :traces_sample_rate to be nil or a value between 0.0 and 1.0 (included), got: #{inspect(value)}"}
end
end

Expand Down
107 changes: 102 additions & 5 deletions lib/sentry/opentelemetry/sampler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,127 @@ if Code.ensure_loaded?(:otel_sampler) do
defmodule Sentry.OpenTelemetry.Sampler do
@moduledoc false

alias OpenTelemetry.{Span, Tracer}
alias Sentry.ClientReport

@behaviour :otel_sampler

@sentry_sample_rate_key "sentry-sample_rate"
@sentry_sample_rand_key "sentry-sample_rand"
@sentry_sampled_key "sentry-sampled"

@impl true
def setup(config) do
config
end

@impl true
def description(_) do
"SentrySampler"
end

@impl true
def should_sample(
_ctx,
ctx,
_trace_id,
_links,
span_name,
_span_kind,
_attributes,
config
) do
if span_name in config[:drop] do
{:drop, [], []}
else
{:record_and_sample, [], []}
result =
if span_name in config[:drop] do
{:drop, [], []}
else
sample_rate = Sentry.Config.traces_sample_rate()

case get_trace_sampling_decision(ctx) do
{:inherit, trace_sampled, tracestate} ->
decision = if trace_sampled, do: :record_and_sample, else: :drop

{decision, [], tracestate}

:no_trace ->
make_sampling_decision(sample_rate)
end
end

case result do
{:drop, _, _} ->
record_discarded_transaction()
result

_ ->
result
end
end

defp get_trace_sampling_decision(ctx) do
case Tracer.current_span_ctx(ctx) do
:undefined ->
:no_trace

span_ctx ->
tracestate = Span.tracestate(span_ctx)
trace_sampled = get_tracestate_value(tracestate, @sentry_sampled_key)

case trace_sampled do
"true" ->
{:inherit, true, tracestate}

"false" ->
{:inherit, false, tracestate}

nil ->
:no_trace
end
end
end

defp make_sampling_decision(sample_rate) do
cond do
is_nil(sample_rate) ->
{:drop, [], []}

sample_rate == 0.0 ->
tracestate = build_tracestate(sample_rate, 1.0, false)
{:drop, [], tracestate}

sample_rate == 1.0 ->
tracestate = build_tracestate(sample_rate, 0.0, true)
{:record_and_sample, [], tracestate}

true ->
random_value = :rand.uniform()
sampled = random_value < sample_rate
tracestate = build_tracestate(sample_rate, random_value, sampled)
decision = if sampled, do: :record_and_sample, else: :drop
{decision, [], tracestate}
end
end

defp build_tracestate(sample_rate, random_value, sampled) do
[
{@sentry_sample_rate_key, Float.to_string(sample_rate)},
{@sentry_sample_rand_key, Float.to_string(random_value)},
{@sentry_sampled_key, to_string(sampled)}
]
end

defp get_tracestate_value({:tracestate, tracestate}, key) do
get_tracestate_value(tracestate, key)
end

defp get_tracestate_value(tracestate, key) when is_list(tracestate) do
case List.keyfind(tracestate, key, 0) do
{^key, value} -> value
nil -> nil
end
end

defp record_discarded_transaction() do
ClientReport.Sender.record_discarded_events(:sample_rate, "transaction")
end
end
end
3 changes: 3 additions & 0 deletions lib/sentry/opentelemetry/span_processor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ if Code.ensure_loaded?(OpenTelemetry) do
:ignored ->
true

:excluded ->
true

{:error, error} ->
Logger.warning("Failed to send transaction to Sentry: #{inspect(error)}")
{:error, :invalid_span}
Expand Down
6 changes: 5 additions & 1 deletion test/sentry/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,12 @@ defmodule Sentry.ConfigTest do
end

test ":traces_sample_rate" do
assert Config.validate!([])[:traces_sample_rate] == nil

assert Config.validate!(traces_sample_rate: nil)[:traces_sample_rate] == nil
assert Config.validate!(traces_sample_rate: 0.0)[:traces_sample_rate] == 0.0
assert Config.validate!(traces_sample_rate: 0.5)[:traces_sample_rate] == 0.5
assert Config.validate!(traces_sample_rate: 1.0)[:traces_sample_rate] == 1.0
assert Config.validate!([])[:traces_sample_rate] == 0.0

assert_raise ArgumentError, ~r/invalid value for :traces_sample_rate option/, fn ->
Config.validate!(traces_sample_rate: 2.0)
Expand Down
Loading