Skip to content

Commit 3c62d91

Browse files
test(client): Add tests for dropped span client reports
1 parent 5a63db3 commit 3c62d91

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed

tests/test_client.py

+191
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import contextlib
12
import os
23
import json
34
import subprocess
45
import sys
56
import time
7+
from collections import Counter, defaultdict
68
from collections.abc import Mapping
79
from textwrap import dedent
810
from unittest import mock
@@ -1214,3 +1216,192 @@ def test_uwsgi_warnings(sentry_init, recwarn, opt, missing_flags):
12141216
assert flag in str(record.message)
12151217
else:
12161218
assert not recwarn
1219+
1220+
1221+
class TestSpanClientReports:
1222+
"""
1223+
Tests for client reports related to spans.
1224+
"""
1225+
1226+
class LostEventCapturingTransport(sentry_sdk.Transport):
1227+
"""
1228+
A transport that captures lost events.
1229+
"""
1230+
1231+
def __init__(self):
1232+
self.record_lost_event_calls = []
1233+
self.record_lost_transaction_calls = []
1234+
1235+
def capture_envelope(self, _):
1236+
pass
1237+
1238+
def record_lost_event(
1239+
self,
1240+
reason,
1241+
data_category=None,
1242+
item=None,
1243+
*,
1244+
quantity=1,
1245+
):
1246+
self.record_lost_event_calls.append((reason, data_category, item, quantity))
1247+
1248+
def record_lost_transaction(
1249+
self,
1250+
reason, # type: str
1251+
span_count, # type: int
1252+
): # type: (...) -> None
1253+
self.record_lost_transaction_calls.append((reason, span_count))
1254+
1255+
@staticmethod
1256+
@contextlib.contextmanager
1257+
def patch_transport():
1258+
"""Patches the transport with a new LostEventCapturingTransport, which we yield."""
1259+
old_transport = sentry_sdk.get_client().transport
1260+
new_transport = TestSpanClientReports.LostEventCapturingTransport()
1261+
sentry_sdk.get_client().transport = new_transport
1262+
1263+
try:
1264+
yield new_transport
1265+
finally:
1266+
sentry_sdk.get_client().transport = old_transport
1267+
1268+
@staticmethod
1269+
def span_dropper(spans_to_drop):
1270+
"""
1271+
Returns a function that can be used to drop spans from an event.
1272+
"""
1273+
1274+
def drop_spans(event, _):
1275+
event["spans"] = event["spans"][spans_to_drop:]
1276+
return event
1277+
1278+
return drop_spans
1279+
1280+
@staticmethod
1281+
def mock_transaction_event(span_count):
1282+
"""
1283+
Returns a mock transaction event with the given number of spans.
1284+
"""
1285+
1286+
return defaultdict(
1287+
mock.MagicMock,
1288+
type="transaction",
1289+
spans=[mock.MagicMock() for _ in range(span_count)],
1290+
)
1291+
1292+
def __init__(self, span_count):
1293+
"""Configures a test case with the number of spans dropped and whether the transaction was dropped."""
1294+
self.span_count = span_count
1295+
self.expected_record_lost_event_calls = Counter()
1296+
self.expected_record_lost_transaction_calls = Counter()
1297+
self.before_send = lambda event, _: event
1298+
self.event_processor = lambda event, _: event
1299+
self.already_dropped_spans = 0
1300+
1301+
def _update_resulting_calls(
1302+
self, reason, drops_transaction=False, drops_spans=None
1303+
):
1304+
"""
1305+
Updates the expected calls with the given resulting calls.
1306+
"""
1307+
if drops_transaction:
1308+
dropped_spans = self.span_count - self.already_dropped_spans
1309+
self.expected_record_lost_transaction_calls[(reason, dropped_spans)] += 1
1310+
1311+
elif drops_spans is not None:
1312+
self.already_dropped_spans += drops_spans
1313+
self.expected_record_lost_event_calls[
1314+
(reason, "span", None, drops_spans)
1315+
] += 1
1316+
1317+
def with_before_send(
1318+
self,
1319+
before_send,
1320+
*,
1321+
drops_transaction=False,
1322+
drops_spans=None,
1323+
):
1324+
"""drops_transaction and drops_spans are mutually exclusive."""
1325+
self.before_send = before_send
1326+
self._update_resulting_calls(
1327+
"before_send",
1328+
drops_transaction,
1329+
drops_spans,
1330+
)
1331+
1332+
return self
1333+
1334+
def with_event_processor(
1335+
self,
1336+
event_processor,
1337+
*,
1338+
drops_transaction=False,
1339+
drops_spans=None,
1340+
):
1341+
self.event_processor = event_processor
1342+
self._update_resulting_calls(
1343+
"event_processor",
1344+
drops_transaction,
1345+
drops_spans,
1346+
)
1347+
1348+
return self
1349+
1350+
def run(self):
1351+
"""Runs the test case with the configured parameters."""
1352+
sentry_sdk.init(before_send_transaction=self.before_send)
1353+
1354+
with sentry_sdk.isolation_scope() as scope:
1355+
scope.add_event_processor(self.event_processor)
1356+
with self.patch_transport() as transport:
1357+
event = self.mock_transaction_event(self.span_count)
1358+
sentry_sdk.get_client().capture_event(event, scope=scope)
1359+
1360+
# We use counters to ensure that the calls are made the expected number of times, disregarding order.
1361+
assert (
1362+
Counter(transport.record_lost_event_calls)
1363+
== self.expected_record_lost_event_calls
1364+
)
1365+
assert (
1366+
Counter(transport.record_lost_transaction_calls)
1367+
== self.expected_record_lost_transaction_calls
1368+
)
1369+
1370+
1371+
@pytest.mark.parametrize(
1372+
"test_config",
1373+
(
1374+
TestSpanClientReports(10), # No spans dropped
1375+
TestSpanClientReports(0).with_before_send(
1376+
lambda e, _: None, drops_transaction=True
1377+
),
1378+
TestSpanClientReports(10).with_before_send(
1379+
lambda e, _: None, drops_transaction=True
1380+
),
1381+
TestSpanClientReports(10).with_before_send(
1382+
TestSpanClientReports.span_dropper(3), drops_spans=3
1383+
),
1384+
TestSpanClientReports(10).with_before_send(
1385+
TestSpanClientReports.span_dropper(10), drops_spans=10
1386+
),
1387+
TestSpanClientReports(10).with_event_processor(
1388+
lambda e, _: None, drops_transaction=True
1389+
),
1390+
TestSpanClientReports(10).with_event_processor(
1391+
TestSpanClientReports.span_dropper(3), drops_spans=3
1392+
),
1393+
TestSpanClientReports(10).with_event_processor(
1394+
TestSpanClientReports.span_dropper(10), drops_spans=10
1395+
),
1396+
TestSpanClientReports(10)
1397+
.with_event_processor(TestSpanClientReports.span_dropper(3), drops_spans=3)
1398+
.with_before_send(TestSpanClientReports.span_dropper(5), drops_spans=5),
1399+
TestSpanClientReports(10)
1400+
.with_event_processor(TestSpanClientReports.span_dropper(3), drops_spans=3)
1401+
.with_before_send(
1402+
lambda e, _: None, drops_transaction=True
1403+
), # Test proper number of spans with each reason
1404+
),
1405+
)
1406+
def test_dropped_transaction(test_config):
1407+
test_config.run()

0 commit comments

Comments
 (0)