sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sentry_sdk/__init__.py +48 -6
- sentry_sdk/_compat.py +64 -56
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +81 -19
- sentry_sdk/_types.py +311 -11
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +409 -67
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +849 -103
- sentry_sdk/consts.py +1389 -34
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +12 -15
- sentry_sdk/envelope.py +112 -61
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +442 -386
- sentry_sdk/integrations/__init__.py +228 -58
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +131 -40
- sentry_sdk/integrations/aiohttp.py +221 -72
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +4 -6
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +237 -135
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +13 -18
- sentry_sdk/integrations/aws_lambda.py +233 -80
- sentry_sdk/integrations/beam.py +27 -35
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +91 -69
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +35 -28
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +32 -8
- sentry_sdk/integrations/django/__init__.py +343 -89
- sentry_sdk/integrations/django/asgi.py +201 -22
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +80 -32
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +69 -2
- sentry_sdk/integrations/django/transactions.py +39 -14
- sentry_sdk/integrations/django/views.py +69 -16
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +19 -13
- sentry_sdk/integrations/executing.py +5 -6
- sentry_sdk/integrations/falcon.py +128 -65
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +114 -75
- sentry_sdk/integrations/gcp.py +67 -36
- sentry_sdk/integrations/gnu_backtrace.py +14 -22
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +261 -85
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +6 -33
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +20 -11
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +71 -60
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +62 -52
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +248 -114
- sentry_sdk/integrations/serverless.py +13 -22
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/spark_driver.py +115 -62
- sentry_sdk/integrations/spark/spark_worker.py +42 -50
- sentry_sdk/integrations/sqlalchemy.py +82 -37
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +100 -58
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +142 -38
- sentry_sdk/integrations/tornado.py +68 -53
- sentry_sdk/integrations/trytond.py +15 -20
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +126 -125
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/scope.py +1542 -112
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +152 -210
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +202 -179
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1202 -294
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +693 -189
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1395 -228
- sentry_sdk/worker.py +30 -17
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/_functools.py +0 -66
- sentry_sdk/integrations/celery.py +0 -275
- sentry_sdk/integrations/redis.py +0 -103
- sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
- sentry_sdk-0.18.0.dist-info/METADATA +0 -66
- sentry_sdk-0.18.0.dist-info/RECORD +0 -65
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/transport.py
CHANGED
|
@@ -1,42 +1,64 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
3
2
|
import io
|
|
4
|
-
import
|
|
5
|
-
import certifi
|
|
3
|
+
import os
|
|
6
4
|
import gzip
|
|
5
|
+
import socket
|
|
6
|
+
import ssl
|
|
7
|
+
import time
|
|
8
|
+
import warnings
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from urllib.request import getproxies
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
try:
|
|
14
|
+
import brotli # type: ignore
|
|
15
|
+
except ImportError:
|
|
16
|
+
brotli = None
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
import urllib3
|
|
19
|
+
import certifi
|
|
20
|
+
|
|
21
|
+
import sentry_sdk
|
|
22
|
+
from sentry_sdk.consts import EndpointType
|
|
23
|
+
from sentry_sdk.utils import Dsn, logger, capture_internal_exceptions
|
|
11
24
|
from sentry_sdk.worker import BackgroundWorker
|
|
12
|
-
from sentry_sdk.envelope import Envelope
|
|
25
|
+
from sentry_sdk.envelope import Envelope, Item, PayloadRef
|
|
13
26
|
|
|
14
|
-
from
|
|
27
|
+
from typing import TYPE_CHECKING, cast, List, Dict
|
|
15
28
|
|
|
16
|
-
if
|
|
29
|
+
if TYPE_CHECKING:
|
|
17
30
|
from typing import Any
|
|
18
31
|
from typing import Callable
|
|
19
|
-
from typing import
|
|
32
|
+
from typing import DefaultDict
|
|
20
33
|
from typing import Iterable
|
|
34
|
+
from typing import Mapping
|
|
21
35
|
from typing import Optional
|
|
36
|
+
from typing import Self
|
|
22
37
|
from typing import Tuple
|
|
23
38
|
from typing import Type
|
|
24
39
|
from typing import Union
|
|
25
40
|
|
|
26
|
-
from urllib3.poolmanager import PoolManager
|
|
41
|
+
from urllib3.poolmanager import PoolManager
|
|
27
42
|
from urllib3.poolmanager import ProxyManager
|
|
28
43
|
|
|
29
|
-
from sentry_sdk._types import Event,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
from sentry_sdk._types import Event, EventDataCategory
|
|
45
|
+
|
|
46
|
+
KEEP_ALIVE_SOCKET_OPTIONS = []
|
|
47
|
+
for option in [
|
|
48
|
+
(socket.SOL_SOCKET, lambda: getattr(socket, "SO_KEEPALIVE"), 1), # noqa: B009
|
|
49
|
+
(socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPIDLE"), 45), # noqa: B009
|
|
50
|
+
(socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPINTVL"), 10), # noqa: B009
|
|
51
|
+
(socket.SOL_TCP, lambda: getattr(socket, "TCP_KEEPCNT"), 6), # noqa: B009
|
|
52
|
+
]:
|
|
53
|
+
try:
|
|
54
|
+
KEEP_ALIVE_SOCKET_OPTIONS.append((option[0], option[1](), option[2]))
|
|
55
|
+
except AttributeError:
|
|
56
|
+
# a specific option might not be available on specific systems,
|
|
57
|
+
# e.g. TCP_KEEPIDLE doesn't exist on macOS
|
|
58
|
+
pass
|
|
37
59
|
|
|
38
60
|
|
|
39
|
-
class Transport(
|
|
61
|
+
class Transport(ABC):
|
|
40
62
|
"""Baseclass for all transports.
|
|
41
63
|
|
|
42
64
|
A transport is used to send an event to sentry.
|
|
@@ -44,212 +66,396 @@ class Transport(object):
|
|
|
44
66
|
|
|
45
67
|
parsed_dsn = None # type: Optional[Dsn]
|
|
46
68
|
|
|
47
|
-
def __init__(
|
|
48
|
-
|
|
49
|
-
):
|
|
50
|
-
# type: (...) -> None
|
|
69
|
+
def __init__(self, options=None):
|
|
70
|
+
# type: (Self, Optional[Dict[str, Any]]) -> None
|
|
51
71
|
self.options = options
|
|
52
72
|
if options and options["dsn"] is not None and options["dsn"]:
|
|
53
73
|
self.parsed_dsn = Dsn(options["dsn"])
|
|
54
74
|
else:
|
|
55
75
|
self.parsed_dsn = None
|
|
56
76
|
|
|
57
|
-
def capture_event(
|
|
58
|
-
|
|
59
|
-
):
|
|
60
|
-
# type: (...) -> None
|
|
77
|
+
def capture_event(self, event):
|
|
78
|
+
# type: (Self, Event) -> None
|
|
61
79
|
"""
|
|
80
|
+
DEPRECATED: Please use capture_envelope instead.
|
|
81
|
+
|
|
62
82
|
This gets invoked with the event dictionary when an event should
|
|
63
83
|
be sent to sentry.
|
|
64
84
|
"""
|
|
65
|
-
raise NotImplementedError()
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
warnings.warn(
|
|
87
|
+
"capture_event is deprecated, please use capture_envelope instead!",
|
|
88
|
+
DeprecationWarning,
|
|
89
|
+
stacklevel=2,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
envelope = Envelope()
|
|
93
|
+
envelope.add_event(event)
|
|
94
|
+
self.capture_envelope(envelope)
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def capture_envelope(self, envelope):
|
|
98
|
+
# type: (Self, Envelope) -> None
|
|
71
99
|
"""
|
|
72
100
|
Send an envelope to Sentry.
|
|
73
101
|
|
|
74
102
|
Envelopes are a data container format that can hold any type of data
|
|
75
|
-
submitted to Sentry. We use it
|
|
76
|
-
|
|
77
|
-
compat.
|
|
103
|
+
submitted to Sentry. We use it to send all event data (including errors,
|
|
104
|
+
transactions, crons check-ins, etc.) to Sentry.
|
|
78
105
|
"""
|
|
79
|
-
|
|
106
|
+
pass
|
|
80
107
|
|
|
81
108
|
def flush(
|
|
82
109
|
self,
|
|
83
|
-
timeout,
|
|
84
|
-
callback=None,
|
|
110
|
+
timeout,
|
|
111
|
+
callback=None,
|
|
85
112
|
):
|
|
86
|
-
# type: (
|
|
87
|
-
"""
|
|
88
|
-
|
|
113
|
+
# type: (Self, float, Optional[Any]) -> None
|
|
114
|
+
"""
|
|
115
|
+
Wait `timeout` seconds for the current events to be sent out.
|
|
116
|
+
|
|
117
|
+
The default implementation is a no-op, since this method may only be relevant to some transports.
|
|
118
|
+
Subclasses should override this method if necessary.
|
|
119
|
+
"""
|
|
120
|
+
return None
|
|
89
121
|
|
|
90
122
|
def kill(self):
|
|
91
|
-
# type: () -> None
|
|
92
|
-
"""
|
|
93
|
-
|
|
123
|
+
# type: (Self) -> None
|
|
124
|
+
"""
|
|
125
|
+
Forcefully kills the transport.
|
|
94
126
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
127
|
+
The default implementation is a no-op, since this method may only be relevant to some transports.
|
|
128
|
+
Subclasses should override this method if necessary.
|
|
129
|
+
"""
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
def record_lost_event(
|
|
133
|
+
self,
|
|
134
|
+
reason, # type: str
|
|
135
|
+
data_category=None, # type: Optional[EventDataCategory]
|
|
136
|
+
item=None, # type: Optional[Item]
|
|
137
|
+
*,
|
|
138
|
+
quantity=1, # type: int
|
|
139
|
+
):
|
|
140
|
+
# type: (...) -> None
|
|
141
|
+
"""This increments a counter for event loss by reason and
|
|
142
|
+
data category by the given positive-int quantity (default 1).
|
|
143
|
+
|
|
144
|
+
If an item is provided, the data category and quantity are
|
|
145
|
+
extracted from the item, and the values passed for
|
|
146
|
+
data_category and quantity are ignored.
|
|
147
|
+
|
|
148
|
+
When recording a lost transaction via data_category="transaction",
|
|
149
|
+
the calling code should also record the lost spans via this method.
|
|
150
|
+
When recording lost spans, `quantity` should be set to the number
|
|
151
|
+
of contained spans, plus one for the transaction itself. When
|
|
152
|
+
passing an Item containing a transaction via the `item` parameter,
|
|
153
|
+
this method automatically records the lost spans.
|
|
154
|
+
"""
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def is_healthy(self):
|
|
158
|
+
# type: (Self) -> bool
|
|
159
|
+
return True
|
|
101
160
|
|
|
102
161
|
|
|
103
162
|
def _parse_rate_limits(header, now=None):
|
|
104
|
-
# type: (
|
|
163
|
+
# type: (str, Optional[datetime]) -> Iterable[Tuple[Optional[EventDataCategory], datetime]]
|
|
105
164
|
if now is None:
|
|
106
|
-
now = datetime.
|
|
165
|
+
now = datetime.now(timezone.utc)
|
|
107
166
|
|
|
108
167
|
for limit in header.split(","):
|
|
109
168
|
try:
|
|
110
|
-
|
|
111
|
-
|
|
169
|
+
parameters = limit.strip().split(":")
|
|
170
|
+
retry_after_val, categories = parameters[:2]
|
|
171
|
+
|
|
172
|
+
retry_after = now + timedelta(seconds=int(retry_after_val))
|
|
112
173
|
for category in categories and categories.split(";") or (None,):
|
|
113
|
-
yield category, retry_after
|
|
174
|
+
yield category, retry_after # type: ignore
|
|
114
175
|
except (LookupError, ValueError):
|
|
115
176
|
continue
|
|
116
177
|
|
|
117
178
|
|
|
118
|
-
class
|
|
119
|
-
"""The
|
|
179
|
+
class BaseHttpTransport(Transport):
|
|
180
|
+
"""The base HTTP transport."""
|
|
120
181
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
):
|
|
124
|
-
# type: (
|
|
182
|
+
TIMEOUT = 30 # seconds
|
|
183
|
+
|
|
184
|
+
def __init__(self, options):
|
|
185
|
+
# type: (Self, Dict[str, Any]) -> None
|
|
125
186
|
from sentry_sdk.consts import VERSION
|
|
126
187
|
|
|
127
188
|
Transport.__init__(self, options)
|
|
128
189
|
assert self.parsed_dsn is not None
|
|
129
|
-
self.
|
|
190
|
+
self.options = options # type: Dict[str, Any]
|
|
191
|
+
self._worker = BackgroundWorker(queue_size=options["transport_queue_size"])
|
|
130
192
|
self._auth = self.parsed_dsn.to_auth("sentry.python/%s" % VERSION)
|
|
131
|
-
self._disabled_until = {} # type: Dict[
|
|
193
|
+
self._disabled_until = {} # type: Dict[Optional[EventDataCategory], datetime]
|
|
194
|
+
# We only use this Retry() class for the `get_retry_after` method it exposes
|
|
132
195
|
self._retry = urllib3.util.Retry()
|
|
133
|
-
self.
|
|
196
|
+
self._discarded_events = defaultdict(int) # type: DefaultDict[Tuple[EventDataCategory, str], int]
|
|
197
|
+
self._last_client_report_sent = time.time()
|
|
134
198
|
|
|
135
|
-
self._pool = self._make_pool(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
199
|
+
self._pool = self._make_pool()
|
|
200
|
+
|
|
201
|
+
# Backwards compatibility for deprecated `self.hub_class` attribute
|
|
202
|
+
self._hub_cls = sentry_sdk.Hub
|
|
203
|
+
|
|
204
|
+
experiments = options.get("_experiments", {})
|
|
205
|
+
compression_level = experiments.get(
|
|
206
|
+
"transport_compression_level",
|
|
207
|
+
experiments.get("transport_zlib_compression_level"),
|
|
140
208
|
)
|
|
209
|
+
compression_algo = experiments.get(
|
|
210
|
+
"transport_compression_algo",
|
|
211
|
+
(
|
|
212
|
+
"gzip"
|
|
213
|
+
# if only compression level is set, assume gzip for backwards compatibility
|
|
214
|
+
# if we don't have brotli available, fallback to gzip
|
|
215
|
+
if compression_level is not None or brotli is None
|
|
216
|
+
else "br"
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if compression_algo == "br" and brotli is None:
|
|
221
|
+
logger.warning(
|
|
222
|
+
"You asked for brotli compression without the Brotli module, falling back to gzip -9"
|
|
223
|
+
)
|
|
224
|
+
compression_algo = "gzip"
|
|
225
|
+
compression_level = None
|
|
226
|
+
|
|
227
|
+
if compression_algo not in ("br", "gzip"):
|
|
228
|
+
logger.warning(
|
|
229
|
+
"Unknown compression algo %s, disabling compression", compression_algo
|
|
230
|
+
)
|
|
231
|
+
self._compression_level = 0
|
|
232
|
+
self._compression_algo = None
|
|
233
|
+
else:
|
|
234
|
+
self._compression_algo = compression_algo
|
|
141
235
|
|
|
142
|
-
|
|
236
|
+
if compression_level is not None:
|
|
237
|
+
self._compression_level = compression_level
|
|
238
|
+
elif self._compression_algo == "gzip":
|
|
239
|
+
self._compression_level = 9
|
|
240
|
+
elif self._compression_algo == "br":
|
|
241
|
+
self._compression_level = 4
|
|
143
242
|
|
|
144
|
-
|
|
243
|
+
def record_lost_event(
|
|
244
|
+
self,
|
|
245
|
+
reason, # type: str
|
|
246
|
+
data_category=None, # type: Optional[EventDataCategory]
|
|
247
|
+
item=None, # type: Optional[Item]
|
|
248
|
+
*,
|
|
249
|
+
quantity=1, # type: int
|
|
250
|
+
):
|
|
251
|
+
# type: (...) -> None
|
|
252
|
+
if not self.options["send_client_reports"]:
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
if item is not None:
|
|
256
|
+
data_category = item.data_category
|
|
257
|
+
quantity = 1 # If an item is provided, we always count it as 1 (except for attachments, handled below).
|
|
258
|
+
|
|
259
|
+
if data_category == "transaction":
|
|
260
|
+
# Also record the lost spans
|
|
261
|
+
event = item.get_transaction_event() or {}
|
|
262
|
+
|
|
263
|
+
# +1 for the transaction itself
|
|
264
|
+
span_count = (
|
|
265
|
+
len(cast(List[Dict[str, object]], event.get("spans") or [])) + 1
|
|
266
|
+
)
|
|
267
|
+
self.record_lost_event(reason, "span", quantity=span_count)
|
|
268
|
+
|
|
269
|
+
elif data_category == "attachment":
|
|
270
|
+
# quantity of 0 is actually 1 as we do not want to count
|
|
271
|
+
# empty attachments as actually empty.
|
|
272
|
+
quantity = len(item.get_bytes()) or 1
|
|
273
|
+
|
|
274
|
+
elif data_category is None:
|
|
275
|
+
raise TypeError("data category not provided")
|
|
276
|
+
|
|
277
|
+
self._discarded_events[data_category, reason] += quantity
|
|
278
|
+
|
|
279
|
+
def _get_header_value(self, response, header):
|
|
280
|
+
# type: (Self, Any, str) -> Optional[str]
|
|
281
|
+
return response.headers.get(header)
|
|
145
282
|
|
|
146
283
|
def _update_rate_limits(self, response):
|
|
147
|
-
# type: (urllib3.
|
|
284
|
+
# type: (Self, Union[urllib3.BaseHTTPResponse, httpcore.Response]) -> None
|
|
148
285
|
|
|
149
286
|
# new sentries with more rate limit insights. We honor this header
|
|
150
287
|
# no matter of the status code to update our internal rate limits.
|
|
151
|
-
header =
|
|
288
|
+
header = self._get_header_value(response, "x-sentry-rate-limits")
|
|
152
289
|
if header:
|
|
290
|
+
logger.warning("Rate-limited via x-sentry-rate-limits")
|
|
153
291
|
self._disabled_until.update(_parse_rate_limits(header))
|
|
154
292
|
|
|
155
293
|
# old sentries only communicate global rate limit hits via the
|
|
156
294
|
# retry-after header on 429. This header can also be emitted on new
|
|
157
295
|
# sentries if a proxy in front wants to globally slow things down.
|
|
158
296
|
elif response.status == 429:
|
|
159
|
-
|
|
160
|
-
|
|
297
|
+
logger.warning("Rate-limited via 429")
|
|
298
|
+
retry_after_value = self._get_header_value(response, "Retry-After")
|
|
299
|
+
retry_after = (
|
|
300
|
+
self._retry.parse_retry_after(retry_after_value)
|
|
301
|
+
if retry_after_value is not None
|
|
302
|
+
else None
|
|
303
|
+
) or 60
|
|
304
|
+
self._disabled_until[None] = datetime.now(timezone.utc) + timedelta(
|
|
305
|
+
seconds=retry_after
|
|
161
306
|
)
|
|
162
307
|
|
|
163
308
|
def _send_request(
|
|
164
309
|
self,
|
|
165
|
-
body,
|
|
166
|
-
headers,
|
|
167
|
-
endpoint_type=
|
|
310
|
+
body,
|
|
311
|
+
headers,
|
|
312
|
+
endpoint_type=EndpointType.ENVELOPE,
|
|
313
|
+
envelope=None,
|
|
168
314
|
):
|
|
169
|
-
# type: (
|
|
315
|
+
# type: (Self, bytes, Dict[str, str], EndpointType, Optional[Envelope]) -> None
|
|
316
|
+
|
|
317
|
+
def record_loss(reason):
|
|
318
|
+
# type: (str) -> None
|
|
319
|
+
if envelope is None:
|
|
320
|
+
self.record_lost_event(reason, data_category="error")
|
|
321
|
+
else:
|
|
322
|
+
for item in envelope.items:
|
|
323
|
+
self.record_lost_event(reason, item=item)
|
|
324
|
+
|
|
170
325
|
headers.update(
|
|
171
326
|
{
|
|
172
327
|
"User-Agent": str(self._auth.client),
|
|
173
328
|
"X-Sentry-Auth": str(self._auth.to_header()),
|
|
174
329
|
}
|
|
175
330
|
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
331
|
+
try:
|
|
332
|
+
response = self._request(
|
|
333
|
+
"POST",
|
|
334
|
+
endpoint_type,
|
|
335
|
+
body,
|
|
336
|
+
headers,
|
|
337
|
+
)
|
|
338
|
+
except Exception:
|
|
339
|
+
self.on_dropped_event("network")
|
|
340
|
+
record_loss("network_error")
|
|
341
|
+
raise
|
|
182
342
|
|
|
183
343
|
try:
|
|
184
344
|
self._update_rate_limits(response)
|
|
185
345
|
|
|
186
346
|
if response.status == 429:
|
|
187
347
|
# if we hit a 429. Something was rate limited but we already
|
|
188
|
-
# acted on this in `self._update_rate_limits`.
|
|
348
|
+
# acted on this in `self._update_rate_limits`. Note that we
|
|
349
|
+
# do not want to record event loss here as we will have recorded
|
|
350
|
+
# an outcome in relay already.
|
|
351
|
+
self.on_dropped_event("status_429")
|
|
189
352
|
pass
|
|
190
353
|
|
|
191
354
|
elif response.status >= 300 or response.status < 200:
|
|
192
355
|
logger.error(
|
|
193
356
|
"Unexpected status code: %s (body: %s)",
|
|
194
357
|
response.status,
|
|
195
|
-
response
|
|
358
|
+
getattr(response, "data", getattr(response, "content", None)),
|
|
196
359
|
)
|
|
360
|
+
self.on_dropped_event("status_{}".format(response.status))
|
|
361
|
+
record_loss("network_error")
|
|
197
362
|
finally:
|
|
198
363
|
response.close()
|
|
199
364
|
|
|
365
|
+
def on_dropped_event(self, _reason):
|
|
366
|
+
# type: (Self, str) -> None
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
def _fetch_pending_client_report(self, force=False, interval=60):
|
|
370
|
+
# type: (Self, bool, int) -> Optional[Item]
|
|
371
|
+
if not self.options["send_client_reports"]:
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
if not (force or self._last_client_report_sent < time.time() - interval):
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
discarded_events = self._discarded_events
|
|
378
|
+
self._discarded_events = defaultdict(int)
|
|
379
|
+
self._last_client_report_sent = time.time()
|
|
380
|
+
|
|
381
|
+
if not discarded_events:
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
return Item(
|
|
385
|
+
PayloadRef(
|
|
386
|
+
json={
|
|
387
|
+
"timestamp": time.time(),
|
|
388
|
+
"discarded_events": [
|
|
389
|
+
{"reason": reason, "category": category, "quantity": quantity}
|
|
390
|
+
for (
|
|
391
|
+
(category, reason),
|
|
392
|
+
quantity,
|
|
393
|
+
) in discarded_events.items()
|
|
394
|
+
],
|
|
395
|
+
}
|
|
396
|
+
),
|
|
397
|
+
type="client_report",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def _flush_client_reports(self, force=False):
|
|
401
|
+
# type: (Self, bool) -> None
|
|
402
|
+
client_report = self._fetch_pending_client_report(force=force, interval=60)
|
|
403
|
+
if client_report is not None:
|
|
404
|
+
self.capture_envelope(Envelope(items=[client_report]))
|
|
405
|
+
|
|
200
406
|
def _check_disabled(self, category):
|
|
201
407
|
# type: (str) -> bool
|
|
202
408
|
def _disabled(bucket):
|
|
203
409
|
# type: (Any) -> bool
|
|
204
410
|
ts = self._disabled_until.get(bucket)
|
|
205
|
-
return ts is not None and ts > datetime.
|
|
411
|
+
return ts is not None and ts > datetime.now(timezone.utc)
|
|
206
412
|
|
|
207
413
|
return _disabled(category) or _disabled(None)
|
|
208
414
|
|
|
209
|
-
def
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
415
|
+
def _is_rate_limited(self):
|
|
416
|
+
# type: (Self) -> bool
|
|
417
|
+
return any(
|
|
418
|
+
ts > datetime.now(timezone.utc) for ts in self._disabled_until.values()
|
|
419
|
+
)
|
|
213
420
|
|
|
214
|
-
|
|
215
|
-
|
|
421
|
+
def _is_worker_full(self):
|
|
422
|
+
# type: (Self) -> bool
|
|
423
|
+
return self._worker.full()
|
|
216
424
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
425
|
+
def is_healthy(self):
|
|
426
|
+
# type: (Self) -> bool
|
|
427
|
+
return not (self._is_worker_full() or self._is_rate_limited())
|
|
220
428
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"Sending event, type:%s level:%s event_id:%s project:%s host:%s"
|
|
224
|
-
% (
|
|
225
|
-
event.get("type") or "null",
|
|
226
|
-
event.get("level") or "null",
|
|
227
|
-
event.get("event_id") or "null",
|
|
228
|
-
self.parsed_dsn.project_id,
|
|
229
|
-
self.parsed_dsn.host,
|
|
230
|
-
)
|
|
231
|
-
)
|
|
232
|
-
self._send_request(
|
|
233
|
-
body.getvalue(),
|
|
234
|
-
headers={"Content-Type": "application/json", "Content-Encoding": "gzip"},
|
|
235
|
-
)
|
|
236
|
-
return None
|
|
237
|
-
|
|
238
|
-
def _send_envelope(
|
|
239
|
-
self, envelope # type: Envelope
|
|
240
|
-
):
|
|
241
|
-
# type: (...) -> None
|
|
429
|
+
def _send_envelope(self, envelope):
|
|
430
|
+
# type: (Self, Envelope) -> None
|
|
242
431
|
|
|
243
432
|
# remove all items from the envelope which are over quota
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
433
|
+
new_items = []
|
|
434
|
+
for item in envelope.items:
|
|
435
|
+
if self._check_disabled(item.data_category):
|
|
436
|
+
if item.data_category in ("transaction", "error", "default", "statsd"):
|
|
437
|
+
self.on_dropped_event("self_rate_limits")
|
|
438
|
+
self.record_lost_event("ratelimit_backoff", item=item)
|
|
439
|
+
else:
|
|
440
|
+
new_items.append(item)
|
|
441
|
+
|
|
442
|
+
# Since we're modifying the envelope here make a copy so that others
|
|
443
|
+
# that hold references do not see their envelope modified.
|
|
444
|
+
envelope = Envelope(headers=envelope.headers, items=new_items)
|
|
445
|
+
|
|
247
446
|
if not envelope.items:
|
|
248
447
|
return None
|
|
249
448
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
449
|
+
# since we're already in the business of sending out an envelope here
|
|
450
|
+
# check if we have one pending for the stats session envelopes so we
|
|
451
|
+
# can attach it to this enveloped scheduled for sending. This will
|
|
452
|
+
# currently typically attach the client report to the most recent
|
|
453
|
+
# session update.
|
|
454
|
+
client_report_item = self._fetch_pending_client_report(interval=30)
|
|
455
|
+
if client_report_item is not None:
|
|
456
|
+
envelope.items.append(client_report_item)
|
|
457
|
+
|
|
458
|
+
content_encoding, body = self._serialize_envelope(envelope)
|
|
253
459
|
|
|
254
460
|
assert self.parsed_dsn is not None
|
|
255
461
|
logger.debug(
|
|
@@ -258,26 +464,49 @@ class HttpTransport(Transport):
|
|
|
258
464
|
self.parsed_dsn.project_id,
|
|
259
465
|
self.parsed_dsn.host,
|
|
260
466
|
)
|
|
467
|
+
|
|
468
|
+
headers = {
|
|
469
|
+
"Content-Type": "application/x-sentry-envelope",
|
|
470
|
+
}
|
|
471
|
+
if content_encoding:
|
|
472
|
+
headers["Content-Encoding"] = content_encoding
|
|
473
|
+
|
|
261
474
|
self._send_request(
|
|
262
475
|
body.getvalue(),
|
|
263
|
-
headers=
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
},
|
|
267
|
-
endpoint_type="envelope",
|
|
476
|
+
headers=headers,
|
|
477
|
+
endpoint_type=EndpointType.ENVELOPE,
|
|
478
|
+
envelope=envelope,
|
|
268
479
|
)
|
|
269
480
|
return None
|
|
270
481
|
|
|
271
|
-
def
|
|
272
|
-
# type: (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
482
|
+
def _serialize_envelope(self, envelope):
|
|
483
|
+
# type: (Self, Envelope) -> tuple[Optional[str], io.BytesIO]
|
|
484
|
+
content_encoding = None
|
|
485
|
+
body = io.BytesIO()
|
|
486
|
+
if self._compression_level == 0 or self._compression_algo is None:
|
|
487
|
+
envelope.serialize_into(body)
|
|
488
|
+
else:
|
|
489
|
+
content_encoding = self._compression_algo
|
|
490
|
+
if self._compression_algo == "br" and brotli is not None:
|
|
491
|
+
body.write(
|
|
492
|
+
brotli.compress(
|
|
493
|
+
envelope.serialize(), quality=self._compression_level
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
else: # assume gzip as we sanitize the algo value in init
|
|
497
|
+
with gzip.GzipFile(
|
|
498
|
+
fileobj=body, mode="w", compresslevel=self._compression_level
|
|
499
|
+
) as f:
|
|
500
|
+
envelope.serialize_into(f)
|
|
501
|
+
|
|
502
|
+
return content_encoding, body
|
|
503
|
+
|
|
504
|
+
def _get_pool_options(self):
|
|
505
|
+
# type: (Self) -> Dict[str, Any]
|
|
506
|
+
raise NotImplementedError()
|
|
278
507
|
|
|
279
508
|
def _in_no_proxy(self, parsed_dsn):
|
|
280
|
-
# type: (Dsn) -> bool
|
|
509
|
+
# type: (Self, Dsn) -> bool
|
|
281
510
|
no_proxy = getproxies().get("no")
|
|
282
511
|
if not no_proxy:
|
|
283
512
|
return False
|
|
@@ -287,105 +516,380 @@ class HttpTransport(Transport):
|
|
|
287
516
|
return True
|
|
288
517
|
return False
|
|
289
518
|
|
|
290
|
-
def _make_pool(
|
|
519
|
+
def _make_pool(self):
|
|
520
|
+
# type: (Self) -> Union[PoolManager, ProxyManager, httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]
|
|
521
|
+
raise NotImplementedError()
|
|
522
|
+
|
|
523
|
+
def _request(
|
|
524
|
+
self,
|
|
525
|
+
method,
|
|
526
|
+
endpoint_type,
|
|
527
|
+
body,
|
|
528
|
+
headers,
|
|
529
|
+
):
|
|
530
|
+
# type: (Self, str, EndpointType, Any, Mapping[str, str]) -> Union[urllib3.BaseHTTPResponse, httpcore.Response]
|
|
531
|
+
raise NotImplementedError()
|
|
532
|
+
|
|
533
|
+
def capture_envelope(
|
|
534
|
+
self,
|
|
535
|
+
envelope, # type: Envelope
|
|
536
|
+
):
|
|
537
|
+
# type: (...) -> None
|
|
538
|
+
def send_envelope_wrapper():
|
|
539
|
+
# type: () -> None
|
|
540
|
+
with capture_internal_exceptions():
|
|
541
|
+
self._send_envelope(envelope)
|
|
542
|
+
self._flush_client_reports()
|
|
543
|
+
|
|
544
|
+
if not self._worker.submit(send_envelope_wrapper):
|
|
545
|
+
self.on_dropped_event("full_queue")
|
|
546
|
+
for item in envelope.items:
|
|
547
|
+
self.record_lost_event("queue_overflow", item=item)
|
|
548
|
+
|
|
549
|
+
def flush(
|
|
291
550
|
self,
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
https_proxy, # type: Optional[str]
|
|
295
|
-
ca_certs, # type: Optional[Any]
|
|
551
|
+
timeout,
|
|
552
|
+
callback=None,
|
|
296
553
|
):
|
|
297
|
-
# type: (
|
|
554
|
+
# type: (Self, float, Optional[Callable[[int, float], None]]) -> None
|
|
555
|
+
logger.debug("Flushing HTTP transport")
|
|
556
|
+
|
|
557
|
+
if timeout > 0:
|
|
558
|
+
self._worker.submit(lambda: self._flush_client_reports(force=True))
|
|
559
|
+
self._worker.flush(timeout, callback)
|
|
560
|
+
|
|
561
|
+
def kill(self):
|
|
562
|
+
# type: (Self) -> None
|
|
563
|
+
logger.debug("Killing HTTP transport")
|
|
564
|
+
self._worker.kill()
|
|
565
|
+
|
|
566
|
+
@staticmethod
|
|
567
|
+
def _warn_hub_cls():
|
|
568
|
+
# type: () -> None
|
|
569
|
+
"""Convenience method to warn users about the deprecation of the `hub_cls` attribute."""
|
|
570
|
+
warnings.warn(
|
|
571
|
+
"The `hub_cls` attribute is deprecated and will be removed in a future release.",
|
|
572
|
+
DeprecationWarning,
|
|
573
|
+
stacklevel=3,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
@property
|
|
577
|
+
def hub_cls(self):
|
|
578
|
+
# type: (Self) -> type[sentry_sdk.Hub]
|
|
579
|
+
"""DEPRECATED: This attribute is deprecated and will be removed in a future release."""
|
|
580
|
+
HttpTransport._warn_hub_cls()
|
|
581
|
+
return self._hub_cls
|
|
582
|
+
|
|
583
|
+
@hub_cls.setter
|
|
584
|
+
def hub_cls(self, value):
|
|
585
|
+
# type: (Self, type[sentry_sdk.Hub]) -> None
|
|
586
|
+
"""DEPRECATED: This attribute is deprecated and will be removed in a future release."""
|
|
587
|
+
HttpTransport._warn_hub_cls()
|
|
588
|
+
self._hub_cls = value
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class HttpTransport(BaseHttpTransport):
|
|
592
|
+
if TYPE_CHECKING:
|
|
593
|
+
_pool: Union[PoolManager, ProxyManager]
|
|
594
|
+
|
|
595
|
+
def _get_pool_options(self):
|
|
596
|
+
# type: (Self) -> Dict[str, Any]
|
|
597
|
+
|
|
598
|
+
num_pools = self.options.get("_experiments", {}).get("transport_num_pools")
|
|
599
|
+
options = {
|
|
600
|
+
"num_pools": 2 if num_pools is None else int(num_pools),
|
|
601
|
+
"cert_reqs": "CERT_REQUIRED",
|
|
602
|
+
"timeout": urllib3.Timeout(total=self.TIMEOUT),
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
socket_options = None # type: Optional[List[Tuple[int, int, int | bytes]]]
|
|
606
|
+
|
|
607
|
+
if self.options["socket_options"] is not None:
|
|
608
|
+
socket_options = self.options["socket_options"]
|
|
609
|
+
|
|
610
|
+
if self.options["keep_alive"]:
|
|
611
|
+
if socket_options is None:
|
|
612
|
+
socket_options = []
|
|
613
|
+
|
|
614
|
+
used_options = {(o[0], o[1]) for o in socket_options}
|
|
615
|
+
for default_option in KEEP_ALIVE_SOCKET_OPTIONS:
|
|
616
|
+
if (default_option[0], default_option[1]) not in used_options:
|
|
617
|
+
socket_options.append(default_option)
|
|
618
|
+
|
|
619
|
+
if socket_options is not None:
|
|
620
|
+
options["socket_options"] = socket_options
|
|
621
|
+
|
|
622
|
+
options["ca_certs"] = (
|
|
623
|
+
self.options["ca_certs"] # User-provided bundle from the SDK init
|
|
624
|
+
or os.environ.get("SSL_CERT_FILE")
|
|
625
|
+
or os.environ.get("REQUESTS_CA_BUNDLE")
|
|
626
|
+
or certifi.where()
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
options["cert_file"] = self.options["cert_file"] or os.environ.get(
|
|
630
|
+
"CLIENT_CERT_FILE"
|
|
631
|
+
)
|
|
632
|
+
options["key_file"] = self.options["key_file"] or os.environ.get(
|
|
633
|
+
"CLIENT_KEY_FILE"
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
return options
|
|
637
|
+
|
|
638
|
+
def _make_pool(self):
|
|
639
|
+
# type: (Self) -> Union[PoolManager, ProxyManager]
|
|
640
|
+
if self.parsed_dsn is None:
|
|
641
|
+
raise ValueError("Cannot create HTTP-based transport without valid DSN")
|
|
642
|
+
|
|
298
643
|
proxy = None
|
|
299
|
-
no_proxy = self._in_no_proxy(parsed_dsn)
|
|
644
|
+
no_proxy = self._in_no_proxy(self.parsed_dsn)
|
|
300
645
|
|
|
301
646
|
# try HTTPS first
|
|
302
|
-
|
|
647
|
+
https_proxy = self.options["https_proxy"]
|
|
648
|
+
if self.parsed_dsn.scheme == "https" and (https_proxy != ""):
|
|
303
649
|
proxy = https_proxy or (not no_proxy and getproxies().get("https"))
|
|
304
650
|
|
|
305
651
|
# maybe fallback to HTTP proxy
|
|
652
|
+
http_proxy = self.options["http_proxy"]
|
|
306
653
|
if not proxy and (http_proxy != ""):
|
|
307
654
|
proxy = http_proxy or (not no_proxy and getproxies().get("http"))
|
|
308
655
|
|
|
309
|
-
opts = self._get_pool_options(
|
|
656
|
+
opts = self._get_pool_options()
|
|
310
657
|
|
|
311
658
|
if proxy:
|
|
312
|
-
|
|
659
|
+
proxy_headers = self.options["proxy_headers"]
|
|
660
|
+
if proxy_headers:
|
|
661
|
+
opts["proxy_headers"] = proxy_headers
|
|
662
|
+
|
|
663
|
+
if proxy.startswith("socks"):
|
|
664
|
+
use_socks_proxy = True
|
|
665
|
+
try:
|
|
666
|
+
# Check if PySocks dependency is available
|
|
667
|
+
from urllib3.contrib.socks import SOCKSProxyManager
|
|
668
|
+
except ImportError:
|
|
669
|
+
use_socks_proxy = False
|
|
670
|
+
logger.warning(
|
|
671
|
+
"You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support. Please add `PySocks` (or `urllib3` with the `[socks]` extra) to your dependencies.",
|
|
672
|
+
proxy,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
if use_socks_proxy:
|
|
676
|
+
return SOCKSProxyManager(proxy, **opts)
|
|
677
|
+
else:
|
|
678
|
+
return urllib3.PoolManager(**opts)
|
|
679
|
+
else:
|
|
680
|
+
return urllib3.ProxyManager(proxy, **opts)
|
|
313
681
|
else:
|
|
314
682
|
return urllib3.PoolManager(**opts)
|
|
315
683
|
|
|
316
|
-
def
|
|
317
|
-
self,
|
|
684
|
+
def _request(
|
|
685
|
+
self,
|
|
686
|
+
method,
|
|
687
|
+
endpoint_type,
|
|
688
|
+
body,
|
|
689
|
+
headers,
|
|
318
690
|
):
|
|
319
|
-
# type: (
|
|
320
|
-
|
|
691
|
+
# type: (Self, str, EndpointType, Any, Mapping[str, str]) -> urllib3.BaseHTTPResponse
|
|
692
|
+
return self._pool.request(
|
|
693
|
+
method,
|
|
694
|
+
self._auth.get_api_url(endpoint_type),
|
|
695
|
+
body=body,
|
|
696
|
+
headers=headers,
|
|
697
|
+
)
|
|
321
698
|
|
|
322
|
-
def send_event_wrapper():
|
|
323
|
-
# type: () -> None
|
|
324
|
-
with hub:
|
|
325
|
-
with capture_internal_exceptions():
|
|
326
|
-
self._send_event(event)
|
|
327
699
|
|
|
328
|
-
|
|
700
|
+
try:
|
|
701
|
+
import httpcore
|
|
702
|
+
import h2 # noqa: F401
|
|
703
|
+
except ImportError:
|
|
704
|
+
# Sorry, no Http2Transport for you
|
|
705
|
+
class Http2Transport(HttpTransport):
|
|
706
|
+
def __init__(self, options):
|
|
707
|
+
# type: (Self, Dict[str, Any]) -> None
|
|
708
|
+
super().__init__(options)
|
|
709
|
+
logger.warning(
|
|
710
|
+
"You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport."
|
|
711
|
+
)
|
|
329
712
|
|
|
330
|
-
|
|
331
|
-
self, envelope # type: Envelope
|
|
332
|
-
):
|
|
333
|
-
# type: (...) -> None
|
|
334
|
-
hub = self.hub_cls.current
|
|
713
|
+
else:
|
|
335
714
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
with hub:
|
|
339
|
-
with capture_internal_exceptions():
|
|
340
|
-
self._send_envelope(envelope)
|
|
715
|
+
class Http2Transport(BaseHttpTransport): # type: ignore
|
|
716
|
+
"""The HTTP2 transport based on httpcore."""
|
|
341
717
|
|
|
342
|
-
|
|
718
|
+
TIMEOUT = 15
|
|
343
719
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
):
|
|
349
|
-
# type: (...) -> None
|
|
350
|
-
logger.debug("Flushing HTTP transport")
|
|
351
|
-
if timeout > 0:
|
|
352
|
-
self._worker.flush(timeout, callback)
|
|
720
|
+
if TYPE_CHECKING:
|
|
721
|
+
_pool: Union[
|
|
722
|
+
httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool
|
|
723
|
+
]
|
|
353
724
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
725
|
+
def _get_header_value(self, response, header):
|
|
726
|
+
# type: (Self, httpcore.Response, str) -> Optional[str]
|
|
727
|
+
return next(
|
|
728
|
+
(
|
|
729
|
+
val.decode("ascii")
|
|
730
|
+
for key, val in response.headers
|
|
731
|
+
if key.decode("ascii").lower() == header
|
|
732
|
+
),
|
|
733
|
+
None,
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
def _request(
|
|
737
|
+
self,
|
|
738
|
+
method,
|
|
739
|
+
endpoint_type,
|
|
740
|
+
body,
|
|
741
|
+
headers,
|
|
742
|
+
):
|
|
743
|
+
# type: (Self, str, EndpointType, Any, Mapping[str, str]) -> httpcore.Response
|
|
744
|
+
response = self._pool.request(
|
|
745
|
+
method,
|
|
746
|
+
self._auth.get_api_url(endpoint_type),
|
|
747
|
+
content=body,
|
|
748
|
+
headers=headers, # type: ignore
|
|
749
|
+
extensions={
|
|
750
|
+
"timeout": {
|
|
751
|
+
"pool": self.TIMEOUT,
|
|
752
|
+
"connect": self.TIMEOUT,
|
|
753
|
+
"write": self.TIMEOUT,
|
|
754
|
+
"read": self.TIMEOUT,
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
)
|
|
758
|
+
return response
|
|
759
|
+
|
|
760
|
+
def _get_pool_options(self):
|
|
761
|
+
# type: (Self) -> Dict[str, Any]
|
|
762
|
+
options = {
|
|
763
|
+
"http2": self.parsed_dsn is not None
|
|
764
|
+
and self.parsed_dsn.scheme == "https",
|
|
765
|
+
"retries": 3,
|
|
766
|
+
} # type: Dict[str, Any]
|
|
767
|
+
|
|
768
|
+
socket_options = (
|
|
769
|
+
self.options["socket_options"]
|
|
770
|
+
if self.options["socket_options"] is not None
|
|
771
|
+
else []
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
used_options = {(o[0], o[1]) for o in socket_options}
|
|
775
|
+
for default_option in KEEP_ALIVE_SOCKET_OPTIONS:
|
|
776
|
+
if (default_option[0], default_option[1]) not in used_options:
|
|
777
|
+
socket_options.append(default_option)
|
|
778
|
+
|
|
779
|
+
options["socket_options"] = socket_options
|
|
780
|
+
|
|
781
|
+
ssl_context = ssl.create_default_context()
|
|
782
|
+
ssl_context.load_verify_locations(
|
|
783
|
+
self.options["ca_certs"] # User-provided bundle from the SDK init
|
|
784
|
+
or os.environ.get("SSL_CERT_FILE")
|
|
785
|
+
or os.environ.get("REQUESTS_CA_BUNDLE")
|
|
786
|
+
or certifi.where()
|
|
787
|
+
)
|
|
788
|
+
cert_file = self.options["cert_file"] or os.environ.get("CLIENT_CERT_FILE")
|
|
789
|
+
key_file = self.options["key_file"] or os.environ.get("CLIENT_KEY_FILE")
|
|
790
|
+
if cert_file is not None:
|
|
791
|
+
ssl_context.load_cert_chain(cert_file, key_file)
|
|
792
|
+
|
|
793
|
+
options["ssl_context"] = ssl_context
|
|
794
|
+
|
|
795
|
+
return options
|
|
796
|
+
|
|
797
|
+
def _make_pool(self):
|
|
798
|
+
# type: (Self) -> Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]
|
|
799
|
+
if self.parsed_dsn is None:
|
|
800
|
+
raise ValueError("Cannot create HTTP-based transport without valid DSN")
|
|
801
|
+
proxy = None
|
|
802
|
+
no_proxy = self._in_no_proxy(self.parsed_dsn)
|
|
803
|
+
|
|
804
|
+
# try HTTPS first
|
|
805
|
+
https_proxy = self.options["https_proxy"]
|
|
806
|
+
if self.parsed_dsn.scheme == "https" and (https_proxy != ""):
|
|
807
|
+
proxy = https_proxy or (not no_proxy and getproxies().get("https"))
|
|
808
|
+
|
|
809
|
+
# maybe fallback to HTTP proxy
|
|
810
|
+
http_proxy = self.options["http_proxy"]
|
|
811
|
+
if not proxy and (http_proxy != ""):
|
|
812
|
+
proxy = http_proxy or (not no_proxy and getproxies().get("http"))
|
|
813
|
+
|
|
814
|
+
opts = self._get_pool_options()
|
|
815
|
+
|
|
816
|
+
if proxy:
|
|
817
|
+
proxy_headers = self.options["proxy_headers"]
|
|
818
|
+
if proxy_headers:
|
|
819
|
+
opts["proxy_headers"] = proxy_headers
|
|
820
|
+
|
|
821
|
+
if proxy.startswith("socks"):
|
|
822
|
+
try:
|
|
823
|
+
if "socket_options" in opts:
|
|
824
|
+
socket_options = opts.pop("socket_options")
|
|
825
|
+
if socket_options:
|
|
826
|
+
logger.warning(
|
|
827
|
+
"You have defined socket_options but using a SOCKS proxy which doesn't support these. We'll ignore socket_options."
|
|
828
|
+
)
|
|
829
|
+
return httpcore.SOCKSProxy(proxy_url=proxy, **opts)
|
|
830
|
+
except RuntimeError:
|
|
831
|
+
logger.warning(
|
|
832
|
+
"You have configured a SOCKS proxy (%s) but support for SOCKS proxies is not installed. Disabling proxy support.",
|
|
833
|
+
proxy,
|
|
834
|
+
)
|
|
835
|
+
else:
|
|
836
|
+
return httpcore.HTTPProxy(proxy_url=proxy, **opts)
|
|
837
|
+
|
|
838
|
+
return httpcore.ConnectionPool(**opts)
|
|
358
839
|
|
|
359
840
|
|
|
360
841
|
class _FunctionTransport(Transport):
|
|
842
|
+
"""
|
|
843
|
+
DEPRECATED: Users wishing to provide a custom transport should subclass
|
|
844
|
+
the Transport class, rather than providing a function.
|
|
845
|
+
"""
|
|
846
|
+
|
|
361
847
|
def __init__(
|
|
362
|
-
self,
|
|
848
|
+
self,
|
|
849
|
+
func, # type: Callable[[Event], None]
|
|
363
850
|
):
|
|
364
851
|
# type: (...) -> None
|
|
365
852
|
Transport.__init__(self)
|
|
366
853
|
self._func = func
|
|
367
854
|
|
|
368
855
|
def capture_event(
|
|
369
|
-
self,
|
|
856
|
+
self,
|
|
857
|
+
event, # type: Event
|
|
370
858
|
):
|
|
371
859
|
# type: (...) -> None
|
|
372
860
|
self._func(event)
|
|
373
861
|
return None
|
|
374
862
|
|
|
863
|
+
def capture_envelope(self, envelope: Envelope) -> None:
|
|
864
|
+
# Since function transports expect to be called with an event, we need
|
|
865
|
+
# to iterate over the envelope and call the function for each event, via
|
|
866
|
+
# the deprecated capture_event method.
|
|
867
|
+
event = envelope.get_event()
|
|
868
|
+
if event is not None:
|
|
869
|
+
self.capture_event(event)
|
|
870
|
+
|
|
375
871
|
|
|
376
872
|
def make_transport(options):
|
|
377
873
|
# type: (Dict[str, Any]) -> Optional[Transport]
|
|
378
874
|
ref_transport = options["transport"]
|
|
379
875
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
876
|
+
use_http2_transport = options.get("_experiments", {}).get("transport_http2", False)
|
|
877
|
+
|
|
878
|
+
# By default, we use the http transport class
|
|
879
|
+
transport_cls = Http2Transport if use_http2_transport else HttpTransport # type: Type[Transport]
|
|
880
|
+
|
|
881
|
+
if isinstance(ref_transport, Transport):
|
|
384
882
|
return ref_transport
|
|
385
883
|
elif isinstance(ref_transport, type) and issubclass(ref_transport, Transport):
|
|
386
884
|
transport_cls = ref_transport
|
|
387
885
|
elif callable(ref_transport):
|
|
388
|
-
|
|
886
|
+
warnings.warn(
|
|
887
|
+
"Function transports are deprecated and will be removed in a future release."
|
|
888
|
+
"Please provide a Transport instance or subclass, instead.",
|
|
889
|
+
DeprecationWarning,
|
|
890
|
+
stacklevel=2,
|
|
891
|
+
)
|
|
892
|
+
return _FunctionTransport(ref_transport)
|
|
389
893
|
|
|
390
894
|
# if a transport class is given only instantiate it if the dsn is not
|
|
391
895
|
# empty or None
|