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..a736e5f827 --- /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_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 diff --git a/sentry_sdk/integrations/asyncpg.py b/sentry_sdk/integrations/asyncpg.py index 7f3591154a..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: @@ -192,6 +193,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 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"]