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,109 +1,162 @@
1
- from __future__ import absolute_import
1
+ import sentry_sdk
2
+ from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
3
+ from sentry_sdk.integrations._wsgi_common import (
4
+ DEFAULT_HTTP_METHODS_TO_CAPTURE,
5
+ RequestExtractor,
6
+ )
7
+ from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
8
+ from sentry_sdk.scope import should_send_default_pii
9
+ from sentry_sdk.tracing import SOURCE_FOR_STYLE
10
+ from sentry_sdk.utils import (
11
+ capture_internal_exceptions,
12
+ ensure_integration_enabled,
13
+ event_from_exception,
14
+ package_version,
15
+ )
2
16
 
3
- import weakref
17
+ from typing import TYPE_CHECKING
4
18
 
5
- from sentry_sdk.hub import Hub, _should_send_default_pii
6
- from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
7
- from sentry_sdk.integrations import Integration
8
- from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
9
- from sentry_sdk.integrations._wsgi_common import RequestExtractor
19
+ if TYPE_CHECKING:
20
+ from typing import Any, Callable, Dict, Union
10
21
 
11
- if False:
22
+ from sentry_sdk._types import Event, EventProcessor
12
23
  from sentry_sdk.integrations.wsgi import _ScopedResponse
13
- from typing import Any
14
- from typing import Dict
15
- from werkzeug.datastructures import ImmutableTypeConversionDict
16
- from werkzeug.datastructures import ImmutableMultiDict
17
- from werkzeug.datastructures import FileStorage
18
- from typing import Union
19
- from typing import Callable
24
+ from werkzeug.datastructures import FileStorage, ImmutableMultiDict
25
+
20
26
 
21
27
  try:
22
28
  import flask_login # type: ignore
23
29
  except ImportError:
24
30
  flask_login = None
25
31
 
26
- from flask import Request, Flask, _request_ctx_stack, _app_ctx_stack # type: ignore
27
- from flask.signals import (
28
- appcontext_pushed,
29
- appcontext_tearing_down,
30
- got_request_exception,
31
- request_started,
32
- )
32
+ try:
33
+ from flask import Flask, Request # type: ignore
34
+ from flask import request as flask_request
35
+ from flask.signals import (
36
+ before_render_template,
37
+ got_request_exception,
38
+ request_started,
39
+ )
40
+ from markupsafe import Markup
41
+ except ImportError:
42
+ raise DidNotEnable("Flask is not installed")
43
+
44
+ try:
45
+ import blinker # noqa
46
+ except ImportError:
47
+ raise DidNotEnable("blinker is not installed")
48
+
49
+ TRANSACTION_STYLE_VALUES = ("endpoint", "url")
33
50
 
34
51
 
35
52
  class FlaskIntegration(Integration):
36
53
  identifier = "flask"
54
+ origin = f"auto.http.{identifier}"
37
55
 
38
- transaction_style = None
56
+ transaction_style = ""
39
57
 
40
- def __init__(self, transaction_style="endpoint"):
41
- # type: (str) -> None
42
- TRANSACTION_STYLE_VALUES = ("endpoint", "url")
58
+ def __init__(
59
+ self,
60
+ transaction_style="endpoint", # type: str
61
+ http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
62
+ ):
63
+ # type: (...) -> None
43
64
  if transaction_style not in TRANSACTION_STYLE_VALUES:
44
65
  raise ValueError(
45
66
  "Invalid value for transaction_style: %s (must be in %s)"
46
67
  % (transaction_style, TRANSACTION_STYLE_VALUES)
47
68
  )
48
69
  self.transaction_style = transaction_style
70
+ self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
49
71
 
50
72
  @staticmethod
51
73
  def setup_once():
52
74
  # type: () -> None
53
- appcontext_pushed.connect(_push_appctx)
54
- appcontext_tearing_down.connect(_pop_appctx)
75
+ try:
76
+ from quart import Quart # type: ignore
77
+
78
+ if Flask == Quart:
79
+ # This is Quart masquerading as Flask, don't enable the Flask
80
+ # integration. See https://github.com/getsentry/sentry-python/issues/2709
81
+ raise DidNotEnable(
82
+ "This is not a Flask app but rather Quart pretending to be Flask"
83
+ )
84
+ except ImportError:
85
+ pass
86
+
87
+ version = package_version("flask")
88
+ _check_minimum_version(FlaskIntegration, version)
89
+
90
+ before_render_template.connect(_add_sentry_trace)
55
91
  request_started.connect(_request_started)
56
92
  got_request_exception.connect(_capture_exception)
57
93
 
58
94
  old_app = Flask.__call__
59
95
 
60
96
  def sentry_patched_wsgi_app(self, environ, start_response):
61
- # type: (Any, Dict[str, str], Callable) -> _ScopedResponse
62
- if Hub.current.get_integration(FlaskIntegration) is None:
97
+ # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse
98
+ if sentry_sdk.get_client().get_integration(FlaskIntegration) is None:
63
99
  return old_app(self, environ, start_response)
64
100
 
65
- return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))(
66
- environ, start_response
67
- )
101
+ integration = sentry_sdk.get_client().get_integration(FlaskIntegration)
68
102
 
69
- Flask.__call__ = sentry_patched_wsgi_app # type: ignore
103
+ middleware = SentryWsgiMiddleware(
104
+ lambda *a, **kw: old_app(self, *a, **kw),
105
+ span_origin=FlaskIntegration.origin,
106
+ http_methods_to_capture=(
107
+ integration.http_methods_to_capture
108
+ if integration
109
+ else DEFAULT_HTTP_METHODS_TO_CAPTURE
110
+ ),
111
+ )
112
+ return middleware(environ, start_response)
70
113
 
114
+ Flask.__call__ = sentry_patched_wsgi_app
71
115
 
72
- def _push_appctx(*args, **kwargs):
73
- # type: (*Flask, **Any) -> None
74
- hub = Hub.current
75
- if hub.get_integration(FlaskIntegration) is not None:
76
- # always want to push scope regardless of whether WSGI app might already
77
- # have (not the case for CLI for example)
78
- scope_manager = hub.push_scope()
79
- scope_manager.__enter__()
80
- _app_ctx_stack.top.sentry_sdk_scope_manager = scope_manager
81
- with hub.configure_scope() as scope:
82
- scope._name = "flask"
83
116
 
117
+ def _add_sentry_trace(sender, template, context, **extra):
118
+ # type: (Flask, Any, Dict[str, Any], **Any) -> None
119
+ if "sentry_trace" in context:
120
+ return
84
121
 
85
- def _pop_appctx(*args, **kwargs):
86
- # type: (*Flask, **Any) -> None
87
- scope_manager = getattr(_app_ctx_stack.top, "sentry_sdk_scope_manager", None)
88
- if scope_manager is not None:
89
- scope_manager.__exit__(None, None, None)
122
+ scope = sentry_sdk.get_current_scope()
123
+ trace_meta = Markup(scope.trace_propagation_meta())
124
+ context["sentry_trace"] = trace_meta # for backwards compatibility
125
+ context["sentry_trace_meta"] = trace_meta
126
+
127
+
128
+ def _set_transaction_name_and_source(scope, transaction_style, request):
129
+ # type: (sentry_sdk.Scope, str, Request) -> None
130
+ try:
131
+ name_for_style = {
132
+ "url": request.url_rule.rule,
133
+ "endpoint": request.url_rule.endpoint,
134
+ }
135
+ scope.set_transaction_name(
136
+ name_for_style[transaction_style],
137
+ source=SOURCE_FOR_STYLE[transaction_style],
138
+ )
139
+ except Exception:
140
+ pass
90
141
 
91
142
 
92
- def _request_started(sender, **kwargs):
143
+ def _request_started(app, **kwargs):
93
144
  # type: (Flask, **Any) -> None
94
- hub = Hub.current
95
- integration = hub.get_integration(FlaskIntegration)
145
+ integration = sentry_sdk.get_client().get_integration(FlaskIntegration)
96
146
  if integration is None:
97
147
  return
98
148
 
99
- weak_request = weakref.ref(_request_ctx_stack.top.request)
100
- app = _app_ctx_stack.top.app
101
- with hub.configure_scope() as scope:
102
- scope.add_event_processor(
103
- _make_request_event_processor( # type: ignore
104
- app, weak_request, integration
105
- )
106
- )
149
+ request = flask_request._get_current_object()
150
+
151
+ # Set the transaction name and source here,
152
+ # but rely on WSGI middleware to actually start the transaction
153
+ _set_transaction_name_and_source(
154
+ sentry_sdk.get_current_scope(), integration.transaction_style, request
155
+ )
156
+
157
+ scope = sentry_sdk.get_isolation_scope()
158
+ evt_processor = _make_request_event_processor(app, request, integration)
159
+ scope.add_event_processor(evt_processor)
107
160
 
108
161
 
109
162
  class FlaskRequestExtractor(RequestExtractor):
@@ -112,31 +165,42 @@ class FlaskRequestExtractor(RequestExtractor):
112
165
  return self.request.environ
113
166
 
114
167
  def cookies(self):
115
- # type: () -> ImmutableTypeConversionDict
116
- return self.request.cookies
168
+ # type: () -> Dict[Any, Any]
169
+ return {
170
+ k: v[0] if isinstance(v, list) and len(v) == 1 else v
171
+ for k, v in self.request.cookies.items()
172
+ }
117
173
 
118
174
  def raw_data(self):
119
175
  # type: () -> bytes
120
- return self.request.data
176
+ return self.request.get_data()
121
177
 
122
178
  def form(self):
123
- # type: () -> ImmutableMultiDict
179
+ # type: () -> ImmutableMultiDict[str, Any]
124
180
  return self.request.form
125
181
 
126
182
  def files(self):
127
- # type: () -> ImmutableMultiDict
183
+ # type: () -> ImmutableMultiDict[str, Any]
128
184
  return self.request.files
129
185
 
186
+ def is_json(self):
187
+ # type: () -> bool
188
+ return self.request.is_json
189
+
190
+ def json(self):
191
+ # type: () -> Any
192
+ return self.request.get_json(silent=True)
193
+
130
194
  def size_of_file(self, file):
131
195
  # type: (FileStorage) -> int
132
196
  return file.content_length
133
197
 
134
198
 
135
- def _make_request_event_processor(app, weak_request, integration):
136
- # type: (Flask, Callable[[], Request], FlaskIntegration) -> Callable
199
+ def _make_request_event_processor(app, request, integration):
200
+ # type: (Flask, Callable[[], Request], FlaskIntegration) -> EventProcessor
201
+
137
202
  def inner(event, hint):
138
- # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
139
- request = weak_request()
203
+ # type: (Event, dict[str, Any]) -> Event
140
204
 
141
205
  # if the request is gone we are fine not logging the data from
142
206
  # it. This might happen if the processor is pushed away to
@@ -144,18 +208,10 @@ def _make_request_event_processor(app, weak_request, integration):
144
208
  if request is None:
145
209
  return event
146
210
 
147
- try:
148
- if integration.transaction_style == "endpoint":
149
- event["transaction"] = request.url_rule.endpoint # type: ignore
150
- elif integration.transaction_style == "url":
151
- event["transaction"] = request.url_rule.rule # type: ignore
152
- except Exception:
153
- pass
154
-
155
211
  with capture_internal_exceptions():
156
212
  FlaskRequestExtractor(request).extract_into_event(event)
157
213
 
158
- if _should_send_default_pii():
214
+ if should_send_default_pii():
159
215
  with capture_internal_exceptions():
160
216
  _add_user_to_event(event)
161
217
 
@@ -164,22 +220,20 @@ def _make_request_event_processor(app, weak_request, integration):
164
220
  return inner
165
221
 
166
222
 
223
+ @ensure_integration_enabled(FlaskIntegration)
167
224
  def _capture_exception(sender, exception, **kwargs):
168
225
  # type: (Flask, Union[ValueError, BaseException], **Any) -> None
169
- hub = Hub.current
170
- if hub.get_integration(FlaskIntegration) is None:
171
- return
172
226
  event, hint = event_from_exception(
173
227
  exception,
174
- client_options=hub.client.options,
228
+ client_options=sentry_sdk.get_client().options,
175
229
  mechanism={"type": "flask", "handled": False},
176
230
  )
177
231
 
178
- hub.capture_event(event, hint=hint)
232
+ sentry_sdk.capture_event(event, hint=hint)
179
233
 
180
234
 
181
235
  def _add_user_to_event(event):
182
- # type: (Dict[str, Any]) -> None
236
+ # type: (Event) -> None
183
237
  if flask_login is None:
184
238
  return
185
239
 
@@ -194,7 +248,7 @@ def _add_user_to_event(event):
194
248
  user_info = event.setdefault("user", {})
195
249
 
196
250
  try:
197
- user_info["id"] = user.get_id()
251
+ user_info.setdefault("id", user.get_id())
198
252
  # TODO: more configurable user attrs here
199
253
  except AttributeError:
200
254
  # might happen if:
@@ -211,11 +265,11 @@ def _add_user_to_event(event):
211
265
  # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names
212
266
 
213
267
  try:
214
- user_info["email"] = user_info["username"] = user.email
268
+ user_info.setdefault("email", user.email)
215
269
  except Exception:
216
270
  pass
217
271
 
218
272
  try:
219
- user_info["username"] = user.username
273
+ user_info.setdefault("username", user.username)
220
274
  except Exception:
221
275
  pass
@@ -0,0 +1,239 @@
1
+ import functools
2
+ import sys
3
+ from copy import deepcopy
4
+ from datetime import datetime, timedelta, timezone
5
+ from os import environ
6
+
7
+ import sentry_sdk
8
+ from sentry_sdk.api import continue_trace
9
+ from sentry_sdk.consts import OP
10
+ from sentry_sdk.integrations import Integration
11
+ from sentry_sdk.integrations._wsgi_common import _filter_headers
12
+ from sentry_sdk.scope import should_send_default_pii
13
+ from sentry_sdk.tracing import TransactionSource
14
+ from sentry_sdk.utils import (
15
+ AnnotatedValue,
16
+ capture_internal_exceptions,
17
+ event_from_exception,
18
+ logger,
19
+ TimeoutThread,
20
+ reraise,
21
+ )
22
+
23
+ from typing import TYPE_CHECKING
24
+
25
+ # Constants
26
+ TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry
27
+ MILLIS_TO_SECONDS = 1000.0
28
+
29
+ if TYPE_CHECKING:
30
+ from typing import Any
31
+ from typing import TypeVar
32
+ from typing import Callable
33
+ from typing import Optional
34
+
35
+ from sentry_sdk._types import EventProcessor, Event, Hint
36
+
37
+ F = TypeVar("F", bound=Callable[..., Any])
38
+
39
+
40
+ def _wrap_func(func):
41
+ # type: (F) -> F
42
+ @functools.wraps(func)
43
+ def sentry_func(functionhandler, gcp_event, *args, **kwargs):
44
+ # type: (Any, Any, *Any, **Any) -> Any
45
+ client = sentry_sdk.get_client()
46
+
47
+ integration = client.get_integration(GcpIntegration)
48
+ if integration is None:
49
+ return func(functionhandler, gcp_event, *args, **kwargs)
50
+
51
+ configured_time = environ.get("FUNCTION_TIMEOUT_SEC")
52
+ if not configured_time:
53
+ logger.debug(
54
+ "The configured timeout could not be fetched from Cloud Functions configuration."
55
+ )
56
+ return func(functionhandler, gcp_event, *args, **kwargs)
57
+
58
+ configured_time = int(configured_time)
59
+
60
+ initial_time = datetime.now(timezone.utc)
61
+
62
+ with sentry_sdk.isolation_scope() as scope:
63
+ with capture_internal_exceptions():
64
+ scope.clear_breadcrumbs()
65
+ scope.add_event_processor(
66
+ _make_request_event_processor(
67
+ gcp_event, configured_time, initial_time
68
+ )
69
+ )
70
+ scope.set_tag("gcp_region", environ.get("FUNCTION_REGION"))
71
+ timeout_thread = None
72
+ if (
73
+ integration.timeout_warning
74
+ and configured_time > TIMEOUT_WARNING_BUFFER
75
+ ):
76
+ waiting_time = configured_time - TIMEOUT_WARNING_BUFFER
77
+
78
+ timeout_thread = TimeoutThread(
79
+ waiting_time,
80
+ configured_time,
81
+ isolation_scope=scope,
82
+ current_scope=sentry_sdk.get_current_scope(),
83
+ )
84
+
85
+ # Starting the thread to raise timeout warning exception
86
+ timeout_thread.start()
87
+
88
+ headers = {}
89
+ if hasattr(gcp_event, "headers"):
90
+ headers = gcp_event.headers
91
+
92
+ transaction = continue_trace(
93
+ headers,
94
+ op=OP.FUNCTION_GCP,
95
+ name=environ.get("FUNCTION_NAME", ""),
96
+ source=TransactionSource.COMPONENT,
97
+ origin=GcpIntegration.origin,
98
+ )
99
+ sampling_context = {
100
+ "gcp_env": {
101
+ "function_name": environ.get("FUNCTION_NAME"),
102
+ "function_entry_point": environ.get("ENTRY_POINT"),
103
+ "function_identity": environ.get("FUNCTION_IDENTITY"),
104
+ "function_region": environ.get("FUNCTION_REGION"),
105
+ "function_project": environ.get("GCP_PROJECT"),
106
+ },
107
+ "gcp_event": gcp_event,
108
+ }
109
+ with sentry_sdk.start_transaction(
110
+ transaction, custom_sampling_context=sampling_context
111
+ ):
112
+ try:
113
+ return func(functionhandler, gcp_event, *args, **kwargs)
114
+ except Exception:
115
+ exc_info = sys.exc_info()
116
+ sentry_event, hint = event_from_exception(
117
+ exc_info,
118
+ client_options=client.options,
119
+ mechanism={"type": "gcp", "handled": False},
120
+ )
121
+ sentry_sdk.capture_event(sentry_event, hint=hint)
122
+ reraise(*exc_info)
123
+ finally:
124
+ if timeout_thread:
125
+ timeout_thread.stop()
126
+ # Flush out the event queue
127
+ client.flush()
128
+
129
+ return sentry_func # type: ignore
130
+
131
+
132
+ class GcpIntegration(Integration):
133
+ identifier = "gcp"
134
+ origin = f"auto.function.{identifier}"
135
+
136
+ def __init__(self, timeout_warning=False):
137
+ # type: (bool) -> None
138
+ self.timeout_warning = timeout_warning
139
+
140
+ @staticmethod
141
+ def setup_once():
142
+ # type: () -> None
143
+ import __main__ as gcp_functions
144
+
145
+ if not hasattr(gcp_functions, "worker_v1"):
146
+ logger.warning(
147
+ "GcpIntegration currently supports only Python 3.7 runtime environment."
148
+ )
149
+ return
150
+
151
+ worker1 = gcp_functions.worker_v1
152
+
153
+ worker1.FunctionHandler.invoke_user_function = _wrap_func(
154
+ worker1.FunctionHandler.invoke_user_function
155
+ )
156
+
157
+
158
+ def _make_request_event_processor(gcp_event, configured_timeout, initial_time):
159
+ # type: (Any, Any, Any) -> EventProcessor
160
+
161
+ def event_processor(event, hint):
162
+ # type: (Event, Hint) -> Optional[Event]
163
+
164
+ final_time = datetime.now(timezone.utc)
165
+ time_diff = final_time - initial_time
166
+
167
+ execution_duration_in_millis = time_diff / timedelta(milliseconds=1)
168
+
169
+ extra = event.setdefault("extra", {})
170
+ extra["google cloud functions"] = {
171
+ "function_name": environ.get("FUNCTION_NAME"),
172
+ "function_entry_point": environ.get("ENTRY_POINT"),
173
+ "function_identity": environ.get("FUNCTION_IDENTITY"),
174
+ "function_region": environ.get("FUNCTION_REGION"),
175
+ "function_project": environ.get("GCP_PROJECT"),
176
+ "execution_duration_in_millis": execution_duration_in_millis,
177
+ "configured_timeout_in_seconds": configured_timeout,
178
+ }
179
+
180
+ extra["google cloud logs"] = {
181
+ "url": _get_google_cloud_logs_url(final_time),
182
+ }
183
+
184
+ request = event.get("request", {})
185
+
186
+ request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME"))
187
+
188
+ if hasattr(gcp_event, "method"):
189
+ request["method"] = gcp_event.method
190
+
191
+ if hasattr(gcp_event, "query_string"):
192
+ request["query_string"] = gcp_event.query_string.decode("utf-8")
193
+
194
+ if hasattr(gcp_event, "headers"):
195
+ request["headers"] = _filter_headers(gcp_event.headers)
196
+
197
+ if should_send_default_pii():
198
+ if hasattr(gcp_event, "data"):
199
+ request["data"] = gcp_event.data
200
+ else:
201
+ if hasattr(gcp_event, "data"):
202
+ # Unfortunately couldn't find a way to get structured body from GCP
203
+ # event. Meaning every body is unstructured to us.
204
+ request["data"] = AnnotatedValue.removed_because_raw_data()
205
+
206
+ event["request"] = deepcopy(request)
207
+
208
+ return event
209
+
210
+ return event_processor
211
+
212
+
213
+ def _get_google_cloud_logs_url(final_time):
214
+ # type: (datetime) -> str
215
+ """
216
+ Generates a Google Cloud Logs console URL based on the environment variables
217
+ Arguments:
218
+ final_time {datetime} -- Final time
219
+ Returns:
220
+ str -- Google Cloud Logs Console URL to logs.
221
+ """
222
+ hour_ago = final_time - timedelta(hours=1)
223
+ formatstring = "%Y-%m-%dT%H:%M:%SZ"
224
+
225
+ url = (
226
+ "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function"
227
+ "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false"
228
+ "&timestamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true"
229
+ "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}"
230
+ "&interval=PT1H&scrollTimestamp={timestamp_end}"
231
+ ).format(
232
+ project=environ.get("GCP_PROJECT"),
233
+ function_name=environ.get("FUNCTION_NAME"),
234
+ region=environ.get("FUNCTION_REGION"),
235
+ timestamp_end=final_time.strftime(formatstring),
236
+ timestamp_start=hour_ago.strftime(formatstring),
237
+ )
238
+
239
+ return url
@@ -0,0 +1,99 @@
1
+ import re
2
+
3
+ import sentry_sdk
4
+ from sentry_sdk.integrations import Integration
5
+ from sentry_sdk.scope import add_global_event_processor
6
+ from sentry_sdk.utils import capture_internal_exceptions
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Any
12
+ from sentry_sdk._types import Event
13
+
14
+ # function is everything between index at @
15
+ # and then we match on the @ plus the hex val
16
+ FUNCTION_RE = r"[^@]+?"
17
+ HEX_ADDRESS = r"\s+@\s+0x[0-9a-fA-F]+"
18
+
19
+ FRAME_RE = r"""
20
+ ^(?P<index>\d+)\.\s+(?P<function>{FUNCTION_RE}){HEX_ADDRESS}(?:\s+in\s+(?P<package>.+))?$
21
+ """.format(
22
+ FUNCTION_RE=FUNCTION_RE,
23
+ HEX_ADDRESS=HEX_ADDRESS,
24
+ )
25
+
26
+ FRAME_RE = re.compile(FRAME_RE, re.MULTILINE | re.VERBOSE)
27
+
28
+
29
+ class GnuBacktraceIntegration(Integration):
30
+ identifier = "gnu_backtrace"
31
+
32
+ @staticmethod
33
+ def setup_once():
34
+ # type: () -> None
35
+ @add_global_event_processor
36
+ def process_gnu_backtrace(event, hint):
37
+ # type: (Event, dict[str, Any]) -> Event
38
+ with capture_internal_exceptions():
39
+ return _process_gnu_backtrace(event, hint)
40
+
41
+
42
+ def _process_gnu_backtrace(event, hint):
43
+ # type: (Event, dict[str, Any]) -> Event
44
+ if sentry_sdk.get_client().get_integration(GnuBacktraceIntegration) is None:
45
+ return event
46
+
47
+ exc_info = hint.get("exc_info", None)
48
+
49
+ if exc_info is None:
50
+ return event
51
+
52
+ exception = event.get("exception", None)
53
+
54
+ if exception is None:
55
+ return event
56
+
57
+ values = exception.get("values", None)
58
+
59
+ if values is None:
60
+ return event
61
+
62
+ for exception in values:
63
+ frames = exception.get("stacktrace", {}).get("frames", [])
64
+ if not frames:
65
+ continue
66
+
67
+ msg = exception.get("value", None)
68
+ if not msg:
69
+ continue
70
+
71
+ additional_frames = []
72
+ new_msg = []
73
+
74
+ for line in msg.splitlines():
75
+ match = FRAME_RE.match(line)
76
+ if match:
77
+ additional_frames.append(
78
+ (
79
+ int(match.group("index")),
80
+ {
81
+ "package": match.group("package") or None,
82
+ "function": match.group("function") or None,
83
+ "platform": "native",
84
+ },
85
+ )
86
+ )
87
+ else:
88
+ # Put garbage lines back into message, not sure what to do with them.
89
+ new_msg.append(line)
90
+
91
+ if additional_frames:
92
+ additional_frames.sort(key=lambda x: -x[0])
93
+ for _, frame in additional_frames:
94
+ frames.append(frame)
95
+
96
+ new_msg.append("<stacktrace parsed and removed by GnuBacktraceIntegration>")
97
+ exception["value"] = "\n".join(new_msg)
98
+
99
+ return event