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
16 changes: 11 additions & 5 deletions sentry_sdk/client.py
Expand Up @@ -454,12 +454,18 @@
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

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("events_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"]

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 Expand Up @@ -556,7 +562,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
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
events_sampler=None, # type: Optional[Callable[[Event, Hint], Union[float, bool]]]
):
# type: (...) -> None
pass
Expand Down
112 changes: 112 additions & 0 deletions tests/test_client.py
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,109 @@ 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(
events_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 events_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),
# events_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,
),
),
)
def test_events_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