Skip to content

feat(span-first): Support before_send_span#6239

Open
sentrivana wants to merge 19 commits into
masterfrom
ivana/span-first-before-send-span
Open

feat(span-first): Support before_send_span#6239
sentrivana wants to merge 19 commits into
masterfrom
ivana/span-first-before-send-span

Conversation

@sentrivana
Copy link
Copy Markdown
Contributor

@sentrivana sentrivana commented May 8, 2026

Description

Add support for before_send_span in span streaming mode.

before_send_span is different from before_send_metric and before_send_log in that:

  • it doesn't allow users to drop a span (i.e., return None)
  • it only allows to modify specific parts of the span

To that end, we're now serializing the span earlier, and exposing the serialized dictionary in the before_send callback. This is consistent with metrics and logs. It also means we're now queuing dictionaries instead of StreamedSpan instances in the span batcher, which should also decrease our memory footprint.

This aligns our implementation with JS.

See https://develop.sentry.dev/sdk/telemetry/spans/scrubbing-data/ for spec.

Issues

@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 8, 2026

PY-2057

@sentrivana sentrivana changed the title feat(span-first): Support before_send_span feat(span-first): Support before_send_span May 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

Codecov Results 📊

13 passed | Total: 13 | Pass Rate: 100% | Execution Time: 3.95s

All tests are passing successfully.

❌ Patch coverage is 15.22%. Project has 16331 uncovered lines.

Files with missing lines (6)
File Patch % Lines
utils.py 50.96% ⚠️ 460 Missing and 79 partials
client.py 42.50% ⚠️ 391 Missing and 63 partials
traces.py 35.83% ⚠️ 206 Missing
_span_batcher.py 23.33% ⚠️ 92 Missing
_types.py 61.90% ⚠️ 16 Missing
consts.py 99.48% ⚠️ 2 Missing

Generated by Codecov Action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

Codecov Results 📊

44 passed | ❌ 13 failed | Total: 57 | Pass Rate: 77.19% | Execution Time: 9.62s

❌ Failed Tests

test_tracing_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 200 + where 500 = <ClientResponse(http://127.0.0.1:42227/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:13 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1159: in test_tracing_span_streaming
    assert resp.status == 200
E   AssertionError: assert 500 == 200
E    +  where 500 = <ClientResponse(http://127.0.0.1:42227/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:13 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_sensitive_header_scrubbing_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 200 + where 500 = <ClientResponse(http://127.0.0.1:43849/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:13 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1227: in test_sensitive_header_scrubbing_span_streaming
    assert resp.status == 200
E   AssertionError: assert 500 == 200
E    +  where 500 = <ClientResponse(http://127.0.0.1:43849/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:13 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_sensitive_header_passthrough_with_pii_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: ValueError: not enough values to unpack (expected 1, got 0)

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1273: in test_sensitive_header_passthrough_with_pii_span_streaming
    (server_span,) = [item.payload for item in items]
E   ValueError: not enough values to unpack (expected 1, got 0)

test_request_body_captured_on_segment_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 200 + where 500 = <ClientResponse(http://127.0.0.1:37321/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:13 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1311: in test_request_body_captured_on_segment_span_streaming
    assert resp.status == 200
E   AssertionError: assert 500 == 200
E    +  where 500 = <ClientResponse(http://127.0.0.1:37321/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:13 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_request_body_not_read_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 200 + where 500 = <ClientResponse(http://127.0.0.1:45665/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:13 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1341: in test_request_body_not_read_span_streaming
    assert resp.status == 200
E   AssertionError: assert 500 == 200
E    +  where 500 = <ClientResponse(http://127.0.0.1:45665/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:13 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_request_body_over_size_limit_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 200 + where 500 = <ClientResponse(http://127.0.0.1:44169/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:14 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1375: in test_request_body_over_size_limit_span_streaming
    assert resp.status == 200
E   AssertionError: assert 500 == 200
E    +  where 500 = <ClientResponse(http://127.0.0.1:44169/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:14 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_url_query_attribute_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 200 + where 500 = <ClientResponse(http://127.0.0.1:44099/?foo=bar&baz=qux) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:14 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1407: in test_url_query_attribute_span_streaming
    assert resp.status == 200
E   AssertionError: assert 500 == 200
E    +  where 500 = <ClientResponse(http://127.0.0.1:44099/?foo=bar&baz=qux) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:14 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_transaction_style_span_streaming[pyloop-/message-handler_name-tests.integrations.aiohttp.test_aiohttp.test_transaction_style_span_streaming.<locals>.hello-component]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 200 + where 500 = <ClientResponse(http://127.0.0.1:42867/message) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:14 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1461: in test_transaction_style_span_streaming
    assert resp.status == 200
E   AssertionError: assert 500 == 200
E    +  where 500 = <ClientResponse(http://127.0.0.1:42867/message) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:14 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_transaction_style_span_streaming[pyloop-/message-method_and_path_pattern-GET /{var}-route]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 200 + where 500 = <ClientResponse(http://127.0.0.1:36003/message) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:14 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1461: in test_transaction_style_span_streaming
    assert resp.status == 200
E   AssertionError: assert 500 == 200
E    +  where 500 = <ClientResponse(http://127.0.0.1:36003/message) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:14 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_server_error_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: assert 2 == 3 + where 2 = len([UnwrappedItem(type='event', payload={'level': 'error', 'exception': {'values': [{'mechanism': {'type': 'aiohttp', 'handled': False}, 'module': None, 'type': 'ZeroDivisionError', 'value': 'division by zero', 'stacktrace': {'frames': [{'filename': 'aiohttp/web_app.py', 'abs_path': '/home/runner/work/sentry-python/sentry-python/.tox/py3.9-aiohttp-v3.7.4/lib/python3.9/site-packages/aiohttp/web_app.py', 'function': '_handle', 'module': 'aiohttp.web_app', 'lineno': 499, 'pre_context': [' partial(m, handler=handler), handler', ' )', ' else:', ' handler = await m(app, handler) # type: ignore', ''], 'context_line': ' resp = await handler(request)', 'post_context': ['', ' return resp', '', ' def call(self) -> "Application":', ' """gunicorn compatibility"""'], 'vars': {'self': {}, 'request': {}, 'loop': '<_UnixSelectorEventLoop running=True closed=False debug=False>', 'debug': 'False', 'match_info': {}, 'resp': 'None', 'expect': 'None', 'handler': '<function test_server_error_span_streaming..hello at 0x7f1991790430>'}, 'in_app': False}, {'filename': 'tests/integrations/aiohttp/test_aiohttp.py', 'abs_path': '/home/runner/work/sentry-python/sentry-python/tests/integrations/aiohttp/test_aiohttp.py', 'function': 'hello', 'module': 'tests.integrations.aiohttp.test_aiohttp', 'lineno': 1482, 'pre_context': [' traces_sample_rate=1.0,', ' _experiments={"trace_lifecycle": "stream"},', ' )', '', ' async def hello(request):'], 'context_line': ' 1 / 0', 'post_context': ['', ' app = web.Application()', ' app.router.add_get("/", hello)', '', ' items = capture_items("event", "span")'], 'vars': {'request': {}}, 'in_app': True}]}}]}, 'event_id': '595dd3acd8fb47ceb8b815b13297740f', 'timestamp': '2026-05-08T13:12:14.841597Z', 'contexts': {'trace': {'trace_id': '357daa0339ac4d38a585a0b806ac6cb2', 'span_id': 'b1e93bf9c589f2e8', 'parent_span_id': 'ab82f4ee25431eae', 'op': 'http.server', 'origin': 'auto.http.aiohttp'}, 'runtime': {'name': 'CPython', 'version': '3.9.25', 'build': '3.9.25 (main, Nov 3 2025, 15:14:54) \n[GCC 11.4.0]'}}, 'transaction': 'tests.integrations.aiohttp.test_aiohttp.test_server_error_span_streaming..hello', 'transaction_info': {'source': <TransactionSource.COMPONENT: 'component'>}, 'breadcrumbs': {'values': []}, 'extra': {'sys.argv': ['/home/runner/work/sentry-python/sentry-python/.tox/py3.9-aiohttp-v3.7.4/lib/python3.9/site-packages/pytest/main.py', '-W', 'error::pytest.PytestUnraisableExceptionWarning', 'tests/integrations/aiohttp', '-o', 'junit_suite_name=py3.9-aiohttp-v3.7.4']}, 'modules': {'sentry-sdk': '2.59.0', 'pytest-aiohttp': '0.3.0', 'multidict': '6.7.1', 'iniconfig': '2.1.0', 'pytest-forked': '1.6.0', 'charset-normalizer': '3.4.7', 'jsonschema': '4.25.1', 'wheel': '0.43.0', 'propcache': '0.4.1', 'tomli': '2.4.1', 'pyyaml': '6.0.3', 'coverage': '7.10.7', 'pytest-localserver': '0.10.0', 'certifi': '2026.4.22', 'hyperframe': '6.1.0', 'colorama': '0.4.6', 'pygments': '2.20.0', 'markupsafe': '3.0.3', 'referencing': '0.36.2', 'pysocks': '1.7.1', 'setuptools': '69.5.1', 'httpcore': '1.0.9', 'urllib3': '2.6.3', 'yarl': '1.22.0', 'h2': '4.3.0', 'hpack': '4.1.0', 'pytest-timeout': '2.4.0', 'asttokens': '3.0.1', 'idna': '3.13', 'py': '1.11.0', 'async-timeout': '3.0.1', 'pytest-watch': '4.2.0', 'pluggy': '1.6.0', 'aiohttp': '3.7.4', 'watchdog': '6.0.0', 'brotli': '1.2.0', 'pytest-cov': '7.1.0', 'responses': '0.26.0', 'socksio': '1.0.0', 'pip': '24.0', 'requests': '2.32.5', 'docopt': '0.6.2', 'typing_extensions': '4.15.0', 'docker': '7.1.0', 'executing': '2.2.1', 'werkzeug': '3.1.8', 'jsonschema-specifications': '2025.9.1', 'h11': '0.16.0', 'packaging': '26.2', 'chardet': '3.0.4', 'attrs': '26.1.0', 'rpds-py': '0.27.1', 'pytest': '8.4.2', 'exceptiongroup': '1.3.1'}, 'request': {'url': 'http://127.0.0.1:41929/', 'query_string': '', 'method': 'GET', 'env': {'REMOTE_ADDR': '127.0.0.1'}, 'headers': {'Host': '127.0.0.1:41929', 'sentry-trace': '357daa0339ac4d38a585a0b806ac6cb2-ab82f4ee25431eae-1', 'baggage': 'sentry-trace_id=357daa0339ac4d38a585a0b806ac6cb2,sentry-sample_rand=0.034682,sentry-environment=production,sentry-release=fe5b14821f6fcc5acfdab3744c00cc8d3876e65f,sentry-transaction=GET%20http%3A//127.0.0.1%3A41929/,sentry-sample_rate=1.0,sentry-sampled=true', 'Accept': '/', 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Python/3.9 aiohttp/3.7.4'}, 'data': None}, 'release': 'fe5b14821f6fcc5acfdab3744c00cc8d3876e65f', 'environment': 'production', 'server_name': 'runnervmrc6n4', 'sdk': {'name': 'sentry.python.aiohttp', 'version': '2.59.0', 'packages': [{'name': 'pypi:sentry-sdk', 'version': '2.59.0'}], 'integrations': ['aiohttp', 'argv', 'atexit', 'dedupe', 'excepthook', 'logging', 'modules', 'stdlib', 'threading']}, 'platform': 'python'}), UnwrappedItem(type='span', payload={'trace_id': '357daa0339ac4d38a585a0b806ac6cb2', 'span_id': 'ab82f4ee25431eae', 'name': 'GET http://127.0.0.1:41929/', 'status': 'error', 'is_segment': True, 'start_timestamp': 1778245934.837718, 'end_timestamp': 1778245934.84913, 'attributes': {'sentry.origin': 'auto.http.aiohttp', 'sentry.op': 'http.client', 'http.request.method': 'GET', 'url.full': 'http://127.0.0.1:41929/', 'thread.id': '139747850922880', 'thread.name': 'MainThread', 'process.command_args': ['/home/runner/work/sentry-python/sentry-python/.tox/py3.9-aiohttp-v3.7.4/lib/python3.9/site-packages/pytest/main.py', '-W', 'error::pytest.PytestUnraisableExceptionWarning', 'tests/integrations/aiohttp', '-o', 'junit_suite_name=py3.9-aiohttp-v3.7.4'], 'http.response.status_code': 500, 'sentry.segment.id': 'ab82f4ee25431eae', 'sentry.segment.name': 'GET http://127.0.0.1:41929/', 'sentry.sdk.name': 'sentry.python.aiohttp', 'sentry.sdk.version': '2.59.0', 'server.address': 'runnervmrc6n4', 'sentry.environment': 'production', 'sentry.release': 'fe5b14821f6fcc5acfdab3744c00cc8d3876e65f'}})])

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1496: in test_server_error_span_streaming
    assert len(items) == 3
E   assert 2 == 3
E    +  where 2 = len([UnwrappedItem(type='event', payload={'level': 'error', 'exception': {'values': [{'mechanism': {'type': 'aiohttp', 'handled': False}, 'module': None, 'type': 'ZeroDivisionError', 'value': 'division by zero', 'stacktrace': {'frames': [{'filename': 'aiohttp/web_app.py', 'abs_path': '/home/runner/work/sentry-python/sentry-python/.tox/py3.9-aiohttp-v3.7.4/lib/python3.9/site-packages/aiohttp/web_app.py', 'function': '_handle', 'module': 'aiohttp.web_app', 'lineno': 499, 'pre_context': ['                                partial(m, handler=handler), handler', '                            )', '                        else:', '                            handler = await m(app, handler)  # type: ignore', ''], 'context_line': '            resp = await handler(request)', 'post_context': ['', '        return resp', '', '    def __call__(self) -> "Application":', '        """gunicorn compatibility"""'], 'vars': {'self': {}, 'request': {}, 'loop': '<_UnixSelectorEventLoop running=True closed=False debug=False>', 'debug': 'False', 'match_info': {}, 'resp': 'None', 'expect': 'None', 'handler': '<function test_server_error_span_streaming.<locals>.hello at 0x7f1991790430>'}, 'in_app': False}, {'filename': 'tests/integrations/aiohttp/test_aiohttp.py', 'abs_path': '/home/runner/work/sentry-python/sentry-python/tests/integrations/aiohttp/test_aiohttp.py', 'function': 'hello', 'module': 'tests.integrations.aiohttp.test_aiohttp', 'lineno': 1482, 'pre_context': ['        traces_sample_rate=1.0,', '        _experiments={"trace_lifecycle": "stream"},', '    )', '', '    async def hello(request):'], 'context_line': '        1 / 0', 'post_context': ['', '    app = web.Application()', '    app.router.add_get("/", hello)', '', '    items = capture_items("event", "span")'], 'vars': {'request': {}}, 'in_app': True}]}}]}, 'event_id': '595dd3acd8fb47ceb8b815b13297740f', 'timestamp': '2026-05-08T13:12:14.841597Z', 'contexts': {'trace': {'trace_id': '357daa0339ac4d38a585a0b806ac6cb2', 'span_id': 'b1e93bf9c589f2e8', 'parent_span_id': 'ab82f4ee25431eae', 'op': 'http.server', 'origin': 'auto.http.aiohttp'}, 'runtime': {'name': 'CPython', 'version': '3.9.25', 'build': '3.9.25 (main, Nov  3 2025, 15:14:54) \n[GCC 11.4.0]'}}, 'transaction': 'tests.integrations.aiohttp.test_aiohttp.test_server_error_span_streaming.<locals>.hello', 'transaction_info': {'source': <TransactionSource.COMPONENT: 'component'>}, 'breadcrumbs': {'values': []}, 'extra': {'sys.argv': ['/home/runner/work/sentry-python/sentry-python/.tox/py3.9-aiohttp-v3.7.4/lib/python3.9/site-packages/pytest/__main__.py', '-W', 'error::pytest.PytestUnraisableExceptionWarning', 'tests/integrations/aiohttp', '-o', 'junit_suite_name=py3.9-aiohttp-v3.7.4']}, 'modules': {'sentry-sdk': '2.59.0', 'pytest-aiohttp': '0.3.0', 'multidict': '6.7.1', 'iniconfig': '2.1.0', 'pytest-forked': '1.6.0', 'charset-normalizer': '3.4.7', 'jsonschema': '4.25.1', 'wheel': '0.43.0', 'propcache': '0.4.1', 'tomli': '2.4.1', 'pyyaml': '6.0.3', 'coverage': '7.10.7', 'pytest-localserver': '0.10.0', 'certifi': '2026.4.22', 'hyperframe': '6.1.0', 'colorama': '0.4.6', 'pygments': '2.20.0', 'markupsafe': '3.0.3', 'referencing': '0.36.2', 'pysocks': '1.7.1', 'setuptools': '69.5.1', 'httpcore': '1.0.9', 'urllib3': '2.6.3', 'yarl': '1.22.0', 'h2': '4.3.0', 'hpack': '4.1.0', 'pytest-timeout': '2.4.0', 'asttokens': '3.0.1', 'idna': '3.13', 'py': '1.11.0', 'async-timeout': '3.0.1', 'pytest-watch': '4.2.0', 'pluggy': '1.6.0', 'aiohttp': '3.7.4', 'watchdog': '6.0.0', 'brotli': '1.2.0', 'pytest-cov': '7.1.0', 'responses': '0.26.0', 'socksio': '1.0.0', 'pip': '24.0', 'requests': '2.32.5', 'docopt': '0.6.2', 'typing_extensions': '4.15.0', 'docker': '7.1.0', 'executing': '2.2.1', 'werkzeug': '3.1.8', 'jsonschema-specifications': '2025.9.1', 'h11': '0.16.0', 'packaging': '26.2', 'chardet': '3.0.4', 'attrs': '26.1.0', 'rpds-py': '0.27.1', 'pytest': '8.4.2', 'exceptiongroup': '1.3.1'}, 'request': {'url': 'http://127.0.0.1:41929/', 'query_string': '', 'method': 'GET', 'env': {'REMOTE_ADDR': '127.0.0.1'}, 'headers': {'Host': '127.0.0.1:41929', 'sentry-trace': '357daa0339ac4d38a585a0b806ac6cb2-ab82f4ee25431eae-1', 'baggage': 'sentry-trace_id=357daa0339ac4d38a585a0b806ac6cb2,sentry-sample_rand=0.034682,sentry-environment=production,sentry-release=fe5b14821f6fcc5acfdab3744c00cc8d3876e65f,sentry-transaction=GET%20http%3A//127.0.0.1%3A41929/,sentry-sample_rate=1.0,sentry-sampled=true', 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Python/3.9 aiohttp/3.7.4'}, 'data': None}, 'release': 'fe5b14821f6fcc5acfdab3744c00cc8d3876e65f', 'environment': 'production', 'server_name': 'runnervmrc6n4', 'sdk': {'name': 'sentry.python.aiohttp', 'version': '2.59.0', 'packages': [{'name': 'pypi:sentry-sdk', 'version': '2.59.0'}], 'integrations': ['aiohttp', 'argv', 'atexit', 'dedupe', 'excepthook', 'logging', 'modules', 'stdlib', 'threading']}, 'platform': 'python'}), UnwrappedItem(type='span', payload={'trace_id': '357daa0339ac4d38a585a0b806ac6cb2', 'span_id': 'ab82f4ee25431eae', 'name': 'GET http://127.0.0.1:41929/', 'status': 'error', 'is_segment': True, 'start_timestamp': 1778245934.837718, 'end_timestamp': 1778245934.84913, 'attributes': {'sentry.origin': 'auto.http.aiohttp', 'sentry.op': 'http.client', 'http.request.method': 'GET', 'url.full': 'http://127.0.0.1:41929/', 'thread.id': '139747850922880', 'thread.name': 'MainThread', 'process.command_args': ['/home/runner/work/sentry-python/sentry-python/.tox/py3.9-aiohttp-v3.7.4/lib/python3.9/site-packages/pytest/__main__.py', '-W', 'error::pytest.PytestUnraisableExceptionWarning', 'tests/integrations/aiohttp', '-o', 'junit_suite_name=py3.9-aiohttp-v3.7.4'], 'http.response.status_code': 500, 'sentry.segment.id': 'ab82f4ee25431eae', 'sentry.segment.name': 'GET http://127.0.0.1:41929/', 'sentry.sdk.name': 'sentry.python.aiohttp', 'sentry.sdk.version': '2.59.0', 'server.address': 'runnervmrc6n4', 'sentry.environment': 'production', 'sentry.release': 'fe5b14821f6fcc5acfdab3744c00cc8d3876e65f'}})])

test_http_exception_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 403 + where 500 = <ClientResponse(http://127.0.0.1:33723/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:15 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1542: in test_http_exception_span_streaming
    assert resp.status == 403
E   AssertionError: assert 500 == 403
E    +  where 500 = <ClientResponse(http://127.0.0.1:33723/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:15 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_http_exception_ok_status_not_overridden_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 500 == 302 + where 500 = <ClientResponse(http://127.0.0.1:41277/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:15 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1580: in test_http_exception_ok_status_not_overridden_span_streaming
    assert resp.status == 302
E   AssertionError: assert 500 == 302
E    +  where 500 = <ClientResponse(http://127.0.0.1:41277/) [500 Internal Server Error]>\n<CIMultiDictProxy('Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '55', 'Date': 'Fri, 08 May 2026 13:12:15 GMT', 'Server': 'Python/3.9 aiohttp/3.7.4', 'Connection': 'close')>\n.status

test_outgoing_client_span_span_streaming[pyloop]

File: tests.integrations.aiohttp.test_aiohttp
Suite: py3.9-aiohttp-v3.7.4
Error: AssertionError: assert 1 == 3 + where 1 = len([UnwrappedItem(type='span', payload={'trace_id': '96650ac078bc44d3a53c8b0a24c75a6c', 'span_id': '9bce2b81809406c2', 'name': 'GET http://127.0.0.1:41523/', 'status': 'error', 'is_segment': True, 'start_timestamp': 1778245935.378089, 'end_timestamp': 1778245935.623357, 'attributes': {'sentry.origin': 'auto.http.aiohttp', 'sentry.op': 'http.client', 'http.request.method': 'GET', 'url.full': 'http://127.0.0.1:41523/', 'thread.id': '139747850922880', 'thread.name': 'MainThread', 'process.command_args': ['/home/runner/work/sentry-python/sentry-python/.tox/py3.9-aiohttp-v3.7.4/lib/python3.9/site-packages/pytest/main.py', '-W', 'error::pytest.PytestUnraisableExceptionWarning', 'tests/integrations/aiohttp', '-o', 'junit_suite_name=py3.9-aiohttp-v3.7.4'], 'http.response.status_code': 500, 'code.line.number': 1624, 'code.namespace': 'tests.integrations.aiohttp.test_aiohttp', 'code.file.path': 'tests/integrations/aiohttp/test_aiohttp.py', 'code.function': 'test_outgoing_client_span_span_streaming', 'sentry.segment.id': '9bce2b81809406c2', 'sentry.segment.name': 'GET http://127.0.0.1:41523/', 'sentry.sdk.name': 'sentry.python.aiohttp', 'sentry.sdk.version': '2.59.0', 'server.address': 'runnervmrc6n4', 'sentry.environment': 'production', 'sentry.release': 'fe5b14821f6fcc5acfdab3744c00cc8d3876e65f'}})])

Stack Trace
tests/integrations/aiohttp/test_aiohttp.py:1632: in test_outgoing_client_span_span_streaming
    assert len(items) == 3
E   AssertionError: assert 1 == 3
E    +  where 1 = len([UnwrappedItem(type='span', payload={'trace_id': '96650ac078bc44d3a53c8b0a24c75a6c', 'span_id': '9bce2b81809406c2', 'name': 'GET http://127.0.0.1:41523/', 'status': 'error', 'is_segment': True, 'start_timestamp': 1778245935.378089, 'end_timestamp': 1778245935.623357, 'attributes': {'sentry.origin': 'auto.http.aiohttp', 'sentry.op': 'http.client', 'http.request.method': 'GET', 'url.full': 'http://127.0.0.1:41523/', 'thread.id': '139747850922880', 'thread.name': 'MainThread', 'process.command_args': ['/home/runner/work/sentry-python/sentry-python/.tox/py3.9-aiohttp-v3.7.4/lib/python3.9/site-packages/pytest/__main__.py', '-W', 'error::pytest.PytestUnraisableExceptionWarning', 'tests/integrations/aiohttp', '-o', 'junit_suite_name=py3.9-aiohttp-v3.7.4'], 'http.response.status_code': 500, 'code.line.number': 1624, 'code.namespace': 'tests.integrations.aiohttp.test_aiohttp', 'code.file.path': 'tests/integrations/aiohttp/test_aiohttp.py', 'code.function': 'test_outgoing_client_span_span_streaming', 'sentry.segment.id': '9bce2b81809406c2', 'sentry.segment.name': 'GET http://127.0.0.1:41523/', 'sentry.sdk.name': 'sentry.python.aiohttp', 'sentry.sdk.version': '2.59.0', 'server.address': 'runnervmrc6n4', 'sentry.environment': 'production', 'sentry.release': 'fe5b14821f6fcc5acfdab3744c00cc8d3876e65f'}})])

❌ Patch coverage is 45.00%. Project has 16033 uncovered lines.

Files with missing lines (3)
File Patch % Lines
utils.py 54.32% ⚠️ 428 Missing and 90 partials
client.py 61.82% ⚠️ 210 Missing and 78 partials
consts.py 99.22% ⚠️ 2 Missing

Generated by Codecov Action

Comment thread sentry_sdk/_span_batcher.py Outdated
Comment thread sentry_sdk/client.py Outdated
Comment thread sentry_sdk/client.py Outdated
Comment thread sentry_sdk/client.py Outdated
Comment thread sentry_sdk/client.py
Comment thread sentry_sdk/client.py
if serialized is None:
return

elif ty == "span" and isinstance(telemetry, StreamedSpan):
Copy link
Copy Markdown
Contributor Author

@sentrivana sentrivana May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isinstance(telemetry, StreamedSpan) part of the condition is only there because mypy was complaining :( It can't deal with dispatchers/generics very well in general.

class fake_record_sql_queries: # noqa: N801
def __init__(self, *args, **kwargs):
with record_sql_queries_supporting_streaming(
self._ctx_mgr = record_sql_queries_supporting_streaming(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to change this test because it was failing -- it was closing the span too early.

@sentrivana sentrivana marked this pull request as ready for review May 13, 2026 11:24
@sentrivana sentrivana requested a review from a team as a code owner May 13, 2026 11:24
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e65b1c2. Configure here.

Comment thread sentry_sdk/_span_batcher.py
Comment thread sentry_sdk/utils.py
Copy link
Copy Markdown
Contributor

@alexander-alderman-webb alexander-alderman-webb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Copy Markdown
Member

@ericapisani ericapisani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM to me overall, just saw a few polishing opportunities. Approving so as not to block :shipit:

# estimate the attributes separately.
estimate = 210
for value in item._attributes.values():
for value in (item.get("attributes") or {}).values():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be made slightly more concise by doing the following:

Suggested change
for value in (item.get("attributes") or {}).values():
for value in (item.get("attributes", {})).values():

Comment thread sentry_sdk/client.py
# Spans can't be dropped in before_send_span by design. They can
# be altered though (e.g. to sanitize). Only allow changes to
# name and attributes.
if isinstance(serialized, dict) and serialized and "name" in serialized:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The and serialized part is unnecessary because if the isinstance(serialized, dict) is truthy, then you can perform the "name" in serialized check without worrying about an exception being raised.

Suggested change
if isinstance(serialized, dict) and serialized and "name" in serialized:
if isinstance(serialized, dict) and "name" in serialized:

Comment thread sentry_sdk/client.py
if isinstance(serialized, dict) and serialized and "name" in serialized:
telemetry.name = serialized["name"] # type: ignore[typeddict-item]
telemetry._attributes = {}
for k, v in (serialized.get("attributes") or {}).items():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comment above, you can remove the or by adding the {} as the fallback on the get:

Suggested change
for k, v in (serialized.get("attributes") or {}).items():
for k, v in (serialized.get("attributes", {})).items():

Comment thread sentry_sdk/client.py

elif ty == "span":
# We need a reference to the segment span in the batcher to populate
# the DSC
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd avoid using acronyms for comments that are explaining behaviour or are intended to provide context - it makes it difficult for those not familiar with Sentry's features/terminology to follow along

Suggested change
# the DSC
# the dynamic sampling context

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add before_send_span

3 participants