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
9 changes: 5 additions & 4 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,10 +456,11 @@ def _should_sample_error(
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.

):
# type: (...) -> bool
not_in_sample_rate = (
self.options["sample_rate"] < 1.0
and random.random() >= self.options["sample_rate"]
)
try:
sample_rate = self.options["issues_sampler"](event)
szokeasaurusrex marked this conversation as resolved.
Show resolved Hide resolved
except (KeyError, TypeError):
sample_rate = self.options["sample_rate"]
not_in_sample_rate = sample_rate < 1.0 and random.random() >= sample_rate
if not_in_sample_rate:
# because we will not sample this event, record a "lost event".
if self.transport:
Expand Down
1 change: 1 addition & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,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
issues_sampler=None, # type: Optional[Callable[[Event], Union[float, bool]]]
):
# type: (...) -> None
pass
Expand Down
100 changes: 100 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,97 @@ 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(
issues_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 issues_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),
# issues_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
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,
),
),
)
def test_issues_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

try:
assert test_config.sampler_function_mock.call_count == 1

# Ensure one argument (the event) was passed to the sampler function
assert len(test_config.sampler_function_mock.call_args[0]) == 1
except AttributeError:
# sampler_function_mock should be None in this case,
# but let's double-check to ensure the test is working correctly
assert test_config.sampler_function_mock is None
szokeasaurusrex marked this conversation as resolved.
Show resolved Hide resolved
Loading