diff --git a/README.md b/README.md
index 03cd846a..658babc2 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,7 @@ Besides the environment variables supported by dd-trace-py, the datadog-lambda-p
| DD_COLD_START_TRACE_SKIP_LIB | optionally skip creating Cold Start Spans for a comma-separated list of libraries. Useful to limit depth or skip known libraries. | `ddtrace.internal.compat,ddtrace.filters` |
| DD_CAPTURE_LAMBDA_PAYLOAD | [Captures incoming and outgoing AWS Lambda payloads][1] in the Datadog APM spans for Lambda invocations. | `false` |
| DD_CAPTURE_LAMBDA_PAYLOAD_MAX_DEPTH | Determines the level of detail captured from AWS Lambda payloads, which are then assigned as tags for the `aws.lambda` span. It specifies the nesting depth of the JSON payload structure to process. Once the specified maximum depth is reached, the tag's value is set to the stringified value of any nested elements beyond this level.
For example, given the input payload:
{
"lv1" : {
"lv2": {
"lv3": "val"
}
}
}
If the depth is set to `2`, the resulting tag's key is set to `function.request.lv1.lv2` and the value is `{\"lv3\": \"val\"}`.
If the depth is set to `0`, the resulting tag's key is set to `function.request` and value is `{\"lv1\":{\"lv2\":{\"lv3\": \"val\"}}}` | `10` |
+| DD_EXCEPTION_REPLAY_ENABLED | When set to `true`, the Lambda will run with Error Tracking Exception Replay enabled, capturing local variables. | `false` |
## Opening Issues
diff --git a/datadog_lambda/trigger.py b/datadog_lambda/trigger.py
index 8090e36e..a2708a59 100644
--- a/datadog_lambda/trigger.py
+++ b/datadog_lambda/trigger.py
@@ -114,10 +114,14 @@ def parse_event_source(event: dict) -> _EventSource:
event_source = None
+ # Get requestContext safely and ensure it's a dictionary
request_context = event.get("requestContext")
+ if not isinstance(request_context, dict):
+ request_context = None
+
if request_context and request_context.get("stage"):
if "domainName" in request_context and detect_lambda_function_url_domain(
- request_context.get("domainName")
+ request_context.get("domainName", "")
):
return _EventSource(EventTypes.LAMBDA_FUNCTION_URL)
event_source = _EventSource(EventTypes.API_GATEWAY)
@@ -171,6 +175,8 @@ def parse_event_source(event: dict) -> _EventSource:
def detect_lambda_function_url_domain(domain: str) -> bool:
# e.g. "etsn5fibjr.lambda-url.eu-south-1.amazonaws.com"
+ if not isinstance(domain, str):
+ return False
domain_parts = domain.split(".")
if len(domain_parts) < 2:
return False
@@ -283,17 +289,28 @@ def extract_http_tags(event):
Extracts HTTP facet tags from the triggering event
"""
http_tags = {}
+
+ # Safely get request_context and ensure it's a dictionary
request_context = event.get("requestContext")
+ if not isinstance(request_context, dict):
+ request_context = None
+
path = event.get("path")
method = event.get("httpMethod")
+
if request_context and request_context.get("stage"):
- if request_context.get("domainName"):
- http_tags["http.url"] = request_context.get("domainName")
+ domain_name = request_context.get("domainName")
+ if domain_name:
+ http_tags["http.url"] = domain_name
path = request_context.get("path")
method = request_context.get("httpMethod")
+
# Version 2.0 HTTP API Gateway
- apigateway_v2_http = request_context.get("http")
+ apigateway_v2_http = request_context.get("http", {})
+ if not isinstance(apigateway_v2_http, dict):
+ apigateway_v2_http = {}
+
if event.get("version") == "2.0" and apigateway_v2_http:
path = apigateway_v2_http.get("path")
method = apigateway_v2_http.get("method")
@@ -303,15 +320,23 @@ def extract_http_tags(event):
if method:
http_tags["http.method"] = method
- headers = event.get("headers")
+ # Safely get headers
+ headers = event.get("headers", {})
+ if not isinstance(headers, dict):
+ headers = {}
+
if headers and headers.get("Referer"):
http_tags["http.referer"] = headers.get("Referer")
# Try to get `routeKey` from API GW v2; otherwise try to get `resource` from API GW v1
route = event.get("routeKey") or event.get("resource")
- if route:
- # "GET /my/endpoint" = > "/my/endpoint"
- http_tags["http.route"] = route.split(" ")[-1]
+ if route and isinstance(route, str):
+ try:
+ # "GET /my/endpoint" = > "/my/endpoint"
+ http_tags["http.route"] = route.split(" ")[-1]
+ except Exception:
+ # If splitting fails, use the route as is
+ http_tags["http.route"] = route
return http_tags
diff --git a/datadog_lambda/version.py b/datadog_lambda/version.py
index c3aaa6b7..9534f0c7 100644
--- a/datadog_lambda/version.py
+++ b/datadog_lambda/version.py
@@ -1 +1 @@
-__version__ = "6.109.0"
+__version__ = "6.110.0"
diff --git a/datadog_lambda/wrapper.py b/datadog_lambda/wrapper.py
index e81b1baa..86bbf04d 100644
--- a/datadog_lambda/wrapper.py
+++ b/datadog_lambda/wrapper.py
@@ -53,6 +53,13 @@
if llmobs_env_var:
from ddtrace.llmobs import LLMObs
+exception_replay_env_var = os.environ.get(
+ "DD_EXCEPTION_REPLAY_ENABLED", "false"
+).lower() in ("true", "1")
+if exception_replay_env_var:
+ from ddtrace.debugging._exception.replay import SpanExceptionHandler
+ from ddtrace.debugging._uploader import LogsIntakeUploaderV1
+
logger = logging.getLogger(__name__)
DD_FLUSH_TO_LOG = "DD_FLUSH_TO_LOG"
@@ -224,6 +231,11 @@ def __init__(self, func):
if llmobs_env_var:
LLMObs.enable()
+ # Enable Exception Replay
+ if exception_replay_env_var:
+ logger.debug("Enabling exception replay")
+ SpanExceptionHandler.enable()
+
logger.debug("datadog_lambda_wrapper initialized")
except Exception as e:
logger.error(format_err_with_traceback(e))
@@ -394,6 +406,10 @@ def _after(self, event, context):
if llmobs_env_var:
LLMObs.flush()
+ # Flush exception replay
+ if exception_replay_env_var:
+ LogsIntakeUploaderV1._instance.periodic()
+
if self.encode_authorizer_context and is_authorizer_response(self.response):
self._inject_authorizer_span_headers(
event.get("requestContext", {}).get("requestId")
diff --git a/pyproject.toml b/pyproject.toml
index cccef63e..ba5bcb17 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "datadog_lambda"
-version = "6.109.0"
+version = "6.110.0"
description = "The Datadog AWS Lambda Library"
authors = ["Datadog, Inc. "]
license = "Apache-2.0"
diff --git a/tests/test_trigger.py b/tests/test_trigger.py
index 9cb088f1..b4da7ff0 100644
--- a/tests/test_trigger.py
+++ b/tests/test_trigger.py
@@ -256,6 +256,30 @@ def test_event_source_unsupported(self):
self.assertEqual(event_source.to_string(), "unknown")
self.assertEqual(event_source_arn, None)
+ def test_event_source_with_non_dict_request_context(self):
+ # Test with requestContext as a string instead of a dict
+ event = {"requestContext": "not_a_dict"}
+ event_source = parse_event_source(event)
+ # Should still return a valid event source (unknown in this case)
+ self.assertEqual(event_source.to_string(), "unknown")
+
+ def test_event_source_with_invalid_domain_name(self):
+ # Test with domainName that isn't a string
+ event = {"requestContext": {"stage": "prod", "domainName": 12345}}
+ event_source = parse_event_source(event)
+ # Should detect as API Gateway since stage is present
+ self.assertEqual(event_source.to_string(), "api-gateway")
+
+ def test_detect_lambda_function_url_domain_with_invalid_input(self):
+ from datadog_lambda.trigger import detect_lambda_function_url_domain
+
+ # Test with non-string input
+ self.assertFalse(detect_lambda_function_url_domain(None))
+ self.assertFalse(detect_lambda_function_url_domain(12345))
+ self.assertFalse(detect_lambda_function_url_domain({"not": "a-string"}))
+ # Test with string that would normally cause an exception when split
+ self.assertFalse(detect_lambda_function_url_domain(""))
+
class GetTriggerTags(unittest.TestCase):
def test_extract_trigger_tags_api_gateway(self):
@@ -530,6 +554,47 @@ def test_extract_trigger_tags_list_type_event(self):
tags = extract_trigger_tags(event, ctx)
self.assertEqual(tags, {})
+ def test_extract_http_tags_with_invalid_request_context(self):
+ from datadog_lambda.trigger import extract_http_tags
+
+ # Test with requestContext as a string instead of a dict
+ event = {"requestContext": "not_a_dict", "path": "/test", "httpMethod": "GET"}
+ http_tags = extract_http_tags(event)
+ # Should still extract valid tags from the event
+ self.assertEqual(
+ http_tags, {"http.url_details.path": "/test", "http.method": "GET"}
+ )
+
+ def test_extract_http_tags_with_invalid_apigateway_http(self):
+ from datadog_lambda.trigger import extract_http_tags
+
+ # Test with http in requestContext that's not a dict
+ event = {
+ "requestContext": {"stage": "prod", "http": "not_a_dict"},
+ "version": "2.0",
+ }
+ http_tags = extract_http_tags(event)
+ # Should not raise an exception
+ self.assertEqual(http_tags, {})
+
+ def test_extract_http_tags_with_invalid_headers(self):
+ from datadog_lambda.trigger import extract_http_tags
+
+ # Test with headers that's not a dict
+ event = {"headers": "not_a_dict"}
+ http_tags = extract_http_tags(event)
+ # Should not raise an exception
+ self.assertEqual(http_tags, {})
+
+ def test_extract_http_tags_with_invalid_route(self):
+ from datadog_lambda.trigger import extract_http_tags
+
+ # Test with routeKey that would cause a split error
+ event = {"routeKey": 12345} # Not a string
+ http_tags = extract_http_tags(event)
+ # Should not raise an exception
+ self.assertEqual(http_tags, {})
+
class ExtractHTTPStatusCodeTag(unittest.TestCase):
def test_extract_http_status_code_tag_from_response_dict(self):