Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): Added error_sampler option #2456

Merged
merged 10 commits into from
Oct 20, 2023
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_sample_error(
self,
event, # type: Event

Choose a reason for hiding this comment

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

event contains information about the logged message via logging?

There is a hint parameter in the before_send that contains this information in log_record

Copy link
Member Author

Choose a reason for hiding this comment

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

@saippuakauppias Could you please clarify your question? How exactly are you logging the message?

Copy link
Member Author

Choose a reason for hiding this comment

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

If you are using the LoggingIntegration, your log message should appear in the event passed to this function.

Copy link

@saippuakauppias saippuakauppias Oct 19, 2023

Choose a reason for hiding this comment

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

Yes, I am using LoggingIntegration and logging all warning messages to sentry. I tried to discard some messages that are not needed and in debug mode found that the path to the file that triggers the message is in hint['log_record'].pathname.

Actually, I need this as a minimum. I would like this feature to have a similar option to filter by module name.

Copy link
Member Author

Choose a reason for hiding this comment

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

@saippuakauppias we decided to also pass the hint into the events_sampler, so you will be able to make your sampling decision based on information contained in the hint.

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)

Check warning on line 464 in sentry_sdk/client.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/client.py#L464

Added line #L464 was not covered by tests
sentrivana marked this conversation as resolved.
Show resolved Hide resolved
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 = (

Check warning on line 471 in sentry_sdk/client.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/client.py#L470-L471

Added lines #L470 - L471 were not covered by tests
("error_sampler", "returned")
if callable(sampler)
else ("sample_rate", "contains")
)
logger.warning(

Check warning on line 476 in sentry_sdk/client.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/client.py#L476

Added line #L476 was not covered by tests
"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

Check warning on line 483 in sentry_sdk/client.py

View check run for this annotation

Codecov / codecov/patch

sentry_sdk/client.py#L483

Added line #L483 was not covered by tests

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 @@
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
Loading