Skip to content
Draft
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
6 changes: 6 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,12 @@ class SPANDATA:
Example: "my_function"
"""

FAAS_TRIGGER = "faas.trigger"
"""
The type of the trigger on which the function is executed.
Example: "pubsub"
"""

FAAS_VERSION = "faas.version"
"""
The version of the function that was invoked.
Expand Down
7 changes: 4 additions & 3 deletions sentry_sdk/integrations/aws_lambda/_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
_get_user_from_event,
_make_request_event_processor,
)
from sentry_sdk.integrations.aws_lambda._trigger import _get_trigger
from sentry_sdk.integrations.aws_lambda.consts import (
CLOUD_PLATFORM,
CLOUD_PROVIDER,
Expand Down Expand Up @@ -64,10 +65,8 @@ def sentry_handler(

if isinstance(aws_event, list) and len(aws_event) >= 1:
request_data = aws_event[0]
batch_size = len(aws_event)
else:
request_data = aws_event
batch_size = 1

if not isinstance(request_data, dict):
# If we're not dealing with a dictionary, we won't be able to get
Expand Down Expand Up @@ -160,11 +159,13 @@ def sentry_handler(
SPANDATA.SENTRY_SEGMENT_NAME_SOURCE: SegmentNameSource.COMPONENT,
SPANDATA.CLOUD_PROVIDER: CLOUD_PROVIDER,
SPANDATA.CLOUD_PLATFORM: CLOUD_PLATFORM,
SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT: batch_size,
**header_attributes,
**additional_attributes,
}

with capture_internal_exceptions():
attributes[SPANDATA.FAAS_TRIGGER] = _get_trigger(aws_event)

with capture_internal_exceptions():
invoked_function_arn = aws_context.invoked_function_arn
function_version = aws_context.function_version
Expand Down
89 changes: 89 additions & 0 deletions sentry_sdk/integrations/aws_lambda/_trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any, Optional

# `faas.trigger` of each `eventSource` of `Records` or of a top-level `eventSource`.
# https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html
_TRIGGERS: "dict[Optional[str], str]" = {
"aws:sqs": "pubsub",
"aws:sns": "pubsub",
"aws:kinesis": "pubsub",
"aws:ses": "pubsub",
"aws:kafka": "pubsub",
"SelfManagedKafka": "pubsub",
"aws:mq": "pubsub",
"aws:rmq": "pubsub",
"aws:s3": "datasource",
"aws:dynamodb": "datasource",
"aws:docdb": "datasource",
}

# https://docs.aws.amazon.com/eventbridge/latest/ref/overiew-event-structure.html
_EVENTBRIDGE_KEYS = frozenset(
(
"version",
"id",
"detail-type",
"source",
"account",
"time",
"region",
"resources",
"detail",
)
)


def _get_records_source(records: "list[Any]") -> "Optional[str]":
"""The records' `eventSource`, one per event; SNS spells it `EventSource`."""
return records[0].get("eventSource") or records[0].get("EventSource")


def _is_appsync_event(aws_event: "Any") -> bool:
# https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference-js.html
return (
isinstance(aws_event, dict)
and "arguments" in aws_event
and "fieldName" in aws_event.get("info", {})
)


def _get_trigger(aws_event: "Any") -> str:
"""`faas.trigger` from the envelope of the delivered event, never from its payload."""
if isinstance(aws_event, list):
# an AppSync batch resolver delivers a list of resolver events.
if aws_event and all(_is_appsync_event(event) for event in aws_event):
return "http"
return "other"

if not isinstance(aws_event, dict):
return "other"

# API Gateway REST APIs and ALB send `httpMethod`, HTTP APIs and function URLs
# `requestContext.http`.
request_context = aws_event.get("requestContext")
if request_context is not None and (
"httpMethod" in aws_event or "http" in request_context
):
return "http"

if _is_appsync_event(aws_event):
return "http"

records = aws_event.get("Records")
if records:
# CloudFront and Lambda@Edge records have no `eventSource`.
if "cf" in records[0]:
return "http"
return _TRIGGERS.get(_get_records_source(records), "other")

if _EVENTBRIDGE_KEYS <= aws_event.keys():
if (
aws_event["source"] == "aws.events"
and aws_event["detail-type"] == "Scheduled Event"
):
return "timer"
return "pubsub"

return _TRIGGERS.get(aws_event.get("eventSource"), "other")
144 changes: 142 additions & 2 deletions tests/integrations/aws_lambda/test_aws_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,8 @@ def test_no_error(lambda_client, test_environment):
assert _get_span_attr(attrs, SPANDATA.AWS_LOG_STREAM_NAMES) == [
LATEST_FUNCTION_VERSION
]
assert _get_span_attr(attrs, SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == 1
assert _get_span_attr(attrs, SPANDATA.FAAS_TRIGGER) == "other"
assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attrs


def test_error(lambda_client, test_environment):
Expand Down Expand Up @@ -810,7 +811,146 @@ def test_error(lambda_client, test_environment):
assert _get_span_attr(attrs, SPANDATA.AWS_LOG_STREAM_NAMES) == [
LATEST_FUNCTION_VERSION
]
assert _get_span_attr(attrs, SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == 1
assert _get_span_attr(attrs, SPANDATA.FAAS_TRIGGER) == "other"
assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attrs


def _sqs_record():
return {
"messageId": "059f36b4-87a3-44ab-83d2-661975830a7d",
"body": "{}",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-1:012345678912:orders",
}


def _sns_record():
return {
"EventSource": "aws:sns",
"Sns": {"TopicArn": "arn:aws:sns:us-east-1:012345678912:alerts"},
}


def _appsync_event():
return {
"arguments": {"id": "1"},
"info": {"fieldName": "getOrder", "parentTypeName": "Query"},
}


def _eventbridge_event(source, detail_type):
return {
"version": "0",
"id": "53dc4d37-cffa-4f76-80c9-8b7d4a4d2eaa",
"detail-type": detail_type,
"source": source,
"account": "012345678912",
"time": "2026-10-26T12:00:00Z",
"region": "us-east-1",
"resources": [],
"detail": {},
}


@pytest.mark.parametrize(
"aws_event,trigger",
[
pytest.param(
{"httpMethod": "GET", "path": "/", "requestContext": {"stage": "prod"}},
"http",
id="api-gateway-rest",
),
pytest.param(
{
"version": "2.0",
"routeKey": "GET /",
"requestContext": {"http": {"method": "GET"}},
},
"http",
id="api-gateway-http",
),
pytest.param(
{"Records": [{"cf": {"request": {"uri": "/"}}}]},
"http",
id="cloudfront",
),
pytest.param(_appsync_event(), "http", id="appsync"),
pytest.param([_appsync_event(), _appsync_event()], "http", id="appsync-batch"),
pytest.param({"Records": [_sqs_record(), _sqs_record()]}, "pubsub", id="sqs"),
pytest.param({"Records": [_sns_record()]}, "pubsub", id="sns"),
pytest.param(
{"Records": [{"eventSource": "aws:kinesis"}]},
"pubsub",
id="kinesis",
),
pytest.param(
{
"eventSource": "aws:kafka",
"records": {
"orders-0": [{"offset": 1}, {"offset": 2}],
"orders-1": [{"offset": 1}],
},
},
"pubsub",
id="msk",
),
pytest.param(
{"eventSource": "SelfManagedKafka", "records": {"orders-0": [{}]}},
"pubsub",
id="self-managed-kafka",
),
pytest.param(
{"eventSource": "aws:mq", "messages": [{}, {}]},
"pubsub",
id="activemq",
),
pytest.param(
{
"eventSource": "aws:rmq",
"rmqMessagesByQueue": {"orders::/": [{}, {}], "alerts::/": [{}]},
},
"pubsub",
id="rabbitmq",
),
pytest.param(
_eventbridge_event("aws.s3", "Object Created"),
"pubsub",
id="eventbridge",
),
pytest.param(
_eventbridge_event("aws.events", "Scheduled Event"),
"timer",
id="eventbridge-schedule",
),
pytest.param({"Records": [{"eventSource": "aws:s3"}]}, "datasource", id="s3"),
pytest.param(
{"Records": [{"eventSource": "aws:dynamodb"}]},
"datasource",
id="dynamodb-streams",
),
pytest.param(
{"eventSource": "aws:docdb", "events": [{}]},
"datasource",
id="documentdb",
),
pytest.param(
[{"good dog": "Maisey"}, {"good dog": "Charlie"}],
"other",
id="list",
),
],
)
def test_trigger(lambda_client, test_environment, aws_event, trigger):
lambda_client.invoke(
FunctionName="BasicOkSpanStreaming",
Payload=json.dumps(aws_event),
)

(segment_span,) = [
s for s in test_environment["server"].span_items if s["is_segment"]
]
attrs = segment_span["attributes"]
assert _get_span_attr(attrs, SPANDATA.FAAS_TRIGGER) == trigger


def test_trace_continuation(lambda_client, test_environment):
Expand Down
Loading