|
| 1 | +import contextlib |
1 | 2 | import os
|
2 | 3 | import json
|
3 | 4 | import subprocess
|
4 | 5 | import sys
|
5 | 6 | import time
|
| 7 | +from collections import Counter, defaultdict |
6 | 8 | from collections.abc import Mapping
|
7 | 9 | from textwrap import dedent
|
8 | 10 | from unittest import mock
|
@@ -1214,3 +1216,192 @@ def test_uwsgi_warnings(sentry_init, recwarn, opt, missing_flags):
|
1214 | 1216 | assert flag in str(record.message)
|
1215 | 1217 | else:
|
1216 | 1218 | 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