Skip to content

Commit

Permalink
feat(api): Added error_sampler option (#2456)
Browse files Browse the repository at this point in the history
* Created issues_sampler

* Verify the event gets passed

* Restructured tests, adding different sample rates based on exception

* Update tests/test_client.py

Co-authored-by: Ivana Kellyerova <[email protected]>

* Pass hint also to the sampler

* Renamed issues_sampler to events_sampler

* Handle invalid events_sampler return value

* Added value to warning

* Rename to `error_sampler`

---------

Co-authored-by: Ivana Kellyerova <[email protected]>
  • Loading branch information
szokeasaurusrex and sentrivana authored Oct 20, 2023
1 parent bf218e9 commit 085595b
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 5 deletions.
32 changes: 27 additions & 5 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,12 +454,34 @@ def _should_capture(
def _should_sample_error(
self,
event, # type: Event
hint, # type: Hint
):
# type: (...) -> bool
not_in_sample_rate = (
self.options["sample_rate"] < 1.0
and random.random() >= self.options["sample_rate"]
)
sampler = self.options.get("error_sampler", None)

if callable(sampler):
with capture_internal_exceptions():
sample_rate = sampler(event, hint)
else:
sample_rate = self.options["sample_rate"]

try:
not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate
except TypeError:
parameter, verb = (
("error_sampler", "returned")
if callable(sampler)
else ("sample_rate", "contains")
)
logger.warning(
"The provided %s %s an invalid value of %s. The value should be a float or a bool. Defaulting to sampling the event."
% (parameter, verb, repr(sample_rate))
)

# If the sample_rate has an invalid value, we should sample the event, since the default behavior
# (when no sample_rate or error_sampler is provided) is to sample all events.
not_in_sample_rate = False

if not_in_sample_rate:
# because we will not sample this event, record a "lost event".
if self.transport:
Expand Down Expand Up @@ -556,7 +578,7 @@ def capture_event(
if (
not is_transaction
and not is_checkin
and not self._should_sample_error(event)
and not self._should_sample_error(event, hint)
):
return None

Expand Down
2 changes: 2 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
BreadcrumbProcessor,
Event,
EventProcessor,
Hint,
ProfilerMode,
TracesSampler,
TransactionProcessor,
Expand Down Expand Up @@ -261,6 +262,7 @@ def __init__(
event_scrubber=None, # type: Optional[sentry_sdk.scrubber.EventScrubber]
max_value_length=DEFAULT_MAX_VALUE_LENGTH, # type: int
enable_backpressure_handling=True, # type: bool
error_sampler=None, # type: Optional[Callable[[Event, Hint], Union[float, bool]]]
):
# type: (...) -> None
pass
Expand Down
117 changes: 117 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
from sentry_sdk.utils import logger
from sentry_sdk.serializer import MAX_DATABAG_BREADTH
from sentry_sdk.consts import DEFAULT_MAX_BREADCRUMBS, DEFAULT_MAX_VALUE_LENGTH
from sentry_sdk._types import TYPE_CHECKING

if TYPE_CHECKING:
from collections.abc import Callable
from typing import Any, Optional, Union
from sentry_sdk._types import Event

try:
from unittest import mock # python 3.3 and above
Expand Down Expand Up @@ -1196,3 +1202,114 @@ def test_debug_option(
assert "something is wrong" in caplog.text
else:
assert "something is wrong" not in caplog.text


class IssuesSamplerTestConfig:
def __init__(
self,
expected_events,
sampler_function=None,
sample_rate=None,
exception_to_raise=Exception,
):
# type: (int, Optional[Callable[[Event], Union[float, bool]]], Optional[float], type[Exception]) -> None
self.sampler_function_mock = (
None
if sampler_function is None
else mock.MagicMock(side_effect=sampler_function)
)
self.expected_events = expected_events
self.sample_rate = sample_rate
self.exception_to_raise = exception_to_raise

def init_sdk(self, sentry_init):
# type: (Callable[[*Any], None]) -> None
sentry_init(
error_sampler=self.sampler_function_mock, sample_rate=self.sample_rate
)

def raise_exception(self):
# type: () -> None
raise self.exception_to_raise()


@mock.patch("sentry_sdk.client.random.random", return_value=0.618)
@pytest.mark.parametrize(
"test_config",
(
# Baseline test with error_sampler only, both floats and bools
IssuesSamplerTestConfig(sampler_function=lambda *_: 1.0, expected_events=1),
IssuesSamplerTestConfig(sampler_function=lambda *_: 0.7, expected_events=1),
IssuesSamplerTestConfig(sampler_function=lambda *_: 0.6, expected_events=0),
IssuesSamplerTestConfig(sampler_function=lambda *_: 0.0, expected_events=0),
IssuesSamplerTestConfig(sampler_function=lambda *_: True, expected_events=1),
IssuesSamplerTestConfig(sampler_function=lambda *_: False, expected_events=0),
# Baseline test with sample_rate only
IssuesSamplerTestConfig(sample_rate=1.0, expected_events=1),
IssuesSamplerTestConfig(sample_rate=0.7, expected_events=1),
IssuesSamplerTestConfig(sample_rate=0.6, expected_events=0),
IssuesSamplerTestConfig(sample_rate=0.0, expected_events=0),
# error_sampler takes precedence over sample_rate
IssuesSamplerTestConfig(
sampler_function=lambda *_: 1.0, sample_rate=0.0, expected_events=1
),
IssuesSamplerTestConfig(
sampler_function=lambda *_: 0.0, sample_rate=1.0, expected_events=0
),
# Different sample rates based on exception, retrieved both from event and hint
IssuesSamplerTestConfig(
sampler_function=lambda event, _: {
"ZeroDivisionError": 1.0,
"AttributeError": 0.0,
}[event["exception"]["values"][0]["type"]],
exception_to_raise=ZeroDivisionError,
expected_events=1,
),
IssuesSamplerTestConfig(
sampler_function=lambda event, _: {
"ZeroDivisionError": 1.0,
"AttributeError": 0.0,
}[event["exception"]["values"][0]["type"]],
exception_to_raise=AttributeError,
expected_events=0,
),
IssuesSamplerTestConfig(
sampler_function=lambda _, hint: {
ZeroDivisionError: 1.0,
AttributeError: 0.0,
}[hint["exc_info"][0]],
exception_to_raise=ZeroDivisionError,
expected_events=1,
),
IssuesSamplerTestConfig(
sampler_function=lambda _, hint: {
ZeroDivisionError: 1.0,
AttributeError: 0.0,
}[hint["exc_info"][0]],
exception_to_raise=AttributeError,
expected_events=0,
),
# If sampler returns invalid value, we should still send the event
IssuesSamplerTestConfig(
sampler_function=lambda *_: "This is an invalid return value for the sampler",
expected_events=1,
),
),
)
def test_error_sampler(_, sentry_init, capture_events, test_config):
test_config.init_sdk(sentry_init)

events = capture_events()

try:
test_config.raise_exception()
except Exception:
capture_exception()

assert len(events) == test_config.expected_events

if test_config.sampler_function_mock is not None:
assert test_config.sampler_function_mock.call_count == 1

# Ensure two arguments (the event and hint) were passed to the sampler function
assert len(test_config.sampler_function_mock.call_args[0]) == 2

0 comments on commit 085595b

Please sign in to comment.