Skip to content

Commit 56b920d

Browse files
committed
fix(aws-lambda): Only set messaging.batch.message_count for messaging batches (closes #8078)
1 parent 7ad4e1a commit 56b920d

3 files changed

Lines changed: 282 additions & 4 deletions

File tree

‎sentry_sdk/integrations/aws_lambda.py‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,32 @@ def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any":
9696
return sentry_init_error # type: ignore
9797

9898

99+
def _get_messaging_batch_count(aws_event: "Any") -> "Optional[int]":
100+
"""
101+
Returns the message count if the event is from a recognized messaging
102+
service (SQS or SNS), or None otherwise.
103+
"""
104+
if not isinstance(aws_event, dict):
105+
return None
106+
107+
records = aws_event.get("Records")
108+
if not isinstance(records, list) or not records:
109+
return None
110+
111+
first_record = records[0]
112+
if not isinstance(first_record, dict):
113+
return None
114+
115+
event_source = (
116+
first_record.get("eventSource") or first_record.get("EventSource") or ""
117+
).lower()
118+
119+
if event_source in ("aws:sqs", "aws:sns") or "Sns" in first_record:
120+
return len(records)
121+
122+
return None
123+
124+
99125
def _wrap_handler(handler: "F") -> "F":
100126
@functools.wraps(handler)
101127
def sentry_handler(
@@ -117,7 +143,11 @@ def sentry_handler(
117143
if integration is None:
118144
return handler(aws_event, aws_context, *args, **kwargs)
119145

120-
if isinstance(aws_event, list) and len(aws_event) >= 1:
146+
messaging_batch_count = _get_messaging_batch_count(aws_event)
147+
if messaging_batch_count is not None:
148+
request_data = aws_event
149+
batch_size = messaging_batch_count
150+
elif isinstance(aws_event, list) and len(aws_event) >= 1:
121151
request_data = aws_event[0]
122152
batch_size = len(aws_event)
123153
else:
@@ -240,9 +270,13 @@ def sentry_handler(
240270
"aws.lambda.invoked_arn": aws_context.invoked_function_arn,
241271
"aws.log.group.names": [aws_context.log_group_name],
242272
"aws.log.stream.names": [aws_context.log_stream_name],
243-
"messaging.batch.message_count": batch_size,
244273
**header_attributes,
245274
**additional_attributes,
275+
**(
276+
{"messaging.batch.message_count": messaging_batch_count}
277+
if messaging_batch_count is not None
278+
else {}
279+
),
246280
},
247281
)
248282
else:

‎tests/integrations/aws_lambda/test_aws_lambda.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -976,7 +976,7 @@ def test_span_streaming_no_error(lambda_client, test_environment):
976976
"aws/lambda/BasicOkSpanStreaming"
977977
]
978978
assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"]
979-
assert _get_span_attr(attrs, "messaging.batch.message_count") == 1
979+
assert "messaging.batch.message_count" not in attrs
980980

981981

982982
def test_span_streaming_error(lambda_client, test_environment):
@@ -1026,7 +1026,7 @@ def test_span_streaming_error(lambda_client, test_environment):
10261026
"aws/lambda/RaiseErrorSpanStreaming"
10271027
]
10281028
assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"]
1029-
assert _get_span_attr(attrs, "messaging.batch.message_count") == 1
1029+
assert "messaging.batch.message_count" not in attrs
10301030

10311031

10321032
def test_span_streaming_trace_continuation(lambda_client, test_environment):
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import sentry_sdk
2+
from sentry_sdk.integrations.aws_lambda import (
3+
AwsLambdaIntegration,
4+
_get_messaging_batch_count,
5+
_wrap_handler,
6+
)
7+
8+
9+
class MockContext:
10+
function_name = "test_function"
11+
function_version = "$LATEST"
12+
invoked_function_arn = (
13+
"arn:aws:lambda:us-east-1:123456789012:function:test_function"
14+
)
15+
aws_request_id = "test-request-id"
16+
log_group_name = "/aws/lambda/test_function"
17+
log_stream_name = "2026/10/29/[$LATEST]test"
18+
19+
def get_remaining_time_in_millis(self):
20+
return 300000
21+
22+
23+
def test_get_messaging_batch_count_sqs():
24+
single_sqs = {
25+
"Records": [
26+
{
27+
"eventSource": "aws:sqs",
28+
"messageId": "1",
29+
"body": "hello",
30+
}
31+
]
32+
}
33+
assert _get_messaging_batch_count(single_sqs) == 1
34+
35+
batch_sqs = {
36+
"Records": [{"eventSource": "aws:sqs", "messageId": str(i)} for i in range(5)]
37+
}
38+
assert _get_messaging_batch_count(batch_sqs) == 5
39+
40+
# Case insensitive EventSource
41+
capitalized_sqs = {
42+
"Records": [
43+
{
44+
"EventSource": "aws:sqs",
45+
"messageId": "1",
46+
}
47+
]
48+
}
49+
assert _get_messaging_batch_count(capitalized_sqs) == 1
50+
51+
52+
def test_get_messaging_batch_count_sns():
53+
single_sns = {
54+
"Records": [
55+
{
56+
"EventSource": "aws:sns",
57+
"Sns": {"Message": "hello"},
58+
}
59+
]
60+
}
61+
assert _get_messaging_batch_count(single_sns) == 1
62+
63+
batch_sns = {
64+
"Records": [
65+
{
66+
"EventSource": "aws:sns",
67+
"Sns": {"Message": f"msg {i}"},
68+
}
69+
for i in range(3)
70+
]
71+
}
72+
assert _get_messaging_batch_count(batch_sns) == 3
73+
74+
75+
def test_get_messaging_batch_count_non_messaging():
76+
assert _get_messaging_batch_count({}) is None
77+
assert _get_messaging_batch_count({"httpMethod": "GET", "path": "/"}) is None
78+
assert _get_messaging_batch_count([{"key": "val"}]) is None
79+
assert _get_messaging_batch_count("string_event") is None
80+
assert _get_messaging_batch_count({"Records": []}) is None
81+
82+
# S3 event
83+
s3_event = {
84+
"Records": [
85+
{
86+
"eventSource": "aws:s3",
87+
"s3": {"bucket": {"name": "my-bucket"}},
88+
}
89+
]
90+
}
91+
assert _get_messaging_batch_count(s3_event) is None
92+
93+
# DynamoDB event
94+
dynamo_event = {
95+
"Records": [
96+
{
97+
"eventSource": "aws:dynamodb",
98+
"eventName": "INSERT",
99+
}
100+
]
101+
}
102+
assert _get_messaging_batch_count(dynamo_event) is None
103+
104+
# Kinesis event
105+
kinesis_event = {
106+
"Records": [
107+
{
108+
"eventSource": "aws:kinesis",
109+
"kinesis": {"data": "dGVzdA=="},
110+
}
111+
]
112+
}
113+
assert _get_messaging_batch_count(kinesis_event) is None
114+
115+
116+
def test_handler_sqs_batch_span_streaming(sentry_init, capture_items):
117+
sentry_init(
118+
integrations=[AwsLambdaIntegration()],
119+
traces_sample_rate=1.0,
120+
trace_lifecycle="stream",
121+
)
122+
items = capture_items("span")
123+
124+
sqs_event = {
125+
"Records": [
126+
{"eventSource": "aws:sqs", "messageId": "1"},
127+
{"eventSource": "aws:sqs", "messageId": "2"},
128+
{"eventSource": "aws:sqs", "messageId": "3"},
129+
]
130+
}
131+
132+
tags_recorded = {}
133+
134+
@_wrap_handler
135+
def handler(event, context):
136+
tags_recorded.update(sentry_sdk.get_isolation_scope()._tags)
137+
return "ok"
138+
139+
res = handler(sqs_event, MockContext())
140+
assert res == "ok"
141+
142+
sentry_sdk.flush()
143+
spans = [item.payload for item in items]
144+
assert len(spans) == 1
145+
span = spans[0]
146+
147+
assert span["attributes"]["messaging.batch.message_count"] == 3
148+
assert tags_recorded.get("batch_request") is True
149+
assert tags_recorded.get("batch_size") == 3
150+
151+
152+
def test_handler_sns_single_span_streaming(sentry_init, capture_items):
153+
sentry_init(
154+
integrations=[AwsLambdaIntegration()],
155+
traces_sample_rate=1.0,
156+
trace_lifecycle="stream",
157+
)
158+
items = capture_items("span")
159+
160+
sns_event = {
161+
"Records": [
162+
{"EventSource": "aws:sns", "Sns": {"Message": "hi"}},
163+
]
164+
}
165+
166+
tags_recorded = {}
167+
168+
@_wrap_handler
169+
def handler(event, context):
170+
tags_recorded.update(sentry_sdk.get_isolation_scope()._tags)
171+
return "ok"
172+
173+
res = handler(sns_event, MockContext())
174+
assert res == "ok"
175+
176+
sentry_sdk.flush()
177+
spans = [item.payload for item in items]
178+
assert len(spans) == 1
179+
span = spans[0]
180+
181+
assert span["attributes"]["messaging.batch.message_count"] == 1
182+
assert "batch_request" not in tags_recorded
183+
assert "batch_size" not in tags_recorded
184+
185+
186+
def test_handler_non_messaging_span_streaming(sentry_init, capture_items):
187+
sentry_init(
188+
integrations=[AwsLambdaIntegration()],
189+
traces_sample_rate=1.0,
190+
trace_lifecycle="stream",
191+
)
192+
items = capture_items("span")
193+
194+
http_event = {"httpMethod": "GET", "path": "/hello"}
195+
196+
tags_recorded = {}
197+
198+
@_wrap_handler
199+
def handler(event, context):
200+
tags_recorded.update(sentry_sdk.get_isolation_scope()._tags)
201+
return "ok"
202+
203+
res = handler(http_event, MockContext())
204+
assert res == "ok"
205+
206+
sentry_sdk.flush()
207+
spans = [item.payload for item in items]
208+
assert len(spans) == 1
209+
span = spans[0]
210+
211+
assert "messaging.batch.message_count" not in span["attributes"]
212+
assert "batch_request" not in tags_recorded
213+
214+
215+
def test_handler_top_level_list_span_streaming(sentry_init, capture_items):
216+
sentry_init(
217+
integrations=[AwsLambdaIntegration()],
218+
traces_sample_rate=1.0,
219+
trace_lifecycle="stream",
220+
)
221+
items = capture_items("span")
222+
223+
list_event = [{"data": 1}, {"data": 2}]
224+
225+
tags_recorded = {}
226+
227+
@_wrap_handler
228+
def handler(event, context):
229+
tags_recorded.update(sentry_sdk.get_isolation_scope()._tags)
230+
return "ok"
231+
232+
res = handler(list_event, MockContext())
233+
assert res == "ok"
234+
235+
sentry_sdk.flush()
236+
spans = [item.payload for item in items]
237+
assert len(spans) == 1
238+
span = spans[0]
239+
240+
# Non-messaging top-level list should not have messaging.batch.message_count
241+
assert "messaging.batch.message_count" not in span["attributes"]
242+
# But batch_request tags are set because len(list_event) > 1
243+
assert tags_recorded.get("batch_request") is True
244+
assert tags_recorded.get("batch_size") == 2

0 commit comments

Comments
 (0)