Repository: getsentry/sentry-python

Issue:
Support for Proxy Authentication
Hi there! Thanks for all you do with Sentry -- it adds incredible value to our team.

I have a question about support for HTTP proxy authentication in `sentry_sdk`.

**TL;DR:** Is proxy authentication supported? If not, can it be? Without support for proxy auth, we can't use Sentry from a significant portion of our services.

**More details:**

I see that we can pass a `https_proxy` kwarg to `sentry_sdk.init()`, such as:

```
sentry_sdk.init("https://...@foo.ingest.sentry.io/...", https_proxy="http://user:pass@proxy")
```

However, it looks like that proxy string eventually gets passed to `urllib3.ProxyManager()` here: https://github.com/getsentry/sentry-python/blob/0da369f839ee2c383659c91ea8858abcac04b869/sentry_sdk/transport.py#L293

Unfortunately, `urllib3.ProxyManager` does not appear to respect credentials passed in this string. The [documentation](https://urllib3.readthedocs.io/en/latest/reference/#urllib3.poolmanager.ProxyManager) isn't super clear on this, but it does allude to it in the description of the `proxy_headers` kwarg: "Could be used for proxy authentication." We verified with packet captures that `urllib3.ProxyManager` is not adding a `Proxy-Authorization` HTTP header to requests when the proxy string includes credentials.

Thanks so much for your consideration and help with this!

Discussion:
vercel[bot]: **The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated |
| :--- | :----- | :------ | :------ |
| **sentry-python** | ✅ Ready ([Inspect](https://vercel.com/sdk-rangers/sentry-python/Hcto46qVQ4t6M1T5LZLC7PNgUoCi)) | [Visit Preview](https://sentry-python-git-neel-proxy-headers-sdk-rangers.vercel.app) | Nov 17, 2022 at 5:33PM (UTC) |



Patch:
diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py
index d07bec23..3393f491 100644
--- a/sentry_sdk/consts.py
+++ b/sentry_sdk/consts.py
@@ -81,6 +81,7 @@ class ClientConstructor(object):
         auto_session_tracking=True,  # type: bool
         send_client_reports=True,  # type: bool
         _experiments={},  # type: Experiments  # noqa: B006
+        proxy_headers=None,  # type: Optional[Dict[str, str]]
     ):
         # type: (...) -> None
         pass
diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py
index fca6fa8a..4937668c 100644
--- a/sentry_sdk/transport.py
+++ b/sentry_sdk/transport.py
@@ -156,6 +156,7 @@ class HttpTransport(Transport):
             http_proxy=options["http_proxy"],
             https_proxy=options["https_proxy"],
             ca_certs=options["ca_certs"],
+            proxy_headers=options["proxy_headers"],
         )
 
         from sentry_sdk import Hub
@@ -420,6 +421,7 @@ class HttpTransport(Transport):
         http_proxy,  # type: Optional[str]
         https_proxy,  # type: Optional[str]
         ca_certs,  # type: Optional[Any]
+        proxy_headers,  # type: Optional[Dict[str, str]]
     ):
         # type: (...) -> Union[PoolManager, ProxyManager]
         proxy = None
@@ -436,6 +438,9 @@ class HttpTransport(Transport):
         opts = self._get_pool_options(ca_certs)
 
         if proxy:
+            if proxy_headers:
+                opts["proxy_headers"] = proxy_headers
+
             return urllib3.ProxyManager(proxy, **opts)
         else:
             return urllib3.PoolManager(**opts)


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 55236478..c0f380d7 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -227,6 +227,16 @@ def test_transport_option(monkeypatch):
             "arg_https_proxy": "https://localhost/123",
             "expected_proxy_scheme": "https",
         },
+        {
+            "dsn": "https://foo@sentry.io/123",
+            "env_http_proxy": None,
+            "env_https_proxy": None,
+            "env_no_proxy": "sentry.io,example.com",
+            "arg_http_proxy": None,
+            "arg_https_proxy": "https://localhost/123",
+            "expected_proxy_scheme": "https",
+            "arg_proxy_headers": {"Test-Header": "foo-bar"},
+        },
     ],
 )
 def test_proxy(monkeypatch, testcase):
@@ -241,12 +251,17 @@ def test_proxy(monkeypatch, testcase):
         kwargs["http_proxy"] = testcase["arg_http_proxy"]
     if testcase["arg_https_proxy"] is not None:
         kwargs["https_proxy"] = testcase["arg_https_proxy"]
+    if testcase.get("arg_proxy_headers") is not None:
+        kwargs["proxy_headers"] = testcase["arg_proxy_headers"]
     client = Client(testcase["dsn"], **kwargs)
     if testcase["expected_proxy_scheme"] is None:
         assert client.transport._pool.proxy is None
     else:
         assert client.transport._pool.proxy.scheme == testcase["expected_proxy_scheme"]
 
+        if testcase.get("arg_proxy_headers") is not None:
+            assert client.transport._pool.proxy_headers == testcase["arg_proxy_headers"]
+
 
 def test_simple_transport(sentry_init):
     events = []