diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 9114cf6b67..4bb5181aab 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -598,14 +598,22 @@ def _prepare_event( before_send = self.options["before_send"] if before_send is not None and event is not None: 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 @@ -853,15 +861,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 + 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 - # 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="trace_metric" + ) return elif ty == "span" and isinstance(telemetry, Span): diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 6fdd209c01..1eea18b299 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -399,7 +399,7 @@ def event_processor( if filtered_qs: request["query_string"] = filtered_qs - if "headers" in aws_event: + if "headers" in aws_event and isinstance(aws_event["headers"], dict): request["headers"] = _filter_headers(aws_event["headers"]) if data_collection["user_info"]: @@ -428,7 +428,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) diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 03bb43810f..3081d95140 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1480,7 +1480,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 = event + with capture_internal_exceptions(): + new_event = error_processor(event, exc_info) if new_event is None: return self._drop(error_processor, "error processor") diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollectionUserInfoOff/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollectionUserInfoOff/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollectionUserInfoOff/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollectionUserInfoOn/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollectionUserInfoOn/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollectionUserInfoOn/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/test_basics.py b/tests/test_basics.py index d34f29e6a8..d746146baa 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -661,6 +661,39 @@ def foo(event, hint): sentry_sdk.scope.global_event_processors = old_processors +@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.parametrize( "installed_integrations, expected_name", [ diff --git a/tests/test_logs.py b/tests/test_logs.py index 3db3c9c8d3..c4998d3216 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -127,6 +127,43 @@ def _before_log(record, hint): assert not logs +@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 + + +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 + + def test_logs_attributes(sentry_init, capture_items): """ Passing arbitrary attributes to log messages. diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 95388db2b3..1e513fbf16 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -200,6 +200,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") diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 1aeaf27785..4ecf8fb211 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -466,6 +466,29 @@ def traces_sampler(sampling_context): assert span["attributes"]["drop"] is False +@pytest.mark.tests_internal_exceptions +def test_traces_sampler_exception_falls_back(sentry_init, capture_items): + def traces_sampler(sampling_context): + raise ValueError("traces_sampler error") + + sentry_init( + traces_sampler=traces_sampler, + traces_sample_rate=1.0, + trace_lifecycle="stream", + ) + + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="test"): + ... + + sentry_sdk.get_client().flush() + spans = [item.payload for item in items] + + # Falls back to traces_sample_rate=1.0, so span should still be sent + assert len(spans) == 1 + + def test_traces_sampler_called_once_per_segment(sentry_init): traces_sampler_called = 0 span_name_in_traces_sampler = None