diff --git a/newrelic/api/time_trace.py b/newrelic/api/time_trace.py index fd0f62fdef..43b1ea2299 100644 --- a/newrelic/api/time_trace.py +++ b/newrelic/api/time_trace.py @@ -360,16 +360,23 @@ def _observe_exception(self, exc_info=None, ignore=None, expected=None, status_c return fullname, message, message_raw, tb, is_expected def notice_error(self, error=None, attributes=None, expected=None, ignore=None, status_code=None): + def error_is_iterable(error): + return hasattr(error, "__iter__") and not isinstance(error, (str, bytes)) + + def none_in_error(error): + return error_is_iterable(error) and None in error + attributes = attributes if attributes is not None else {} # If no exception details provided, use current exception. - # Pull from sys.exc_info if no exception is passed - if not error or None in error: + # Pull from sys.exc_info() if no exception is passed + # Check that the error exists and that it is a fully populated iterable + if not error or none_in_error(error) or (error and not error_is_iterable(error)): error = sys.exc_info() # If no exception to report, exit - if not error or None in error: + if not error or none_in_error(error) or (error and not error_is_iterable(error)): return exc, value, tb = error diff --git a/newrelic/core/stats_engine.py b/newrelic/core/stats_engine.py index f44f82fe13..b6d2092d99 100644 --- a/newrelic/core/stats_engine.py +++ b/newrelic/core/stats_engine.py @@ -676,9 +676,14 @@ def record_time_metrics(self, metrics): self.record_time_metric(metric) def notice_error(self, error=None, attributes=None, expected=None, ignore=None, status_code=None): + def error_is_iterable(error): + return hasattr(error, "__iter__") and not isinstance(error, (str, bytes)) + + def none_in_error(error): + return error_is_iterable(error) and None in error + attributes = attributes if attributes is not None else {} settings = self.__settings - if not settings: return @@ -691,11 +696,12 @@ def notice_error(self, error=None, attributes=None, expected=None, ignore=None, return # Pull from sys.exc_info if no exception is passed - if not error or None in error: + # Check that the error exists and that it is a fully populated iterable + if not error or none_in_error(error) or (error and not error_is_iterable(error)): error = sys.exc_info() # If no exception to report, exit - if not error or None in error: + if not error or none_in_error(error) or (error and not error_is_iterable(error)): return exc, value, tb = error diff --git a/tests/agent_features/test_notice_error.py b/tests/agent_features/test_notice_error.py index e698dee7be..636dadc09c 100644 --- a/tests/agent_features/test_notice_error.py +++ b/tests/agent_features/test_notice_error.py @@ -39,6 +39,25 @@ # =============== Test errors during a transaction =============== +_key_error_name = callable_name(KeyError) + +_test_notice_error_exception_object = [(_key_error_name, "'f'")] + + +@validate_transaction_errors(errors=_test_notice_error_exception_object) +@background_task() +def test_notice_error_non_iterable_object(): + """Test that notice_error works when passed an exception object directly""" + try: + test_dict = {"a": 4, "b": 5} + # Force a KeyError + test_dict["f"] + except KeyError as e: + # The caught exception here is a non-iterable, singular KeyError object with no associated traceback + # This will exercise logic to pull from sys.exc_info() instead of using the exception directly + notice_error(e) + + _test_notice_error_sys_exc_info = [(_runtime_error_name, "one")]