Skip to content

Commit c0d7e10

Browse files
committed
Merge branch 'ivana/move-http-client-breadcrumbs-1' into ivana/move-http-client-breadcrumbs-2
2 parents 27401f0 + 6825f47 commit c0d7e10

12 files changed

Lines changed: 880 additions & 680 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ typing = [
7373
"pydantic>=2.13.4",
7474
"pydantic-ai-slim>=2.23.0",
7575
"langchain-core>=1.5.3",
76+
"huggingface-hub>=1.26.1",
7677
]
7778
test = [
7879
"dataclasses ; python_full_version < '3.7'",

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,7 @@ class OP:
12571257
SUBPROCESS_WAIT = "subprocess.wait"
12581258
SUBPROCESS_COMMUNICATE = "subprocess.communicate"
12591259
TEMPLATE_RENDER = "template.render"
1260+
VIEW_AUTHENTICATE = "view.authenticate"
12601261
VIEW_RENDER = "view.render"
12611262
VIEW_RESPONSE_RENDER = "view.response.render"
12621263
WEBSOCKET_SERVER = "websocket.server"

sentry_sdk/integrations/django/__init__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,10 @@ def _patch_drf() -> None:
298298
DRF request object, such that we can later use either in
299299
`DjangoRequestExtractor`.
300300
301+
We also patch DRF's authentication to create a span, so that the work done
302+
by the configured authentication classes (which often involves database
303+
queries) doesn't show up as part of the view itself.
304+
301305
This function is not called directly on SDK setup, because importing almost
302306
any part of Django Rest Framework will try to access Django settings (where
303307
`sentry_sdk.init()` might be called from in the first place). Instead we
@@ -339,6 +343,43 @@ def sentry_patched_drf_initial(
339343

340344
APIView.initial = sentry_patched_drf_initial
341345

346+
with capture_internal_exceptions():
347+
try:
348+
from rest_framework.request import Request # type: ignore
349+
except ImportError:
350+
pass
351+
else:
352+
old_drf_authenticate = Request._authenticate
353+
354+
def sentry_patched_drf_authenticate(self: "Request") -> "Any":
355+
client = sentry_sdk.get_client()
356+
integration = client.get_integration(DjangoIntegration)
357+
# Nothing to time if there are no authenticators configured
358+
# for this view.
359+
if integration is None or not getattr(self, "authenticators", None):
360+
return old_drf_authenticate(self)
361+
362+
if has_span_streaming_enabled(client.options):
363+
if sentry_sdk.traces.get_current_span() is None:
364+
return old_drf_authenticate(self)
365+
with sentry_sdk.traces.start_span(
366+
name="authenticate",
367+
attributes={
368+
"sentry.op": OP.VIEW_AUTHENTICATE,
369+
"sentry.origin": DjangoIntegration.origin,
370+
},
371+
):
372+
return old_drf_authenticate(self)
373+
else:
374+
with sentry_sdk.start_span(
375+
op=OP.VIEW_AUTHENTICATE,
376+
name="authenticate",
377+
origin=DjangoIntegration.origin,
378+
):
379+
return old_drf_authenticate(self)
380+
381+
Request._authenticate = sentry_patched_drf_authenticate
382+
342383

343384
def _patch_channels() -> None:
344385
try:

sentry_sdk/integrations/huggingface_hub.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import inspect
22
import sys
33
from functools import wraps
4-
from typing import TYPE_CHECKING
4+
from typing import TYPE_CHECKING, cast
55

66
import sentry_sdk
77
from sentry_sdk.ai.monitoring import record_token_usage
@@ -24,6 +24,10 @@
2424
if TYPE_CHECKING:
2525
from typing import Any, Callable, Iterable, Union
2626

27+
from huggingface_hub import (
28+
ChatCompletionStreamOutput,
29+
)
30+
2731
from sentry_sdk.tracing import Span
2832

2933
try:
@@ -44,13 +48,13 @@ def __init__(
4448
@staticmethod
4549
def setup_once() -> None:
4650
# Other tasks that can be called: https://huggingface.co/docs/huggingface_hub/guides/inference#supported-providers-and-tasks
47-
huggingface_hub.inference._client.InferenceClient.text_generation = (
51+
huggingface_hub.inference._client.InferenceClient.text_generation = ( # type: ignore[method-assign]
4852
_wrap_huggingface_task(
4953
huggingface_hub.inference._client.InferenceClient.text_generation,
5054
OP.GEN_AI_TEXT_COMPLETION,
5155
)
5256
)
53-
huggingface_hub.inference._client.InferenceClient.chat_completion = (
57+
huggingface_hub.inference._client.InferenceClient.chat_completion = ( # type: ignore[method-assign]
5458
_wrap_huggingface_task(
5559
huggingface_hub.inference._client.InferenceClient.chat_completion,
5660
OP.GEN_AI_CHAT,
@@ -302,15 +306,15 @@ def new_details_iterator() -> "Iterable[Any]":
302306

303307
else:
304308
# chat-completion stream output
305-
def new_iterator() -> "Iterable[str]":
309+
def new_iterator() -> "Iterable[ChatCompletionStreamOutput]":
306310
finish_reason = None
307311
response_model = None
308312
response_text_buffer: "list[str]" = []
309313
tool_calls = None
310314
usage = None
311315

312316
with capture_internal_exceptions():
313-
for chunk in res:
317+
for chunk in cast("Iterable[ChatCompletionStreamOutput]", res):
314318
if hasattr(chunk, "model") and chunk.model is not None:
315319
response_model = chunk.model
316320

sentry_sdk/integrations/mcp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222
from sentry_sdk.tracing_utils import has_span_streaming_enabled
2323
from sentry_sdk.utils import nullcontext, package_version, safe_serialize
2424

25-
MCP_PACKAGE_VERSION = package_version("mcp")
26-
2725
try:
2826
from mcp.server.lowlevel import Server
2927
from mcp.server.streamable_http import (
3028
StreamableHTTPServerTransport,
3129
)
3230

31+
MCP_PACKAGE_VERSION = package_version("mcp")
32+
3333
if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0):
3434
from mcp.server.lowlevel.server import ( # type: ignore[attr-defined]
3535
request_ctx,

sentry_sdk/integrations/redis/_async_common.py

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
4545
if client.get_integration(RedisIntegration) is None:
4646
return await old_execute(self, *args, **kwargs)
4747

48+
sentry_sdk.add_breadcrumb(
49+
message="redis.pipeline.execute",
50+
type="redis",
51+
category="redis",
52+
data={
53+
"redis.is_cluster": is_cluster,
54+
"redis.transaction": False if is_cluster else self.is_transaction,
55+
},
56+
)
57+
4858
span_streaming = has_span_streaming_enabled(client.options)
4959

5060
span: "Union[Span, StreamedSpan]"
@@ -84,20 +94,7 @@ async def _sentry_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
8494
command_seq,
8595
)
8696

87-
rv = await old_execute(self, *args, **kwargs)
88-
89-
with capture_internal_exceptions():
90-
sentry_sdk.add_breadcrumb(
91-
message="redis.pipeline.execute",
92-
type="redis",
93-
category="redis",
94-
data={
95-
"redis.is_cluster": is_cluster,
96-
"redis.transaction": False if is_cluster else self.is_transaction,
97-
},
98-
)
99-
100-
return rv
97+
return await old_execute(self, *args, **kwargs)
10198

10299
pipeline_cls.execute = _sentry_execute # type: ignore
103100

@@ -119,6 +116,24 @@ async def _sentry_execute_command(
119116
if integration is None:
120117
return await old_execute_command(self, name, *args, **kwargs)
121118

119+
db_properties = _compile_db_span_properties(integration, name, args)
120+
121+
breadcrumb_data = {
122+
"redis.is_cluster": is_cluster,
123+
"redis.command": name,
124+
"db.operation": name,
125+
}
126+
key = _extract_key(name, args)
127+
if key is not None:
128+
breadcrumb_data["redis.key"] = key
129+
130+
sentry_sdk.add_breadcrumb(
131+
message=db_properties["description"],
132+
type="redis",
133+
category="redis",
134+
data=breadcrumb_data,
135+
)
136+
122137
span_streaming = has_span_streaming_enabled(client.options)
123138

124139
if span_streaming and sentry_sdk.traces.get_current_span() is None:
@@ -156,8 +171,6 @@ async def _sentry_execute_command(
156171
)
157172
cache_span.__enter__()
158173

159-
db_properties = _compile_db_span_properties(integration, name, args)
160-
161174
additional_db_span_attributes = {}
162175
with capture_internal_exceptions():
163176
additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command(
@@ -193,23 +206,6 @@ async def _sentry_execute_command(
193206
_set_cache_data(cache_span, self, cache_properties, value)
194207
cache_span.__exit__(None, None, None)
195208

196-
with capture_internal_exceptions():
197-
data = {
198-
"redis.is_cluster": is_cluster,
199-
"redis.command": name,
200-
"db.operation": name,
201-
}
202-
key = _extract_key(name, args)
203-
if key is not None:
204-
data["redis.key"] = key
205-
206-
sentry_sdk.add_breadcrumb(
207-
message=db_properties["description"],
208-
type="redis",
209-
category="redis",
210-
data=data,
211-
)
212-
213209
return value
214210

215211
cls.execute_command = _sentry_execute_command # type: ignore

sentry_sdk/integrations/redis/_sync_common.py

Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,17 @@ def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
4242
if client.get_integration(RedisIntegration) is None:
4343
return old_execute(self, *args, **kwargs)
4444

45-
span_streaming = has_span_streaming_enabled(client.options)
45+
sentry_sdk.add_breadcrumb(
46+
message="redis.pipeline.execute",
47+
type="redis",
48+
category="redis",
49+
data={
50+
"redis.is_cluster": is_cluster,
51+
"redis.transaction": False if is_cluster else self.transaction,
52+
},
53+
)
4654

55+
span_streaming = has_span_streaming_enabled(client.options)
4756
span: "Union[Span, StreamedSpan]"
4857
if span_streaming:
4958
if sentry_sdk.traces.get_current_span() is None:
@@ -79,20 +88,7 @@ def sentry_patched_execute(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
7988
command_seq,
8089
)
8190

82-
rv = old_execute(self, *args, **kwargs)
83-
84-
with capture_internal_exceptions():
85-
sentry_sdk.add_breadcrumb(
86-
message="redis.pipeline.execute",
87-
type="redis",
88-
category="redis",
89-
data={
90-
"redis.is_cluster": is_cluster,
91-
"redis.transaction": False if is_cluster else self.transaction,
92-
},
93-
)
94-
95-
return rv
91+
return old_execute(self, *args, **kwargs)
9692

9793
pipeline_cls.execute = sentry_patched_execute
9894

@@ -118,6 +114,24 @@ def sentry_patched_execute_command(
118114
if integration is None:
119115
return old_execute_command(self, name, *args, **kwargs)
120116

117+
db_properties = _compile_db_span_properties(integration, name, args)
118+
119+
breadcrumb_data = {
120+
"redis.is_cluster": is_cluster,
121+
"redis.command": name,
122+
"db.operation": name,
123+
}
124+
key = _extract_key(name, args)
125+
if key is not None:
126+
breadcrumb_data["redis.key"] = key
127+
128+
sentry_sdk.add_breadcrumb(
129+
message=db_properties["description"],
130+
type="redis",
131+
category="redis",
132+
data=breadcrumb_data,
133+
)
134+
121135
span_streaming = has_span_streaming_enabled(client.options)
122136

123137
if span_streaming and sentry_sdk.traces.get_current_span() is None:
@@ -155,8 +169,6 @@ def sentry_patched_execute_command(
155169
)
156170
cache_span.__enter__()
157171

158-
db_properties = _compile_db_span_properties(integration, name, args)
159-
160172
additional_db_span_attributes = {}
161173
with capture_internal_exceptions():
162174
additional_db_span_attributes[SPANDATA.DB_QUERY_TEXT] = _get_safe_command(
@@ -192,23 +204,6 @@ def sentry_patched_execute_command(
192204
_set_cache_data(cache_span, self, cache_properties, value)
193205
cache_span.__exit__(None, None, None)
194206

195-
with capture_internal_exceptions():
196-
data = {
197-
"redis.is_cluster": is_cluster,
198-
"redis.command": name,
199-
"db.operation": name,
200-
}
201-
key = _extract_key(name, args)
202-
if key is not None:
203-
data["redis.key"] = key
204-
205-
sentry_sdk.add_breadcrumb(
206-
message=db_properties["description"],
207-
type="redis",
208-
category="redis",
209-
data=data,
210-
)
211-
212207
return value
213208

214209
cls.execute_command = sentry_patched_execute_command

sentry_sdk/integrations/stdlib.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,13 @@ def sentry_patched_popen_init(
323323

324324
env = None
325325

326+
sentry_sdk.add_breadcrumb(
327+
type="subprocess",
328+
category="subprocess",
329+
message=description,
330+
data={"subprocess.cwd": cwd} if cwd else {},
331+
)
332+
326333
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
327334
span: "Union[Span, StreamedSpan]"
328335
if span_streaming:
@@ -367,15 +374,6 @@ def sentry_patched_popen_init(
367374
else:
368375
span.set_tag("subprocess.pid", self.pid)
369376

370-
with capture_internal_exceptions():
371-
breadcrumb_data = {"subprocess.cwd": cwd} if cwd else {}
372-
sentry_sdk.add_breadcrumb(
373-
type="subprocess",
374-
category="subprocess",
375-
message=description,
376-
data=breadcrumb_data,
377-
)
378-
379377
return rv
380378

381379
subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore

tests/integrations/django/myapp/urls.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,20 @@ def path(path, *args, **kwargs):
150150
)
151151
)
152152
urlpatterns.append(path("rest-hello", views.rest_hello, name="rest_hello"))
153+
urlpatterns.append(
154+
path(
155+
"rest-authenticated-hello",
156+
views.rest_authenticated_hello,
157+
name="rest_authenticated_hello",
158+
)
159+
)
160+
urlpatterns.append(
161+
path(
162+
"rest-unauthenticated-hello",
163+
views.rest_unauthenticated_hello,
164+
name="rest_unauthenticated_hello",
165+
)
166+
)
153167
urlpatterns.append(
154168
path("rest-json-response", views.rest_json_response, name="rest_json_response")
155169
)

0 commit comments

Comments
 (0)