Skip to content

Commit 1faec55

Browse files
authored
feat(huggingface_hub): Migrate to span first (#6124)
Migrates the huggingface_hub integration to the span-first (streaming span) architecture. When `_experiments={"trace_lifecycle": "stream"}` is enabled, the integration now uses `StreamedSpan` via `sentry_sdk.traces.start_span` instead of the legacy `Span`-based (transactions) path. The legacy path remains unchanged for backwards compatibility. This is the first AI integration to emit `StreamedSpan`s, so the shared AI monitoring utilities needed to accept both span types. `record_token_usage` (`sentry_sdk/ai/monitoring.py`) and `set_data_normalized` (`sentry_sdk/ai/utils.py`) now accept `Union[Span, StreamedSpan]` and route attribute writes through a new private `_set_span_data_attribute` helper in `sentry_sdk.ai.utils` — calling `set_attribute` on `StreamedSpan` and `set_data` on `Span`. Existing AI integrations (anthropic, openai, cohere, langchain, langgraph, litellm, google_genai, openai_agents, pydantic_ai) are unaffected at runtime: they continue to pass `Span` instances, which route to `set_data` via the isinstance check. Test coverage mirrors the existing non-streaming suite under streaming mode (text_generation / chat_completion, sync and streaming, with and without tools, plus an error-path test) parametrized over `send_default_pii` and `include_prompts`. Part of the broader span-first integration migration. Fixes PY-2332 Fixes #6030
1 parent 6e4354a commit 1faec55

4 files changed

Lines changed: 505 additions & 25 deletions

File tree

sentry_sdk/ai/monitoring.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
import sys
33
from functools import wraps
44

5+
from sentry_sdk.ai.utils import _set_span_data_attribute
56
from sentry_sdk.consts import SPANDATA
67
import sentry_sdk.utils
78
from sentry_sdk import start_span
89
from sentry_sdk.tracing import Span
10+
from sentry_sdk.traces import StreamedSpan
911
from sentry_sdk.utils import ContextVar, reraise, capture_internal_exceptions
1012

1113
from typing import TYPE_CHECKING
@@ -97,7 +99,7 @@ async def async_wrapped(*args: "Any", **kwargs: "Any") -> "Any":
9799

98100

99101
def record_token_usage(
100-
span: "Span",
102+
span: "Union[Span, StreamedSpan]",
101103
input_tokens: "Optional[int]" = None,
102104
input_tokens_cached: "Optional[int]" = None,
103105
input_tokens_cache_write: "Optional[int]" = None,
@@ -108,28 +110,33 @@ def record_token_usage(
108110
# TODO: move pipeline name elsewhere
109111
ai_pipeline_name = get_ai_pipeline_name()
110112
if ai_pipeline_name:
111-
span.set_data(SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name)
113+
_set_span_data_attribute(span, SPANDATA.GEN_AI_PIPELINE_NAME, ai_pipeline_name)
112114

113115
if input_tokens is not None:
114-
span.set_data(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens)
116+
_set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens)
115117

116118
if input_tokens_cached is not None:
117-
span.set_data(
119+
_set_span_data_attribute(
120+
span,
118121
SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHED,
119122
input_tokens_cached,
120123
)
121124

122125
if input_tokens_cache_write is not None:
123-
span.set_data(
126+
_set_span_data_attribute(
127+
span,
124128
SPANDATA.GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE,
125129
input_tokens_cache_write,
126130
)
127131

128132
if output_tokens is not None:
129-
span.set_data(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens)
133+
_set_span_data_attribute(
134+
span, SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens
135+
)
130136

131137
if output_tokens_reasoning is not None:
132-
span.set_data(
138+
_set_span_data_attribute(
139+
span,
133140
SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS_REASONING,
134141
output_tokens_reasoning,
135142
)
@@ -138,4 +145,4 @@ def record_token_usage(
138145
total_tokens = input_tokens + output_tokens
139146

140147
if total_tokens is not None:
141-
span.set_data(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens)
148+
_set_span_data_attribute(span, SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens)

sentry_sdk/ai/utils.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX
88

99
if TYPE_CHECKING:
10-
from typing import Any, Callable, Dict, List, Optional, Tuple
10+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
1111

1212
from sentry_sdk.tracing import Span
1313

@@ -490,13 +490,25 @@ def _normalize_data(data: "Any", unpack: bool = True) -> "Any":
490490

491491

492492
def set_data_normalized(
493-
span: "Span", key: str, value: "Any", unpack: bool = True
493+
span: "Union[Span, StreamedSpan]",
494+
key: str,
495+
value: "Any",
496+
unpack: bool = True,
494497
) -> None:
495498
normalized = _normalize_data(value, unpack=unpack)
496499
if isinstance(normalized, (int, float, bool, str)):
497-
span.set_data(key, normalized)
500+
_set_span_data_attribute(span, key, normalized)
498501
else:
499-
span.set_data(key, json.dumps(normalized))
502+
_set_span_data_attribute(span, key, json.dumps(normalized))
503+
504+
505+
def _set_span_data_attribute(
506+
span: "Union[Span, StreamedSpan]", key: str, value: "Any"
507+
) -> None:
508+
if isinstance(span, StreamedSpan):
509+
span.set_attribute(key, value)
510+
else:
511+
span.set_data(key, value)
500512

501513

502514
def normalize_message_role(role: str) -> str:

sentry_sdk/integrations/huggingface_hub.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@
55

66
import sentry_sdk
77
from sentry_sdk.ai.monitoring import record_token_usage
8-
from sentry_sdk.ai.utils import set_data_normalized
8+
from sentry_sdk.ai.utils import _set_span_data_attribute, set_data_normalized
99
from sentry_sdk.consts import OP, SPANDATA
1010
from sentry_sdk.integrations import DidNotEnable, Integration
1111
from sentry_sdk.scope import should_send_default_pii
12+
from sentry_sdk.traces import StreamedSpan
13+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
1214
from sentry_sdk.utils import (
1315
capture_internal_exceptions,
1416
event_from_exception,
1517
reraise,
1618
)
1719

1820
if TYPE_CHECKING:
19-
from typing import Any, Callable, Iterable
21+
from typing import Any, Callable, Iterable, Union
22+
23+
from sentry_sdk.tracing import Span
2024

2125
try:
2226
import huggingface_hub.inference._client
@@ -83,17 +87,27 @@ def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any":
8387
model = client.model or kwargs.get("model") or ""
8488
operation_name = op.split(".")[-1]
8589

86-
span = sentry_sdk.start_span(
87-
op=op,
88-
name=f"{operation_name} {model}",
89-
origin=HuggingfaceHubIntegration.origin,
90-
)
90+
span: "Union[Span, StreamedSpan]"
91+
if has_span_streaming_enabled(sentry_sdk.get_client().options):
92+
span = sentry_sdk.traces.start_span(
93+
name=f"{operation_name} {model}",
94+
attributes={
95+
"sentry.op": op,
96+
"sentry.origin": HuggingfaceHubIntegration.origin,
97+
},
98+
)
99+
else:
100+
span = sentry_sdk.start_span(
101+
op=op,
102+
name=f"{operation_name} {model}",
103+
origin=HuggingfaceHubIntegration.origin,
104+
)
91105
span.__enter__()
92106

93-
span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, operation_name)
107+
_set_span_data_attribute(span, SPANDATA.GEN_AI_OPERATION_NAME, operation_name)
94108

95109
if model:
96-
span.set_data(SPANDATA.GEN_AI_REQUEST_MODEL, model)
110+
_set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model)
97111

98112
# Input attributes
99113
if should_send_default_pii() and integration.include_prompts:
@@ -116,7 +130,7 @@ def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any":
116130
value = kwargs.get(attribute, None)
117131
if value is not None:
118132
if isinstance(value, (int, float, bool, str)):
119-
span.set_data(span_attribute, value)
133+
_set_span_data_attribute(span, span_attribute, value)
120134
else:
121135
set_data_normalized(span, span_attribute, value, unpack=False)
122136

@@ -177,7 +191,9 @@ def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any":
177191
response_text_buffer.append(choice.message.content)
178192

179193
if response_model is not None:
180-
span.set_data(SPANDATA.GEN_AI_RESPONSE_MODEL, response_model)
194+
_set_span_data_attribute(
195+
span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model
196+
)
181197

182198
if finish_reason is not None:
183199
set_data_normalized(
@@ -328,8 +344,8 @@ def new_iterator() -> "Iterable[str]":
328344
yield chunk
329345

330346
if response_model is not None:
331-
span.set_data(
332-
SPANDATA.GEN_AI_RESPONSE_MODEL, response_model
347+
_set_span_data_attribute(
348+
span, SPANDATA.GEN_AI_RESPONSE_MODEL, response_model
333349
)
334350

335351
if finish_reason is not None:

0 commit comments

Comments
 (0)