Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,14 +930,22 @@ def _prepare_event(
and event.get("type") != "transaction"
):
new_event = None
exception_raised_in_before_send = False
with capture_internal_exceptions():
new_event = before_send(event, hint or {})
try:
new_event = before_send(event, hint or {})
except Exception:
exception_raised_in_before_send = True
raise
if new_event is None:
logger.info("before send dropped event")
if self.transport:
self.transport.record_lost_event(
"before_send", data_category="error"
reason = (
"callback_error"
if exception_raised_in_before_send
else "before_send"
)
self.transport.record_lost_event(reason, data_category="error")

# If this is an exception, reset the DedupeIntegration. It still
# remembers the dropped exception as the last exception, meaning
Expand All @@ -955,17 +963,29 @@ def _prepare_event(
and event.get("type") == "transaction"
):
new_event = None
exception_raised_in_before_send_transaction = False
spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))

with capture_internal_exceptions():
new_event = before_send_transaction(event, hint or {})
try:
new_event = before_send_transaction(event, hint or {})
except Exception:
exception_raised_in_before_send_transaction = True
raise

if new_event is None:
logger.info("before send transaction dropped event")
if self.transport:
reason = (
"callback_error"
if exception_raised_in_before_send_transaction
else "before_send"
)
self.transport.record_lost_event(
reason="before_send", data_category="transaction"
reason=reason, data_category="transaction"
)
self.transport.record_lost_event(
reason="before_send",
reason=reason,
data_category="span",
quantity=spans_before + 1, # +1 for the transaction itself
)
Expand Down Expand Up @@ -1265,15 +1285,32 @@ def _capture_telemetry(
exception_raised_in_before_send_func = True
raise

if ty in ("log", "metric"):
# We are ok with dropping metrics and logs when an exception is raised
# because we allow users to drop them in their respect before_send_*
# functions.
if ty == "log":
if exception_raised_in_before_send_func:
if self.transport:
self.transport.record_lost_event(
"callback_error", data_category="log_item"
)
return
# Logs and metrics can be dropped in their respective
# before_send, so if we get None, don't queue them for sending.
if serialized is None:
if self.transport:
self.transport.record_lost_event(
"before_send", data_category="log_item"
)
return

elif ty == "metric":
if exception_raised_in_before_send_func:
if self.transport:
self.transport.record_lost_event(
"callback_error", data_category="trace_metric"
)
return
if serialized is None:
if self.transport:
self.transport.record_lost_event(
"before_send", data_category="trace_metric"
)
return

elif ty == "span" and isinstance(telemetry, StreamedSpan):
Expand Down
4 changes: 2 additions & 2 deletions sentry_sdk/integrations/aws_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ def event_processor(
else:
request["query_string"] = query_string

if "headers" in aws_event:
if "headers" in aws_event and isinstance(aws_event["headers"], dict):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change was required because without it, the event_processor throws, and with this PR, that means the event won't be sent. Some tests were relying on the event still being sent.

request["headers"] = _filter_headers(aws_event["headers"])

client_options = sentry_sdk.get_client().options
Expand Down Expand Up @@ -508,7 +508,7 @@ def _get_url(aws_event: "Any", aws_context: "Any") -> str:
path = aws_event.get("path", None)

headers = aws_event.get("headers")
if headers is None:
if not isinstance(headers, dict):
headers = {}

host = headers.get("Host", None)
Expand Down
11 changes: 10 additions & 1 deletion sentry_sdk/profiler/transaction_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,16 @@ def _set_initial_sampling_decision(
options = client.options

if callable(options.get("profiles_sampler")):
sample_rate = options["profiles_sampler"](sampling_context)
try:
sample_rate = options["profiles_sampler"](sampling_context)
except Exception:
logger.warning(
"[Profiling] profiles_sampler raised; falling back to profiles_sample_rate",
exc_info=True,
)
sample_rate = options["profiles_sample_rate"]
if sample_rate is None:
sample_rate = options["_experiments"].get("profiles_sample_rate")
elif options["profiles_sample_rate"] is not None:
sample_rate = options["profiles_sample_rate"]
else:
Expand Down
6 changes: 4 additions & 2 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -1828,7 +1828,9 @@ def run_error_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]
)

for error_processor in error_processors:
new_event = error_processor(event, exc_info)
new_event = None
with capture_internal_exceptions():
new_event = error_processor(event, exc_info)
if new_event is None:
return self._drop(error_processor, "error processor")

Expand Down Expand Up @@ -1856,7 +1858,7 @@ def run_event_processors(self, event: "Event", hint: "Hint") -> "Optional[Event]
)

for event_processor in event_processors:
new_event = event
new_event = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Processor errors leave Dedupe stale

Medium Severity

Event processors that throw now drop the event, but DedupeIntegration still records that exception as last-seen. A later capture of the same exception object is then discarded as a duplicate, so the error never reaches Sentry. before_send already resets Dedupe when it drops an exception event; the new processor-error path does not.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d22684f. Configure here.

with capture_internal_exceptions():
new_event = event_processor(event, hint)
if new_event is None:
Expand Down
31 changes: 31 additions & 0 deletions tests/profiler/test_transaction_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,37 @@ def test_profiles_sampler(
assert record_lost_event_calls == [("sample_rate", "profile", None, 1)]


@pytest.mark.tests_internal_exceptions
@mock.patch("sentry_sdk.profiler.transaction_profiler.PROFILE_MINIMUM_SAMPLES", 0)
def test_profiles_sampler_exception_falls_back_to_profiles_sample_rate(
sentry_init,
capture_envelopes,
teardown_profiling,
):
def bad_sampler(sampling_context):
raise ValueError("profiles_sampler error")

sentry_init(
traces_sample_rate=1.0,
profiles_sampler=bad_sampler,
profiles_sample_rate=1.0,
)

envelopes = capture_envelopes()

with start_transaction(name="profiling"):
pass

items = defaultdict(list)
for envelope in envelopes:
for item in envelope.items:
items[item.type].append(item)

assert len(items["transaction"]) == 1
# Falls back to profiles_sample_rate=1.0, so profile should be sent
assert len(items["profile"]) == 1


def test_minimum_unique_samples_required(
sentry_init,
capture_envelopes,
Expand Down
102 changes: 102 additions & 0 deletions tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,108 @@
sentry_sdk.scope.global_event_processors = old_processors


@pytest.mark.tests_internal_exceptions
def test_event_processor_exception_drops_event_and_records_client_report(
sentry_init, capture_events, capture_record_lost_event_calls
):
sentry_init(default_integrations=False)
events = capture_events()
record_lost_event_calls = capture_record_lost_event_calls()

scope = sentry_sdk.get_isolation_scope()

@scope.add_event_processor
def bad_processor(event, hint):
raise ValueError("processor error")

capture_message("should be dropped")

assert len(events) == 0
assert ("event_processor", "error", None, 1) in record_lost_event_calls

Check warning on line 788 in tests/test_basics.py

View check run for this annotation

@sentry/warden / warden: code-review

SDK event processor errors can discard captured events

Have you considered isolating failures in SDK-owned event processors before treating an exception as an intentional drop? `run_event_processors()` leaves `new_event` as `None` when a processor raises, so an SDK integration’s enrichment failure discards the user event. For example, AWS Lambda’s `_make_request_event_processor` has no internal exception guard and returns the event only after enrichment completes. Guard SDK-owned enrichment failures and preserve the event, while keeping drop-on-error behavior for user callbacks.


@pytest.mark.tests_internal_exceptions
def test_error_processor_exception_drops_event(
sentry_init, capture_events, capture_record_lost_event_calls
):
sentry_init(default_integrations=False)
events = capture_events()
record_lost_event_calls = capture_record_lost_event_calls()

scope = sentry_sdk.get_isolation_scope()

@scope.add_error_processor
def bad_error_processor(event, exc_info):
raise ValueError("error processor error")

try:
raise ValueError("original error")
except Exception:
capture_exception()

assert len(events) == 0
assert ("event_processor", "error", None, 1) in record_lost_event_calls


@pytest.mark.tests_internal_exceptions
def test_before_send_exception_records_callback_error(
sentry_init, capture_events, capture_record_lost_event_calls
):
def bad_before_send(event, hint):
raise ValueError("before_send error")

sentry_init(before_send=bad_before_send, default_integrations=False)
events = capture_events()
record_lost_event_calls = capture_record_lost_event_calls()

capture_message("should be dropped")

assert len(events) == 0
assert ("callback_error", "error", None, 1) in record_lost_event_calls


def test_before_send_returning_none_records_before_send(
sentry_init, capture_events, capture_record_lost_event_calls
):
def dropping_before_send(event, hint):
return None

sentry_init(before_send=dropping_before_send)
events = capture_events()
record_lost_event_calls = capture_record_lost_event_calls()

capture_message("should be dropped")

assert len(events) == 0
assert ("before_send", "error", None, 1) in record_lost_event_calls


@pytest.mark.tests_internal_exceptions
def test_before_send_transaction_exception_records_callback_error(
sentry_init, capture_events, capture_record_lost_event_calls
):
def bad_before_send_transaction(event, hint):
raise ValueError("before_send_transaction error")

sentry_init(
traces_sample_rate=1.0,
before_send_transaction=bad_before_send_transaction,
default_integrations=False,
)
events = capture_events()
record_lost_event_calls = capture_record_lost_event_calls()

with start_transaction(name="test"):
pass

assert not any(e.get("type") == "transaction" for e in events)
assert ("callback_error", "transaction", None, 1) in record_lost_event_calls
assert any(
reason == "callback_error" and category == "span"
for reason, category, _, _ in record_lost_event_calls
)


@pytest.mark.parametrize(
"installed_integrations, expected_name",
[
Expand Down
39 changes: 39 additions & 0 deletions tests/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,45 @@ def _before_log(record, hint):
assert not logs


@minimum_python_37
@pytest.mark.tests_internal_exceptions
def test_logs_before_send_log_raises_records_callback_error(
sentry_init, capture_items, capture_record_lost_event_calls
):
def _before_log(record, hint):
raise ValueError("before_send_log error")

sentry_init(before_send_log=_before_log)
items = capture_items("log")
record_lost_event_calls = capture_record_lost_event_calls()

sentry_sdk.logger.error("This is an error log...")

get_client().flush()

assert not items
assert ("callback_error", "log_item", None, 1) in record_lost_event_calls


@minimum_python_37
def test_logs_before_send_log_returns_none_records_before_send(
sentry_init, capture_items, capture_record_lost_event_calls
):
def _before_log(record, hint):
return None

sentry_init(before_send_log=_before_log)
items = capture_items("log")
record_lost_event_calls = capture_record_lost_event_calls()

sentry_sdk.logger.error("This is an error log...")

get_client().flush()

assert not items
assert ("before_send", "log_item", None, 1) in record_lost_event_calls


@minimum_python_37
def test_logs_attributes(sentry_init, capture_items):
"""
Expand Down
37 changes: 37 additions & 0 deletions tests/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,43 @@ def _before_metric(record, hint):
assert not metrics


@pytest.mark.tests_internal_exceptions
def test_metrics_before_send_raises_records_callback_error(
sentry_init, capture_items, capture_record_lost_event_calls
):
def _before_metric(record, hint):
raise ValueError("before_send_metric error")

sentry_init(before_send_metric=_before_metric)
items = capture_items("trace_metric")
record_lost_event_calls = capture_record_lost_event_calls()

sentry_sdk.metrics.count("test.keep", 1)

get_client().flush()

assert not items
assert ("callback_error", "trace_metric", None, 1) in record_lost_event_calls


def test_metrics_before_send_returns_none_records_before_send(
sentry_init, capture_items, capture_record_lost_event_calls
):
def _before_metric(record, hint):
return None

sentry_init(before_send_metric=_before_metric)
items = capture_items("trace_metric")
record_lost_event_calls = capture_record_lost_event_calls()

sentry_sdk.metrics.count("test.drop", 1)

get_client().flush()

assert not items
assert ("before_send", "trace_metric", None, 1) in record_lost_event_calls


def test_transport_format(sentry_init, capture_envelopes):
sentry_init(server_name="test-server", release="1.0.0")

Expand Down
Loading
Loading