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,66 +1,92 @@
1
- # -*- coding: utf-8 -*-
2
- from __future__ import absolute_import
3
-
1
+ import inspect
4
2
  import sys
3
+ import threading
5
4
  import weakref
6
-
7
- from django import VERSION as DJANGO_VERSION # type: ignore
8
- from django.db.models.query import QuerySet # type: ignore
9
- from django.core import signals # type: ignore
10
-
11
- if False:
12
- from typing import Any
13
- from typing import Dict
14
- from typing import Tuple
15
- from typing import Union
16
- from sentry_sdk.integrations.wsgi import _ScopedResponse
17
- from typing import Callable
18
- from django.core.handlers.wsgi import WSGIRequest # type: ignore
19
- from django.http.response import HttpResponse # type: ignore
20
- from django.http.request import QueryDict # type: ignore
21
- from django.utils.datastructures import MultiValueDict # type: ignore
22
- from typing import List
5
+ from importlib import import_module
6
+
7
+ import sentry_sdk
8
+ from sentry_sdk.consts import OP, SPANDATA
9
+ from sentry_sdk.scope import add_global_event_processor, should_send_default_pii
10
+ from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type
11
+ from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
12
+ from sentry_sdk.tracing_utils import add_query_source, record_sql_queries
13
+ from sentry_sdk.utils import (
14
+ AnnotatedValue,
15
+ HAS_REAL_CONTEXTVARS,
16
+ CONTEXTVARS_ERROR_MESSAGE,
17
+ SENSITIVE_DATA_SUBSTITUTE,
18
+ logger,
19
+ capture_internal_exceptions,
20
+ ensure_integration_enabled,
21
+ event_from_exception,
22
+ transaction_from_function,
23
+ walk_exception_chain,
24
+ )
25
+ from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable
26
+ from sentry_sdk.integrations.logging import ignore_logger
27
+ from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
28
+ from sentry_sdk.integrations._wsgi_common import (
29
+ DEFAULT_HTTP_METHODS_TO_CAPTURE,
30
+ RequestExtractor,
31
+ )
23
32
 
24
33
  try:
25
- import psycopg2.sql # type: ignore
34
+ from django import VERSION as DJANGO_VERSION
35
+ from django.conf import settings as django_settings
36
+ from django.core import signals
37
+ from django.conf import settings
38
+
39
+ try:
40
+ from django.urls import resolve
41
+ except ImportError:
42
+ from django.core.urlresolvers import resolve
26
43
 
27
- def sql_to_string(sql):
28
- # type: (Any) -> str
29
- if isinstance(sql, psycopg2.sql.SQL):
30
- return sql.string
31
- return sql
44
+ try:
45
+ from django.urls import Resolver404
46
+ except ImportError:
47
+ from django.core.urlresolvers import Resolver404
32
48
 
49
+ # Only available in Django 3.0+
50
+ try:
51
+ from django.core.handlers.asgi import ASGIRequest
52
+ except Exception:
53
+ ASGIRequest = None
33
54
 
34
55
  except ImportError:
56
+ raise DidNotEnable("Django not installed")
35
57
 
36
- def sql_to_string(sql):
37
- # type: (Any) -> str
38
- return sql
58
+ from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
59
+ from sentry_sdk.integrations.django.templates import (
60
+ get_template_frame_from_exception,
61
+ patch_templates,
62
+ )
63
+ from sentry_sdk.integrations.django.middleware import patch_django_middlewares
64
+ from sentry_sdk.integrations.django.signals_handlers import patch_signals
65
+ from sentry_sdk.integrations.django.views import patch_views
39
66
 
67
+ if DJANGO_VERSION[:2] > (1, 8):
68
+ from sentry_sdk.integrations.django.caching import patch_caching
69
+ else:
70
+ patch_caching = None # type: ignore
40
71
 
41
- try:
42
- from django.urls import resolve # type: ignore
43
- except ImportError:
44
- from django.core.urlresolvers import resolve # type: ignore
72
+ from typing import TYPE_CHECKING
45
73
 
46
- from sentry_sdk import Hub
47
- from sentry_sdk.hub import _should_send_default_pii
48
- from sentry_sdk.scope import add_global_event_processor
49
- from sentry_sdk.utils import (
50
- add_global_repr_processor,
51
- capture_internal_exceptions,
52
- event_from_exception,
53
- safe_repr,
54
- format_and_strip,
55
- transaction_from_function,
56
- walk_exception_chain,
57
- )
58
- from sentry_sdk.integrations import Integration
59
- from sentry_sdk.integrations.logging import ignore_logger
60
- from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
61
- from sentry_sdk.integrations._wsgi_common import RequestExtractor
62
- from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
63
- from sentry_sdk.integrations.django.templates import get_template_frame_from_exception
74
+ if TYPE_CHECKING:
75
+ from typing import Any
76
+ from typing import Callable
77
+ from typing import Dict
78
+ from typing import Optional
79
+ from typing import Union
80
+ from typing import List
81
+
82
+ from django.core.handlers.wsgi import WSGIRequest
83
+ from django.http.response import HttpResponse
84
+ from django.http.request import QueryDict
85
+ from django.utils.datastructures import MultiValueDict
86
+
87
+ from sentry_sdk.tracing import Span
88
+ from sentry_sdk.integrations.wsgi import _ScopedResponse
89
+ from sentry_sdk._types import Event, Hint, EventProcessor, NotImplementedType
64
90
 
65
91
 
66
92
  if DJANGO_VERSION < (1, 10):
@@ -69,7 +95,6 @@ if DJANGO_VERSION < (1, 10):
69
95
  # type: (Any) -> bool
70
96
  return request_user.is_authenticated()
71
97
 
72
-
73
98
  else:
74
99
 
75
100
  def is_authenticated(request_user):
@@ -77,24 +102,60 @@ else:
77
102
  return request_user.is_authenticated
78
103
 
79
104
 
105
+ TRANSACTION_STYLE_VALUES = ("function_name", "url")
106
+
107
+
80
108
  class DjangoIntegration(Integration):
81
- identifier = "django"
109
+ """
110
+ Auto instrument a Django application.
82
111
 
83
- transaction_style = None
112
+ :param transaction_style: How to derive transaction names. Either `"function_name"` or `"url"`. Defaults to `"url"`.
113
+ :param middleware_spans: Whether to create spans for middleware. Defaults to `True`.
114
+ :param signals_spans: Whether to create spans for signals. Defaults to `True`.
115
+ :param signals_denylist: A list of signals to ignore when creating spans.
116
+ :param cache_spans: Whether to create spans for cache operations. Defaults to `False`.
117
+ """
84
118
 
85
- def __init__(self, transaction_style="url"):
86
- # type: (str) -> None
87
- TRANSACTION_STYLE_VALUES = ("function_name", "url")
119
+ identifier = "django"
120
+ origin = f"auto.http.{identifier}"
121
+ origin_db = f"auto.db.{identifier}"
122
+
123
+ transaction_style = ""
124
+ middleware_spans = None
125
+ signals_spans = None
126
+ cache_spans = None
127
+ signals_denylist = [] # type: list[signals.Signal]
128
+
129
+ def __init__(
130
+ self,
131
+ transaction_style="url", # type: str
132
+ middleware_spans=True, # type: bool
133
+ signals_spans=True, # type: bool
134
+ cache_spans=False, # type: bool
135
+ signals_denylist=None, # type: Optional[list[signals.Signal]]
136
+ http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
137
+ ):
138
+ # type: (...) -> None
88
139
  if transaction_style not in TRANSACTION_STYLE_VALUES:
89
140
  raise ValueError(
90
141
  "Invalid value for transaction_style: %s (must be in %s)"
91
142
  % (transaction_style, TRANSACTION_STYLE_VALUES)
92
143
  )
93
144
  self.transaction_style = transaction_style
145
+ self.middleware_spans = middleware_spans
146
+
147
+ self.signals_spans = signals_spans
148
+ self.signals_denylist = signals_denylist or []
149
+
150
+ self.cache_spans = cache_spans
151
+
152
+ self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
94
153
 
95
154
  @staticmethod
96
155
  def setup_once():
97
156
  # type: () -> None
157
+ _check_minimum_version(DjangoIntegration, DJANGO_VERSION)
158
+
98
159
  install_sql_hook()
99
160
  # Patch in our custom middleware.
100
161
 
@@ -106,85 +167,330 @@ class DjangoIntegration(Integration):
106
167
 
107
168
  old_app = WSGIHandler.__call__
108
169
 
170
+ @ensure_integration_enabled(DjangoIntegration, old_app)
109
171
  def sentry_patched_wsgi_handler(self, environ, start_response):
110
- # type: (Any, Dict[str, str], Callable) -> _ScopedResponse
111
- if Hub.current.get_integration(DjangoIntegration) is None:
112
- return old_app(self, environ, start_response)
172
+ # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse
173
+ bound_old_app = old_app.__get__(self, WSGIHandler)
113
174
 
114
- return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))(
115
- environ, start_response
116
- )
175
+ from django.conf import settings
117
176
 
118
- WSGIHandler.__call__ = sentry_patched_wsgi_handler
177
+ use_x_forwarded_for = settings.USE_X_FORWARDED_HOST
178
+
179
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
119
180
 
120
- # patch get_response, because at that point we have the Django request
121
- # object
122
- from django.core.handlers.base import BaseHandler # type: ignore
181
+ middleware = SentryWsgiMiddleware(
182
+ bound_old_app,
183
+ use_x_forwarded_for,
184
+ span_origin=DjangoIntegration.origin,
185
+ http_methods_to_capture=(
186
+ integration.http_methods_to_capture
187
+ if integration
188
+ else DEFAULT_HTTP_METHODS_TO_CAPTURE
189
+ ),
190
+ )
191
+ return middleware(environ, start_response)
123
192
 
124
- old_get_response = BaseHandler.get_response
193
+ WSGIHandler.__call__ = sentry_patched_wsgi_handler
125
194
 
126
- def sentry_patched_get_response(self, request):
127
- # type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException]
128
- hub = Hub.current
129
- integration = hub.get_integration(DjangoIntegration)
130
- if integration is not None:
131
- with hub.configure_scope() as scope:
132
- scope.add_event_processor(
133
- _make_event_processor(weakref.ref(request), integration)
134
- )
135
- return old_get_response(self, request)
195
+ _patch_get_response()
136
196
 
137
- BaseHandler.get_response = sentry_patched_get_response
197
+ _patch_django_asgi_handler()
138
198
 
139
199
  signals.got_request_exception.connect(_got_request_exception)
140
200
 
141
201
  @add_global_event_processor
142
202
  def process_django_templates(event, hint):
143
- # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
144
- if hint.get("exc_info", None) is None:
203
+ # type: (Event, Optional[Hint]) -> Optional[Event]
204
+ if hint is None:
145
205
  return event
146
206
 
147
- if "exception" not in event:
207
+ exc_info = hint.get("exc_info", None)
208
+
209
+ if exc_info is None:
148
210
  return event
149
211
 
150
- exception = event["exception"]
212
+ exception = event.get("exception", None)
151
213
 
152
- if "values" not in exception:
214
+ if exception is None:
215
+ return event
216
+
217
+ values = exception.get("values", None)
218
+
219
+ if values is None:
153
220
  return event
154
221
 
155
222
  for exception, (_, exc_value, _) in zip(
156
- exception["values"], walk_exception_chain(hint["exc_info"])
223
+ reversed(values), walk_exception_chain(exc_info)
157
224
  ):
158
225
  frame = get_template_frame_from_exception(exc_value)
159
226
  if frame is not None:
160
- frames = exception.setdefault("stacktrace", {}).setdefault(
161
- "frames", []
162
- )
163
- frames.append(frame)
227
+ frames = exception.get("stacktrace", {}).get("frames", [])
228
+
229
+ for i in reversed(range(len(frames))):
230
+ f = frames[i]
231
+ if (
232
+ f.get("function") in ("Parser.parse", "parse", "render")
233
+ and f.get("module") == "django.template.base"
234
+ ):
235
+ i += 1
236
+ break
237
+ else:
238
+ i = len(frames)
239
+
240
+ frames.insert(i, frame)
164
241
 
165
242
  return event
166
243
 
167
244
  @add_global_repr_processor
168
245
  def _django_queryset_repr(value, hint):
169
- if not isinstance(value, QuerySet) or value._result_cache:
246
+ # type: (Any, Dict[str, Any]) -> Union[NotImplementedType, str]
247
+ try:
248
+ # Django 1.6 can fail to import `QuerySet` when Django settings
249
+ # have not yet been initialized.
250
+ #
251
+ # If we fail to import, return `NotImplemented`. It's at least
252
+ # unlikely that we have a query set in `value` when importing
253
+ # `QuerySet` fails.
254
+ from django.db.models.query import QuerySet
255
+ except Exception:
170
256
  return NotImplemented
171
257
 
172
- # Do not call Hub.get_integration here. It is intentional that
173
- # running under a new hub does not suddenly start executing
174
- # querysets. This might be surprising to the user but it's likely
175
- # less annoying.
258
+ if not isinstance(value, QuerySet) or value._result_cache:
259
+ return NotImplemented
176
260
 
177
- return u"<%s from %s at 0x%x>" % (
261
+ return "<%s from %s at 0x%x>" % (
178
262
  value.__class__.__name__,
179
263
  value.__module__,
180
264
  id(value),
181
265
  )
182
266
 
267
+ _patch_channels()
268
+ patch_django_middlewares()
269
+ patch_views()
270
+ patch_templates()
271
+ patch_signals()
272
+ add_template_context_repr_sequence()
273
+
274
+ if patch_caching is not None:
275
+ patch_caching()
276
+
277
+
278
+ _DRF_PATCHED = False
279
+ _DRF_PATCH_LOCK = threading.Lock()
280
+
281
+
282
+ def _patch_drf():
283
+ # type: () -> None
284
+ """
285
+ Patch Django Rest Framework for more/better request data. DRF's request
286
+ type is a wrapper around Django's request type. The attribute we're
287
+ interested in is `request.data`, which is a cached property containing a
288
+ parsed request body. Reading a request body from that property is more
289
+ reliable than reading from any of Django's own properties, as those don't
290
+ hold payloads in memory and therefore can only be accessed once.
291
+
292
+ We patch the Django request object to include a weak backreference to the
293
+ DRF request object, such that we can later use either in
294
+ `DjangoRequestExtractor`.
295
+
296
+ This function is not called directly on SDK setup, because importing almost
297
+ any part of Django Rest Framework will try to access Django settings (where
298
+ `sentry_sdk.init()` might be called from in the first place). Instead we
299
+ run this function on every request and do the patching on the first
300
+ request.
301
+ """
302
+
303
+ global _DRF_PATCHED
304
+
305
+ if _DRF_PATCHED:
306
+ # Double-checked locking
307
+ return
308
+
309
+ with _DRF_PATCH_LOCK:
310
+ if _DRF_PATCHED:
311
+ return
312
+
313
+ # We set this regardless of whether the code below succeeds or fails.
314
+ # There is no point in trying to patch again on the next request.
315
+ _DRF_PATCHED = True
316
+
317
+ with capture_internal_exceptions():
318
+ try:
319
+ from rest_framework.views import APIView # type: ignore
320
+ except ImportError:
321
+ pass
322
+ else:
323
+ old_drf_initial = APIView.initial
324
+
325
+ def sentry_patched_drf_initial(self, request, *args, **kwargs):
326
+ # type: (APIView, Any, *Any, **Any) -> Any
327
+ with capture_internal_exceptions():
328
+ request._request._sentry_drf_request_backref = weakref.ref(
329
+ request
330
+ )
331
+ pass
332
+ return old_drf_initial(self, request, *args, **kwargs)
333
+
334
+ APIView.initial = sentry_patched_drf_initial
335
+
336
+
337
+ def _patch_channels():
338
+ # type: () -> None
339
+ try:
340
+ from channels.http import AsgiHandler # type: ignore
341
+ except ImportError:
342
+ return
343
+
344
+ if not HAS_REAL_CONTEXTVARS:
345
+ # We better have contextvars or we're going to leak state between
346
+ # requests.
347
+ #
348
+ # We cannot hard-raise here because channels may not be used at all in
349
+ # the current process. That is the case when running traditional WSGI
350
+ # workers in gunicorn+gevent and the websocket stuff in a separate
351
+ # process.
352
+ logger.warning(
353
+ "We detected that you are using Django channels 2.0."
354
+ + CONTEXTVARS_ERROR_MESSAGE
355
+ )
356
+
357
+ from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl
358
+
359
+ patch_channels_asgi_handler_impl(AsgiHandler)
360
+
361
+
362
+ def _patch_django_asgi_handler():
363
+ # type: () -> None
364
+ try:
365
+ from django.core.handlers.asgi import ASGIHandler
366
+ except ImportError:
367
+ return
368
+
369
+ if not HAS_REAL_CONTEXTVARS:
370
+ # We better have contextvars or we're going to leak state between
371
+ # requests.
372
+ #
373
+ # We cannot hard-raise here because Django's ASGI stuff may not be used
374
+ # at all.
375
+ logger.warning(
376
+ "We detected that you are using Django 3." + CONTEXTVARS_ERROR_MESSAGE
377
+ )
378
+
379
+ from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl
380
+
381
+ patch_django_asgi_handler_impl(ASGIHandler)
382
+
183
383
 
184
- def _make_event_processor(weak_request, integration):
185
- # type: (Callable[[], WSGIRequest], DjangoIntegration) -> Callable
186
- def event_processor(event, hint):
187
- # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
384
+ def _set_transaction_name_and_source(scope, transaction_style, request):
385
+ # type: (sentry_sdk.Scope, str, WSGIRequest) -> None
386
+ try:
387
+ transaction_name = None
388
+ if transaction_style == "function_name":
389
+ fn = resolve(request.path).func
390
+ transaction_name = transaction_from_function(getattr(fn, "view_class", fn))
391
+
392
+ elif transaction_style == "url":
393
+ if hasattr(request, "urlconf"):
394
+ transaction_name = LEGACY_RESOLVER.resolve(
395
+ request.path_info, urlconf=request.urlconf
396
+ )
397
+ else:
398
+ transaction_name = LEGACY_RESOLVER.resolve(request.path_info)
399
+
400
+ if transaction_name is None:
401
+ transaction_name = request.path_info
402
+ source = TransactionSource.URL
403
+ else:
404
+ source = SOURCE_FOR_STYLE[transaction_style]
405
+
406
+ scope.set_transaction_name(
407
+ transaction_name,
408
+ source=source,
409
+ )
410
+ except Resolver404:
411
+ urlconf = import_module(settings.ROOT_URLCONF)
412
+ # This exception only gets thrown when transaction_style is `function_name`
413
+ # So we don't check here what style is configured
414
+ if hasattr(urlconf, "handler404"):
415
+ handler = urlconf.handler404
416
+ if isinstance(handler, str):
417
+ scope.transaction = handler
418
+ else:
419
+ scope.transaction = transaction_from_function(
420
+ getattr(handler, "view_class", handler)
421
+ )
422
+ except Exception:
423
+ pass
424
+
425
+
426
+ def _before_get_response(request):
427
+ # type: (WSGIRequest) -> None
428
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
429
+ if integration is None:
430
+ return
431
+
432
+ _patch_drf()
433
+
434
+ scope = sentry_sdk.get_current_scope()
435
+ # Rely on WSGI middleware to start a trace
436
+ _set_transaction_name_and_source(scope, integration.transaction_style, request)
437
+
438
+ scope.add_event_processor(
439
+ _make_wsgi_request_event_processor(weakref.ref(request), integration)
440
+ )
441
+
442
+
443
+ def _attempt_resolve_again(request, scope, transaction_style):
444
+ # type: (WSGIRequest, sentry_sdk.Scope, str) -> None
445
+ """
446
+ Some django middlewares overwrite request.urlconf
447
+ so we need to respect that contract,
448
+ so we try to resolve the url again.
449
+ """
450
+ if not hasattr(request, "urlconf"):
451
+ return
452
+
453
+ _set_transaction_name_and_source(scope, transaction_style, request)
454
+
455
+
456
+ def _after_get_response(request):
457
+ # type: (WSGIRequest) -> None
458
+ integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
459
+ if integration is None or integration.transaction_style != "url":
460
+ return
461
+
462
+ scope = sentry_sdk.get_current_scope()
463
+ _attempt_resolve_again(request, scope, integration.transaction_style)
464
+
465
+
466
+ def _patch_get_response():
467
+ # type: () -> None
468
+ """
469
+ patch get_response, because at that point we have the Django request object
470
+ """
471
+ from django.core.handlers.base import BaseHandler
472
+
473
+ old_get_response = BaseHandler.get_response
474
+
475
+ def sentry_patched_get_response(self, request):
476
+ # type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException]
477
+ _before_get_response(request)
478
+ rv = old_get_response(self, request)
479
+ _after_get_response(request)
480
+ return rv
481
+
482
+ BaseHandler.get_response = sentry_patched_get_response
483
+
484
+ if hasattr(BaseHandler, "get_response_async"):
485
+ from sentry_sdk.integrations.django.asgi import patch_get_response_async
486
+
487
+ patch_get_response_async(BaseHandler, _before_get_response)
488
+
489
+
490
+ def _make_wsgi_request_event_processor(weak_request, integration):
491
+ # type: (Callable[[], WSGIRequest], DjangoIntegration) -> EventProcessor
492
+ def wsgi_request_event_processor(event, hint):
493
+ # type: (Event, dict[str, Any]) -> Event
188
494
  # if the request is gone we are fine not logging the data from
189
495
  # it. This might happen if the processor is pushed away to
190
496
  # another thread.
@@ -192,49 +498,72 @@ def _make_event_processor(weak_request, integration):
192
498
  if request is None:
193
499
  return event
194
500
 
195
- try:
196
- if integration.transaction_style == "function_name":
197
- event["transaction"] = transaction_from_function(
198
- resolve(request.path).func
199
- )
200
- elif integration.transaction_style == "url":
201
- event["transaction"] = LEGACY_RESOLVER.resolve(request.path)
202
- except Exception:
203
- pass
501
+ django_3 = ASGIRequest is not None
502
+ if django_3 and type(request) == ASGIRequest:
503
+ # We have a `asgi_request_event_processor` for this.
504
+ return event
204
505
 
205
506
  with capture_internal_exceptions():
206
507
  DjangoRequestExtractor(request).extract_into_event(event)
207
508
 
208
- if _should_send_default_pii():
509
+ if should_send_default_pii():
209
510
  with capture_internal_exceptions():
210
511
  _set_user_info(request, event)
211
512
 
212
513
  return event
213
514
 
214
- return event_processor
515
+ return wsgi_request_event_processor
215
516
 
216
517
 
217
518
  def _got_request_exception(request=None, **kwargs):
218
519
  # type: (WSGIRequest, **Any) -> None
219
- hub = Hub.current
220
- integration = hub.get_integration(DjangoIntegration)
221
- if integration is not None:
222
- event, hint = event_from_exception(
223
- sys.exc_info(),
224
- client_options=hub.client.options,
225
- mechanism={"type": "django", "handled": False},
226
- )
227
- hub.capture_event(event, hint=hint)
520
+ client = sentry_sdk.get_client()
521
+ integration = client.get_integration(DjangoIntegration)
522
+ if integration is None:
523
+ return
524
+
525
+ if request is not None and integration.transaction_style == "url":
526
+ scope = sentry_sdk.get_current_scope()
527
+ _attempt_resolve_again(request, scope, integration.transaction_style)
528
+
529
+ event, hint = event_from_exception(
530
+ sys.exc_info(),
531
+ client_options=client.options,
532
+ mechanism={"type": "django", "handled": False},
533
+ )
534
+ sentry_sdk.capture_event(event, hint=hint)
228
535
 
229
536
 
230
537
  class DjangoRequestExtractor(RequestExtractor):
538
+ def __init__(self, request):
539
+ # type: (Union[WSGIRequest, ASGIRequest]) -> None
540
+ try:
541
+ drf_request = request._sentry_drf_request_backref()
542
+ if drf_request is not None:
543
+ request = drf_request
544
+ except AttributeError:
545
+ pass
546
+ self.request = request
547
+
231
548
  def env(self):
232
549
  # type: () -> Dict[str, str]
233
550
  return self.request.META
234
551
 
235
552
  def cookies(self):
236
- # type: () -> Dict[str, str]
237
- return self.request.COOKIES
553
+ # type: () -> Dict[str, Union[str, AnnotatedValue]]
554
+ privacy_cookies = [
555
+ django_settings.CSRF_COOKIE_NAME,
556
+ django_settings.SESSION_COOKIE_NAME,
557
+ ]
558
+
559
+ clean_cookies = {} # type: Dict[str, Union[str, AnnotatedValue]]
560
+ for key, val in self.request.COOKIES.items():
561
+ if key in privacy_cookies:
562
+ clean_cookies[key] = SENSITIVE_DATA_SUBSTITUTE
563
+ else:
564
+ clean_cookies[key] = val
565
+
566
+ return clean_cookies
238
567
 
239
568
  def raw_data(self):
240
569
  # type: () -> bytes
@@ -249,11 +578,19 @@ class DjangoRequestExtractor(RequestExtractor):
249
578
  return self.request.FILES
250
579
 
251
580
  def size_of_file(self, file):
581
+ # type: (Any) -> int
252
582
  return file.size
253
583
 
584
+ def parsed_body(self):
585
+ # type: () -> Optional[Dict[str, Any]]
586
+ try:
587
+ return self.request.data
588
+ except Exception:
589
+ return RequestExtractor.parsed_body(self)
590
+
254
591
 
255
592
  def _set_user_info(request, event):
256
- # type: (WSGIRequest, Dict[str, Any]) -> None
593
+ # type: (WSGIRequest, Event) -> None
257
594
  user_info = event.setdefault("user", {})
258
595
 
259
596
  user = getattr(request, "user", None)
@@ -262,104 +599,160 @@ def _set_user_info(request, event):
262
599
  return
263
600
 
264
601
  try:
265
- user_info["id"] = str(user.pk)
602
+ user_info.setdefault("id", str(user.pk))
266
603
  except Exception:
267
604
  pass
268
605
 
269
606
  try:
270
- user_info["email"] = user.email
607
+ user_info.setdefault("email", user.email)
271
608
  except Exception:
272
609
  pass
273
610
 
274
611
  try:
275
- user_info["username"] = user.get_username()
612
+ user_info.setdefault("username", user.get_username())
276
613
  except Exception:
277
614
  pass
278
615
 
279
616
 
280
- class _FormatConverter(object):
281
- def __init__(self, param_mapping):
282
- # type: (Dict[str, int]) -> None
283
-
284
- self.param_mapping = param_mapping
285
- self.params = [] # type: List[Any]
286
-
287
- def __getitem__(self, val):
288
- # type: (str) -> str
289
- self.params.append(self.param_mapping.get(val))
290
- return "%s"
291
-
292
-
293
- def format_sql(sql, params):
294
- # type: (Any, Any) -> Tuple[str, List[str]]
295
- rv = []
296
-
297
- if isinstance(params, dict):
298
- # convert sql with named parameters to sql with unnamed parameters
299
- conv = _FormatConverter(params)
300
- if params:
301
- sql = sql_to_string(sql)
302
- sql = sql % conv
303
- params = conv.params
304
- else:
305
- params = ()
306
-
307
- for param in params or ():
308
- if param is None:
309
- rv.append("NULL")
310
- param = safe_repr(param)
311
- rv.append(param)
312
-
313
- return sql, rv
314
-
315
-
316
- def record_sql(sql, params):
317
- # type: (Any, Any) -> None
318
- hub = Hub.current
319
- if hub.get_integration(DjangoIntegration) is None:
320
- return
321
- real_sql, real_params = format_sql(sql, params)
322
-
323
- if real_params:
324
- try:
325
- real_sql = format_and_strip(real_sql, real_params)
326
- except Exception:
327
- pass
328
-
329
- hub.add_breadcrumb(message=real_sql, category="query")
330
-
331
-
332
617
  def install_sql_hook():
333
618
  # type: () -> None
334
619
  """If installed this causes Django's queries to be captured."""
335
620
  try:
336
- from django.db.backends.utils import CursorWrapper # type: ignore
621
+ from django.db.backends.utils import CursorWrapper
337
622
  except ImportError:
338
- from django.db.backends.util import CursorWrapper # type: ignore
623
+ from django.db.backends.util import CursorWrapper
624
+
625
+ try:
626
+ # django 1.6 and 1.7 compatability
627
+ from django.db.backends import BaseDatabaseWrapper
628
+ except ImportError:
629
+ # django 1.8 or later
630
+ from django.db.backends.base.base import BaseDatabaseWrapper
339
631
 
340
632
  try:
341
633
  real_execute = CursorWrapper.execute
342
634
  real_executemany = CursorWrapper.executemany
635
+ real_connect = BaseDatabaseWrapper.connect
343
636
  except AttributeError:
344
637
  # This won't work on Django versions < 1.6
345
638
  return
346
639
 
347
- def record_many_sql(sql, param_list):
348
- for params in param_list:
349
- record_sql(sql, params)
350
-
640
+ @ensure_integration_enabled(DjangoIntegration, real_execute)
351
641
  def execute(self, sql, params=None):
352
- try:
353
- return real_execute(self, sql, params)
354
- finally:
355
- record_sql(sql, params)
642
+ # type: (CursorWrapper, Any, Optional[Any]) -> Any
643
+ with record_sql_queries(
644
+ cursor=self.cursor,
645
+ query=sql,
646
+ params_list=params,
647
+ paramstyle="format",
648
+ executemany=False,
649
+ span_origin=DjangoIntegration.origin_db,
650
+ ) as span:
651
+ _set_db_data(span, self)
652
+ result = real_execute(self, sql, params)
356
653
 
654
+ with capture_internal_exceptions():
655
+ add_query_source(span)
656
+
657
+ return result
658
+
659
+ @ensure_integration_enabled(DjangoIntegration, real_executemany)
357
660
  def executemany(self, sql, param_list):
358
- try:
359
- return real_executemany(self, sql, param_list)
360
- finally:
361
- record_many_sql(sql, param_list)
661
+ # type: (CursorWrapper, Any, List[Any]) -> Any
662
+ with record_sql_queries(
663
+ cursor=self.cursor,
664
+ query=sql,
665
+ params_list=param_list,
666
+ paramstyle="format",
667
+ executemany=True,
668
+ span_origin=DjangoIntegration.origin_db,
669
+ ) as span:
670
+ _set_db_data(span, self)
671
+
672
+ result = real_executemany(self, sql, param_list)
673
+
674
+ with capture_internal_exceptions():
675
+ add_query_source(span)
676
+
677
+ return result
678
+
679
+ @ensure_integration_enabled(DjangoIntegration, real_connect)
680
+ def connect(self):
681
+ # type: (BaseDatabaseWrapper) -> None
682
+ with capture_internal_exceptions():
683
+ sentry_sdk.add_breadcrumb(message="connect", category="query")
684
+
685
+ with sentry_sdk.start_span(
686
+ op=OP.DB,
687
+ name="connect",
688
+ origin=DjangoIntegration.origin_db,
689
+ ) as span:
690
+ _set_db_data(span, self)
691
+ return real_connect(self)
362
692
 
363
693
  CursorWrapper.execute = execute
364
694
  CursorWrapper.executemany = executemany
695
+ BaseDatabaseWrapper.connect = connect
365
696
  ignore_logger("django.db.backends")
697
+
698
+
699
+ def _set_db_data(span, cursor_or_db):
700
+ # type: (Span, Any) -> None
701
+ db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db
702
+ vendor = db.vendor
703
+ span.set_data(SPANDATA.DB_SYSTEM, vendor)
704
+
705
+ # Some custom backends override `__getattr__`, making it look like `cursor_or_db`
706
+ # actually has a `connection` and the `connection` has a `get_dsn_parameters`
707
+ # attribute, only to throw an error once you actually want to call it.
708
+ # Hence the `inspect` check whether `get_dsn_parameters` is an actual callable
709
+ # function.
710
+ is_psycopg2 = (
711
+ hasattr(cursor_or_db, "connection")
712
+ and hasattr(cursor_or_db.connection, "get_dsn_parameters")
713
+ and inspect.isroutine(cursor_or_db.connection.get_dsn_parameters)
714
+ )
715
+ if is_psycopg2:
716
+ connection_params = cursor_or_db.connection.get_dsn_parameters()
717
+ else:
718
+ try:
719
+ # psycopg3, only extract needed params as get_parameters
720
+ # can be slow because of the additional logic to filter out default
721
+ # values
722
+ connection_params = {
723
+ "dbname": cursor_or_db.connection.info.dbname,
724
+ "port": cursor_or_db.connection.info.port,
725
+ }
726
+ # PGhost returns host or base dir of UNIX socket as an absolute path
727
+ # starting with /, use it only when it contains host
728
+ pg_host = cursor_or_db.connection.info.host
729
+ if pg_host and not pg_host.startswith("/"):
730
+ connection_params["host"] = pg_host
731
+ except Exception:
732
+ connection_params = db.get_connection_params()
733
+
734
+ db_name = connection_params.get("dbname") or connection_params.get("database")
735
+ if db_name is not None:
736
+ span.set_data(SPANDATA.DB_NAME, db_name)
737
+
738
+ server_address = connection_params.get("host")
739
+ if server_address is not None:
740
+ span.set_data(SPANDATA.SERVER_ADDRESS, server_address)
741
+
742
+ server_port = connection_params.get("port")
743
+ if server_port is not None:
744
+ span.set_data(SPANDATA.SERVER_PORT, str(server_port))
745
+
746
+ server_socket_address = connection_params.get("unix_socket")
747
+ if server_socket_address is not None:
748
+ span.set_data(SPANDATA.SERVER_SOCKET_ADDRESS, server_socket_address)
749
+
750
+
751
+ def add_template_context_repr_sequence():
752
+ # type: () -> None
753
+ try:
754
+ from django.template.context import BaseContext
755
+
756
+ add_repr_sequence_type(BaseContext)
757
+ except Exception:
758
+ pass