From 58535a347cf911cbc2a676cd36d9e35d5c1cb94a Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 10 Oct 2025 16:12:31 +0200 Subject: [PATCH 1/6] fix(aws): Inject scopes in TimeoutThread exception with AWS lambda --- sentry_sdk/integrations/aws_lambda.py | 2 ++ sentry_sdk/utils.py | 27 +++++++++++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 4990fd6e6a..85d1a6c28c 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -138,6 +138,8 @@ def sentry_handler(aws_event, aws_context, *args, **kwargs): timeout_thread = TimeoutThread( waiting_time, configured_time / MILLIS_TO_SECONDS, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), ) # Starting the thread to raise timeout warning exception diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index cd825b29e2..db2cfc66e9 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1484,11 +1484,17 @@ class TimeoutThread(threading.Thread): waiting_time and raises a custom ServerlessTimeout exception. """ - def __init__(self, waiting_time, configured_timeout): - # type: (float, int) -> None + def __init__( + self, waiting_time, configured_timeout, isolation_scope=None, current_scope=None + ): + # type: (float, int, Optional[Scope], Optional[Scope]) -> None threading.Thread.__init__(self) self.waiting_time = waiting_time self.configured_timeout = configured_timeout + + self.isolation_scope = isolation_scope + self.current_scope = current_scope + self._stop_event = threading.Event() def stop(self): @@ -1509,12 +1515,17 @@ def run(self): if integer_configured_timeout < self.configured_timeout: integer_configured_timeout = integer_configured_timeout + 1 - # Raising Exception after timeout duration is reached - raise ServerlessTimeoutWarning( - "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format( - integer_configured_timeout - ) - ) + from sentry_sdk.scope import use_isolation_scope, use_scope + + with use_isolation_scope(self.isolation_scope): + with use_scope(self.current_scope): + # with use_scope(self.current_scope): + # Raising Exception after timeout duration is reached + raise ServerlessTimeoutWarning( + "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format( + integer_configured_timeout + ) + ) def to_base64(original): From 2f0e1f4b8eb372cf2f2c603b5b41cff55f94fadc Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 10 Oct 2025 16:16:13 +0200 Subject: [PATCH 2/6] ensure scopes are not none --- sentry_sdk/utils.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index db2cfc66e9..9970f8797e 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1517,15 +1517,16 @@ def run(self): from sentry_sdk.scope import use_isolation_scope, use_scope - with use_isolation_scope(self.isolation_scope): - with use_scope(self.current_scope): - # with use_scope(self.current_scope): - # Raising Exception after timeout duration is reached - raise ServerlessTimeoutWarning( - "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format( - integer_configured_timeout + if self.isolation_scope is not None and self.current_scope is not None: + with use_isolation_scope(self.isolation_scope): + with use_scope(self.current_scope): + # with use_scope(self.current_scope): + # Raising Exception after timeout duration is reached + raise ServerlessTimeoutWarning( + "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format( + integer_configured_timeout + ) ) - ) def to_base64(original): From 826f49b3bfcdc09b4ed495edd1e85d75e1dc95cf Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 10 Oct 2025 16:33:24 +0200 Subject: [PATCH 3/6] actually capture exception with injected scopes --- sentry_sdk/utils.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 9970f8797e..f01f7344d4 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1501,6 +1501,20 @@ def stop(self): # type: () -> None self._stop_event.set() + def _capture_exception(self): + # type: () -> ExcInfo + exc_info = sys.exc_info() + + client = sentry_sdk.get_client() + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "threading", "handled": False}, + ) + sentry_sdk.capture_event(event, hint=hint) + + return exc_info + def run(self): # type: () -> None @@ -1515,18 +1529,19 @@ def run(self): if integer_configured_timeout < self.configured_timeout: integer_configured_timeout = integer_configured_timeout + 1 - from sentry_sdk.scope import use_isolation_scope, use_scope - if self.isolation_scope is not None and self.current_scope is not None: - with use_isolation_scope(self.isolation_scope): - with use_scope(self.current_scope): - # with use_scope(self.current_scope): - # Raising Exception after timeout duration is reached - raise ServerlessTimeoutWarning( - "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format( - integer_configured_timeout + with sentry_sdk.scope.use_isolation_scope(self.isolation_scope): + with sentry_sdk.scope.use_scope(self.current_scope): + try: + # with use_scope(self.current_scope): + # Raising Exception after timeout duration is reached + raise ServerlessTimeoutWarning( + "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format( + integer_configured_timeout + ) ) - ) + except Exception: + reraise(*self._capture_exception()) def to_base64(original): From b4c993eabfeaa64b924b1162c93e96b6482080dc Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 10 Oct 2025 16:40:36 +0200 Subject: [PATCH 4/6] handle all cases --- sentry_sdk/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index f01f7344d4..74430f20df 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1487,7 +1487,7 @@ class TimeoutThread(threading.Thread): def __init__( self, waiting_time, configured_timeout, isolation_scope=None, current_scope=None ): - # type: (float, int, Optional[Scope], Optional[Scope]) -> None + # type: (float, int, Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]) -> None threading.Thread.__init__(self) self.waiting_time = waiting_time self.configured_timeout = configured_timeout @@ -1543,6 +1543,12 @@ def run(self): except Exception: reraise(*self._capture_exception()) + raise ServerlessTimeoutWarning( + "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format( + integer_configured_timeout + ) + ) + def to_base64(original): # type: (str) -> Optional[str] From 2ff1ba274f30e6583270f42b6775c61970065a68 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Fri, 10 Oct 2025 16:41:47 +0200 Subject: [PATCH 5/6] cleanup comments --- sentry_sdk/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 74430f20df..3496178228 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1529,12 +1529,11 @@ def run(self): if integer_configured_timeout < self.configured_timeout: integer_configured_timeout = integer_configured_timeout + 1 + # Raising Exception after timeout duration is reached if self.isolation_scope is not None and self.current_scope is not None: with sentry_sdk.scope.use_isolation_scope(self.isolation_scope): with sentry_sdk.scope.use_scope(self.current_scope): try: - # with use_scope(self.current_scope): - # Raising Exception after timeout duration is reached raise ServerlessTimeoutWarning( "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format( integer_configured_timeout From a0b8ffce7ab8b450717b2936590c2d56a92bf7f1 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Mon, 13 Oct 2025 09:06:23 +0200 Subject: [PATCH 6/6] add test --- .../TimeoutErrorScopeModified/.gitignore | 11 ++++++++ .../TimeoutErrorScopeModified/index.py | 19 ++++++++++++++ .../aws_lambda/test_aws_lambda.py | 25 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TimeoutErrorScopeModified/.gitignore create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TimeoutErrorScopeModified/index.py diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TimeoutErrorScopeModified/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TimeoutErrorScopeModified/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TimeoutErrorScopeModified/.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/TimeoutErrorScopeModified/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TimeoutErrorScopeModified/index.py new file mode 100644 index 0000000000..109245b90d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/TimeoutErrorScopeModified/index.py @@ -0,0 +1,19 @@ +import os +import time + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + integrations=[AwsLambdaIntegration(timeout_warning=True)], +) + + +def handler(event, context): + sentry_sdk.set_tag("custom_tag", "custom_value") + time.sleep(15) + return { + "event": event, + } diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 85da7e0b14..664220464c 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -223,6 +223,31 @@ def test_timeout_error(lambda_client, test_environment): assert exception["mechanism"]["type"] == "threading" +def test_timeout_error_scope_modified(lambda_client, test_environment): + lambda_client.invoke( + FunctionName="TimeoutErrorScopeModified", + Payload=json.dumps({}), + ) + envelopes = test_environment["server"].envelopes + + (error_event,) = envelopes + + assert error_event["level"] == "error" + assert ( + error_event["extra"]["lambda"]["function_name"] == "TimeoutErrorScopeModified" + ) + + (exception,) = error_event["exception"]["values"] + assert not exception["mechanism"]["handled"] + assert exception["type"] == "ServerlessTimeoutWarning" + assert exception["value"].startswith( + "WARNING : Function is expected to get timed out. Configured timeout duration =" + ) + assert exception["mechanism"]["type"] == "threading" + + assert error_event["tags"]["custom_tag"] == "custom_value" + + @pytest.mark.parametrize( "aws_event, has_request_data, batch_size", [