Repository: getsentry/sentry-python

Issue:
`http.response.status_code` normalization for Metrics & Alerts
Background:
http.status is set in different places in different SDKs which creates some problems in processing for metrics extraction and then

https://www.notion.so/sentry/Expand-Metrics-for-Alerts-cdfa32d7624e498b8246c3f131f753f1#17480955e2fb4407b619b9236bc2723a

**Requirements:**
Need status code number (standard HTTP status code 2xx, 4xx, 5xx, etc)
The SDK should never on it's own set this as a tag - as this adds expense due to indexing being based on tags
the location of storage needs to be in a standard location based on span schema
follow existing standard in [develop docs](https://develop.sentry.dev/sdk/performance/span-operations/)
Starfish has a workstream to standardize span schema https://github.com/getsentry/rfcs/pull/89

Discussion:
antonpirker: A note:

The OTel docs for their semantic conventions [1] say that it is called `http.response.status_code` (this is what we implemented)

But the OTel Python SDK still uses `http.status_code` [2].

Anyhow, the Sentry Otel integrations uses the variables from the OTel Python SDK, so if an OTel span has `http.status_code` set it will be transformed into `http.response.status_code` in the Sentry span and everything is fine.

1: https://opentelemetry.io/docs/specs/otel/metrics/semantic_conventions/http-metrics/#http-server
2: https://github.com/open-telemetry/opentelemetry-python/blob/9706ed004ae3c35ab393fb48c3d71d0c40e8c62b/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py#L279

Patch:
diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py
index 524d8e05..0fc94686 100644
--- a/sentry_sdk/consts.py
+++ b/sentry_sdk/consts.py
@@ -101,6 +101,12 @@ class SPANDATA:
     Example: GET
     """
 
+    HTTP_STATUS_CODE = "http.response.status_code"
+    """
+    The HTTP status code as an integer.
+    Example: 418
+    """
+
 
 class OP:
     CACHE_GET_ITEM = "cache.get_item"
diff --git a/sentry_sdk/integrations/httpx.py b/sentry_sdk/integrations/httpx.py
index a7319d9d..358562f7 100644
--- a/sentry_sdk/integrations/httpx.py
+++ b/sentry_sdk/integrations/httpx.py
@@ -64,7 +64,6 @@ def _install_httpx_client():
 
             rv = real_send(self, request, **kwargs)
 
-            span.set_data("status_code", rv.status_code)
             span.set_http_status(rv.status_code)
             span.set_data("reason", rv.reason_phrase)
 
@@ -105,7 +104,6 @@ def _install_httpx_async_client():
 
             rv = await real_send(self, request, **kwargs)
 
-            span.set_data("status_code", rv.status_code)
             span.set_http_status(rv.status_code)
             span.set_data("reason", rv.reason_phrase)
 
diff --git a/sentry_sdk/integrations/stdlib.py b/sentry_sdk/integrations/stdlib.py
index 17b30102..0add046b 100644
--- a/sentry_sdk/integrations/stdlib.py
+++ b/sentry_sdk/integrations/stdlib.py
@@ -120,7 +120,6 @@ def _install_httplib():
 
         rv = real_getresponse(self, *args, **kwargs)
 
-        span.set_data("status_code", rv.status)
         span.set_http_status(int(rv.status))
         span.set_data("reason", rv.reason)
         span.finish()
diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py
index 35d77ae4..97c3277b 100644
--- a/sentry_sdk/tracing.py
+++ b/sentry_sdk/tracing.py
@@ -7,6 +7,7 @@ import sentry_sdk
 from sentry_sdk.consts import INSTRUMENTER
 from sentry_sdk.utils import is_valid_sample_rate, logger, nanosecond_time
 from sentry_sdk._compat import PY2
+from sentry_sdk.consts import SPANDATA
 from sentry_sdk._types import TYPE_CHECKING
 
 
@@ -370,7 +371,10 @@ class Span(object):
 
     def set_http_status(self, http_status):
         # type: (int) -> None
-        self.set_tag("http.status_code", str(http_status))
+        self.set_tag(
+            "http.status_code", str(http_status)
+        )  # we keep this for backwards compatability
+        self.set_data(SPANDATA.HTTP_STATUS_CODE, http_status)
 
         if http_status < 400:
             self.set_status("ok")


Test patch:
diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py
index dd5e752c..c9489015 100644
--- a/tests/integrations/httpx/test_httpx.py
+++ b/tests/integrations/httpx/test_httpx.py
@@ -46,7 +46,7 @@ def test_crumb_capture_and_hint(sentry_init, capture_events, httpx_client):
             SPANDATA.HTTP_METHOD: "GET",
             SPANDATA.HTTP_FRAGMENT: "",
             SPANDATA.HTTP_QUERY: "",
-            "status_code": 200,
+            SPANDATA.HTTP_STATUS_CODE: 200,
             "reason": "OK",
             "extra": "foo",
         }
diff --git a/tests/integrations/opentelemetry/test_span_processor.py b/tests/integrations/opentelemetry/test_span_processor.py
index 0467da76..8659e548 100644
--- a/tests/integrations/opentelemetry/test_span_processor.py
+++ b/tests/integrations/opentelemetry/test_span_processor.py
@@ -190,11 +190,10 @@ def test_update_span_with_otel_data_http_method():
 
     assert sentry_span.op == "http.client"
     assert sentry_span.description == "GET example.com /"
-    assert sentry_span._tags["http.status_code"] == "429"
     assert sentry_span.status == "resource_exhausted"
 
     assert sentry_span._data["http.method"] == "GET"
-    assert sentry_span._data["http.status_code"] == 429
+    assert sentry_span._data["http.response.status_code"] == 429
     assert sentry_span._data["http.status_text"] == "xxx"
     assert sentry_span._data["http.user_agent"] == "curl/7.64.1"
     assert sentry_span._data["net.peer.name"] == "example.com"
@@ -220,11 +219,10 @@ def test_update_span_with_otel_data_http_method2():
 
     assert sentry_span.op == "http.server"
     assert sentry_span.description == "GET https://example.com/status/403"
-    assert sentry_span._tags["http.status_code"] == "429"
     assert sentry_span.status == "resource_exhausted"
 
     assert sentry_span._data["http.method"] == "GET"
-    assert sentry_span._data["http.status_code"] == 429
+    assert sentry_span._data["http.response.status_code"] == 429
     assert sentry_span._data["http.status_text"] == "xxx"
     assert sentry_span._data["http.user_agent"] == "curl/7.64.1"
     assert (
diff --git a/tests/integrations/requests/test_requests.py b/tests/integrations/requests/test_requests.py
index 324379fc..9c77b290 100644
--- a/tests/integrations/requests/test_requests.py
+++ b/tests/integrations/requests/test_requests.py
@@ -28,6 +28,6 @@ def test_crumb_capture(sentry_init, capture_events):
         SPANDATA.HTTP_METHOD: "GET",
         SPANDATA.HTTP_FRAGMENT: "",
         SPANDATA.HTTP_QUERY: "",
-        "status_code": response.status_code,
+        SPANDATA.HTTP_STATUS_CODE: response.status_code,
         "reason": response.reason,
     }
diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py
index 959ad165..769d3dfe 100644
--- a/tests/integrations/stdlib/test_httplib.py
+++ b/tests/integrations/stdlib/test_httplib.py
@@ -49,7 +49,7 @@ def test_crumb_capture(sentry_init, capture_events):
     assert crumb["data"] == {
         "url": url,
         SPANDATA.HTTP_METHOD: "GET",
-        "status_code": 200,
+        SPANDATA.HTTP_STATUS_CODE: 200,
         "reason": "OK",
         SPANDATA.HTTP_FRAGMENT: "",
         SPANDATA.HTTP_QUERY: "",
@@ -76,7 +76,7 @@ def test_crumb_capture_hint(sentry_init, capture_events):
     assert crumb["data"] == {
         "url": url,
         SPANDATA.HTTP_METHOD: "GET",
-        "status_code": 200,
+        SPANDATA.HTTP_STATUS_CODE: 200,
         "reason": "OK",
         "extra": "foo",
         SPANDATA.HTTP_FRAGMENT: "",
@@ -134,7 +134,7 @@ def test_httplib_misuse(sentry_init, capture_events, request):
     assert crumb["data"] == {
         "url": "http://localhost:{}/200".format(PORT),
         SPANDATA.HTTP_METHOD: "GET",
-        "status_code": 200,
+        SPANDATA.HTTP_STATUS_CODE: 200,
         "reason": "OK",
         SPANDATA.HTTP_FRAGMENT: "",
         SPANDATA.HTTP_QUERY: "",
diff --git a/tests/tracing/test_noop_span.py b/tests/tracing/test_noop_span.py
index 92cba75a..9896afb0 100644
--- a/tests/tracing/test_noop_span.py
+++ b/tests/tracing/test_noop_span.py
@@ -27,7 +27,7 @@ def test_noop_start_span(sentry_init):
         assert isinstance(span, NoOpSpan)
         assert sentry_sdk.Hub.current.scope.span is span
 
-        span.set_tag("http.status_code", "418")
+        span.set_tag("http.response.status_code", 418)
         span.set_data("http.entity_type", "teapot")