Skip to content

Commit db14450

Browse files
merge master
2 parents 4f84f98 + 6837662 commit db14450

9 files changed

Lines changed: 145 additions & 15 deletions

File tree

CONTRIBUTING.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ lldb -- ../../uwsgi/uwsgi --pythonpath "$PWD/.venv/lib/python3.14/site-packages"
126126
--need-app \
127127
```
128128

129+
### Troubleshooting "module not found" error in relation to the `sentry-sdk` when it's installed in editable mode
130+
131+
If you are trying to debug a Django applicaton such as that described above, and have the Sentry SDK installed in editable mode in the Django project, you will likely encounter this error because the editable package is not being found in the uWSGI's python path. To fix this, the above command needs to be updated to include a `--pythonpath` argument that passes in where your local `sentry-python` codebase is:
132+
133+
```bash
134+
lldb -- ../../uwsgi/uwsgi --pythonpath "$PWD/.venv/lib/python3.14/site-packages" \
135+
--pythonpath "<path-to-local-sentry-python-directory>" \
136+
--http :8000 \
137+
--module mysite.wsgi:application \
138+
--home "$PWD/.venv" \
139+
--need-app \
140+
```
141+
142+
129143
## Adding a New Integration
130144

131145
### SDK Contract

codecov.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ coverage:
22
status:
33
project:
44
default:
5-
target: auto # auto compares coverage to the previous base commit
6-
threshold: 10% # this allows a 10% drop from the previous base commit coverage
5+
target: auto # auto compares coverage to the previous base commit
6+
threshold: 10% # this allows a 10% drop from the previous base commit coverage
77
informational: true
88

99
ignore:
1010
- "tests"
1111
- "sentry_sdk/_types.py"
1212

1313
comment: true
14+
config:
15+
files: changed
1416

1517
github_checks:
1618
annotations: false

sentry_sdk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"last_event_id",
3838
"new_scope",
3939
"push_scope",
40+
"remove_attribute",
41+
"set_attribute",
4042
"set_context",
4143
"set_extra",
4244
"set_level",

sentry_sdk/api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ def overload(x: "T") -> "T":
7070
"last_event_id",
7171
"new_scope",
7272
"push_scope",
73+
"remove_attribute",
74+
"set_attribute",
7375
"set_context",
7476
"set_extra",
7577
"set_level",
@@ -287,6 +289,28 @@ def push_scope( # noqa: F811
287289
return _ScopeManager()
288290

289291

292+
@scopemethod
293+
def set_attribute(attribute: str, value: "Any") -> None:
294+
"""
295+
Set an attribute.
296+
297+
Any attributes-based telemetry (logs, metrics) captured in this scope will
298+
include this attribute.
299+
"""
300+
return get_isolation_scope().set_attribute(attribute, value)
301+
302+
303+
@scopemethod
304+
def remove_attribute(attribute: str) -> None:
305+
"""
306+
Remove an attribute.
307+
308+
If the attribute doesn't exist, this function will not have any effect and
309+
it will also not raise an exception.
310+
"""
311+
return get_isolation_scope().remove_attribute(attribute)
312+
313+
290314
@scopemethod
291315
def set_tag(key: str, value: "Any") -> None:
292316
return get_isolation_scope().set_tag(key, value)

sentry_sdk/integrations/httpx.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,8 @@ async def send(
135135
key=key, value=value, url=request.url
136136
)
137137
)
138-
if key == BAGGAGE_HEADER_NAME and request.headers.get(
139-
BAGGAGE_HEADER_NAME
140-
):
141-
# do not overwrite any existing baggage, just append to it
142-
request.headers[key] += "," + value
138+
if key == BAGGAGE_HEADER_NAME:
139+
add_sentry_baggage_to_headers(request.headers, value)
143140
else:
144141
request.headers[key] = value
145142

sentry_sdk/integrations/openai.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ def _set_embeddings_input_data(
462462
_commmon_set_input_data(span, kwargs)
463463

464464

465-
def _common_set_output_data(
465+
def _set_common_output_data(
466466
span: "Span",
467467
response: "Any",
468468
input: "Any",
@@ -723,7 +723,7 @@ def _set_completions_api_output_data(
723723
if messages is not None and isinstance(messages, str):
724724
messages = [messages]
725725

726-
_common_set_output_data(
726+
_set_common_output_data(
727727
span,
728728
response,
729729
messages,
@@ -746,7 +746,7 @@ def _set_streaming_completions_api_output_data(
746746
if messages is not None and isinstance(messages, str):
747747
messages = [messages]
748748

749-
_common_set_output_data(
749+
_set_common_output_data(
750750
span,
751751
response,
752752
messages,
@@ -769,7 +769,7 @@ def _set_responses_api_output_data(
769769
if input is not None and isinstance(input, str):
770770
input = [input]
771771

772-
_common_set_output_data(
772+
_set_common_output_data(
773773
span,
774774
response,
775775
input,
@@ -792,7 +792,7 @@ def _set_streaming_responses_api_output_data(
792792
if input is not None and isinstance(input, str):
793793
input = [input]
794794

795-
_common_set_output_data(
795+
_set_common_output_data(
796796
span,
797797
response,
798798
input,
@@ -815,7 +815,7 @@ def _set_embeddings_output_data(
815815
if input is not None and isinstance(input, str):
816816
input = [input]
817817

818-
_common_set_output_data(
818+
_set_common_output_data(
819819
span,
820820
response,
821821
input,

sentry_sdk/integrations/wsgi.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def __init__(
8686

8787
def __call__(
8888
self, environ: "Dict[str, str]", start_response: "Callable[..., Any]"
89-
) -> "_ScopedResponse":
89+
) -> "Any":
9090
if _wsgi_middleware_applied.get(False):
9191
return self.app(environ, start_response)
9292

@@ -135,6 +135,29 @@ def __call__(
135135
finally:
136136
_wsgi_middleware_applied.set(False)
137137

138+
# Within the uWSGI subhandler, the use of the "offload" mechanism for file responses
139+
# is determined by a pointer equality check on the response object
140+
# (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278).
141+
#
142+
# If we were to return a _ScopedResponse, this would cause the check to always fail
143+
# since it's checking the files are exactly the same.
144+
#
145+
# To avoid this and ensure that the offloading mechanism works as expected when it's
146+
# enabled, we check if the response is a file-like object (determined by the presence
147+
# of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so,
148+
# it would've been used in handling the file in the response).
149+
#
150+
# Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects,
151+
# so we want to make sure we don't interfere with those either.
152+
#
153+
# If all conditions are met, we return the original response object directly,
154+
# allowing uWSGI to handle it as intended.
155+
if (
156+
environ.get("wsgi.file_wrapper")
157+
and getattr(response, "fileno", None) is not None
158+
):
159+
return response
160+
138161
return _ScopedResponse(scope, response)
139162

140163

tests/integrations/wsgi/test_wsgi.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import sentry_sdk
88
from sentry_sdk import capture_message
9-
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
9+
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware, _ScopedResponse
1010

1111

1212
@pytest.fixture
@@ -500,3 +500,50 @@ def dogpark(environ, start_response):
500500
(event,) = events
501501

502502
assert event["contexts"]["trace"]["origin"] == "auto.dogpark.deluxe"
503+
504+
505+
@pytest.mark.parametrize(
506+
"has_file_wrapper, has_fileno, expect_wrapped",
507+
[
508+
(True, True, False), # both conditions met → unwrapped
509+
(False, True, True), # no file_wrapper → wrapped
510+
(True, False, True), # no fileno → wrapped
511+
(False, False, True), # neither condition → wrapped
512+
],
513+
)
514+
def test_file_response_wrapping(
515+
sentry_init, has_file_wrapper, has_fileno, expect_wrapped
516+
):
517+
sentry_init()
518+
519+
response_mock = mock.MagicMock()
520+
if not has_fileno:
521+
del response_mock.fileno
522+
523+
def app(environ, start_response):
524+
start_response("200 OK", [])
525+
return response_mock
526+
527+
environ_extra = {}
528+
if has_file_wrapper:
529+
environ_extra["wsgi.file_wrapper"] = mock.MagicMock()
530+
531+
middleware = SentryWsgiMiddleware(app)
532+
533+
result = middleware(
534+
{
535+
"REQUEST_METHOD": "GET",
536+
"PATH_INFO": "/",
537+
"SERVER_NAME": "localhost",
538+
"SERVER_PORT": "80",
539+
"wsgi.url_scheme": "http",
540+
"wsgi.input": mock.MagicMock(),
541+
**environ_extra,
542+
},
543+
lambda status, headers: None,
544+
)
545+
546+
if expect_wrapped:
547+
assert isinstance(result, _ScopedResponse)
548+
else:
549+
assert result is response_mock

tests/test_attributes.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@
33
from tests.test_metrics import envelopes_to_metrics
44

55

6+
def test_top_level_api(sentry_init, capture_envelopes):
7+
sentry_init()
8+
9+
envelopes = capture_envelopes()
10+
11+
sentry_sdk.set_attribute("set", "value")
12+
sentry_sdk.set_attribute("removed", "value")
13+
sentry_sdk.remove_attribute("removed")
14+
# Attempting to remove a nonexistent attribute should not raise
15+
sentry_sdk.remove_attribute("nonexistent")
16+
17+
sentry_sdk.metrics.count("test", 1)
18+
sentry_sdk.get_client().flush()
19+
20+
metrics = envelopes_to_metrics(envelopes)
21+
(metric,) = metrics
22+
23+
assert metric["attributes"]["set"] == "value"
24+
assert "removed" not in metric["attributes"]
25+
26+
627
def test_scope_precedence(sentry_init, capture_envelopes):
728
# Order of precedence, from most important to least:
829
# 1. telemetry attributes (directly supplying attributes on creation or using set_attribute)

0 commit comments

Comments
 (0)