sentry-sdk 0.7.5__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.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -30
  2. sentry_sdk/_compat.py +74 -61
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +289 -0
  8. sentry_sdk/_types.py +338 -0
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +134 -0
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +141 -0
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +112 -68
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -1,165 +1,245 @@
1
1
  import sys
2
-
3
- from sentry_sdk.hub import Hub, _should_send_default_pii
4
- from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
5
- from sentry_sdk._compat import PY2, reraise
6
- from sentry_sdk.integrations._wsgi_common import _filter_headers
7
-
8
- if False:
2
+ from functools import partial
3
+
4
+ import sentry_sdk
5
+ from sentry_sdk._werkzeug import get_host, _get_headers
6
+ from sentry_sdk.api import continue_trace
7
+ from sentry_sdk.consts import OP
8
+ from sentry_sdk.scope import should_send_default_pii
9
+ from sentry_sdk.integrations._wsgi_common import (
10
+ DEFAULT_HTTP_METHODS_TO_CAPTURE,
11
+ _filter_headers,
12
+ nullcontext,
13
+ )
14
+ from sentry_sdk.sessions import track_session
15
+ from sentry_sdk.scope import use_isolation_scope
16
+ from sentry_sdk.tracing import Transaction, TransactionSource
17
+ from sentry_sdk.utils import (
18
+ ContextVar,
19
+ capture_internal_exceptions,
20
+ event_from_exception,
21
+ reraise,
22
+ )
23
+
24
+ from typing import TYPE_CHECKING
25
+
26
+ if TYPE_CHECKING:
9
27
  from typing import Callable
10
28
  from typing import Dict
11
- from typing import List
12
29
  from typing import Iterator
13
30
  from typing import Any
14
31
  from typing import Tuple
15
32
  from typing import Optional
33
+ from typing import TypeVar
34
+ from typing import Protocol
16
35
 
17
36
  from sentry_sdk.utils import ExcInfo
37
+ from sentry_sdk._types import Event, EventProcessor
18
38
 
39
+ WsgiResponseIter = TypeVar("WsgiResponseIter")
40
+ WsgiResponseHeaders = TypeVar("WsgiResponseHeaders")
41
+ WsgiExcInfo = TypeVar("WsgiExcInfo")
19
42
 
20
- if PY2:
21
-
22
- def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
23
- # type: (str, str, str) -> str
24
- return s.decode(charset, errors)
25
-
43
+ class StartResponse(Protocol):
44
+ def __call__(self, status, response_headers, exc_info=None): # type: ignore
45
+ # type: (str, WsgiResponseHeaders, Optional[WsgiExcInfo]) -> WsgiResponseIter
46
+ pass
26
47
 
27
- else:
28
48
 
29
- def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
30
- # type: (str, str, str) -> str
31
- return s.encode("latin1").decode(charset, errors)
49
+ _wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
32
50
 
33
51
 
34
- def get_host(environ):
35
- # type: (Dict[str, str]) -> str
36
- """Return the host for the given WSGI environment. Yanked from Werkzeug."""
37
- if environ.get("HTTP_HOST"):
38
- rv = environ["HTTP_HOST"]
39
- if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
40
- rv = rv[:-3]
41
- elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
42
- rv = rv[:-4]
43
- elif environ.get("SERVER_NAME"):
44
- rv = environ["SERVER_NAME"]
45
- if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in (
46
- ("https", "443"),
47
- ("http", "80"),
48
- ):
49
- rv += ":" + environ["SERVER_PORT"]
50
- else:
51
- # In spite of the WSGI spec, SERVER_NAME might not be present.
52
- rv = "unknown"
53
-
54
- return rv
52
+ def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
53
+ # type: (str, str, str) -> str
54
+ return s.encode("latin1").decode(charset, errors)
55
55
 
56
56
 
57
- def get_request_url(environ):
58
- # type: (Dict[str, str]) -> str
57
+ def get_request_url(environ, use_x_forwarded_for=False):
58
+ # type: (Dict[str, str], bool) -> str
59
59
  """Return the absolute URL without query string for the given WSGI
60
60
  environment."""
61
+ script_name = environ.get("SCRIPT_NAME", "").rstrip("/")
62
+ path_info = environ.get("PATH_INFO", "").lstrip("/")
63
+ path = f"{script_name}/{path_info}"
64
+
61
65
  return "%s://%s/%s" % (
62
66
  environ.get("wsgi.url_scheme"),
63
- get_host(environ),
64
- wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"),
67
+ get_host(environ, use_x_forwarded_for),
68
+ wsgi_decoding_dance(path).lstrip("/"),
65
69
  )
66
70
 
67
71
 
68
- class SentryWsgiMiddleware(object):
69
- __slots__ = ("app",)
72
+ class SentryWsgiMiddleware:
73
+ __slots__ = (
74
+ "app",
75
+ "use_x_forwarded_for",
76
+ "span_origin",
77
+ "http_methods_to_capture",
78
+ )
70
79
 
71
- def __init__(self, app):
72
- # type: (Callable) -> None
80
+ def __init__(
81
+ self,
82
+ app, # type: Callable[[Dict[str, str], Callable[..., Any]], Any]
83
+ use_x_forwarded_for=False, # type: bool
84
+ span_origin="manual", # type: str
85
+ http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
86
+ ):
87
+ # type: (...) -> None
73
88
  self.app = app
89
+ self.use_x_forwarded_for = use_x_forwarded_for
90
+ self.span_origin = span_origin
91
+ self.http_methods_to_capture = http_methods_to_capture
74
92
 
75
93
  def __call__(self, environ, start_response):
76
- # type: (Dict[str, str], Callable) -> _ScopedResponse
77
- hub = Hub(Hub.current)
78
-
79
- with hub:
80
- with capture_internal_exceptions():
81
- with hub.configure_scope() as scope:
82
- scope._name = "wsgi"
83
- scope.add_event_processor(_make_wsgi_event_processor(environ))
84
-
85
- try:
86
- rv = self.app(environ, start_response)
87
- except Exception:
88
- reraise(*_capture_exception(hub))
89
-
90
- return _ScopedResponse(hub, rv)
94
+ # type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse
95
+ if _wsgi_middleware_applied.get(False):
96
+ return self.app(environ, start_response)
97
+
98
+ _wsgi_middleware_applied.set(True)
99
+ try:
100
+ with sentry_sdk.isolation_scope() as scope:
101
+ with track_session(scope, session_mode="request"):
102
+ with capture_internal_exceptions():
103
+ scope.clear_breadcrumbs()
104
+ scope._name = "wsgi"
105
+ scope.add_event_processor(
106
+ _make_wsgi_event_processor(
107
+ environ, self.use_x_forwarded_for
108
+ )
109
+ )
110
+
111
+ method = environ.get("REQUEST_METHOD", "").upper()
112
+ transaction = None
113
+ if method in self.http_methods_to_capture:
114
+ transaction = continue_trace(
115
+ environ,
116
+ op=OP.HTTP_SERVER,
117
+ name="generic WSGI request",
118
+ source=TransactionSource.ROUTE,
119
+ origin=self.span_origin,
120
+ )
121
+
122
+ transaction_context = (
123
+ sentry_sdk.start_transaction(
124
+ transaction,
125
+ custom_sampling_context={"wsgi_environ": environ},
126
+ )
127
+ if transaction is not None
128
+ else nullcontext()
129
+ )
130
+ with transaction_context:
131
+ try:
132
+ response = self.app(
133
+ environ,
134
+ partial(
135
+ _sentry_start_response, start_response, transaction
136
+ ),
137
+ )
138
+ except BaseException:
139
+ reraise(*_capture_exception())
140
+ finally:
141
+ _wsgi_middleware_applied.set(False)
142
+
143
+ return _ScopedResponse(scope, response)
144
+
145
+
146
+ def _sentry_start_response( # type: ignore
147
+ old_start_response, # type: StartResponse
148
+ transaction, # type: Optional[Transaction]
149
+ status, # type: str
150
+ response_headers, # type: WsgiResponseHeaders
151
+ exc_info=None, # type: Optional[WsgiExcInfo]
152
+ ):
153
+ # type: (...) -> WsgiResponseIter
154
+ with capture_internal_exceptions():
155
+ status_int = int(status.split(" ", 1)[0])
156
+ if transaction is not None:
157
+ transaction.set_http_status(status_int)
158
+
159
+ if exc_info is None:
160
+ # The Django Rest Framework WSGI test client, and likely other
161
+ # (incorrect) implementations, cannot deal with the exc_info argument
162
+ # if one is present. Avoid providing a third argument if not necessary.
163
+ return old_start_response(status, response_headers)
164
+ else:
165
+ return old_start_response(status, response_headers, exc_info)
91
166
 
92
167
 
93
168
  def _get_environ(environ):
94
169
  # type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
95
170
  """
96
- Returns our whitelisted environment variables.
171
+ Returns our explicitly included environment variables we want to
172
+ capture (server name, port and remote addr if pii is enabled).
97
173
  """
98
- keys = ("SERVER_NAME", "SERVER_PORT")
99
- if _should_send_default_pii():
100
- keys += ("REMOTE_ADDR",) # type: ignore
174
+ keys = ["SERVER_NAME", "SERVER_PORT"]
175
+ if should_send_default_pii():
176
+ # make debugging of proxy setup easier. Proxy headers are
177
+ # in headers.
178
+ keys += ["REMOTE_ADDR"]
101
179
 
102
180
  for key in keys:
103
181
  if key in environ:
104
182
  yield key, environ[key]
105
183
 
106
184
 
107
- # `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
108
- #
109
- # We need this function because Django does not give us a "pure" http header
110
- # dict. So we might as well use it for all WSGI integrations.
111
- def _get_headers(environ):
112
- # type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
113
- """
114
- Returns only proper HTTP headers.
115
-
116
- """
117
- for key, value in environ.items():
118
- key = str(key)
119
- if key.startswith("HTTP_") and key not in (
120
- "HTTP_CONTENT_TYPE",
121
- "HTTP_CONTENT_LENGTH",
122
- ):
123
- yield key[5:].replace("_", "-").title(), value
124
- elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
125
- yield key.replace("_", "-").title(), value
126
-
127
-
128
185
  def get_client_ip(environ):
129
186
  # type: (Dict[str, str]) -> Optional[Any]
130
187
  """
131
- Naively yank the first IP address in an X-Forwarded-For header
132
- and assume this is correct.
133
-
134
- Note: Don't use this in security sensitive situations since this
135
- value may be forged from a client.
188
+ Infer the user IP address from various headers. This cannot be used in
189
+ security sensitive situations since the value may be forged from a client,
190
+ but it's good enough for the event payload.
136
191
  """
137
192
  try:
138
193
  return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip()
139
194
  except (KeyError, IndexError):
140
- return environ.get("REMOTE_ADDR")
195
+ pass
141
196
 
197
+ try:
198
+ return environ["HTTP_X_REAL_IP"]
199
+ except KeyError:
200
+ pass
201
+
202
+ return environ.get("REMOTE_ADDR")
142
203
 
143
- def _capture_exception(hub):
144
- # type: (Hub) -> ExcInfo
145
- # Check client here as it might have been unset while streaming response
146
- if hub.client is not None:
147
- exc_info = sys.exc_info()
204
+
205
+ def _capture_exception():
206
+ # type: () -> ExcInfo
207
+ """
208
+ Captures the current exception and sends it to Sentry.
209
+ Returns the ExcInfo tuple to it can be reraised afterwards.
210
+ """
211
+ exc_info = sys.exc_info()
212
+ e = exc_info[1]
213
+
214
+ # SystemExit(0) is the only uncaught exception that is expected behavior
215
+ should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None)
216
+ if not should_skip_capture:
148
217
  event, hint = event_from_exception(
149
218
  exc_info,
150
- client_options=hub.client.options,
219
+ client_options=sentry_sdk.get_client().options,
151
220
  mechanism={"type": "wsgi", "handled": False},
152
221
  )
153
- hub.capture_event(event, hint=hint)
222
+ sentry_sdk.capture_event(event, hint=hint)
223
+
154
224
  return exc_info
155
225
 
156
226
 
157
- class _ScopedResponse(object):
158
- __slots__ = ("_response", "_hub")
227
+ class _ScopedResponse:
228
+ """
229
+ Users a separate scope for each response chunk.
230
+
231
+ This will make WSGI apps more tolerant against:
232
+ - WSGI servers streaming responses from a different thread/from
233
+ different threads than the one that called start_response
234
+ - close() not being called
235
+ - WSGI servers streaming responses interleaved from the same thread
236
+ """
237
+
238
+ __slots__ = ("_response", "_scope")
159
239
 
160
- def __init__(self, hub, response):
161
- # type: (Hub, List[bytes]) -> None
162
- self._hub = hub
240
+ def __init__(self, scope, response):
241
+ # type: (sentry_sdk.scope.Scope, Iterator[bytes]) -> None
242
+ self._scope = scope
163
243
  self._response = response
164
244
 
165
245
  def __iter__(self):
@@ -167,32 +247,33 @@ class _ScopedResponse(object):
167
247
  iterator = iter(self._response)
168
248
 
169
249
  while True:
170
- with self._hub:
250
+ with use_isolation_scope(self._scope):
171
251
  try:
172
252
  chunk = next(iterator)
173
253
  except StopIteration:
174
254
  break
175
- except Exception:
176
- reraise(*_capture_exception(self._hub))
255
+ except BaseException:
256
+ reraise(*_capture_exception())
177
257
 
178
258
  yield chunk
179
259
 
180
260
  def close(self):
181
- with self._hub:
261
+ # type: () -> None
262
+ with use_isolation_scope(self._scope):
182
263
  try:
183
- self._response.close()
264
+ self._response.close() # type: ignore
184
265
  except AttributeError:
185
266
  pass
186
- except Exception:
187
- reraise(*_capture_exception(self._hub))
267
+ except BaseException:
268
+ reraise(*_capture_exception())
188
269
 
189
270
 
190
- def _make_wsgi_event_processor(environ):
191
- # type: (Dict[str, str]) -> Callable
271
+ def _make_wsgi_event_processor(environ, use_x_forwarded_for):
272
+ # type: (Dict[str, str], bool) -> EventProcessor
192
273
  # It's a bit unfortunate that we have to extract and parse the request data
193
274
  # from the environ so eagerly, but there are a few good reasons for this.
194
275
  #
195
- # We might be in a situation where the scope/hub never gets torn down
276
+ # We might be in a situation where the scope never gets torn down
196
277
  # properly. In that case we will have an unnecessary strong reference to
197
278
  # all objects in the environ (some of which may take a lot of memory) when
198
279
  # we're really just interested in a few of them.
@@ -202,21 +283,22 @@ def _make_wsgi_event_processor(environ):
202
283
  # https://github.com/unbit/uwsgi/issues/1950
203
284
 
204
285
  client_ip = get_client_ip(environ)
205
- request_url = get_request_url(environ)
286
+ request_url = get_request_url(environ, use_x_forwarded_for)
206
287
  query_string = environ.get("QUERY_STRING")
207
288
  method = environ.get("REQUEST_METHOD")
208
289
  env = dict(_get_environ(environ))
209
290
  headers = _filter_headers(dict(_get_headers(environ)))
210
291
 
211
292
  def event_processor(event, hint):
212
- # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
293
+ # type: (Event, Dict[str, Any]) -> Event
213
294
  with capture_internal_exceptions():
214
295
  # if the code below fails halfway through we at least have some data
215
296
  request_info = event.setdefault("request", {})
216
297
 
217
- if _should_send_default_pii():
298
+ if should_send_default_pii():
218
299
  user_info = event.setdefault("user", {})
219
- user_info["ip_address"] = client_ip
300
+ if client_ip:
301
+ user_info.setdefault("ip_address", client_ip)
220
302
 
221
303
  request_info["url"] = request_url
222
304
  request_info["query_string"] = query_string
sentry_sdk/logger.py ADDED
@@ -0,0 +1,96 @@
1
+ # NOTE: this is the logger sentry exposes to users, not some generic logger.
2
+ import functools
3
+ import time
4
+ from typing import Any
5
+
6
+ from sentry_sdk import get_client
7
+ from sentry_sdk.utils import safe_repr, capture_internal_exceptions
8
+
9
+ OTEL_RANGES = [
10
+ # ((severity level range), severity text)
11
+ # https://opentelemetry.io/docs/specs/otel/logs/data-model
12
+ ((1, 4), "trace"),
13
+ ((5, 8), "debug"),
14
+ ((9, 12), "info"),
15
+ ((13, 16), "warn"),
16
+ ((17, 20), "error"),
17
+ ((21, 24), "fatal"),
18
+ ]
19
+
20
+
21
+ class _dict_default_key(dict): # type: ignore[type-arg]
22
+ """dict that returns the key if missing."""
23
+
24
+ def __missing__(self, key):
25
+ # type: (str) -> str
26
+ return "{" + key + "}"
27
+
28
+
29
+ def _capture_log(severity_text, severity_number, template, **kwargs):
30
+ # type: (str, int, str, **Any) -> None
31
+ client = get_client()
32
+
33
+ body = template
34
+ attrs = {} # type: dict[str, str | bool | float | int]
35
+ if "attributes" in kwargs:
36
+ attrs.update(kwargs.pop("attributes"))
37
+ for k, v in kwargs.items():
38
+ attrs[f"sentry.message.parameter.{k}"] = v
39
+ if kwargs:
40
+ # only attach template if there are parameters
41
+ attrs["sentry.message.template"] = template
42
+
43
+ with capture_internal_exceptions():
44
+ body = template.format_map(_dict_default_key(kwargs))
45
+
46
+ attrs = {
47
+ k: (
48
+ v
49
+ if (
50
+ isinstance(v, str)
51
+ or isinstance(v, int)
52
+ or isinstance(v, bool)
53
+ or isinstance(v, float)
54
+ )
55
+ else safe_repr(v)
56
+ )
57
+ for (k, v) in attrs.items()
58
+ }
59
+
60
+ # noinspection PyProtectedMember
61
+ client._capture_log(
62
+ {
63
+ "severity_text": severity_text,
64
+ "severity_number": severity_number,
65
+ "attributes": attrs,
66
+ "body": body,
67
+ "time_unix_nano": time.time_ns(),
68
+ "trace_id": None,
69
+ },
70
+ )
71
+
72
+
73
+ trace = functools.partial(_capture_log, "trace", 1)
74
+ debug = functools.partial(_capture_log, "debug", 5)
75
+ info = functools.partial(_capture_log, "info", 9)
76
+ warning = functools.partial(_capture_log, "warn", 13)
77
+ error = functools.partial(_capture_log, "error", 17)
78
+ fatal = functools.partial(_capture_log, "fatal", 21)
79
+
80
+
81
+ def _otel_severity_text(otel_severity_number):
82
+ # type: (int) -> str
83
+ for (lower, upper), severity in OTEL_RANGES:
84
+ if lower <= otel_severity_number <= upper:
85
+ return severity
86
+
87
+ return "default"
88
+
89
+
90
+ def _log_level_to_otel(level, mapping):
91
+ # type: (int, dict[Any, int]) -> tuple[int, str]
92
+ for py_level, otel_severity_number in sorted(mapping.items(), reverse=True):
93
+ if level >= py_level:
94
+ return otel_severity_number, _otel_severity_text(otel_severity_number)
95
+
96
+ return 0, "default"
sentry_sdk/metrics.py ADDED
@@ -0,0 +1,81 @@
1
+ """
2
+ NOTE: This file contains experimental code that may be changed or removed at any
3
+ time without prior notice.
4
+ """
5
+
6
+ import time
7
+ from typing import Any, Optional, TYPE_CHECKING, Union
8
+
9
+ import sentry_sdk
10
+ from sentry_sdk.utils import safe_repr
11
+
12
+ if TYPE_CHECKING:
13
+ from sentry_sdk._types import Metric, MetricType
14
+
15
+
16
+ def _capture_metric(
17
+ name, # type: str
18
+ metric_type, # type: MetricType
19
+ value, # type: float
20
+ unit=None, # type: Optional[str]
21
+ attributes=None, # type: Optional[dict[str, Any]]
22
+ ):
23
+ # type: (...) -> None
24
+ client = sentry_sdk.get_client()
25
+
26
+ attrs = {} # type: dict[str, Union[str, bool, float, int]]
27
+ if attributes:
28
+ for k, v in attributes.items():
29
+ attrs[k] = (
30
+ v
31
+ if (
32
+ isinstance(v, str)
33
+ or isinstance(v, int)
34
+ or isinstance(v, bool)
35
+ or isinstance(v, float)
36
+ )
37
+ else safe_repr(v)
38
+ )
39
+
40
+ metric = {
41
+ "timestamp": time.time(),
42
+ "trace_id": None,
43
+ "span_id": None,
44
+ "name": name,
45
+ "type": metric_type,
46
+ "value": float(value),
47
+ "unit": unit,
48
+ "attributes": attrs,
49
+ } # type: Metric
50
+
51
+ client._capture_metric(metric)
52
+
53
+
54
+ def count(
55
+ name, # type: str
56
+ value, # type: float
57
+ unit=None, # type: Optional[str]
58
+ attributes=None, # type: Optional[dict[str, Any]]
59
+ ):
60
+ # type: (...) -> None
61
+ _capture_metric(name, "counter", value, unit, attributes)
62
+
63
+
64
+ def gauge(
65
+ name, # type: str
66
+ value, # type: float
67
+ unit=None, # type: Optional[str]
68
+ attributes=None, # type: Optional[dict[str, Any]]
69
+ ):
70
+ # type: (...) -> None
71
+ _capture_metric(name, "gauge", value, unit, attributes)
72
+
73
+
74
+ def distribution(
75
+ name, # type: str
76
+ value, # type: float
77
+ unit=None, # type: Optional[str]
78
+ attributes=None, # type: Optional[dict[str, Any]]
79
+ ):
80
+ # type: (...) -> None
81
+ _capture_metric(name, "distribution", value, unit, attributes)