Skip to content

feat: Add sentry.is_localhost to server spans - #7767

Closed
sentrivana wants to merge 0 commit into
masterfrom
ivana/is-localhost
Closed

sentrivana wants to merge 0 commit into
masterfrom
ivana/is-localhost

Conversation

@sentrivana

Copy link
Copy Markdown
Contributor

Description

  • WSGI: We were already extracting server.address as a span attribute. Use it to set sentry.is_localhost on scope so that all associated spans inherit it.
  • ASGI: We were not extracting server.address from the ASGI scope. Added that and using it to set sentry.is_localhost.
  • Integrations that don't use the WSGI or ASGI integration:
    • Tornado: TODO
    • aiohttp: TODO
    • Sanic: TODO

Also TODO: tests

Issues

Closes https://linear.app/getsentry/issue/PY-2891/implement-sentryis-localhost

Reminders

@linear-code

linear-code Bot commented Sep 29, 2026

Copy link
Copy Markdown

PY-2891

@github-actions

Copy link
Copy Markdown
Contributor

Codecov Results 📊

✅ 129639 passed | ❌ 1 failed | ⏭️ 7169 skipped | Total: 136809 | Pass Rate: 94.76% | Execution Time: 436m 43s

📊 Comparison with Base Branch

Metric Change
Total Tests 📉 -50
Passed Tests 📉 -51
Failed Tests 📈 +1
Skipped Tests —

➕ New Tests (1)

View new tests
  • test_remote_addr_data_collection[data_collection_default]
    • File: tests.integrations.sanic.test_sanic
    • Status: ❌ Failing

❌ Failed Tests

test_remote_addr_data_collection[data_collection_default]

File: tests.integrations.sanic.test_sanic
Suite: py3.6-sanic-v0.8.3
Error: AttributeError: 'NoneType' object has no attribute 'status'

Stack Trace
tests/integrations/sanic/test_sanic.py:885: in test_remote_addr_data_collection
    assert response.status == 200
E   AttributeError: 'NoneType' object has no attribute 'status'

✅ Patch coverage is 96.30%. Project has 2547 uncovered lines.
✅ Project coverage is 90.29%. Comparing base (2a499ec) to head (eee3388).

Files with missing lines (2)
File Patch % Lines
sentry_sdk/integrations/wsgi.py 100.00% ⚠️ 1 partials
sentry_sdk/utils.py 90.00% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##        master       #PR       +/-##
==========================================
+ Coverage    90.25%    90.29%    +0.04%
==========================================
  Files          195       195         —
  Lines        26195     26221       +26
  Branches      9758      9768       +10
==========================================
+ Hits         23641     23674       +33
- Misses        2554      2547        -7
- Partials      1484      1483        -1

Generated by Codecov Action

Comment thread sentry_sdk/integrations/_asgi_common.py Outdated
Comment on lines +233 to +237
server = asgi_scope.get("server")
if server is not None:
attributes["server.address"] = server[0]
if server[1] is not None:
attributes["server.port"] = server[1]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tests for new ASGI server attributes and localhost marker

Please add ASGI tests for extracting server.address and server.port from the scope, including a Unix-socket case where the port is None. Also assert that the sentry.is_localhost scope attribute is set from the extracted address and inherited by associated spans.

Evidence
  • _get_request_attributes() copies server[0] to server.address and emits server.port only when server[1] is not None.
  • SentryAsgiMiddleware._run_app() uses the extracted address to set sentry.is_localhost on the scope for streamed spans.
  • tests/integrations/asgi/test_asgi.py has no assertions for these server attributes or the localhost marker, including no Unix-socket case.

Identified by Warden · code-review · WYG-ABN

Comment thread sentry_sdk/integrations/asgi.py Outdated
capture_internal_exceptions,
event_from_exception,
has_data_collection_enabled,
is_localhost,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcard bind address is classified as localhost

In ASGI/WSGI streaming mode, 0.0.0.0 can be used as server.address and is classified as localhost. Production apps bound to all interfaces may therefore be marked sentry.is_localhost=true; if an inbound filter uses that attribute, it can drop their events. Treat the wildcard bind address separately from loopback addresses.

Evidence
  • _get_request_attributes in _asgi_common.py sets server.address from scope["server"][0].
  • In the ASGI middleware, the localhost attribute is set when the request span is a StreamedSpan; WSGI has the corresponding streamed-span path.
  • utils.is_localhost explicitly includes 0.0.0.0 in _LOCALHOST_NAMES and returns True before checking IP loopback status.
  • SPANDATA.SENTRY_IS_LOCALHOST is documented in consts.py as being used in inbound filters.

Identified by Warden · code-review · PNP-J3M

Comment thread sentry_sdk/integrations/asgi.py Outdated
Comment on lines +346 to +352
if SPANDATA.SERVER_ADDRESS in request_attributes:
sentry_scope.set_attribute(
SPANDATA.SENTRY_IS_LOCALHOST,
is_localhost(
request_attributes[SPANDATA.SERVER_ADDRESS]
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ASGI bind address can classify production server spans as localhost

When trace_lifecycle="stream" is enabled, the ASGI middleware derives sentry.is_localhost from scope["server"][0], which is the server-side address rather than the request’s Host. Production traffic can therefore be marked localhost when the app binds to 0.0.0.0 or to loopback behind a proxy, potentially causing inbound filters to drop it. Consider deriving this classification from the request-facing host or otherwise distinguishing bind addresses from localhost.

Evidence
  • _get_request_attributes() in sentry_sdk/integrations/_asgi_common.py sets server.address from the first value of the ASGI server tuple.
  • In SentryAsgiMiddleware._run_app(), the StreamedSpan path passes that server.address to is_localhost() and sets sentry.is_localhost on the isolation scope; this path is used when span streaming is enabled.
  • is_localhost() explicitly treats 0.0.0.0 and loopback addresses as local, including cases where a proxy forwards production requests to a loopback-bound app.
  • SPANDATA.SENTRY_IS_LOCALHOST is documented as being used in inbound filters, so this false classification can affect production telemetry.

Identified by Warden · code-review · PWJ-GAU

Comment thread sentry_sdk/integrations/wsgi.py Outdated
capture_internal_exceptions,
event_from_exception,
has_data_collection_enabled,
is_localhost,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcard bind address is incorrectly flagged as localhost

is_localhost() explicitly treats 0.0.0.0 as localhost. With ASGI trace streaming enabled, the server address from scope["server"][0] is passed to this helper, so an app bound to all interfaces can set sentry.is_localhost=true on its server span and trigger localhost inbound filtering for production telemetry. Treat the unspecified wildcard address as non-local while retaining the checks for actual localhost names and loopback addresses.

Evidence
  • utils.is_localhost() returns True for 0.0.0.0 before checking ip.is_loopback; that address is unspecified, not loopback.
  • _get_request_attributes() in _asgi_common.py copies scope["server"][0] to server.address.
  • In SentryAsgiMiddleware._run_app(), streamed spans pass server.address to is_localhost() and set SPANDATA.SENTRY_IS_LOCALHOST; this path runs when span streaming is enabled.
  • SPANDATA.SENTRY_IS_LOCALHOST in consts.py is documented as being used in inbound filters.

Identified by Warden · code-review · AK7-SWG

Comment thread sentry_sdk/integrations/wsgi.py Outdated
Comment on lines 191 to 198
if SPANDATA.SERVER_ADDRESS in request_attributes:
scope.set_attribute(
SPANDATA.SENTRY_IS_LOCALHOST,
is_localhost(
request_attributes[SPANDATA.SERVER_ADDRESS]
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tests for sentry.is_localhost or is_localhost()

Please add unit tests for is_localhost() edge cases and a WSGI integration test asserting sentry.is_localhost is set on the isolation scope/spans when span streaming is enabled.

Evidence
  • PR body explicitly lists Also TODO: tests.
  • grep over tests/**/*.py finds no coverage of is_localhost, SENTRY_IS_LOCALHOST, or sentry.is_localhost.
  • New behavior in this hunk sets SPANDATA.SENTRY_IS_LOCALHOST from request_attributes[SPANDATA.SERVER_ADDRESS] only on the StreamedSpan path.

Identified by Warden · code-review · KKZ-3U5

Comment thread sentry_sdk/integrations/wsgi.py Outdated
Comment on lines +194 to +197
is_localhost(
request_attributes[SPANDATA.SERVER_ADDRESS]
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcard bind address is classified as localhost

When server.address is 0.0.0.0, is_localhost() marks the span as local even though this is an unspecified all-interfaces address, not a loopback address. ASGI or WSGI deployments reporting this address can therefore have production spans marked local and affected by inbound filters. Consider limiting this classification to loopback addresses.

Evidence
  • is_localhost() includes 0.0.0.0 in _LOCALHOST_NAMES and returns True before checking ip.is_loopback.
  • ASGI request attributes take server.address from the ASGI scope’s server tuple; WSGI takes it from SERVER_NAME.
  • The ASGI and WSGI integrations pass that address to is_localhost() and set sentry.is_localhost from the result.
  • SPANDATA.SENTRY_IS_LOCALHOST is documented as used in inbound filters.

Identified by Warden · code-review · EF8-X9G

Comment thread sentry_sdk/utils.py Outdated
@@ -1,4 +1,5 @@
import copy
import ipaddress

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcard ASGI bind address is classified as localhost

When span streaming is enabled, ASGI requests served by a server bound to 0.0.0.0 are marked sentry.is_localhost=true: the middleware passes server.address to is_localhost(), which explicitly classifies 0.0.0.0 as localhost. Since this is an unspecified bind address, not a loopback address, the flag can cause production requests to be dropped by a configured localhost inbound filter. Consider excluding 0.0.0.0 from localhost detection.

Evidence
  • _get_request_attributes() copies asgi_scope["server"][0] into server.address.
  • In the StreamedSpan path, SentryAsgiMiddleware._run_app() passes that value to is_localhost() and sets sentry.is_localhost on the isolation scope.
  • is_localhost() explicitly returns True for 0.0.0.0, and SPANDATA.SENTRY_IS_LOCALHOST is documented as being used in inbound filters.

Identified by Warden · code-review · TXD-3R9

Comment thread sentry_sdk/utils.py Outdated
Comment on lines +2216 to +2219
_LOCALHOST_NAMES = {"localhost", "127.0.0.1", "::1", "0.0.0.0", "0:0:0:0:0:0:0:1"}

if address in _LOCALHOST_NAMES:
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.0.0.0 is classified as localhost for server spans

When span streaming is enabled, WSGI and ASGI use is_localhost on the extracted server.address. Since is_localhost explicitly treats 0.0.0.0 as localhost—even though it is an unspecified bind-all address—deployments that report that listen address can mark production server spans as local. This may cause inbound localhost filters to discard those spans. Please remove 0.0.0.0 from the localhost set.

Evidence
  • is_localhost explicitly returns True for 0.0.0.0, bypassing the subsequent ip.is_loopback check; the address is unspecified, not loopback.
  • In the span-streaming path, WSGI extracts SERVER_NAME as server.address and ASGI extracts scope["server"][0]; each passes that value to is_localhost and sets SPANDATA.SENTRY_IS_LOCALHOST on the scope.
  • SPANDATA.SENTRY_IS_LOCALHOST is documented as used in inbound filters, so a production server reporting 0.0.0.0 can be classified as local for filtering.

Identified by Warden · code-review · CW6-JPB

Comment thread sentry_sdk/utils.py Outdated
@@ -1,4 +1,5 @@
import copy
import ipaddress

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_localhost treats 0.0.0.0 as localhost

is_localhost() (utils.py ~2216) hardcodes 0.0.0.0 as local even though it is an unspecified bind-all address, not loopback—ASGI/WSGI streamed spans that report that listen address can set sentry.is_localhost=true and be dropped by inbound filters. Remove 0.0.0.0 and rely on ip.is_loopback plus localhost names.

Evidence
  • The new import ipaddress supports is_localhost() in sentry_sdk/utils.py, which includes 0.0.0.0 in _LOCALHOST_NAMES and returns True before any loopback check.
  • ipaddress.ip_address("0.0.0.0").is_loopback is False; the address is unspecified, not loopback.
  • ASGI _get_request_attributes() sets server.address from scope["server"][0] (the listen/bind host), commonly 0.0.0.0 in Docker/uvicorn/gunicorn.
  • ASGI and WSGI stream paths call is_localhost(server.address) and set SPANDATA.SENTRY_IS_LOCALHOST on the isolation scope; that attribute is documented for inbound filters.
Also found at 4 additional locations
  • sentry_sdk/integrations/asgi.py:51
  • sentry_sdk/integrations/asgi.py:349-351
  • sentry_sdk/integrations/wsgi.py:194-196
  • sentry_sdk/utils.py:2216-2219

Identified by Warden · find-bugs · 9ZH-U3Y

Comment thread sentry_sdk/integrations/wsgi.py Outdated
Comment on lines +191 to +197
if SPANDATA.SERVER_ADDRESS in request_attributes:
scope.set_attribute(
SPANDATA.SENTRY_IS_LOCALHOST,
is_localhost(
request_attributes[SPANDATA.SERVER_ADDRESS]
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localhost flag derived from internal SERVER_NAME

This path derives sentry.is_localhost from SERVER_NAME/server.address (internal server address), so reverse-proxied apps listening on loopback can mark production spans local; use the request Host (HTTP_HOST/get_host()) instead.

Evidence
  • The streamed-span path sets sentry.is_localhost only from request_attributes[SPANDATA.SERVER_ADDRESS].
  • WSGI _get_request_attributes() copies raw environ["SERVER_NAME"] into server.address, not HTTP_HOST.
  • _werkzeug.get_host() documents that SERVER_NAME/SERVER_PORT describe the internal server and prefers HTTP_HOST when building the public host.
  • A production app behind a proxy with internal SERVER_NAME of 127.0.0.1 therefore gets is_localhost() == True and inherits that scope attribute on spans.

Identified by Warden · find-bugs · RD5-R97

@sentrivana sentrivana closed this Sep 30, 2026
@sentrivana
sentrivana deleted the ivana/is-localhost branch October 1, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant