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.
- sentry_sdk/__init__.py +48 -30
- sentry_sdk/_compat.py +74 -61
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +289 -0
- sentry_sdk/_types.py +338 -0
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +496 -80
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +1023 -103
- sentry_sdk/consts.py +1438 -66
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +15 -14
- sentry_sdk/envelope.py +369 -0
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +611 -280
- sentry_sdk/integrations/__init__.py +276 -49
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +180 -44
- sentry_sdk/integrations/aiohttp.py +291 -42
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +9 -8
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +341 -0
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +17 -10
- sentry_sdk/integrations/aws_lambda.py +377 -62
- sentry_sdk/integrations/beam.py +176 -0
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +221 -0
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +134 -0
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +48 -14
- sentry_sdk/integrations/django/__init__.py +584 -191
- sentry_sdk/integrations/django/asgi.py +245 -0
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +187 -0
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +79 -5
- sentry_sdk/integrations/django/transactions.py +49 -22
- sentry_sdk/integrations/django/views.py +96 -0
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +50 -13
- sentry_sdk/integrations/executing.py +67 -0
- sentry_sdk/integrations/falcon.py +272 -0
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +142 -88
- sentry_sdk/integrations/gcp.py +239 -0
- sentry_sdk/integrations/gnu_backtrace.py +99 -0
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +307 -96
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +14 -31
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +141 -0
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +112 -68
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +95 -37
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +294 -123
- sentry_sdk/integrations/serverless.py +48 -19
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/__init__.py +4 -0
- sentry_sdk/integrations/spark/spark_driver.py +316 -0
- sentry_sdk/integrations/spark/spark_worker.py +116 -0
- sentry_sdk/integrations/sqlalchemy.py +142 -0
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +235 -29
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +158 -28
- sentry_sdk/integrations/tornado.py +84 -52
- sentry_sdk/integrations/trytond.py +50 -0
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +201 -119
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/py.typed +0 -0
- sentry_sdk/scope.py +1713 -85
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +405 -0
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +275 -0
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1486 -0
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +806 -134
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1625 -465
- sentry_sdk/worker.py +54 -25
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/integrations/celery.py +0 -119
- sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
- sentry_sdk-0.7.5.dist-info/METADATA +0 -36
- sentry_sdk-0.7.5.dist-info/RECORD +0 -39
- {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/integrations/flask.py
CHANGED
|
@@ -1,109 +1,162 @@
|
|
|
1
|
-
|
|
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
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
4
18
|
|
|
5
|
-
|
|
6
|
-
from
|
|
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
|
-
|
|
22
|
+
from sentry_sdk._types import Event, EventProcessor
|
|
12
23
|
from sentry_sdk.integrations.wsgi import _ScopedResponse
|
|
13
|
-
from
|
|
14
|
-
|
|
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
|
-
|
|
27
|
-
from flask
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 =
|
|
56
|
+
transaction_style = ""
|
|
39
57
|
|
|
40
|
-
def __init__(
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
66
|
-
environ, start_response
|
|
67
|
-
)
|
|
101
|
+
integration = sentry_sdk.get_client().get_integration(FlaskIntegration)
|
|
68
102
|
|
|
69
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
143
|
+
def _request_started(app, **kwargs):
|
|
93
144
|
# type: (Flask, **Any) -> None
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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: () ->
|
|
116
|
-
return
|
|
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.
|
|
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,
|
|
136
|
-
# type: (Flask, Callable[[], Request], FlaskIntegration) ->
|
|
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: (
|
|
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
|
|
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=
|
|
228
|
+
client_options=sentry_sdk.get_client().options,
|
|
175
229
|
mechanism={"type": "flask", "handled": False},
|
|
176
230
|
)
|
|
177
231
|
|
|
178
|
-
|
|
232
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
179
233
|
|
|
180
234
|
|
|
181
235
|
def _add_user_to_event(event):
|
|
182
|
-
# type: (
|
|
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
|
|
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
|
|
268
|
+
user_info.setdefault("email", user.email)
|
|
215
269
|
except Exception:
|
|
216
270
|
pass
|
|
217
271
|
|
|
218
272
|
try:
|
|
219
|
-
user_info
|
|
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
|
+
"×tamp={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
|