From cfffb9bb362ab173db5c274415b3c88b9e242b6f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 6 Aug 2026 12:40:57 +0200 Subject: [PATCH 1/2] chore: Drop eventlet from conftest --- tests/conftest.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5662cc6e2f..f9ec80f393 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,11 +48,6 @@ except ImportError: gevent = None -try: - import eventlet -except ImportError: - eventlet = None - import sentry_sdk import sentry_sdk.utils from sentry_sdk.envelope import Envelope, parse_json @@ -523,23 +518,11 @@ def read_flush(self): # scope=session ensures that fixture is run earlier @pytest.fixture( scope="session", - params=[None, "eventlet", "gevent"], - ids=("threads", "eventlet", "greenlet"), + params=[None, "gevent"], + ids=("threads", "greenlet"), ) def maybe_monkeypatched_threading(request): - if request.param == "eventlet": - if eventlet is None: - pytest.skip("no eventlet installed") - - try: - eventlet.monkey_patch() - except AttributeError as e: - if "'thread.RLock' object has no attribute" in str(e): - # https://bitbucket.org/pypy/pypy/issues/2962/gevent-cannot-patch-rlock-under-pypy-27-7 - pytest.skip("https://github.com/eventlet/eventlet/issues/546") - else: - raise - elif request.param == "gevent": + if request.param == "gevent": if gevent is None: pytest.skip("no gevent installed") try: From 0eb890dfbbc04c3e040a796451c5044c653e9492 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 6 Aug 2026 12:49:31 +0200 Subject: [PATCH 2/2] chore: Remove ai_track decorator --- MIGRATION_GUIDE.md | 1 + sentry_sdk/ai/monitoring.py | 162 ------------------- tests/test_ai_monitoring.py | 315 ------------------------------------ 3 files changed, 1 insertion(+), 477 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 54c38d4851..dd1865257f 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -43,6 +43,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - Dropped support for Sanic below 22.0. - Removed the possibility to supply a specific client to the LaunchDarklyIntegration. - The `enable_tracing` option was removed. Use `traces_sample_rate=1.0` instead. +- The deprecated `@ai_track` decorator was removed. - The deprecated `push_scope` and `configure_scope` APIs have been removed. Use `with new_scope():` to push a new scope and `scope = get_current_scope()` to retrieve the current scope instead. - Transaction profiling and related code was removed. - Removed the deprecated Hub class and all uses of hub throughout the SDK in arguments, options, etc. Use a scope instead. diff --git a/sentry_sdk/ai/monitoring.py b/sentry_sdk/ai/monitoring.py index a8cce22cb0..a43f25234d 100644 --- a/sentry_sdk/ai/monitoring.py +++ b/sentry_sdk/ai/monitoring.py @@ -1,18 +1,10 @@ -import inspect -import sys -import warnings from contextvars import ContextVar -from functools import wraps from typing import TYPE_CHECKING -import sentry_sdk.utils -from sentry_sdk import start_span from sentry_sdk.ai.utils import _set_span_data_attribute from sentry_sdk.consts import SPANDATA from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled -from sentry_sdk.utils import capture_internal_exceptions, reraise if TYPE_CHECKING: from typing import Any, Awaitable, Callable, Optional, TypeVar, Union @@ -32,160 +24,6 @@ def get_ai_pipeline_name() -> "Optional[str]": return _ai_pipeline_name.get() -def ai_track(description: str, **span_kwargs: "Any") -> "Callable[[F], F]": - warnings.warn( - "sentry_sdk.ai.ai_track is deprecated and will be removed in version 3.0 of sentry-sdk. " - "Use the manual span API instead, e.g. sentry_sdk.start_span().", - DeprecationWarning, - stacklevel=2, - ) - - def decorator(f: "F") -> "F": - def sync_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=description, attributes={"sentry.op": op} - ) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_attribute(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_attribute(k, v) - - if curr_pipeline: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": "ai_monitoring", - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - else: - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": "ai_monitoring", - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any": - client = sentry_sdk.get_client() - - curr_pipeline = _ai_pipeline_name.get() - op = span_kwargs.pop("op", "ai.run" if curr_pipeline else "ai.pipeline") - - if has_span_streaming_enabled(client.options): - with sentry_sdk.traces.start_span( - name=description, attributes={"sentry.op": op} - ) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_attribute(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_attribute(k, v) - - if curr_pipeline: - span.set_attribute(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return await f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = await f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": "ai_monitoring", - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - else: - with start_span(name=description, op=op, **span_kwargs) as span: - for k, v in kwargs.pop("sentry_tags", {}).items(): - span.set_tag(k, v) - for k, v in kwargs.pop("sentry_data", {}).items(): - span.set_data(k, v) - if curr_pipeline: - span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, curr_pipeline) - return await f(*args, **kwargs) - else: - _ai_pipeline_name.set(description) - try: - res = await f(*args, **kwargs) - except Exception as e: - exc_info = sys.exc_info() - with capture_internal_exceptions(): - event, hint = sentry_sdk.utils.event_from_exception( - e, - client_options=sentry_sdk.get_client().options, - mechanism={ - "type": "ai_monitoring", - "handled": False, - }, - ) - sentry_sdk.capture_event(event, hint=hint) - reraise(*exc_info) - finally: - _ai_pipeline_name.set(None) - return res - - if inspect.iscoroutinefunction(f): - return wraps(f)(async_wrapped) # type: ignore - else: - return wraps(f)(sync_wrapped) # type: ignore - - return decorator - - def record_token_usage( span: "Union[Span, StreamedSpan]", input_tokens: "Optional[int]" = None, diff --git a/tests/test_ai_monitoring.py b/tests/test_ai_monitoring.py index fa4722db34..22a88f2214 100644 --- a/tests/test_ai_monitoring.py +++ b/tests/test_ai_monitoring.py @@ -1,11 +1,9 @@ import pytest -import sentry_sdk from sentry_sdk._types import ( BLOB_DATA_SUBSTITUTE, AnnotatedValue, ) -from sentry_sdk.ai.monitoring import ai_track from sentry_sdk.ai.utils import ( MAX_GEN_AI_MESSAGE_BYTES, MAX_SINGLE_MESSAGE_CONTENT_CHARS, @@ -25,319 +23,6 @@ from sentry_sdk.utils import safe_serialize -def test_ai_track(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - events = capture_events() - - @ai_track("my tool") - def tool(**kwargs): - pass - - @ai_track("some test pipeline") - def pipeline(): - tool() - - with sentry_sdk.start_transaction(): - pipeline() - - transaction = events[0] - assert transaction["type"] == "transaction" - assert len(transaction["spans"]) == 2 - spans = transaction["spans"] - - ai_pipeline_span = spans[0] if spans[0]["op"] == "ai.pipeline" else spans[1] - ai_run_span = spans[0] if spans[0]["op"] == "ai.run" else spans[1] - - assert ai_pipeline_span["description"] == "some test pipeline" - assert ai_run_span["description"] == "my tool" - - -def test_ai_track_span_streaming(sentry_init, capture_items): - sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") - items = capture_items("span") - - @ai_track("my tool") - def tool(**kwargs): - pass - - @ai_track("some test pipeline") - def pipeline(): - tool() - - with sentry_sdk.traces.start_span(name="span"): - pipeline() - - sentry_sdk.flush() - - spans = [item.payload for item in items] - assert len(spans) == 3 - ai_run_span, ai_pipeline_span, segment = spans - - assert ai_pipeline_span["name"] == "some test pipeline" - assert ai_run_span["name"] == "my tool" - - -def test_ai_track_with_tags(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - events = capture_events() - - @ai_track("my tool") - def tool(**kwargs): - pass - - @ai_track("some test pipeline") - def pipeline(): - tool() - - with sentry_sdk.start_transaction(): - pipeline(sentry_tags={"user": "colin"}, sentry_data={"some_data": "value"}) - - transaction = events[0] - assert transaction["type"] == "transaction" - assert len(transaction["spans"]) == 2 - spans = transaction["spans"] - - ai_pipeline_span = spans[0] if spans[0]["op"] == "ai.pipeline" else spans[1] - ai_run_span = spans[0] if spans[0]["op"] == "ai.run" else spans[1] - - assert ai_pipeline_span["description"] == "some test pipeline" - assert ai_pipeline_span["tags"]["user"] == "colin" - assert ai_pipeline_span["data"]["some_data"] == "value" - assert ai_run_span["description"] == "my tool" - - -def test_ai_track_with_attributes_span_streaming(sentry_init, capture_items): - sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") - items = capture_items("span") - - @ai_track("my tool") - def tool(**kwargs): - pass - - @ai_track("some test pipeline") - def pipeline(): - tool() - - with sentry_sdk.traces.start_span(name="span"): - pipeline(sentry_tags={"user": "colin"}, sentry_data={"some_data": "value"}) - - sentry_sdk.flush() - - spans = [item.payload for item in items] - - assert len(spans) == 3 - ai_run_span, ai_pipeline_span, segment = spans - - assert ai_pipeline_span["name"] == "some test pipeline" - assert ai_pipeline_span["attributes"]["user"] == "colin" - assert ai_pipeline_span["attributes"]["some_data"] == "value" - assert ai_run_span["name"] == "my tool" - - -@pytest.mark.asyncio -async def test_ai_track_async(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - events = capture_events() - - @ai_track("my async tool") - async def async_tool(**kwargs): - pass - - @ai_track("some async test pipeline") - async def async_pipeline(): - await async_tool() - - with sentry_sdk.start_transaction(): - await async_pipeline() - - transaction = events[0] - assert transaction["type"] == "transaction" - assert len(transaction["spans"]) == 2 - spans = transaction["spans"] - - ai_pipeline_span = spans[0] if spans[0]["op"] == "ai.pipeline" else spans[1] - ai_run_span = spans[0] if spans[0]["op"] == "ai.run" else spans[1] - - assert ai_pipeline_span["description"] == "some async test pipeline" - assert ai_run_span["description"] == "my async tool" - - -@pytest.mark.asyncio -async def test_ai_track_async_span_streaming(sentry_init, capture_items): - sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") - items = capture_items() - - @ai_track("my async tool") - async def async_tool(**kwargs): - pass - - @ai_track("some async test pipeline") - async def async_pipeline(): - await async_tool() - - with sentry_sdk.traces.start_span(name="span"): - await async_pipeline() - - sentry_sdk.flush() - - spans = [item.payload for item in items] - assert len(spans) == 3 - ai_run_span, ai_pipeline_span, segment = spans - - assert ai_pipeline_span["name"] == "some async test pipeline" - assert ai_run_span["name"] == "my async tool" - - -@pytest.mark.asyncio -async def test_ai_track_async_with_tags(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - events = capture_events() - - @ai_track("my async tool") - async def async_tool(**kwargs): - pass - - @ai_track("some async test pipeline") - async def async_pipeline(): - await async_tool() - - with sentry_sdk.start_transaction(): - await async_pipeline( - sentry_tags={"user": "czyber"}, sentry_data={"some_data": "value"} - ) - - transaction = events[0] - assert transaction["type"] == "transaction" - assert len(transaction["spans"]) == 2 - spans = transaction["spans"] - - ai_pipeline_span = spans[0] if spans[0]["op"] == "ai.pipeline" else spans[1] - ai_run_span = spans[0] if spans[0]["op"] == "ai.run" else spans[1] - - assert ai_pipeline_span["description"] == "some async test pipeline" - assert ai_pipeline_span["tags"]["user"] == "czyber" - assert ai_pipeline_span["data"]["some_data"] == "value" - assert ai_run_span["description"] == "my async tool" - - -@pytest.mark.asyncio -async def test_ai_track_async_with_attributes_span_streaming( - sentry_init, capture_items -): - sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") - items = capture_items("span") - - @ai_track("my async tool") - async def async_tool(**kwargs): - pass - - @ai_track("some async test pipeline") - async def async_pipeline(): - await async_tool() - - with sentry_sdk.traces.start_span(name="span"): - await async_pipeline( - sentry_tags={"user": "czyber"}, sentry_data={"some_data": "value"} - ) - - sentry_sdk.flush() - - spans = [item.payload for item in items] - assert len(spans) == 3 - ai_run_span, ai_pipeline_span, segment = spans - - assert ai_pipeline_span["name"] == "some async test pipeline" - assert ai_pipeline_span["attributes"]["user"] == "czyber" - assert ai_pipeline_span["attributes"]["some_data"] == "value" - assert ai_run_span["name"] == "my async tool" - - -def test_ai_track_with_explicit_op(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - events = capture_events() - - @ai_track("my tool", op="custom.operation") - def tool(**kwargs): - pass - - with sentry_sdk.start_transaction(): - tool() - - transaction = events[0] - assert transaction["type"] == "transaction" - assert len(transaction["spans"]) == 1 - span = transaction["spans"][0] - - assert span["description"] == "my tool" - assert span["op"] == "custom.operation" - - -def test_ai_track_with_explicit_op_span_streaming(sentry_init, capture_items): - sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") - items = capture_items("span") - - @ai_track("my tool", op="custom.operation") - def tool(**kwargs): - pass - - with sentry_sdk.traces.start_span(name="span"): - tool() - - sentry_sdk.flush() - - spans = [item.payload for item in items] - assert len(spans) == 2 - ai_run_span, segment = spans - - assert ai_run_span["name"] == "my tool" - assert ai_run_span["attributes"]["sentry.op"] == "custom.operation" - - -@pytest.mark.asyncio -async def test_ai_track_async_with_explicit_op(sentry_init, capture_events): - sentry_init(traces_sample_rate=1.0) - events = capture_events() - - @ai_track("my async tool", op="custom.async.operation") - async def async_tool(**kwargs): - pass - - with sentry_sdk.start_transaction(): - await async_tool() - - transaction = events[0] - assert transaction["type"] == "transaction" - assert len(transaction["spans"]) == 1 - span = transaction["spans"][0] - - assert span["description"] == "my async tool" - assert span["op"] == "custom.async.operation" - - -@pytest.mark.asyncio -async def test_ai_track_async_with_explicit_op_span_streaming( - sentry_init, capture_items -): - sentry_init(traces_sample_rate=1.0, trace_lifecycle="stream") - items = capture_items() - - @ai_track("my async tool", op="custom.async.operation") - async def async_tool(**kwargs): - pass - - with sentry_sdk.traces.start_span(name="span"): - await async_tool() - - sentry_sdk.flush() - - spans = [item.payload for item in items] - assert len(spans) == 2 - ai_run_span, segment = spans - - assert ai_run_span["name"] == "my async tool" - assert ai_run_span["attributes"]["sentry.op"] == "custom.async.operation" - - @pytest.fixture def sample_messages(): """Sample messages similar to what gen_ai integrations would use"""