From 0680d183b4d9d081d1bdb3f4a9075fc00d2e44dc Mon Sep 17 00:00:00 2001 From: harryautomazione Date: Thu, 19 Mar 2026 14:45:11 +0100 Subject: [PATCH 1/6] docs: Document span filtering migration to span first --- MIGRATION_GUIDE.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 53396a37ba..6b7e56dcbf 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -190,3 +190,41 @@ Looking to upgrade from Sentry SDK 1.x to 2.x? Here's a comprehensive list of wh - Deprecated `sentry_sdk.transport.Transport.capture_event`. Please use `sentry_sdk.transport.Transport.capture_envelope`, instead. - Passing a function to `sentry_sdk.init`'s `transport` keyword argument has been deprecated. If you wish to provide a custom transport, please pass a `sentry_sdk.transport.Transport` instance or a subclass. - The parameter `propagate_hub` in `ThreadingIntegration()` was deprecated and renamed to `propagate_scope`. + +## Migrating Span Filtering to Span First + +If you are using the experimental **Span First** mode (also known as span streaming, enabled via `_experiments={"trace_lifecycle": "stream"}`), you might notice that individual spans are sent in real-time and do not pass together through `before_send_transaction`. + +To filter or ignore individual spans in Span First mode, you can use the experimental `ignore_spans` configuration option: + +```python +import sentry_sdk +import re + +sentry_sdk.init( + dsn="...", + traces_sample_rate=1.0, # Required for tracing + _experiments={ + "trace_lifecycle": "stream", + "ignore_spans": [ + "ignore_this_exact_name", # 1. Match by name (String) + re.compile(r"^GET /static/.*"), # 2. Match by name (Regex Range) + {"name": "ignore_by_name_dict"}, # 3. Match by name (Dict) + { # 4. Match by Span Attribute + "attributes": { + "http.status_code": 200, + } + } + ] + } +) +``` + +### Key Differences: +- **Before (Transaction Mode)**: You filtered child spans by mutating `event["spans"]` inside `before_send_transaction`. +- **After (Span First)**: You define rules up-front in `ignore_spans`. If a rule matches, a `NoOpStreamedSpan` is returned immediately resulting in no overhead. +- **Inheritance**: Any child span started with an ignored parent will automatically be ignored as well. + +> [!NOTE] +> `ignore_spans` is currently an experimental feature reading from `_experiments`. + From cd8189025173395f78a12a5d172a9704a2a4f859 Mon Sep 17 00:00:00 2001 From: harryautomazione Date: Thu, 19 Mar 2026 15:30:56 +0100 Subject: [PATCH 2/6] feat(db): Distinguish async vs sync database drivers with db.driver attribute --- sentry_sdk/integrations/asyncpg.py | 1 + sentry_sdk/integrations/sqlalchemy.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/sentry_sdk/integrations/asyncpg.py b/sentry_sdk/integrations/asyncpg.py index 7f3591154a..873d620361 100644 --- a/sentry_sdk/integrations/asyncpg.py +++ b/sentry_sdk/integrations/asyncpg.py @@ -192,6 +192,7 @@ async def _inner(*args: "Any", **kwargs: "Any") -> "T": def _set_db_data(span: "Span", conn: "Any") -> None: span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + span.set_data("db.driver", "asyncpg") addr = conn._addr if addr: diff --git a/sentry_sdk/integrations/sqlalchemy.py b/sentry_sdk/integrations/sqlalchemy.py index 7d3ed95373..c29d5a84ab 100644 --- a/sentry_sdk/integrations/sqlalchemy.py +++ b/sentry_sdk/integrations/sqlalchemy.py @@ -137,6 +137,9 @@ def _set_db_data(span: "Span", conn: "Any") -> None: if db_system is not None: span.set_data(SPANDATA.DB_SYSTEM, db_system) + if hasattr(conn.engine, "dialect") and hasattr(conn.engine.dialect, "driver"): + span.set_data("db.driver", conn.engine.dialect.driver) + if conn.engine.url is None: return From 8c0ea16de1e8191d12f55c3c17ffbecdaad2db82 Mon Sep 17 00:00:00 2001 From: harryautomazione Date: Thu, 19 Mar 2026 16:30:47 +0100 Subject: [PATCH 3/6] fix(asyncpg): add db.driver to connect span and cleanup docs --- MIGRATION_GUIDE.md | 38 ------------------------------ sentry_sdk/integrations/asyncpg.py | 1 + 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 6b7e56dcbf..53396a37ba 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -190,41 +190,3 @@ Looking to upgrade from Sentry SDK 1.x to 2.x? Here's a comprehensive list of wh - Deprecated `sentry_sdk.transport.Transport.capture_event`. Please use `sentry_sdk.transport.Transport.capture_envelope`, instead. - Passing a function to `sentry_sdk.init`'s `transport` keyword argument has been deprecated. If you wish to provide a custom transport, please pass a `sentry_sdk.transport.Transport` instance or a subclass. - The parameter `propagate_hub` in `ThreadingIntegration()` was deprecated and renamed to `propagate_scope`. - -## Migrating Span Filtering to Span First - -If you are using the experimental **Span First** mode (also known as span streaming, enabled via `_experiments={"trace_lifecycle": "stream"}`), you might notice that individual spans are sent in real-time and do not pass together through `before_send_transaction`. - -To filter or ignore individual spans in Span First mode, you can use the experimental `ignore_spans` configuration option: - -```python -import sentry_sdk -import re - -sentry_sdk.init( - dsn="...", - traces_sample_rate=1.0, # Required for tracing - _experiments={ - "trace_lifecycle": "stream", - "ignore_spans": [ - "ignore_this_exact_name", # 1. Match by name (String) - re.compile(r"^GET /static/.*"), # 2. Match by name (Regex Range) - {"name": "ignore_by_name_dict"}, # 3. Match by name (Dict) - { # 4. Match by Span Attribute - "attributes": { - "http.status_code": 200, - } - } - ] - } -) -``` - -### Key Differences: -- **Before (Transaction Mode)**: You filtered child spans by mutating `event["spans"]` inside `before_send_transaction`. -- **After (Span First)**: You define rules up-front in `ignore_spans`. If a rule matches, a `NoOpStreamedSpan` is returned immediately resulting in no overhead. -- **Inheritance**: Any child span started with an ignored parent will automatically be ignored as well. - -> [!NOTE] -> `ignore_spans` is currently an experimental feature reading from `_experiments`. - diff --git a/sentry_sdk/integrations/asyncpg.py b/sentry_sdk/integrations/asyncpg.py index 873d620361..fa042f955e 100644 --- a/sentry_sdk/integrations/asyncpg.py +++ b/sentry_sdk/integrations/asyncpg.py @@ -169,6 +169,7 @@ async def _inner(*args: "Any", **kwargs: "Any") -> "T": origin=AsyncPGIntegration.origin, ) as span: span.set_data(SPANDATA.DB_SYSTEM, "postgresql") + span.set_data("db.driver", "asyncpg") addr = kwargs.get("addr") if addr: try: From 397f9611f7fd4a4490e2e4657c40ed3da9f85efe Mon Sep 17 00:00:00 2001 From: harryautomazione Date: Thu, 19 Mar 2026 17:32:36 +0100 Subject: [PATCH 4/6] Update docs with new Tracing and Scope APIs (#5508) --- docs/api.rst | 4 +++ docs/index.rst | 1 + docs/tracing.rst | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 docs/tracing.rst diff --git a/docs/api.rst b/docs/api.rst index 802abee75d..1e384e6b2e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -66,3 +66,7 @@ Managing Scope (advanced) .. autofunction:: sentry_sdk.api.push_scope .. autofunction:: sentry_sdk.api.new_scope +.. autofunction:: sentry_sdk.api.isolation_scope + +.. autofunction:: sentry_sdk.api.get_current_scope +.. autofunction:: sentry_sdk.api.get_isolation_scope diff --git a/docs/index.rst b/docs/index.rst index 12668a2825..fe004863e8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,5 +8,6 @@ visit the `GitHub repository `_. .. toctree:: api + tracing integrations apidocs diff --git a/docs/tracing.rst b/docs/tracing.rst new file mode 100644 index 0000000000..de08b4aaf4 --- /dev/null +++ b/docs/tracing.rst @@ -0,0 +1,66 @@ +======= +Tracing +======= + +With Performance Monitoring, Sentry tracks your software's performance, measuring variables such as throughput and latency. + +Manual Instrumentation +===================== + +You can manually start transactions and spans to trace custom operations in your application. + +Transactions +------------ +A transaction represents a single instance of a service being called. It forms the root of a trace tree. + +.. code-block:: python + + import sentry_sdk + + # Start a transaction as a context manager + with sentry_sdk.start_transaction(name="process-order"): + # Your application logic here + pass + +Spans +----- +Spans represent individual units of work within a transaction, such as a database query or an API call. + +.. code-block:: python + + import sentry_sdk + + # Start a child span under the current transaction + with sentry_sdk.start_child_span(op="db.query", name="SELECT * FROM users"): + # Your operation here + pass + + +Managing Context with Scopes +============================ + +Sentry use **Scopes** to manage execution context and event enrichment. In SDK 2.x, top-level APIs replace the deprecated Hub model. + +Isolation Scope +--------------- +The `isolation_scope` should be used for isolating data that belongs to a single request or job lifecycle. It propagates data across child scopes. + +.. code-block:: python + + import sentry_sdk + + with sentry_sdk.isolation_scope() as scope: + scope.set_tag("user_type", "admin") + # Operations triggered here will include the tag + +New Scope +--------- +The `new_scope` forks the current scope for local, short-lived modifications. + +.. code-block:: python + + import sentry_sdk + + with sentry_sdk.new_scope() as scope: + scope.set_extra("temp_debug_data", 123) + # Changes are discarded when existing the block From 61a0b65ea23616465a69a3f26813945e5d28358c Mon Sep 17 00:00:00 2001 From: harryautomazione Date: Thu, 19 Mar 2026 17:41:52 +0100 Subject: [PATCH 5/6] fix: Correct start_child_span to start_span in docs --- docs/tracing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tracing.rst b/docs/tracing.rst index de08b4aaf4..a736e5f827 100644 --- a/docs/tracing.rst +++ b/docs/tracing.rst @@ -31,7 +31,7 @@ Spans represent individual units of work within a transaction, such as a databas import sentry_sdk # Start a child span under the current transaction - with sentry_sdk.start_child_span(op="db.query", name="SELECT * FROM users"): + with sentry_sdk.start_span(op="db.query", name="SELECT * FROM users"): # Your operation here pass From 7bb6846ea283f33ed4fc043827757ba2288f548f Mon Sep 17 00:00:00 2001 From: harryautomazione Date: Thu, 19 Mar 2026 21:09:04 +0100 Subject: [PATCH 6/6] refactor: Remove internal function usage in test_pydantic_ai --- .../pydantic_ai/test_pydantic_ai.py | 110 ++++++------------ 1 file changed, 37 insertions(+), 73 deletions(-) diff --git a/tests/integrations/pydantic_ai/test_pydantic_ai.py b/tests/integrations/pydantic_ai/test_pydantic_ai.py index f0ddc6c4ed..d5be7bf6c4 100644 --- a/tests/integrations/pydantic_ai/test_pydantic_ai.py +++ b/tests/integrations/pydantic_ai/test_pydantic_ai.py @@ -1637,7 +1637,7 @@ async def test_input_messages_error_handling(sentry_init, capture_events): Test that _set_input_messages handles errors gracefully. """ import sentry_sdk - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages + from sentry_sdk.integrations.pydantic_ai.spans.ai_client import ai_client_span sentry_init( integrations=[PydanticAIIntegration()], @@ -1646,14 +1646,11 @@ async def test_input_messages_error_handling(sentry_init, capture_events): ) with sentry_sdk.start_transaction(op="test", name="test") as transaction: - span = sentry_sdk.start_span(op="test_span") - # Pass invalid messages that would cause an error invalid_messages = [object()] # Plain object without expected attributes - # Should not raise, error is caught internally - _set_input_messages(span, invalid_messages) - + # Should not raise, error is caught internally via ai_client_span + span = ai_client_span(invalid_messages, None, None, None) span.finish() # Should not crash @@ -1789,9 +1786,7 @@ async def test_message_parts_with_list_content(sentry_init, capture_events): """ Test that message parts with list content are handled correctly. """ - import sentry_sdk - from unittest.mock import MagicMock - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages + from pydantic_ai import Agent sentry_init( integrations=[PydanticAIIntegration()], @@ -1799,24 +1794,13 @@ async def test_message_parts_with_list_content(sentry_init, capture_events): send_default_pii=True, ) - with sentry_sdk.start_transaction(op="test", name="test") as transaction: - span = sentry_sdk.start_span(op="test_span") - - # Create message with list content - mock_msg = MagicMock() - mock_part = MagicMock() - mock_part.content = ["item1", "item2", {"complex": "item"}] - mock_msg.parts = [mock_part] - mock_msg.instructions = None - - messages = [mock_msg] - - # Should handle list content - _set_input_messages(span, messages) + events = capture_events() + agent = Agent("test") - span.finish() + # Run with list content + await agent.run(["item1", "item2"]) - # Should not crash + (transaction,) = events assert transaction is not None @@ -1896,10 +1880,7 @@ async def test_message_with_system_prompt_part(sentry_init, capture_events): """ Test that SystemPromptPart is handled with correct role. """ - import sentry_sdk - from unittest.mock import MagicMock - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages - from pydantic_ai import messages + from pydantic_ai import Agent, messages sentry_init( integrations=[PydanticAIIntegration()], @@ -1907,24 +1888,19 @@ async def test_message_with_system_prompt_part(sentry_init, capture_events): send_default_pii=True, ) - with sentry_sdk.start_transaction(op="test", name="test") as transaction: - span = sentry_sdk.start_span(op="test_span") - - # Create message with SystemPromptPart - system_part = messages.SystemPromptPart(content="You are a helpful assistant") - - mock_msg = MagicMock() - mock_msg.parts = [system_part] - mock_msg.instructions = None - - msgs = [mock_msg] + events = capture_events() + agent = Agent("test") - # Should handle system prompt - _set_input_messages(span, msgs) + # Create message with SystemPromptPart + system_part = messages.SystemPromptPart(content="You are a helpful assistant") + history = [ + messages.ModelRequest(parts=[system_part]) + ] - span.finish() + # Run with history + await agent.run("What did I say?", message_history=history) - # Should not crash + (transaction,) = events assert transaction is not None @@ -1935,7 +1911,7 @@ async def test_message_with_instructions(sentry_init, capture_events): """ import sentry_sdk from unittest.mock import MagicMock - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages + from sentry_sdk.integrations.pydantic_ai.spans.ai_client import ai_client_span sentry_init( integrations=[PydanticAIIntegration()], @@ -1944,8 +1920,6 @@ async def test_message_with_instructions(sentry_init, capture_events): ) with sentry_sdk.start_transaction(op="test", name="test") as transaction: - span = sentry_sdk.start_span(op="test_span") - # Create message with instructions mock_msg = MagicMock() mock_msg.instructions = "System instructions here" @@ -1955,9 +1929,8 @@ async def test_message_with_instructions(sentry_init, capture_events): msgs = [mock_msg] - # Should extract system prompt from instructions - _set_input_messages(span, msgs) - + # Should handle system prompt via ai_client_span wrappers + span = ai_client_span(msgs, None, None, None) span.finish() # Should not crash @@ -1969,8 +1942,7 @@ async def test_set_input_messages_without_prompts(sentry_init, capture_events): """ Test that _set_input_messages respects _should_send_prompts(). """ - import sentry_sdk - from sentry_sdk.integrations.pydantic_ai.spans.ai_client import _set_input_messages + from pydantic_ai import Agent sentry_init( integrations=[PydanticAIIntegration(include_prompts=False)], @@ -1978,16 +1950,13 @@ async def test_set_input_messages_without_prompts(sentry_init, capture_events): send_default_pii=True, ) - with sentry_sdk.start_transaction(op="test", name="test") as transaction: - span = sentry_sdk.start_span(op="test_span") + events = capture_events() + agent = Agent("test") - # Even with messages, should not set them - messages = ["test"] - _set_input_messages(span, messages) + # Run with prompts disabled + await agent.run("test") - span.finish() - - # Should not crash and should not set messages + (transaction,) = events assert transaction is not None @@ -2705,6 +2674,8 @@ async def test_binary_content_encoding_image(sentry_init, capture_events): @pytest.mark.asyncio async def test_binary_content_encoding_mixed_content(sentry_init, capture_events): """Test that BinaryContent mixed with text content is properly handled.""" + from pydantic_ai import Agent + sentry_init( integrations=[PydanticAIIntegration()], traces_sample_rate=1.0, @@ -2712,21 +2683,14 @@ async def test_binary_content_encoding_mixed_content(sentry_init, capture_events ) events = capture_events() + agent = Agent("test") - with sentry_sdk.start_transaction(op="test", name="test"): - span = sentry_sdk.start_span(op="test_span") - binary_content = BinaryContent( - data=b"fake_image_bytes", media_type="image/jpeg" - ) - user_part = UserPromptPart( - content=["Here is an image:", binary_content, "What do you see?"] - ) - mock_msg = MagicMock() - mock_msg.parts = [user_part] - mock_msg.instructions = None + binary_content = BinaryContent( + data=b"fake_image_bytes", media_type="image/jpeg" + ) - _set_input_messages(span, [mock_msg]) - span.finish() + # Run with mixed content + await agent.run(["Here is an image:", binary_content, "What do you see?"]) (event,) = events span_data = event["spans"][0]["data"]