summaryrefslogtreecommitdiffstats
path: root/third_party/python/sentry-sdk/sentry_sdk/integrations
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/python/sentry-sdk/sentry_sdk/integrations
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/python/sentry-sdk/sentry_sdk/integrations')
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/__init__.py183
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/_wsgi_common.py180
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/aiohttp.py211
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/argv.py33
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/asgi.py194
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/atexit.py62
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/aws_lambda.py254
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/beam.py184
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/bottle.py199
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/celery.py258
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/dedupe.py43
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/django/__init__.py484
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/django/asgi.py47
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/django/middleware.py136
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/django/templates.py121
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/django/transactions.py134
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/excepthook.py76
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/falcon.py209
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/flask.py260
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/gnu_backtrace.py107
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/logging.py237
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/modules.py56
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/pyramid.py217
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/redis.py70
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/rq.py150
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/sanic.py233
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/serverless.py87
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/spark/__init__.py4
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_driver.py263
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_worker.py120
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/sqlalchemy.py86
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/stdlib.py230
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/threading.py90
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/tornado.py203
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/trytond.py55
-rw-r--r--third_party/python/sentry-sdk/sentry_sdk/integrations/wsgi.py309
36 files changed, 5785 insertions, 0 deletions
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/__init__.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/__init__.py
new file mode 100644
index 0000000000..f264bc4855
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/__init__.py
@@ -0,0 +1,183 @@
+"""This package"""
+from __future__ import absolute_import
+
+from threading import Lock
+
+from sentry_sdk._compat import iteritems
+from sentry_sdk.utils import logger
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Callable
+ from typing import Dict
+ from typing import Iterator
+ from typing import List
+ from typing import Set
+ from typing import Tuple
+ from typing import Type
+
+
+_installer_lock = Lock()
+_installed_integrations = set() # type: Set[str]
+
+
+def _generate_default_integrations_iterator(integrations, auto_enabling_integrations):
+ # type: (Tuple[str, ...], Tuple[str, ...]) -> Callable[[bool], Iterator[Type[Integration]]]
+
+ def iter_default_integrations(with_auto_enabling_integrations):
+ # type: (bool) -> Iterator[Type[Integration]]
+ """Returns an iterator of the default integration classes:
+ """
+ from importlib import import_module
+
+ if with_auto_enabling_integrations:
+ all_import_strings = integrations + auto_enabling_integrations
+ else:
+ all_import_strings = integrations
+
+ for import_string in all_import_strings:
+ try:
+ module, cls = import_string.rsplit(".", 1)
+ yield getattr(import_module(module), cls)
+ except (DidNotEnable, SyntaxError) as e:
+ logger.debug(
+ "Did not import default integration %s: %s", import_string, e
+ )
+
+ if isinstance(iter_default_integrations.__doc__, str):
+ for import_string in integrations:
+ iter_default_integrations.__doc__ += "\n- `{}`".format(import_string)
+
+ return iter_default_integrations
+
+
+_AUTO_ENABLING_INTEGRATIONS = (
+ "sentry_sdk.integrations.django.DjangoIntegration",
+ "sentry_sdk.integrations.flask.FlaskIntegration",
+ "sentry_sdk.integrations.bottle.BottleIntegration",
+ "sentry_sdk.integrations.falcon.FalconIntegration",
+ "sentry_sdk.integrations.sanic.SanicIntegration",
+ "sentry_sdk.integrations.celery.CeleryIntegration",
+ "sentry_sdk.integrations.rq.RqIntegration",
+ "sentry_sdk.integrations.aiohttp.AioHttpIntegration",
+ "sentry_sdk.integrations.tornado.TornadoIntegration",
+ "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration",
+)
+
+
+iter_default_integrations = _generate_default_integrations_iterator(
+ integrations=(
+ # stdlib/base runtime integrations
+ "sentry_sdk.integrations.logging.LoggingIntegration",
+ "sentry_sdk.integrations.stdlib.StdlibIntegration",
+ "sentry_sdk.integrations.excepthook.ExcepthookIntegration",
+ "sentry_sdk.integrations.dedupe.DedupeIntegration",
+ "sentry_sdk.integrations.atexit.AtexitIntegration",
+ "sentry_sdk.integrations.modules.ModulesIntegration",
+ "sentry_sdk.integrations.argv.ArgvIntegration",
+ "sentry_sdk.integrations.threading.ThreadingIntegration",
+ ),
+ auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS,
+)
+
+del _generate_default_integrations_iterator
+
+
+def setup_integrations(
+ integrations, with_defaults=True, with_auto_enabling_integrations=False
+):
+ # type: (List[Integration], bool, bool) -> Dict[str, Integration]
+ """Given a list of integration instances this installs them all. When
+ `with_defaults` is set to `True` then all default integrations are added
+ unless they were already provided before.
+ """
+ integrations = dict(
+ (integration.identifier, integration) for integration in integrations or ()
+ )
+
+ logger.debug("Setting up integrations (with default = %s)", with_defaults)
+
+ # Integrations that are not explicitly set up by the user.
+ used_as_default_integration = set()
+
+ if with_defaults:
+ for integration_cls in iter_default_integrations(
+ with_auto_enabling_integrations
+ ):
+ if integration_cls.identifier not in integrations:
+ instance = integration_cls()
+ integrations[instance.identifier] = instance
+ used_as_default_integration.add(instance.identifier)
+
+ for identifier, integration in iteritems(integrations):
+ with _installer_lock:
+ if identifier not in _installed_integrations:
+ logger.debug(
+ "Setting up previously not enabled integration %s", identifier
+ )
+ try:
+ type(integration).setup_once()
+ except NotImplementedError:
+ if getattr(integration, "install", None) is not None:
+ logger.warning(
+ "Integration %s: The install method is "
+ "deprecated. Use `setup_once`.",
+ identifier,
+ )
+ integration.install()
+ else:
+ raise
+ except DidNotEnable as e:
+ if identifier not in used_as_default_integration:
+ raise
+
+ logger.debug(
+ "Did not enable default integration %s: %s", identifier, e
+ )
+
+ _installed_integrations.add(identifier)
+
+ for identifier in integrations:
+ logger.debug("Enabling integration %s", identifier)
+
+ return integrations
+
+
+class DidNotEnable(Exception):
+ """
+ The integration could not be enabled due to a trivial user error like
+ `flask` not being installed for the `FlaskIntegration`.
+
+ This exception is silently swallowed for default integrations, but reraised
+ for explicitly enabled integrations.
+ """
+
+
+class Integration(object):
+ """Baseclass for all integrations.
+
+ To accept options for an integration, implement your own constructor that
+ saves those options on `self`.
+ """
+
+ install = None
+ """Legacy method, do not implement."""
+
+ identifier = None # type: str
+ """String unique ID of integration type"""
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ """
+ Initialize the integration.
+
+ This function is only called once, ever. Configuration is not available
+ at this point, so the only thing to do here is to hook into exception
+ handlers, and perhaps do monkeypatches.
+
+ Inside those hooks `Integration.current` can be used to access the
+ instance again.
+ """
+ raise NotImplementedError()
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/_wsgi_common.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/_wsgi_common.py
new file mode 100644
index 0000000000..f874663883
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/_wsgi_common.py
@@ -0,0 +1,180 @@
+import json
+
+from sentry_sdk.hub import Hub, _should_send_default_pii
+from sentry_sdk.utils import AnnotatedValue
+from sentry_sdk._compat import text_type, iteritems
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ import sentry_sdk
+
+ from typing import Any
+ from typing import Dict
+ from typing import Optional
+ from typing import Union
+
+
+SENSITIVE_ENV_KEYS = (
+ "REMOTE_ADDR",
+ "HTTP_X_FORWARDED_FOR",
+ "HTTP_SET_COOKIE",
+ "HTTP_COOKIE",
+ "HTTP_AUTHORIZATION",
+ "HTTP_X_FORWARDED_FOR",
+ "HTTP_X_REAL_IP",
+)
+
+SENSITIVE_HEADERS = tuple(
+ x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_")
+)
+
+
+def request_body_within_bounds(client, content_length):
+ # type: (Optional[sentry_sdk.Client], int) -> bool
+ if client is None:
+ return False
+
+ bodies = client.options["request_bodies"]
+ return not (
+ bodies == "never"
+ or (bodies == "small" and content_length > 10 ** 3)
+ or (bodies == "medium" and content_length > 10 ** 4)
+ )
+
+
+class RequestExtractor(object):
+ def __init__(self, request):
+ # type: (Any) -> None
+ self.request = request
+
+ def extract_into_event(self, event):
+ # type: (Dict[str, Any]) -> None
+ client = Hub.current.client
+ if client is None:
+ return
+
+ data = None # type: Optional[Union[AnnotatedValue, Dict[str, Any]]]
+
+ content_length = self.content_length()
+ request_info = event.get("request", {})
+
+ if _should_send_default_pii():
+ request_info["cookies"] = dict(self.cookies())
+
+ if not request_body_within_bounds(client, content_length):
+ data = AnnotatedValue(
+ "",
+ {"rem": [["!config", "x", 0, content_length]], "len": content_length},
+ )
+ else:
+ parsed_body = self.parsed_body()
+ if parsed_body is not None:
+ data = parsed_body
+ elif self.raw_data():
+ data = AnnotatedValue(
+ "",
+ {"rem": [["!raw", "x", 0, content_length]], "len": content_length},
+ )
+ else:
+ data = None
+
+ if data is not None:
+ request_info["data"] = data
+
+ event["request"] = request_info
+
+ def content_length(self):
+ # type: () -> int
+ try:
+ return int(self.env().get("CONTENT_LENGTH", 0))
+ except ValueError:
+ return 0
+
+ def cookies(self):
+ # type: () -> Dict[str, Any]
+ raise NotImplementedError()
+
+ def raw_data(self):
+ # type: () -> Optional[Union[str, bytes]]
+ raise NotImplementedError()
+
+ def form(self):
+ # type: () -> Optional[Dict[str, Any]]
+ raise NotImplementedError()
+
+ def parsed_body(self):
+ # type: () -> Optional[Dict[str, Any]]
+ form = self.form()
+ files = self.files()
+ if form or files:
+ data = dict(iteritems(form))
+ for k, v in iteritems(files):
+ size = self.size_of_file(v)
+ data[k] = AnnotatedValue(
+ "", {"len": size, "rem": [["!raw", "x", 0, size]]}
+ )
+
+ return data
+
+ return self.json()
+
+ def is_json(self):
+ # type: () -> bool
+ return _is_json_content_type(self.env().get("CONTENT_TYPE"))
+
+ def json(self):
+ # type: () -> Optional[Any]
+ try:
+ if not self.is_json():
+ return None
+
+ raw_data = self.raw_data()
+ if raw_data is None:
+ return None
+
+ if isinstance(raw_data, text_type):
+ return json.loads(raw_data)
+ else:
+ return json.loads(raw_data.decode("utf-8"))
+ except ValueError:
+ pass
+
+ return None
+
+ def files(self):
+ # type: () -> Optional[Dict[str, Any]]
+ raise NotImplementedError()
+
+ def size_of_file(self, file):
+ # type: (Any) -> int
+ raise NotImplementedError()
+
+ def env(self):
+ # type: () -> Dict[str, Any]
+ raise NotImplementedError()
+
+
+def _is_json_content_type(ct):
+ # type: (Optional[str]) -> bool
+ mt = (ct or "").split(";", 1)[0]
+ return (
+ mt == "application/json"
+ or (mt.startswith("application/"))
+ and mt.endswith("+json")
+ )
+
+
+def _filter_headers(headers):
+ # type: (Dict[str, str]) -> Dict[str, str]
+ if _should_send_default_pii():
+ return headers
+
+ return {
+ k: (
+ v
+ if k.upper().replace("-", "_") not in SENSITIVE_HEADERS
+ else AnnotatedValue("", {"rem": [["!config", "x", 0, len(v)]]})
+ )
+ for k, v in iteritems(headers)
+ }
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/aiohttp.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/aiohttp.py
new file mode 100644
index 0000000000..02c76df7ef
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/aiohttp.py
@@ -0,0 +1,211 @@
+import sys
+import weakref
+
+from sentry_sdk._compat import reraise
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations.logging import ignore_logger
+from sentry_sdk.integrations._wsgi_common import (
+ _filter_headers,
+ request_body_within_bounds,
+)
+from sentry_sdk.tracing import Span
+from sentry_sdk.utils import (
+ capture_internal_exceptions,
+ event_from_exception,
+ transaction_from_function,
+ HAS_REAL_CONTEXTVARS,
+ AnnotatedValue,
+)
+
+try:
+ import asyncio
+
+ from aiohttp import __version__ as AIOHTTP_VERSION
+ from aiohttp.web import Application, HTTPException, UrlDispatcher
+except ImportError:
+ raise DidNotEnable("AIOHTTP not installed")
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from aiohttp.web_request import Request
+ from aiohttp.abc import AbstractMatchInfo
+ from typing import Any
+ from typing import Dict
+ from typing import Optional
+ from typing import Tuple
+ from typing import Callable
+ from typing import Union
+
+ from sentry_sdk.utils import ExcInfo
+ from sentry_sdk._types import EventProcessor
+
+
+class AioHttpIntegration(Integration):
+ identifier = "aiohttp"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+
+ try:
+ version = tuple(map(int, AIOHTTP_VERSION.split(".")))
+ except (TypeError, ValueError):
+ raise DidNotEnable("AIOHTTP version unparseable: {}".format(version))
+
+ if version < (3, 4):
+ raise DidNotEnable("AIOHTTP 3.4 or newer required.")
+
+ if not HAS_REAL_CONTEXTVARS:
+ # We better have contextvars or we're going to leak state between
+ # requests.
+ raise RuntimeError(
+ "The aiohttp integration for Sentry requires Python 3.7+ "
+ " or aiocontextvars package"
+ )
+
+ ignore_logger("aiohttp.server")
+
+ old_handle = Application._handle
+
+ async def sentry_app_handle(self, request, *args, **kwargs):
+ # type: (Any, Request, *Any, **Any) -> Any
+ async def inner():
+ # type: () -> Any
+ hub = Hub.current
+ if hub.get_integration(AioHttpIntegration) is None:
+ return await old_handle(self, request, *args, **kwargs)
+
+ weak_request = weakref.ref(request)
+
+ with Hub(Hub.current) as hub:
+ with hub.configure_scope() as scope:
+ scope.clear_breadcrumbs()
+ scope.add_event_processor(_make_request_processor(weak_request))
+
+ span = Span.continue_from_headers(request.headers)
+ span.op = "http.server"
+ # If this transaction name makes it to the UI, AIOHTTP's
+ # URL resolver did not find a route or died trying.
+ span.transaction = "generic AIOHTTP request"
+
+ with hub.start_span(span):
+ try:
+ response = await old_handle(self, request)
+ except HTTPException as e:
+ span.set_http_status(e.status_code)
+ raise
+ except asyncio.CancelledError:
+ span.set_status("cancelled")
+ raise
+ except Exception:
+ # This will probably map to a 500 but seems like we
+ # have no way to tell. Do not set span status.
+ reraise(*_capture_exception(hub))
+
+ span.set_http_status(response.status)
+ return response
+
+ # Explicitly wrap in task such that current contextvar context is
+ # copied. Just doing `return await inner()` will leak scope data
+ # between requests.
+ return await asyncio.get_event_loop().create_task(inner())
+
+ Application._handle = sentry_app_handle
+
+ old_urldispatcher_resolve = UrlDispatcher.resolve
+
+ async def sentry_urldispatcher_resolve(self, request):
+ # type: (UrlDispatcher, Request) -> AbstractMatchInfo
+ rv = await old_urldispatcher_resolve(self, request)
+
+ name = None
+
+ try:
+ name = transaction_from_function(rv.handler)
+ except Exception:
+ pass
+
+ if name is not None:
+ with Hub.current.configure_scope() as scope:
+ scope.transaction = name
+
+ return rv
+
+ UrlDispatcher.resolve = sentry_urldispatcher_resolve
+
+
+def _make_request_processor(weak_request):
+ # type: (Callable[[], Request]) -> EventProcessor
+ def aiohttp_processor(
+ event, # type: Dict[str, Any]
+ hint, # type: Dict[str, Tuple[type, BaseException, Any]]
+ ):
+ # type: (...) -> Dict[str, Any]
+ request = weak_request()
+ if request is None:
+ return event
+
+ with capture_internal_exceptions():
+ request_info = event.setdefault("request", {})
+
+ request_info["url"] = "%s://%s%s" % (
+ request.scheme,
+ request.host,
+ request.path,
+ )
+
+ request_info["query_string"] = request.query_string
+ request_info["method"] = request.method
+ request_info["env"] = {"REMOTE_ADDR": request.remote}
+
+ hub = Hub.current
+ request_info["headers"] = _filter_headers(dict(request.headers))
+
+ # Just attach raw data here if it is within bounds, if available.
+ # Unfortunately there's no way to get structured data from aiohttp
+ # without awaiting on some coroutine.
+ request_info["data"] = get_aiohttp_request_data(hub, request)
+
+ return event
+
+ return aiohttp_processor
+
+
+def _capture_exception(hub):
+ # type: (Hub) -> ExcInfo
+ exc_info = sys.exc_info()
+ event, hint = event_from_exception(
+ exc_info,
+ client_options=hub.client.options, # type: ignore
+ mechanism={"type": "aiohttp", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+ return exc_info
+
+
+BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]"
+
+
+def get_aiohttp_request_data(hub, request):
+ # type: (Hub, Request) -> Union[Optional[str], AnnotatedValue]
+ bytes_body = request._read_bytes
+
+ if bytes_body is not None:
+ # we have body to show
+ if not request_body_within_bounds(hub.client, len(bytes_body)):
+
+ return AnnotatedValue(
+ "",
+ {"rem": [["!config", "x", 0, len(bytes_body)]], "len": len(bytes_body)},
+ )
+ encoding = request.charset or "utf-8"
+ return bytes_body.decode(encoding, "replace")
+
+ if request.can_read_body:
+ # body exists but we can't show it
+ return BODY_NOT_READ_MESSAGE
+
+ # request has no body
+ return None
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/argv.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/argv.py
new file mode 100644
index 0000000000..f005521d32
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/argv.py
@@ -0,0 +1,33 @@
+from __future__ import absolute_import
+
+import sys
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration
+from sentry_sdk.scope import add_global_event_processor
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Optional
+
+ from sentry_sdk._types import Event, Hint
+
+
+class ArgvIntegration(Integration):
+ identifier = "argv"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ @add_global_event_processor
+ def processor(event, hint):
+ # type: (Event, Optional[Hint]) -> Optional[Event]
+ if Hub.current.get_integration(ArgvIntegration) is not None:
+ extra = event.setdefault("extra", {})
+ # If some event processor decided to set extra to e.g. an
+ # `int`, don't crash. Not here.
+ if isinstance(extra, dict):
+ extra["sys.argv"] = sys.argv
+
+ return event
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/asgi.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/asgi.py
new file mode 100644
index 0000000000..762634f82f
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/asgi.py
@@ -0,0 +1,194 @@
+"""
+An ASGI middleware.
+
+Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi>`_.
+"""
+
+import asyncio
+import functools
+import inspect
+import urllib
+
+from sentry_sdk._types import MYPY
+from sentry_sdk.hub import Hub, _should_send_default_pii
+from sentry_sdk.integrations._wsgi_common import _filter_headers
+from sentry_sdk.utils import ContextVar, event_from_exception, transaction_from_function
+from sentry_sdk.tracing import Span
+
+if MYPY:
+ from typing import Dict
+ from typing import Any
+ from typing import Optional
+ from typing import Callable
+
+ from sentry_sdk._types import Event, Hint
+
+
+_asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied")
+
+
+def _capture_exception(hub, exc):
+ # type: (Hub, Any) -> None
+
+ # Check client here as it might have been unset while streaming response
+ if hub.client is not None:
+ event, hint = event_from_exception(
+ exc,
+ client_options=hub.client.options,
+ mechanism={"type": "asgi", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+
+def _looks_like_asgi3(app):
+ # type: (Any) -> bool
+ """
+ Try to figure out if an application object supports ASGI3.
+
+ This is how uvicorn figures out the application version as well.
+ """
+ if inspect.isclass(app):
+ return hasattr(app, "__await__")
+ elif inspect.isfunction(app):
+ return asyncio.iscoroutinefunction(app)
+ else:
+ call = getattr(app, "__call__", None) # noqa
+ return asyncio.iscoroutinefunction(call)
+
+
+class SentryAsgiMiddleware:
+ __slots__ = ("app", "__call__")
+
+ def __init__(self, app):
+ # type: (Any) -> None
+ self.app = app
+
+ if _looks_like_asgi3(app):
+ self.__call__ = self._run_asgi3 # type: Callable[..., Any]
+ else:
+ self.__call__ = self._run_asgi2
+
+ def _run_asgi2(self, scope):
+ # type: (Any) -> Any
+ async def inner(receive, send):
+ # type: (Any, Any) -> Any
+ return await self._run_app(scope, lambda: self.app(scope)(receive, send))
+
+ return inner
+
+ async def _run_asgi3(self, scope, receive, send):
+ # type: (Any, Any, Any) -> Any
+ return await self._run_app(scope, lambda: self.app(scope, receive, send))
+
+ async def _run_app(self, scope, callback):
+ # type: (Any, Any) -> Any
+ if _asgi_middleware_applied.get(False):
+ return await callback()
+
+ _asgi_middleware_applied.set(True)
+ try:
+ hub = Hub(Hub.current)
+ with hub:
+ with hub.configure_scope() as sentry_scope:
+ sentry_scope.clear_breadcrumbs()
+ sentry_scope._name = "asgi"
+ processor = functools.partial(
+ self.event_processor, asgi_scope=scope
+ )
+ sentry_scope.add_event_processor(processor)
+
+ if scope["type"] in ("http", "websocket"):
+ span = Span.continue_from_headers(dict(scope["headers"]))
+ span.op = "{}.server".format(scope["type"])
+ else:
+ span = Span()
+ span.op = "asgi.server"
+
+ span.set_tag("asgi.type", scope["type"])
+ span.transaction = "generic ASGI request"
+
+ with hub.start_span(span) as span:
+ # XXX: Would be cool to have correct span status, but we
+ # would have to wrap send(). That is a bit hard to do with
+ # the current abstraction over ASGI 2/3.
+ try:
+ return await callback()
+ except Exception as exc:
+ _capture_exception(hub, exc)
+ raise exc from None
+ finally:
+ _asgi_middleware_applied.set(False)
+
+ def event_processor(self, event, hint, asgi_scope):
+ # type: (Event, Hint, Any) -> Optional[Event]
+ request_info = event.get("request", {})
+
+ if asgi_scope["type"] in ("http", "websocket"):
+ request_info["url"] = self.get_url(asgi_scope)
+ request_info["method"] = asgi_scope["method"]
+ request_info["headers"] = _filter_headers(self.get_headers(asgi_scope))
+ request_info["query_string"] = self.get_query(asgi_scope)
+
+ if asgi_scope.get("client") and _should_send_default_pii():
+ request_info["env"] = {"REMOTE_ADDR": asgi_scope["client"][0]}
+
+ if asgi_scope.get("endpoint"):
+ # Webframeworks like Starlette mutate the ASGI env once routing is
+ # done, which is sometime after the request has started. If we have
+ # an endpoint, overwrite our path-based transaction name.
+ event["transaction"] = self.get_transaction(asgi_scope)
+
+ event["request"] = request_info
+
+ return event
+
+ def get_url(self, scope):
+ # type: (Any) -> str
+ """
+ Extract URL from the ASGI scope, without also including the querystring.
+ """
+ scheme = scope.get("scheme", "http")
+ server = scope.get("server", None)
+ path = scope.get("root_path", "") + scope["path"]
+
+ for key, value in scope["headers"]:
+ if key == b"host":
+ host_header = value.decode("latin-1")
+ return "%s://%s%s" % (scheme, host_header, path)
+
+ if server is not None:
+ host, port = server
+ default_port = {"http": 80, "https": 443, "ws": 80, "wss": 443}[scheme]
+ if port != default_port:
+ return "%s://%s:%s%s" % (scheme, host, port, path)
+ return "%s://%s%s" % (scheme, host, path)
+ return path
+
+ def get_query(self, scope):
+ # type: (Any) -> Any
+ """
+ Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
+ """
+ return urllib.parse.unquote(scope["query_string"].decode("latin-1"))
+
+ def get_headers(self, scope):
+ # type: (Any) -> Dict[str, Any]
+ """
+ Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
+ """
+ headers = {} # type: Dict[str, str]
+ for raw_key, raw_value in scope["headers"]:
+ key = raw_key.decode("latin-1")
+ value = raw_value.decode("latin-1")
+ if key in headers:
+ headers[key] = headers[key] + ", " + value
+ else:
+ headers[key] = value
+ return headers
+
+ def get_transaction(self, scope):
+ # type: (Any) -> Optional[str]
+ """
+ Return a transaction string to identify the routed endpoint.
+ """
+ return transaction_from_function(scope["endpoint"])
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/atexit.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/atexit.py
new file mode 100644
index 0000000000..18fe657bff
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/atexit.py
@@ -0,0 +1,62 @@
+from __future__ import absolute_import
+
+import os
+import sys
+import atexit
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.utils import logger
+from sentry_sdk.integrations import Integration
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+
+ from typing import Any
+ from typing import Optional
+
+
+def default_callback(pending, timeout):
+ # type: (int, int) -> None
+ """This is the default shutdown callback that is set on the options.
+ It prints out a message to stderr that informs the user that some events
+ are still pending and the process is waiting for them to flush out.
+ """
+
+ def echo(msg):
+ # type: (str) -> None
+ sys.stderr.write(msg + "\n")
+
+ echo("Sentry is attempting to send %i pending error messages" % pending)
+ echo("Waiting up to %s seconds" % timeout)
+ echo("Press Ctrl-%s to quit" % (os.name == "nt" and "Break" or "C"))
+ sys.stderr.flush()
+
+
+class AtexitIntegration(Integration):
+ identifier = "atexit"
+
+ def __init__(self, callback=None):
+ # type: (Optional[Any]) -> None
+ if callback is None:
+ callback = default_callback
+ self.callback = callback
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ @atexit.register
+ def _shutdown():
+ # type: () -> None
+ logger.debug("atexit: got shutdown signal")
+ hub = Hub.main
+ integration = hub.get_integration(AtexitIntegration)
+ if integration is not None:
+ logger.debug("atexit: shutting down client")
+
+ # If there is a session on the hub, close it now.
+ hub.end_session()
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+ client.close(callback=integration.callback)
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/aws_lambda.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/aws_lambda.py
new file mode 100644
index 0000000000..3a08d998db
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/aws_lambda.py
@@ -0,0 +1,254 @@
+from datetime import datetime, timedelta
+from os import environ
+import sys
+
+from sentry_sdk.hub import Hub, _should_send_default_pii
+from sentry_sdk._compat import reraise
+from sentry_sdk.utils import (
+ AnnotatedValue,
+ capture_internal_exceptions,
+ event_from_exception,
+ logger,
+)
+from sentry_sdk.integrations import Integration
+from sentry_sdk.integrations._wsgi_common import _filter_headers
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import TypeVar
+ from typing import Callable
+ from typing import Optional
+
+ from sentry_sdk._types import EventProcessor, Event, Hint
+
+ F = TypeVar("F", bound=Callable[..., Any])
+
+
+def _wrap_handler(handler):
+ # type: (F) -> F
+ def sentry_handler(event, context, *args, **kwargs):
+ # type: (Any, Any, *Any, **Any) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(AwsLambdaIntegration)
+ if integration is None:
+ return handler(event, context, *args, **kwargs)
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ with hub.push_scope() as scope:
+ with capture_internal_exceptions():
+ scope.clear_breadcrumbs()
+ scope.transaction = context.function_name
+ scope.add_event_processor(_make_request_event_processor(event, context))
+
+ try:
+ return handler(event, context, *args, **kwargs)
+ except Exception:
+ exc_info = sys.exc_info()
+ event, hint = event_from_exception(
+ exc_info,
+ client_options=client.options,
+ mechanism={"type": "aws_lambda", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+ reraise(*exc_info)
+
+ return sentry_handler # type: ignore
+
+
+def _drain_queue():
+ # type: () -> None
+ with capture_internal_exceptions():
+ hub = Hub.current
+ integration = hub.get_integration(AwsLambdaIntegration)
+ if integration is not None:
+ # Flush out the event queue before AWS kills the
+ # process.
+ hub.flush()
+
+
+class AwsLambdaIntegration(Integration):
+ identifier = "aws_lambda"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ import __main__ as lambda_bootstrap # type: ignore
+
+ pre_37 = True # Python 3.6 or 2.7
+
+ if not hasattr(lambda_bootstrap, "handle_http_request"):
+ try:
+ import bootstrap as lambda_bootstrap # type: ignore
+
+ pre_37 = False # Python 3.7
+ except ImportError:
+ pass
+
+ if not hasattr(lambda_bootstrap, "handle_event_request"):
+ logger.warning(
+ "Not running in AWS Lambda environment, "
+ "AwsLambdaIntegration disabled"
+ )
+ return
+
+ if pre_37:
+ old_handle_event_request = lambda_bootstrap.handle_event_request
+
+ def sentry_handle_event_request(request_handler, *args, **kwargs):
+ # type: (Any, *Any, **Any) -> Any
+ request_handler = _wrap_handler(request_handler)
+ return old_handle_event_request(request_handler, *args, **kwargs)
+
+ lambda_bootstrap.handle_event_request = sentry_handle_event_request
+
+ old_handle_http_request = lambda_bootstrap.handle_http_request
+
+ def sentry_handle_http_request(request_handler, *args, **kwargs):
+ # type: (Any, *Any, **Any) -> Any
+ request_handler = _wrap_handler(request_handler)
+ return old_handle_http_request(request_handler, *args, **kwargs)
+
+ lambda_bootstrap.handle_http_request = sentry_handle_http_request
+
+ # Patch to_json to drain the queue. This should work even when the
+ # SDK is initialized inside of the handler
+
+ old_to_json = lambda_bootstrap.to_json
+
+ def sentry_to_json(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ _drain_queue()
+ return old_to_json(*args, **kwargs)
+
+ lambda_bootstrap.to_json = sentry_to_json
+ else:
+ old_handle_event_request = lambda_bootstrap.handle_event_request
+
+ def sentry_handle_event_request( # type: ignore
+ lambda_runtime_client, request_handler, *args, **kwargs
+ ):
+ request_handler = _wrap_handler(request_handler)
+ return old_handle_event_request(
+ lambda_runtime_client, request_handler, *args, **kwargs
+ )
+
+ lambda_bootstrap.handle_event_request = sentry_handle_event_request
+
+ # Patch the runtime client to drain the queue. This should work
+ # even when the SDK is initialized inside of the handler
+
+ def _wrap_post_function(f):
+ # type: (F) -> F
+ def inner(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ _drain_queue()
+ return f(*args, **kwargs)
+
+ return inner # type: ignore
+
+ lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = _wrap_post_function(
+ lambda_bootstrap.LambdaRuntimeClient.post_invocation_result
+ )
+ lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = _wrap_post_function(
+ lambda_bootstrap.LambdaRuntimeClient.post_invocation_error
+ )
+
+
+def _make_request_event_processor(aws_event, aws_context):
+ # type: (Any, Any) -> EventProcessor
+ start_time = datetime.now()
+
+ def event_processor(event, hint, start_time=start_time):
+ # type: (Event, Hint, datetime) -> Optional[Event]
+ extra = event.setdefault("extra", {})
+ extra["lambda"] = {
+ "function_name": aws_context.function_name,
+ "function_version": aws_context.function_version,
+ "invoked_function_arn": aws_context.invoked_function_arn,
+ "remaining_time_in_millis": aws_context.get_remaining_time_in_millis(),
+ "aws_request_id": aws_context.aws_request_id,
+ }
+
+ extra["cloudwatch logs"] = {
+ "url": _get_cloudwatch_logs_url(aws_context, start_time),
+ "log_group": aws_context.log_group_name,
+ "log_stream": aws_context.log_stream_name,
+ }
+
+ request = event.get("request", {})
+
+ if "httpMethod" in aws_event:
+ request["method"] = aws_event["httpMethod"]
+
+ request["url"] = _get_url(aws_event, aws_context)
+
+ if "queryStringParameters" in aws_event:
+ request["query_string"] = aws_event["queryStringParameters"]
+
+ if "headers" in aws_event:
+ request["headers"] = _filter_headers(aws_event["headers"])
+
+ if aws_event.get("body", None):
+ # Unfortunately couldn't find a way to get structured body from AWS
+ # event. Meaning every body is unstructured to us.
+ request["data"] = AnnotatedValue("", {"rem": [["!raw", "x", 0, 0]]})
+
+ if _should_send_default_pii():
+ user_info = event.setdefault("user", {})
+
+ id = aws_event.get("identity", {}).get("userArn")
+ if id is not None:
+ user_info.setdefault("id", id)
+
+ ip = aws_event.get("identity", {}).get("sourceIp")
+ if ip is not None:
+ user_info.setdefault("ip_address", ip)
+
+ event["request"] = request
+
+ return event
+
+ return event_processor
+
+
+def _get_url(event, context):
+ # type: (Any, Any) -> str
+ path = event.get("path", None)
+ headers = event.get("headers", {})
+ host = headers.get("Host", None)
+ proto = headers.get("X-Forwarded-Proto", None)
+ if proto and host and path:
+ return "{}://{}{}".format(proto, host, path)
+ return "awslambda:///{}".format(context.function_name)
+
+
+def _get_cloudwatch_logs_url(context, start_time):
+ # type: (Any, datetime) -> str
+ """
+ Generates a CloudWatchLogs console URL based on the context object
+
+ Arguments:
+ context {Any} -- context from lambda handler
+
+ Returns:
+ str -- AWS Console URL to logs.
+ """
+ formatstring = "%Y-%m-%dT%H:%M:%S"
+
+ url = (
+ "https://console.aws.amazon.com/cloudwatch/home?region={region}"
+ "#logEventViewer:group={log_group};stream={log_stream}"
+ ";start={start_time};end={end_time}"
+ ).format(
+ region=environ.get("AWS_REGION"),
+ log_group=context.log_group_name,
+ log_stream=context.log_stream_name,
+ start_time=(start_time - timedelta(seconds=1)).strftime(formatstring),
+ end_time=(datetime.now() + timedelta(seconds=2)).strftime(formatstring),
+ )
+
+ return url
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/beam.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/beam.py
new file mode 100644
index 0000000000..7252746a7f
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/beam.py
@@ -0,0 +1,184 @@
+from __future__ import absolute_import
+
+import sys
+import types
+from functools import wraps
+
+from sentry_sdk.hub import Hub
+from sentry_sdk._compat import reraise
+from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
+from sentry_sdk.integrations import Integration
+from sentry_sdk.integrations.logging import ignore_logger
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Iterator
+ from typing import TypeVar
+ from typing import Optional
+ from typing import Callable
+
+ from sentry_sdk.client import Client
+ from sentry_sdk._types import ExcInfo
+
+ T = TypeVar("T")
+ F = TypeVar("F", bound=Callable[..., Any])
+
+
+WRAPPED_FUNC = "_wrapped_{}_"
+INSPECT_FUNC = "_inspect_{}" # Required format per apache_beam/transforms/core.py
+USED_FUNC = "_sentry_used_"
+
+
+class BeamIntegration(Integration):
+ identifier = "beam"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ from apache_beam.transforms.core import DoFn, ParDo # type: ignore
+
+ ignore_logger("root")
+ ignore_logger("bundle_processor.create")
+
+ function_patches = ["process", "start_bundle", "finish_bundle", "setup"]
+ for func_name in function_patches:
+ setattr(
+ DoFn,
+ INSPECT_FUNC.format(func_name),
+ _wrap_inspect_call(DoFn, func_name),
+ )
+
+ old_init = ParDo.__init__
+
+ def sentry_init_pardo(self, fn, *args, **kwargs):
+ # type: (ParDo, Any, *Any, **Any) -> Any
+ # Do not monkey patch init twice
+ if not getattr(self, "_sentry_is_patched", False):
+ for func_name in function_patches:
+ if not hasattr(fn, func_name):
+ continue
+ wrapped_func = WRAPPED_FUNC.format(func_name)
+
+ # Check to see if inspect is set and process is not
+ # to avoid monkey patching process twice.
+ # Check to see if function is part of object for
+ # backwards compatibility.
+ process_func = getattr(fn, func_name)
+ inspect_func = getattr(fn, INSPECT_FUNC.format(func_name))
+ if not getattr(inspect_func, USED_FUNC, False) and not getattr(
+ process_func, USED_FUNC, False
+ ):
+ setattr(fn, wrapped_func, process_func)
+ setattr(fn, func_name, _wrap_task_call(process_func))
+
+ self._sentry_is_patched = True
+ old_init(self, fn, *args, **kwargs)
+
+ ParDo.__init__ = sentry_init_pardo
+
+
+def _wrap_inspect_call(cls, func_name):
+ # type: (Any, Any) -> Any
+ from apache_beam.typehints.decorators import getfullargspec # type: ignore
+
+ if not hasattr(cls, func_name):
+ return None
+
+ def _inspect(self):
+ # type: (Any) -> Any
+ """
+ Inspect function overrides the way Beam gets argspec.
+ """
+ wrapped_func = WRAPPED_FUNC.format(func_name)
+ if hasattr(self, wrapped_func):
+ process_func = getattr(self, wrapped_func)
+ else:
+ process_func = getattr(self, func_name)
+ setattr(self, func_name, _wrap_task_call(process_func))
+ setattr(self, wrapped_func, process_func)
+
+ # getfullargspec is deprecated in more recent beam versions and get_function_args_defaults
+ # (which uses Signatures internally) should be used instead.
+ try:
+ from apache_beam.transforms.core import get_function_args_defaults
+
+ return get_function_args_defaults(process_func)
+ except ImportError:
+ return getfullargspec(process_func)
+
+ setattr(_inspect, USED_FUNC, True)
+ return _inspect
+
+
+def _wrap_task_call(func):
+ # type: (F) -> F
+ """
+ Wrap task call with a try catch to get exceptions.
+ Pass the client on to raise_exception so it can get rebinded.
+ """
+ client = Hub.current.client
+
+ @wraps(func)
+ def _inner(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ try:
+ gen = func(*args, **kwargs)
+ except Exception:
+ raise_exception(client)
+
+ if not isinstance(gen, types.GeneratorType):
+ return gen
+ return _wrap_generator_call(gen, client)
+
+ setattr(_inner, USED_FUNC, True)
+ return _inner # type: ignore
+
+
+def _capture_exception(exc_info, hub):
+ # type: (ExcInfo, Hub) -> None
+ """
+ Send Beam exception to Sentry.
+ """
+ integration = hub.get_integration(BeamIntegration)
+ if integration is None:
+ return
+
+ client = hub.client
+ if client is None:
+ return
+
+ event, hint = event_from_exception(
+ exc_info,
+ client_options=client.options,
+ mechanism={"type": "beam", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+
+def raise_exception(client):
+ # type: (Optional[Client]) -> None
+ """
+ Raise an exception. If the client is not in the hub, rebind it.
+ """
+ hub = Hub.current
+ if hub.client is None:
+ hub.bind_client(client)
+ exc_info = sys.exc_info()
+ with capture_internal_exceptions():
+ _capture_exception(exc_info, hub)
+ reraise(*exc_info)
+
+
+def _wrap_generator_call(gen, client):
+ # type: (Iterator[T], Optional[Client]) -> Iterator[T]
+ """
+ Wrap the generator to handle any failures.
+ """
+ while True:
+ try:
+ yield next(gen)
+ except StopIteration:
+ break
+ except Exception:
+ raise_exception(client)
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/bottle.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/bottle.py
new file mode 100644
index 0000000000..80224e4dc4
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/bottle.py
@@ -0,0 +1,199 @@
+from __future__ import absolute_import
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.utils import (
+ capture_internal_exceptions,
+ event_from_exception,
+ transaction_from_function,
+)
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
+from sentry_sdk.integrations._wsgi_common import RequestExtractor
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from sentry_sdk.integrations.wsgi import _ScopedResponse
+ from typing import Any
+ from typing import Dict
+ from typing import Callable
+ from typing import Optional
+ from bottle import FileUpload, FormsDict, LocalRequest # type: ignore
+
+ from sentry_sdk._types import EventProcessor
+
+try:
+ from bottle import (
+ Bottle,
+ Route,
+ request as bottle_request,
+ HTTPResponse,
+ __version__ as BOTTLE_VERSION,
+ )
+except ImportError:
+ raise DidNotEnable("Bottle not installed")
+
+
+TRANSACTION_STYLE_VALUES = ("endpoint", "url")
+
+
+class BottleIntegration(Integration):
+ identifier = "bottle"
+
+ transaction_style = None
+
+ def __init__(self, transaction_style="endpoint"):
+ # type: (str) -> None
+
+ if transaction_style not in TRANSACTION_STYLE_VALUES:
+ raise ValueError(
+ "Invalid value for transaction_style: %s (must be in %s)"
+ % (transaction_style, TRANSACTION_STYLE_VALUES)
+ )
+ self.transaction_style = transaction_style
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+
+ try:
+ version = tuple(map(int, BOTTLE_VERSION.split(".")))
+ except (TypeError, ValueError):
+ raise DidNotEnable("Unparseable Bottle version: {}".format(version))
+
+ if version < (0, 12):
+ raise DidNotEnable("Bottle 0.12 or newer required.")
+
+ # monkey patch method Bottle.__call__
+ old_app = Bottle.__call__
+
+ def sentry_patched_wsgi_app(self, environ, start_response):
+ # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse
+
+ hub = Hub.current
+ integration = hub.get_integration(BottleIntegration)
+ if integration is None:
+ return old_app(self, environ, start_response)
+
+ return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))(
+ environ, start_response
+ )
+
+ Bottle.__call__ = sentry_patched_wsgi_app
+
+ # monkey patch method Bottle._handle
+ old_handle = Bottle._handle
+
+ def _patched_handle(self, environ):
+ # type: (Bottle, Dict[str, Any]) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(BottleIntegration)
+ if integration is None:
+ return old_handle(self, environ)
+
+ # create new scope
+ scope_manager = hub.push_scope()
+
+ with scope_manager:
+ app = self
+ with hub.configure_scope() as scope:
+ scope._name = "bottle"
+ scope.add_event_processor(
+ _make_request_event_processor(app, bottle_request, integration)
+ )
+ res = old_handle(self, environ)
+
+ # scope cleanup
+ return res
+
+ Bottle._handle = _patched_handle
+
+ # monkey patch method Route._make_callback
+ old_make_callback = Route._make_callback
+
+ def patched_make_callback(self, *args, **kwargs):
+ # type: (Route, *object, **object) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(BottleIntegration)
+ prepared_callback = old_make_callback(self, *args, **kwargs)
+ if integration is None:
+ return prepared_callback
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ def wrapped_callback(*args, **kwargs):
+ # type: (*object, **object) -> Any
+
+ try:
+ res = prepared_callback(*args, **kwargs)
+ except HTTPResponse:
+ raise
+ except Exception as exception:
+ event, hint = event_from_exception(
+ exception,
+ client_options=client.options,
+ mechanism={"type": "bottle", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+ raise exception
+
+ return res
+
+ return wrapped_callback
+
+ Route._make_callback = patched_make_callback
+
+
+class BottleRequestExtractor(RequestExtractor):
+ def env(self):
+ # type: () -> Dict[str, str]
+ return self.request.environ
+
+ def cookies(self):
+ # type: () -> Dict[str, str]
+ return self.request.cookies
+
+ def raw_data(self):
+ # type: () -> bytes
+ return self.request.body.read()
+
+ def form(self):
+ # type: () -> FormsDict
+ if self.is_json():
+ return None
+ return self.request.forms.decode()
+
+ def files(self):
+ # type: () -> Optional[Dict[str, str]]
+ if self.is_json():
+ return None
+
+ return self.request.files
+
+ def size_of_file(self, file):
+ # type: (FileUpload) -> int
+ return file.content_length
+
+
+def _make_request_event_processor(app, request, integration):
+ # type: (Bottle, LocalRequest, BottleIntegration) -> EventProcessor
+ def inner(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+
+ try:
+ if integration.transaction_style == "endpoint":
+ event["transaction"] = request.route.name or transaction_from_function(
+ request.route.callback
+ )
+ elif integration.transaction_style == "url":
+ event["transaction"] = request.route.rule
+ except Exception:
+ pass
+
+ with capture_internal_exceptions():
+ BottleRequestExtractor(request).extract_into_event(event)
+
+ return event
+
+ return inner
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/celery.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/celery.py
new file mode 100644
index 0000000000..9b58796173
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/celery.py
@@ -0,0 +1,258 @@
+from __future__ import absolute_import
+
+import functools
+import sys
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
+from sentry_sdk.tracing import Span
+from sentry_sdk._compat import reraise
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations.logging import ignore_logger
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import TypeVar
+ from typing import Callable
+ from typing import Optional
+
+ from sentry_sdk._types import EventProcessor, Event, Hint, ExcInfo
+
+ F = TypeVar("F", bound=Callable[..., Any])
+
+
+try:
+ from celery import VERSION as CELERY_VERSION # type: ignore
+ from celery.exceptions import ( # type: ignore
+ SoftTimeLimitExceeded,
+ Retry,
+ Ignore,
+ Reject,
+ )
+except ImportError:
+ raise DidNotEnable("Celery not installed")
+
+
+CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject)
+
+
+class CeleryIntegration(Integration):
+ identifier = "celery"
+
+ def __init__(self, propagate_traces=True):
+ # type: (bool) -> None
+ self.propagate_traces = propagate_traces
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ if CELERY_VERSION < (3,):
+ raise DidNotEnable("Celery 3 or newer required.")
+
+ import celery.app.trace as trace # type: ignore
+
+ old_build_tracer = trace.build_tracer
+
+ def sentry_build_tracer(name, task, *args, **kwargs):
+ # type: (Any, Any, *Any, **Any) -> Any
+ if not getattr(task, "_sentry_is_patched", False):
+ # Need to patch both methods because older celery sometimes
+ # short-circuits to task.run if it thinks it's safe.
+ task.__call__ = _wrap_task_call(task, task.__call__)
+ task.run = _wrap_task_call(task, task.run)
+ task.apply_async = _wrap_apply_async(task, task.apply_async)
+
+ # `build_tracer` is apparently called for every task
+ # invocation. Can't wrap every celery task for every invocation
+ # or we will get infinitely nested wrapper functions.
+ task._sentry_is_patched = True
+
+ return _wrap_tracer(task, old_build_tracer(name, task, *args, **kwargs))
+
+ trace.build_tracer = sentry_build_tracer
+
+ _patch_worker_exit()
+
+ # This logger logs every status of every task that ran on the worker.
+ # Meaning that every task's breadcrumbs are full of stuff like "Task
+ # <foo> raised unexpected <bar>".
+ ignore_logger("celery.worker.job")
+ ignore_logger("celery.app.trace")
+
+ # This is stdout/err redirected to a logger, can't deal with this
+ # (need event_level=logging.WARN to reproduce)
+ ignore_logger("celery.redirected")
+
+
+def _wrap_apply_async(task, f):
+ # type: (Any, F) -> F
+ @functools.wraps(f)
+ def apply_async(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(CeleryIntegration)
+ if integration is not None and integration.propagate_traces:
+ headers = None
+ for key, value in hub.iter_trace_propagation_headers():
+ if headers is None:
+ headers = dict(kwargs.get("headers") or {})
+ headers[key] = value
+ if headers is not None:
+ kwargs["headers"] = headers
+
+ with hub.start_span(op="celery.submit", description=task.name):
+ return f(*args, **kwargs)
+ else:
+ return f(*args, **kwargs)
+
+ return apply_async # type: ignore
+
+
+def _wrap_tracer(task, f):
+ # type: (Any, F) -> F
+
+ # Need to wrap tracer for pushing the scope before prerun is sent, and
+ # popping it after postrun is sent.
+ #
+ # This is the reason we don't use signals for hooking in the first place.
+ # Also because in Celery 3, signal dispatch returns early if one handler
+ # crashes.
+ @functools.wraps(f)
+ def _inner(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ hub = Hub.current
+ if hub.get_integration(CeleryIntegration) is None:
+ return f(*args, **kwargs)
+
+ with hub.push_scope() as scope:
+ scope._name = "celery"
+ scope.clear_breadcrumbs()
+ scope.add_event_processor(_make_event_processor(task, *args, **kwargs))
+
+ span = Span.continue_from_headers(args[3].get("headers") or {})
+ span.op = "celery.task"
+ span.transaction = "unknown celery task"
+
+ # Could possibly use a better hook than this one
+ span.set_status("ok")
+
+ with capture_internal_exceptions():
+ # Celery task objects are not a thing to be trusted. Even
+ # something such as attribute access can fail.
+ span.transaction = task.name
+
+ with hub.start_span(span):
+ return f(*args, **kwargs)
+
+ return _inner # type: ignore
+
+
+def _wrap_task_call(task, f):
+ # type: (Any, F) -> F
+
+ # Need to wrap task call because the exception is caught before we get to
+ # see it. Also celery's reported stacktrace is untrustworthy.
+
+ # functools.wraps is important here because celery-once looks at this
+ # method's name.
+ # https://github.com/getsentry/sentry-python/issues/421
+ @functools.wraps(f)
+ def _inner(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ try:
+ return f(*args, **kwargs)
+ except Exception:
+ exc_info = sys.exc_info()
+ with capture_internal_exceptions():
+ _capture_exception(task, exc_info)
+ reraise(*exc_info)
+
+ return _inner # type: ignore
+
+
+def _make_event_processor(task, uuid, args, kwargs, request=None):
+ # type: (Any, Any, Any, Any, Optional[Any]) -> EventProcessor
+ def event_processor(event, hint):
+ # type: (Event, Hint) -> Optional[Event]
+
+ with capture_internal_exceptions():
+ tags = event.setdefault("tags", {})
+ tags["celery_task_id"] = uuid
+ extra = event.setdefault("extra", {})
+ extra["celery-job"] = {
+ "task_name": task.name,
+ "args": args,
+ "kwargs": kwargs,
+ }
+
+ if "exc_info" in hint:
+ with capture_internal_exceptions():
+ if issubclass(hint["exc_info"][0], SoftTimeLimitExceeded):
+ event["fingerprint"] = [
+ "celery",
+ "SoftTimeLimitExceeded",
+ getattr(task, "name", task),
+ ]
+
+ return event
+
+ return event_processor
+
+
+def _capture_exception(task, exc_info):
+ # type: (Any, ExcInfo) -> None
+ hub = Hub.current
+
+ if hub.get_integration(CeleryIntegration) is None:
+ return
+ if isinstance(exc_info[1], CELERY_CONTROL_FLOW_EXCEPTIONS):
+ # ??? Doesn't map to anything
+ _set_status(hub, "aborted")
+ return
+
+ _set_status(hub, "internal_error")
+
+ if hasattr(task, "throws") and isinstance(exc_info[1], task.throws):
+ return
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ event, hint = event_from_exception(
+ exc_info,
+ client_options=client.options,
+ mechanism={"type": "celery", "handled": False},
+ )
+
+ hub.capture_event(event, hint=hint)
+
+
+def _set_status(hub, status):
+ # type: (Hub, str) -> None
+ with capture_internal_exceptions():
+ with hub.configure_scope() as scope:
+ if scope.span is not None:
+ scope.span.set_status(status)
+
+
+def _patch_worker_exit():
+ # type: () -> None
+
+ # Need to flush queue before worker shutdown because a crashing worker will
+ # call os._exit
+ from billiard.pool import Worker # type: ignore
+
+ old_workloop = Worker.workloop
+
+ def sentry_workloop(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ try:
+ return old_workloop(*args, **kwargs)
+ finally:
+ with capture_internal_exceptions():
+ hub = Hub.current
+ if hub.get_integration(CeleryIntegration) is not None:
+ hub.flush()
+
+ Worker.workloop = sentry_workloop
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/dedupe.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/dedupe.py
new file mode 100644
index 0000000000..b023df2042
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/dedupe.py
@@ -0,0 +1,43 @@
+from sentry_sdk.hub import Hub
+from sentry_sdk.utils import ContextVar
+from sentry_sdk.integrations import Integration
+from sentry_sdk.scope import add_global_event_processor
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Optional
+
+ from sentry_sdk._types import Event, Hint
+
+
+class DedupeIntegration(Integration):
+ identifier = "dedupe"
+
+ def __init__(self):
+ # type: () -> None
+ self._last_seen = ContextVar("last-seen")
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ @add_global_event_processor
+ def processor(event, hint):
+ # type: (Event, Optional[Hint]) -> Optional[Event]
+ if hint is None:
+ return event
+
+ integration = Hub.current.get_integration(DedupeIntegration)
+
+ if integration is None:
+ return event
+
+ exc_info = hint.get("exc_info", None)
+ if exc_info is None:
+ return event
+
+ exc = exc_info[1]
+ if integration._last_seen.get(None) is exc:
+ return None
+ integration._last_seen.set(exc)
+ return event
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/__init__.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/__init__.py
new file mode 100644
index 0000000000..4e62fe3b74
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/__init__.py
@@ -0,0 +1,484 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import sys
+import threading
+import weakref
+
+from sentry_sdk._types import MYPY
+from sentry_sdk.hub import Hub, _should_send_default_pii
+from sentry_sdk.scope import add_global_event_processor
+from sentry_sdk.serializer import add_global_repr_processor
+from sentry_sdk.tracing import record_sql_queries
+from sentry_sdk.utils import (
+ HAS_REAL_CONTEXTVARS,
+ logger,
+ capture_internal_exceptions,
+ event_from_exception,
+ transaction_from_function,
+ walk_exception_chain,
+)
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations.logging import ignore_logger
+from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
+from sentry_sdk.integrations._wsgi_common import RequestExtractor
+
+try:
+ from django import VERSION as DJANGO_VERSION
+ from django.core import signals
+
+ try:
+ from django.urls import resolve
+ except ImportError:
+ from django.core.urlresolvers import resolve
+except ImportError:
+ raise DidNotEnable("Django not installed")
+
+
+from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
+from sentry_sdk.integrations.django.templates import get_template_frame_from_exception
+from sentry_sdk.integrations.django.middleware import patch_django_middlewares
+
+
+if MYPY:
+ from typing import Any
+ from typing import Callable
+ from typing import Dict
+ from typing import Optional
+ from typing import Union
+ from typing import List
+
+ from django.core.handlers.wsgi import WSGIRequest
+ from django.http.response import HttpResponse
+ from django.http.request import QueryDict
+ from django.utils.datastructures import MultiValueDict
+
+ from sentry_sdk.integrations.wsgi import _ScopedResponse
+ from sentry_sdk._types import Event, Hint, EventProcessor, NotImplementedType
+
+
+if DJANGO_VERSION < (1, 10):
+
+ def is_authenticated(request_user):
+ # type: (Any) -> bool
+ return request_user.is_authenticated()
+
+
+else:
+
+ def is_authenticated(request_user):
+ # type: (Any) -> bool
+ return request_user.is_authenticated
+
+
+TRANSACTION_STYLE_VALUES = ("function_name", "url")
+
+
+class DjangoIntegration(Integration):
+ identifier = "django"
+
+ transaction_style = None
+ middleware_spans = None
+
+ def __init__(self, transaction_style="url", middleware_spans=True):
+ # type: (str, bool) -> None
+ if transaction_style not in TRANSACTION_STYLE_VALUES:
+ raise ValueError(
+ "Invalid value for transaction_style: %s (must be in %s)"
+ % (transaction_style, TRANSACTION_STYLE_VALUES)
+ )
+ self.transaction_style = transaction_style
+ self.middleware_spans = middleware_spans
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+
+ if DJANGO_VERSION < (1, 6):
+ raise DidNotEnable("Django 1.6 or newer is required.")
+
+ install_sql_hook()
+ # Patch in our custom middleware.
+
+ # logs an error for every 500
+ ignore_logger("django.server")
+ ignore_logger("django.request")
+
+ from django.core.handlers.wsgi import WSGIHandler
+
+ old_app = WSGIHandler.__call__
+
+ def sentry_patched_wsgi_handler(self, environ, start_response):
+ # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse
+ if Hub.current.get_integration(DjangoIntegration) is None:
+ return old_app(self, environ, start_response)
+
+ bound_old_app = old_app.__get__(self, WSGIHandler)
+
+ return SentryWsgiMiddleware(bound_old_app)(environ, start_response)
+
+ WSGIHandler.__call__ = sentry_patched_wsgi_handler
+
+ _patch_django_asgi_handler()
+
+ # patch get_response, because at that point we have the Django request
+ # object
+ from django.core.handlers.base import BaseHandler
+
+ old_get_response = BaseHandler.get_response
+
+ def sentry_patched_get_response(self, request):
+ # type: (Any, WSGIRequest) -> Union[HttpResponse, BaseException]
+ hub = Hub.current
+ integration = hub.get_integration(DjangoIntegration)
+ if integration is not None:
+ _patch_drf()
+
+ with hub.configure_scope() as scope:
+ # Rely on WSGI middleware to start a trace
+ try:
+ if integration.transaction_style == "function_name":
+ scope.transaction = transaction_from_function(
+ resolve(request.path).func
+ )
+ elif integration.transaction_style == "url":
+ scope.transaction = LEGACY_RESOLVER.resolve(request.path)
+ except Exception:
+ pass
+
+ scope.add_event_processor(
+ _make_event_processor(weakref.ref(request), integration)
+ )
+ return old_get_response(self, request)
+
+ BaseHandler.get_response = sentry_patched_get_response
+
+ signals.got_request_exception.connect(_got_request_exception)
+
+ @add_global_event_processor
+ def process_django_templates(event, hint):
+ # type: (Event, Optional[Hint]) -> Optional[Event]
+ if hint is None:
+ return event
+
+ exc_info = hint.get("exc_info", None)
+
+ if exc_info is None:
+ return event
+
+ exception = event.get("exception", None)
+
+ if exception is None:
+ return event
+
+ values = exception.get("values", None)
+
+ if values is None:
+ return event
+
+ for exception, (_, exc_value, _) in zip(
+ reversed(values), walk_exception_chain(exc_info)
+ ):
+ frame = get_template_frame_from_exception(exc_value)
+ if frame is not None:
+ frames = exception.get("stacktrace", {}).get("frames", [])
+
+ for i in reversed(range(len(frames))):
+ f = frames[i]
+ if (
+ f.get("function") in ("parse", "render")
+ and f.get("module") == "django.template.base"
+ ):
+ i += 1
+ break
+ else:
+ i = len(frames)
+
+ frames.insert(i, frame)
+
+ return event
+
+ @add_global_repr_processor
+ def _django_queryset_repr(value, hint):
+ # type: (Any, Dict[str, Any]) -> Union[NotImplementedType, str]
+ try:
+ # Django 1.6 can fail to import `QuerySet` when Django settings
+ # have not yet been initialized.
+ #
+ # If we fail to import, return `NotImplemented`. It's at least
+ # unlikely that we have a query set in `value` when importing
+ # `QuerySet` fails.
+ from django.db.models.query import QuerySet
+ except Exception:
+ return NotImplemented
+
+ if not isinstance(value, QuerySet) or value._result_cache:
+ return NotImplemented
+
+ # Do not call Hub.get_integration here. It is intentional that
+ # running under a new hub does not suddenly start executing
+ # querysets. This might be surprising to the user but it's likely
+ # less annoying.
+
+ return u"<%s from %s at 0x%x>" % (
+ value.__class__.__name__,
+ value.__module__,
+ id(value),
+ )
+
+ _patch_channels()
+ patch_django_middlewares()
+
+
+_DRF_PATCHED = False
+_DRF_PATCH_LOCK = threading.Lock()
+
+
+def _patch_drf():
+ # type: () -> None
+ """
+ Patch Django Rest Framework for more/better request data. DRF's request
+ type is a wrapper around Django's request type. The attribute we're
+ interested in is `request.data`, which is a cached property containing a
+ parsed request body. Reading a request body from that property is more
+ reliable than reading from any of Django's own properties, as those don't
+ hold payloads in memory and therefore can only be accessed once.
+
+ We patch the Django request object to include a weak backreference to the
+ DRF request object, such that we can later use either in
+ `DjangoRequestExtractor`.
+
+ This function is not called directly on SDK setup, because importing almost
+ any part of Django Rest Framework will try to access Django settings (where
+ `sentry_sdk.init()` might be called from in the first place). Instead we
+ run this function on every request and do the patching on the first
+ request.
+ """
+
+ global _DRF_PATCHED
+
+ if _DRF_PATCHED:
+ # Double-checked locking
+ return
+
+ with _DRF_PATCH_LOCK:
+ if _DRF_PATCHED:
+ return
+
+ # We set this regardless of whether the code below succeeds or fails.
+ # There is no point in trying to patch again on the next request.
+ _DRF_PATCHED = True
+
+ with capture_internal_exceptions():
+ try:
+ from rest_framework.views import APIView # type: ignore
+ except ImportError:
+ pass
+ else:
+ old_drf_initial = APIView.initial
+
+ def sentry_patched_drf_initial(self, request, *args, **kwargs):
+ # type: (APIView, Any, *Any, **Any) -> Any
+ with capture_internal_exceptions():
+ request._request._sentry_drf_request_backref = weakref.ref(
+ request
+ )
+ pass
+ return old_drf_initial(self, request, *args, **kwargs)
+
+ APIView.initial = sentry_patched_drf_initial
+
+
+def _patch_channels():
+ # type: () -> None
+ try:
+ from channels.http import AsgiHandler # type: ignore
+ except ImportError:
+ return
+
+ if not HAS_REAL_CONTEXTVARS:
+ # We better have contextvars or we're going to leak state between
+ # requests.
+ #
+ # We cannot hard-raise here because channels may not be used at all in
+ # the current process.
+ logger.warning(
+ "We detected that you are using Django channels 2.0. To get proper "
+ "instrumentation for ASGI requests, the Sentry SDK requires "
+ "Python 3.7+ or the aiocontextvars package from PyPI."
+ )
+
+ from sentry_sdk.integrations.django.asgi import patch_channels_asgi_handler_impl
+
+ patch_channels_asgi_handler_impl(AsgiHandler)
+
+
+def _patch_django_asgi_handler():
+ # type: () -> None
+ try:
+ from django.core.handlers.asgi import ASGIHandler
+ except ImportError:
+ return
+
+ if not HAS_REAL_CONTEXTVARS:
+ # We better have contextvars or we're going to leak state between
+ # requests.
+ #
+ # We cannot hard-raise here because Django may not be used at all in
+ # the current process.
+ logger.warning(
+ "We detected that you are using Django 3. To get proper "
+ "instrumentation for ASGI requests, the Sentry SDK requires "
+ "Python 3.7+ or the aiocontextvars package from PyPI."
+ )
+
+ from sentry_sdk.integrations.django.asgi import patch_django_asgi_handler_impl
+
+ patch_django_asgi_handler_impl(ASGIHandler)
+
+
+def _make_event_processor(weak_request, integration):
+ # type: (Callable[[], WSGIRequest], DjangoIntegration) -> EventProcessor
+ def event_processor(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+ # if the request is gone we are fine not logging the data from
+ # it. This might happen if the processor is pushed away to
+ # another thread.
+ request = weak_request()
+ if request is None:
+ return event
+
+ try:
+ drf_request = request._sentry_drf_request_backref()
+ if drf_request is not None:
+ request = drf_request
+ except AttributeError:
+ pass
+
+ with capture_internal_exceptions():
+ DjangoRequestExtractor(request).extract_into_event(event)
+
+ if _should_send_default_pii():
+ with capture_internal_exceptions():
+ _set_user_info(request, event)
+
+ return event
+
+ return event_processor
+
+
+def _got_request_exception(request=None, **kwargs):
+ # type: (WSGIRequest, **Any) -> None
+ hub = Hub.current
+ integration = hub.get_integration(DjangoIntegration)
+ if integration is not None:
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ event, hint = event_from_exception(
+ sys.exc_info(),
+ client_options=client.options,
+ mechanism={"type": "django", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+
+class DjangoRequestExtractor(RequestExtractor):
+ def env(self):
+ # type: () -> Dict[str, str]
+ return self.request.META
+
+ def cookies(self):
+ # type: () -> Dict[str, str]
+ return self.request.COOKIES
+
+ def raw_data(self):
+ # type: () -> bytes
+ return self.request.body
+
+ def form(self):
+ # type: () -> QueryDict
+ return self.request.POST
+
+ def files(self):
+ # type: () -> MultiValueDict
+ return self.request.FILES
+
+ def size_of_file(self, file):
+ # type: (Any) -> int
+ return file.size
+
+ def parsed_body(self):
+ # type: () -> Optional[Dict[str, Any]]
+ try:
+ return self.request.data
+ except AttributeError:
+ return RequestExtractor.parsed_body(self)
+
+
+def _set_user_info(request, event):
+ # type: (WSGIRequest, Dict[str, Any]) -> None
+ user_info = event.setdefault("user", {})
+
+ user = getattr(request, "user", None)
+
+ if user is None or not is_authenticated(user):
+ return
+
+ try:
+ user_info.setdefault("id", str(user.pk))
+ except Exception:
+ pass
+
+ try:
+ user_info.setdefault("email", user.email)
+ except Exception:
+ pass
+
+ try:
+ user_info.setdefault("username", user.get_username())
+ except Exception:
+ pass
+
+
+def install_sql_hook():
+ # type: () -> None
+ """If installed this causes Django's queries to be captured."""
+ try:
+ from django.db.backends.utils import CursorWrapper
+ except ImportError:
+ from django.db.backends.util import CursorWrapper
+
+ try:
+ real_execute = CursorWrapper.execute
+ real_executemany = CursorWrapper.executemany
+ except AttributeError:
+ # This won't work on Django versions < 1.6
+ return
+
+ def execute(self, sql, params=None):
+ # type: (CursorWrapper, Any, Optional[Any]) -> Any
+ hub = Hub.current
+ if hub.get_integration(DjangoIntegration) is None:
+ return real_execute(self, sql, params)
+
+ with record_sql_queries(
+ hub, self.cursor, sql, params, paramstyle="format", executemany=False
+ ):
+ return real_execute(self, sql, params)
+
+ def executemany(self, sql, param_list):
+ # type: (CursorWrapper, Any, List[Any]) -> Any
+ hub = Hub.current
+ if hub.get_integration(DjangoIntegration) is None:
+ return real_executemany(self, sql, param_list)
+
+ with record_sql_queries(
+ hub, self.cursor, sql, param_list, paramstyle="format", executemany=True
+ ):
+ return real_executemany(self, sql, param_list)
+
+ CursorWrapper.execute = execute
+ CursorWrapper.executemany = executemany
+ ignore_logger("django.db.backends")
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/asgi.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/asgi.py
new file mode 100644
index 0000000000..96ae3e0809
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/asgi.py
@@ -0,0 +1,47 @@
+"""
+Instrumentation for Django 3.0
+
+Since this file contains `async def` it is conditionally imported in
+`sentry_sdk.integrations.django` (depending on the existence of
+`django.core.handlers.asgi`.
+"""
+
+from sentry_sdk import Hub
+from sentry_sdk._types import MYPY
+
+from sentry_sdk.integrations.django import DjangoIntegration
+from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
+
+if MYPY:
+ from typing import Any
+
+
+def patch_django_asgi_handler_impl(cls):
+ # type: (Any) -> None
+ old_app = cls.__call__
+
+ async def sentry_patched_asgi_handler(self, scope, receive, send):
+ # type: (Any, Any, Any, Any) -> Any
+ if Hub.current.get_integration(DjangoIntegration) is None:
+ return await old_app(self, scope, receive, send)
+
+ middleware = SentryAsgiMiddleware(old_app.__get__(self, cls))._run_asgi3
+ return await middleware(scope, receive, send)
+
+ cls.__call__ = sentry_patched_asgi_handler
+
+
+def patch_channels_asgi_handler_impl(cls):
+ # type: (Any) -> None
+ old_app = cls.__call__
+
+ async def sentry_patched_asgi_handler(self, receive, send):
+ # type: (Any, Any, Any) -> Any
+ if Hub.current.get_integration(DjangoIntegration) is None:
+ return await old_app(self, receive, send)
+
+ middleware = SentryAsgiMiddleware(lambda _scope: old_app.__get__(self, cls))
+
+ return await middleware(self.scope)(receive, send)
+
+ cls.__call__ = sentry_patched_asgi_handler
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/middleware.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/middleware.py
new file mode 100644
index 0000000000..edbeccb093
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/middleware.py
@@ -0,0 +1,136 @@
+"""
+Create spans from Django middleware invocations
+"""
+
+from functools import wraps
+
+from django import VERSION as DJANGO_VERSION
+
+from sentry_sdk import Hub
+from sentry_sdk.utils import (
+ ContextVar,
+ transaction_from_function,
+ capture_internal_exceptions,
+)
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Callable
+ from typing import TypeVar
+
+ F = TypeVar("F", bound=Callable[..., Any])
+
+_import_string_should_wrap_middleware = ContextVar(
+ "import_string_should_wrap_middleware"
+)
+
+if DJANGO_VERSION < (1, 7):
+ import_string_name = "import_by_path"
+else:
+ import_string_name = "import_string"
+
+
+def patch_django_middlewares():
+ # type: () -> None
+ from django.core.handlers import base
+
+ old_import_string = getattr(base, import_string_name)
+
+ def sentry_patched_import_string(dotted_path):
+ # type: (str) -> Any
+ rv = old_import_string(dotted_path)
+
+ if _import_string_should_wrap_middleware.get(None):
+ rv = _wrap_middleware(rv, dotted_path)
+
+ return rv
+
+ setattr(base, import_string_name, sentry_patched_import_string)
+
+ old_load_middleware = base.BaseHandler.load_middleware
+
+ def sentry_patched_load_middleware(self):
+ # type: (base.BaseHandler) -> Any
+ _import_string_should_wrap_middleware.set(True)
+ try:
+ return old_load_middleware(self)
+ finally:
+ _import_string_should_wrap_middleware.set(False)
+
+ base.BaseHandler.load_middleware = sentry_patched_load_middleware
+
+
+def _wrap_middleware(middleware, middleware_name):
+ # type: (Any, str) -> Any
+ from sentry_sdk.integrations.django import DjangoIntegration
+
+ def _get_wrapped_method(old_method):
+ # type: (F) -> F
+ with capture_internal_exceptions():
+
+ def sentry_wrapped_method(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(DjangoIntegration)
+ if integration is None or not integration.middleware_spans:
+ return old_method(*args, **kwargs)
+
+ function_name = transaction_from_function(old_method)
+
+ description = middleware_name
+ function_basename = getattr(old_method, "__name__", None)
+ if function_basename:
+ description = "{}.{}".format(description, function_basename)
+
+ with hub.start_span(
+ op="django.middleware", description=description
+ ) as span:
+ span.set_tag("django.function_name", function_name)
+ span.set_tag("django.middleware_name", middleware_name)
+ return old_method(*args, **kwargs)
+
+ try:
+ # fails for __call__ of function on Python 2 (see py2.7-django-1.11)
+ return wraps(old_method)(sentry_wrapped_method) # type: ignore
+ except Exception:
+ return sentry_wrapped_method # type: ignore
+
+ return old_method
+
+ class SentryWrappingMiddleware(object):
+ def __init__(self, *args, **kwargs):
+ # type: (*Any, **Any) -> None
+ self._inner = middleware(*args, **kwargs)
+ self._call_method = None
+
+ # We need correct behavior for `hasattr()`, which we can only determine
+ # when we have an instance of the middleware we're wrapping.
+ def __getattr__(self, method_name):
+ # type: (str) -> Any
+ if method_name not in (
+ "process_request",
+ "process_view",
+ "process_template_response",
+ "process_response",
+ "process_exception",
+ ):
+ raise AttributeError()
+
+ old_method = getattr(self._inner, method_name)
+ rv = _get_wrapped_method(old_method)
+ self.__dict__[method_name] = rv
+ return rv
+
+ def __call__(self, *args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ f = self._call_method
+ if f is None:
+ self._call_method = f = _get_wrapped_method(self._inner.__call__)
+ return f(*args, **kwargs)
+
+ if hasattr(middleware, "__name__"):
+ SentryWrappingMiddleware.__name__ = middleware.__name__
+
+ return SentryWrappingMiddleware
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/templates.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/templates.py
new file mode 100644
index 0000000000..2285644909
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/templates.py
@@ -0,0 +1,121 @@
+from django.template import TemplateSyntaxError
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Dict
+ from typing import Optional
+ from typing import Iterator
+ from typing import Tuple
+
+try:
+ # support Django 1.9
+ from django.template.base import Origin
+except ImportError:
+ # backward compatibility
+ from django.template.loader import LoaderOrigin as Origin
+
+
+def get_template_frame_from_exception(exc_value):
+ # type: (Optional[BaseException]) -> Optional[Dict[str, Any]]
+
+ # As of Django 1.9 or so the new template debug thing showed up.
+ if hasattr(exc_value, "template_debug"):
+ return _get_template_frame_from_debug(exc_value.template_debug) # type: ignore
+
+ # As of r16833 (Django) all exceptions may contain a
+ # ``django_template_source`` attribute (rather than the legacy
+ # ``TemplateSyntaxError.source`` check)
+ if hasattr(exc_value, "django_template_source"):
+ return _get_template_frame_from_source(
+ exc_value.django_template_source # type: ignore
+ )
+
+ if isinstance(exc_value, TemplateSyntaxError) and hasattr(exc_value, "source"):
+ source = exc_value.source
+ if isinstance(source, (tuple, list)) and isinstance(source[0], Origin):
+ return _get_template_frame_from_source(source) # type: ignore
+
+ return None
+
+
+def _get_template_frame_from_debug(debug):
+ # type: (Dict[str, Any]) -> Dict[str, Any]
+ if debug is None:
+ return None
+
+ lineno = debug["line"]
+ filename = debug["name"]
+ if filename is None:
+ filename = "<django template>"
+
+ pre_context = []
+ post_context = []
+ context_line = None
+
+ for i, line in debug["source_lines"]:
+ if i < lineno:
+ pre_context.append(line)
+ elif i > lineno:
+ post_context.append(line)
+ else:
+ context_line = line
+
+ return {
+ "filename": filename,
+ "lineno": lineno,
+ "pre_context": pre_context[-5:],
+ "post_context": post_context[:5],
+ "context_line": context_line,
+ "in_app": True,
+ }
+
+
+def _linebreak_iter(template_source):
+ # type: (str) -> Iterator[int]
+ yield 0
+ p = template_source.find("\n")
+ while p >= 0:
+ yield p + 1
+ p = template_source.find("\n", p + 1)
+
+
+def _get_template_frame_from_source(source):
+ # type: (Tuple[Origin, Tuple[int, int]]) -> Optional[Dict[str, Any]]
+ if not source:
+ return None
+
+ origin, (start, end) = source
+ filename = getattr(origin, "loadname", None)
+ if filename is None:
+ filename = "<django template>"
+ template_source = origin.reload()
+ lineno = None
+ upto = 0
+ pre_context = []
+ post_context = []
+ context_line = None
+
+ for num, next in enumerate(_linebreak_iter(template_source)):
+ line = template_source[upto:next]
+ if start >= upto and end <= next:
+ lineno = num
+ context_line = line
+ elif lineno is None:
+ pre_context.append(line)
+ else:
+ post_context.append(line)
+
+ upto = next
+
+ if context_line is None or lineno is None:
+ return None
+
+ return {
+ "filename": filename,
+ "lineno": lineno,
+ "pre_context": pre_context[-5:],
+ "post_context": post_context[:5],
+ "context_line": context_line,
+ }
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/django/transactions.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/transactions.py
new file mode 100644
index 0000000000..f20866ef95
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/django/transactions.py
@@ -0,0 +1,134 @@
+"""
+Copied from raven-python. Used for
+`DjangoIntegration(transaction_fron="raven_legacy")`.
+"""
+
+from __future__ import absolute_import
+
+import re
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from django.urls.resolvers import URLResolver
+ from typing import Dict
+ from typing import List
+ from typing import Optional
+ from django.urls.resolvers import URLPattern
+ from typing import Tuple
+ from typing import Union
+ from re import Pattern
+
+try:
+ from django.urls import get_resolver
+except ImportError:
+ from django.core.urlresolvers import get_resolver
+
+
+def get_regex(resolver_or_pattern):
+ # type: (Union[URLPattern, URLResolver]) -> Pattern[str]
+ """Utility method for django's deprecated resolver.regex"""
+ try:
+ regex = resolver_or_pattern.regex
+ except AttributeError:
+ regex = resolver_or_pattern.pattern.regex
+ return regex
+
+
+class RavenResolver(object):
+ _optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)")
+ _named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)")
+ _non_named_group_matcher = re.compile(r"\([^\)]+\)")
+ # [foo|bar|baz]
+ _either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]")
+ _camel_re = re.compile(r"([A-Z]+)([a-z])")
+
+ _cache = {} # type: Dict[URLPattern, str]
+
+ def _simplify(self, pattern):
+ # type: (str) -> str
+ r"""
+ Clean up urlpattern regexes into something readable by humans:
+
+ From:
+ > "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$"
+
+ To:
+ > "{sport_slug}/athletes/{athlete_slug}/"
+ """
+ # remove optional params
+ # TODO(dcramer): it'd be nice to change these into [%s] but it currently
+ # conflicts with the other rules because we're doing regexp matches
+ # rather than parsing tokens
+ result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), pattern)
+
+ # handle named groups first
+ result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result)
+
+ # handle non-named groups
+ result = self._non_named_group_matcher.sub("{var}", result)
+
+ # handle optional params
+ result = self._either_option_matcher.sub(lambda m: m.group(1), result)
+
+ # clean up any outstanding regex-y characters.
+ result = (
+ result.replace("^", "")
+ .replace("$", "")
+ .replace("?", "")
+ .replace("//", "/")
+ .replace("\\", "")
+ )
+
+ return result
+
+ def _resolve(self, resolver, path, parents=None):
+ # type: (URLResolver, str, Optional[List[URLResolver]]) -> Optional[str]
+
+ match = get_regex(resolver).search(path) # Django < 2.0
+
+ if not match:
+ return None
+
+ if parents is None:
+ parents = [resolver]
+ elif resolver not in parents:
+ parents = parents + [resolver]
+
+ new_path = path[match.end() :]
+ for pattern in resolver.url_patterns:
+ # this is an include()
+ if not pattern.callback:
+ match_ = self._resolve(pattern, new_path, parents)
+ if match_:
+ return match_
+ continue
+ elif not get_regex(pattern).search(new_path):
+ continue
+
+ try:
+ return self._cache[pattern]
+ except KeyError:
+ pass
+
+ prefix = "".join(self._simplify(get_regex(p).pattern) for p in parents)
+ result = prefix + self._simplify(get_regex(pattern).pattern)
+ if not result.startswith("/"):
+ result = "/" + result
+ self._cache[pattern] = result
+ return result
+
+ return None
+
+ def resolve(
+ self,
+ path, # type: str
+ urlconf=None, # type: Union[None, Tuple[URLPattern, URLPattern, URLResolver], Tuple[URLPattern]]
+ ):
+ # type: (...) -> str
+ resolver = get_resolver(urlconf)
+ match = self._resolve(resolver, path)
+ return match or path
+
+
+LEGACY_RESOLVER = RavenResolver()
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/excepthook.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/excepthook.py
new file mode 100644
index 0000000000..d8aead097a
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/excepthook.py
@@ -0,0 +1,76 @@
+import sys
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
+from sentry_sdk.integrations import Integration
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Callable
+ from typing import Any
+ from typing import Type
+
+ from types import TracebackType
+
+ Excepthook = Callable[
+ [Type[BaseException], BaseException, TracebackType], Any,
+ ]
+
+
+class ExcepthookIntegration(Integration):
+ identifier = "excepthook"
+
+ always_run = False
+
+ def __init__(self, always_run=False):
+ # type: (bool) -> None
+
+ if not isinstance(always_run, bool):
+ raise ValueError(
+ "Invalid value for always_run: %s (must be type boolean)"
+ % (always_run,)
+ )
+ self.always_run = always_run
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ sys.excepthook = _make_excepthook(sys.excepthook)
+
+
+def _make_excepthook(old_excepthook):
+ # type: (Excepthook) -> Excepthook
+ def sentry_sdk_excepthook(type_, value, traceback):
+ # type: (Type[BaseException], BaseException, TracebackType) -> None
+ hub = Hub.current
+ integration = hub.get_integration(ExcepthookIntegration)
+
+ if integration is not None and _should_send(integration.always_run):
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ with capture_internal_exceptions():
+ event, hint = event_from_exception(
+ (type_, value, traceback),
+ client_options=client.options,
+ mechanism={"type": "excepthook", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+ return old_excepthook(type_, value, traceback)
+
+ return sentry_sdk_excepthook
+
+
+def _should_send(always_run=False):
+ # type: (bool) -> bool
+ if always_run:
+ return True
+
+ if hasattr(sys, "ps1"):
+ # Disable the excepthook for interactive Python shells, otherwise
+ # every typo gets sent to Sentry.
+ return False
+
+ return True
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/falcon.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/falcon.py
new file mode 100644
index 0000000000..b24aac41c6
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/falcon.py
@@ -0,0 +1,209 @@
+from __future__ import absolute_import
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations._wsgi_common import RequestExtractor
+from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
+from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Dict
+ from typing import Optional
+
+ from sentry_sdk._types import EventProcessor
+
+try:
+ import falcon # type: ignore
+ import falcon.api_helpers # type: ignore
+
+ from falcon import __version__ as FALCON_VERSION
+except ImportError:
+ raise DidNotEnable("Falcon not installed")
+
+
+class FalconRequestExtractor(RequestExtractor):
+ def env(self):
+ # type: () -> Dict[str, Any]
+ return self.request.env
+
+ def cookies(self):
+ # type: () -> Dict[str, Any]
+ return self.request.cookies
+
+ def form(self):
+ # type: () -> None
+ return None # No such concept in Falcon
+
+ def files(self):
+ # type: () -> None
+ return None # No such concept in Falcon
+
+ def raw_data(self):
+ # type: () -> Optional[str]
+
+ # As request data can only be read once we won't make this available
+ # to Sentry. Just send back a dummy string in case there was a
+ # content length.
+ # TODO(jmagnusson): Figure out if there's a way to support this
+ content_length = self.content_length()
+ if content_length > 0:
+ return "[REQUEST_CONTAINING_RAW_DATA]"
+ else:
+ return None
+
+ def json(self):
+ # type: () -> Optional[Dict[str, Any]]
+ try:
+ return self.request.media
+ except falcon.errors.HTTPBadRequest:
+ # NOTE(jmagnusson): We return `falcon.Request._media` here because
+ # falcon 1.4 doesn't do proper type checking in
+ # `falcon.Request.media`. This has been fixed in 2.0.
+ # Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
+ return self.request._media
+
+
+class SentryFalconMiddleware(object):
+ """Captures exceptions in Falcon requests and send to Sentry"""
+
+ def process_request(self, req, resp, *args, **kwargs):
+ # type: (Any, Any, *Any, **Any) -> None
+ hub = Hub.current
+ integration = hub.get_integration(FalconIntegration)
+ if integration is None:
+ return
+
+ with hub.configure_scope() as scope:
+ scope._name = "falcon"
+ scope.add_event_processor(_make_request_event_processor(req, integration))
+
+
+TRANSACTION_STYLE_VALUES = ("uri_template", "path")
+
+
+class FalconIntegration(Integration):
+ identifier = "falcon"
+
+ transaction_style = None
+
+ def __init__(self, transaction_style="uri_template"):
+ # type: (str) -> None
+ if transaction_style not in TRANSACTION_STYLE_VALUES:
+ raise ValueError(
+ "Invalid value for transaction_style: %s (must be in %s)"
+ % (transaction_style, TRANSACTION_STYLE_VALUES)
+ )
+ self.transaction_style = transaction_style
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ try:
+ version = tuple(map(int, FALCON_VERSION.split(".")))
+ except (ValueError, TypeError):
+ raise DidNotEnable("Unparseable Falcon version: {}".format(FALCON_VERSION))
+
+ if version < (1, 4):
+ raise DidNotEnable("Falcon 1.4 or newer required.")
+
+ _patch_wsgi_app()
+ _patch_handle_exception()
+ _patch_prepare_middleware()
+
+
+def _patch_wsgi_app():
+ # type: () -> None
+ original_wsgi_app = falcon.API.__call__
+
+ def sentry_patched_wsgi_app(self, env, start_response):
+ # type: (falcon.API, Any, Any) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(FalconIntegration)
+ if integration is None:
+ return original_wsgi_app(self, env, start_response)
+
+ sentry_wrapped = SentryWsgiMiddleware(
+ lambda envi, start_resp: original_wsgi_app(self, envi, start_resp)
+ )
+
+ return sentry_wrapped(env, start_response)
+
+ falcon.API.__call__ = sentry_patched_wsgi_app
+
+
+def _patch_handle_exception():
+ # type: () -> None
+ original_handle_exception = falcon.API._handle_exception
+
+ def sentry_patched_handle_exception(self, *args):
+ # type: (falcon.API, *Any) -> Any
+ # NOTE(jmagnusson): falcon 2.0 changed falcon.API._handle_exception
+ # method signature from `(ex, req, resp, params)` to
+ # `(req, resp, ex, params)`
+ if isinstance(args[0], Exception):
+ ex = args[0]
+ else:
+ ex = args[2]
+
+ was_handled = original_handle_exception(self, *args)
+
+ hub = Hub.current
+ integration = hub.get_integration(FalconIntegration)
+
+ if integration is not None and not _is_falcon_http_error(ex):
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ event, hint = event_from_exception(
+ ex,
+ client_options=client.options,
+ mechanism={"type": "falcon", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+ return was_handled
+
+ falcon.API._handle_exception = sentry_patched_handle_exception
+
+
+def _patch_prepare_middleware():
+ # type: () -> None
+ original_prepare_middleware = falcon.api_helpers.prepare_middleware
+
+ def sentry_patched_prepare_middleware(
+ middleware=None, independent_middleware=False
+ ):
+ # type: (Any, Any) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(FalconIntegration)
+ if integration is not None:
+ middleware = [SentryFalconMiddleware()] + (middleware or [])
+ return original_prepare_middleware(middleware, independent_middleware)
+
+ falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware
+
+
+def _is_falcon_http_error(ex):
+ # type: (BaseException) -> bool
+ return isinstance(ex, (falcon.HTTPError, falcon.http_status.HTTPStatus))
+
+
+def _make_request_event_processor(req, integration):
+ # type: (falcon.Request, FalconIntegration) -> EventProcessor
+
+ def inner(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+ if integration.transaction_style == "uri_template":
+ event["transaction"] = req.uri_template
+ elif integration.transaction_style == "path":
+ event["transaction"] = req.path
+
+ with capture_internal_exceptions():
+ FalconRequestExtractor(req).extract_into_event(event)
+
+ return event
+
+ return inner
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/flask.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/flask.py
new file mode 100644
index 0000000000..ef6ae0e4f0
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/flask.py
@@ -0,0 +1,260 @@
+from __future__ import absolute_import
+
+import weakref
+
+from sentry_sdk.hub import Hub, _should_send_default_pii
+from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
+from sentry_sdk.integrations._wsgi_common import RequestExtractor
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from sentry_sdk.integrations.wsgi import _ScopedResponse
+ from typing import Any
+ from typing import Dict
+ from werkzeug.datastructures import ImmutableTypeConversionDict
+ from werkzeug.datastructures import ImmutableMultiDict
+ from werkzeug.datastructures import FileStorage
+ from typing import Union
+ from typing import Callable
+
+ from sentry_sdk._types import EventProcessor
+
+
+try:
+ import flask_login # type: ignore
+except ImportError:
+ flask_login = None
+
+try:
+ from flask import ( # type: ignore
+ Request,
+ Flask,
+ _request_ctx_stack,
+ _app_ctx_stack,
+ __version__ as FLASK_VERSION,
+ )
+ from flask.signals import (
+ appcontext_pushed,
+ appcontext_tearing_down,
+ got_request_exception,
+ request_started,
+ )
+except ImportError:
+ raise DidNotEnable("Flask is not installed")
+
+
+TRANSACTION_STYLE_VALUES = ("endpoint", "url")
+
+
+class FlaskIntegration(Integration):
+ identifier = "flask"
+
+ transaction_style = None
+
+ def __init__(self, transaction_style="endpoint"):
+ # type: (str) -> None
+ if transaction_style not in TRANSACTION_STYLE_VALUES:
+ raise ValueError(
+ "Invalid value for transaction_style: %s (must be in %s)"
+ % (transaction_style, TRANSACTION_STYLE_VALUES)
+ )
+ self.transaction_style = transaction_style
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ try:
+ version = tuple(map(int, FLASK_VERSION.split(".")[:3]))
+ except (ValueError, TypeError):
+ raise DidNotEnable("Unparseable Flask version: {}".format(FLASK_VERSION))
+
+ if version < (0, 11):
+ raise DidNotEnable("Flask 0.11 or newer is required.")
+
+ appcontext_pushed.connect(_push_appctx)
+ appcontext_tearing_down.connect(_pop_appctx)
+ request_started.connect(_request_started)
+ got_request_exception.connect(_capture_exception)
+
+ old_app = Flask.__call__
+
+ def sentry_patched_wsgi_app(self, environ, start_response):
+ # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse
+ if Hub.current.get_integration(FlaskIntegration) is None:
+ return old_app(self, environ, start_response)
+
+ return SentryWsgiMiddleware(lambda *a, **kw: old_app(self, *a, **kw))(
+ environ, start_response
+ )
+
+ Flask.__call__ = sentry_patched_wsgi_app # type: ignore
+
+
+def _push_appctx(*args, **kwargs):
+ # type: (*Flask, **Any) -> None
+ hub = Hub.current
+ if hub.get_integration(FlaskIntegration) is not None:
+ # always want to push scope regardless of whether WSGI app might already
+ # have (not the case for CLI for example)
+ scope_manager = hub.push_scope()
+ scope_manager.__enter__()
+ _app_ctx_stack.top.sentry_sdk_scope_manager = scope_manager
+ with hub.configure_scope() as scope:
+ scope._name = "flask"
+
+
+def _pop_appctx(*args, **kwargs):
+ # type: (*Flask, **Any) -> None
+ scope_manager = getattr(_app_ctx_stack.top, "sentry_sdk_scope_manager", None)
+ if scope_manager is not None:
+ scope_manager.__exit__(None, None, None)
+
+
+def _request_started(sender, **kwargs):
+ # type: (Flask, **Any) -> None
+ hub = Hub.current
+ integration = hub.get_integration(FlaskIntegration)
+ if integration is None:
+ return
+
+ app = _app_ctx_stack.top.app
+ with hub.configure_scope() as scope:
+ request = _request_ctx_stack.top.request
+
+ # Rely on WSGI middleware to start a trace
+ try:
+ if integration.transaction_style == "endpoint":
+ scope.transaction = request.url_rule.endpoint
+ elif integration.transaction_style == "url":
+ scope.transaction = request.url_rule.rule
+ except Exception:
+ pass
+
+ weak_request = weakref.ref(request)
+ evt_processor = _make_request_event_processor(
+ app, weak_request, integration # type: ignore
+ )
+ scope.add_event_processor(evt_processor)
+
+
+class FlaskRequestExtractor(RequestExtractor):
+ def env(self):
+ # type: () -> Dict[str, str]
+ return self.request.environ
+
+ def cookies(self):
+ # type: () -> ImmutableTypeConversionDict[Any, Any]
+ return self.request.cookies
+
+ def raw_data(self):
+ # type: () -> bytes
+ return self.request.get_data()
+
+ def form(self):
+ # type: () -> ImmutableMultiDict[str, Any]
+ return self.request.form
+
+ def files(self):
+ # type: () -> ImmutableMultiDict[str, Any]
+ return self.request.files
+
+ def is_json(self):
+ # type: () -> bool
+ return self.request.is_json
+
+ def json(self):
+ # type: () -> Any
+ return self.request.get_json()
+
+ def size_of_file(self, file):
+ # type: (FileStorage) -> int
+ return file.content_length
+
+
+def _make_request_event_processor(app, weak_request, integration):
+ # type: (Flask, Callable[[], Request], FlaskIntegration) -> EventProcessor
+ def inner(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+ request = weak_request()
+
+ # if the request is gone we are fine not logging the data from
+ # it. This might happen if the processor is pushed away to
+ # another thread.
+ if request is None:
+ return event
+
+ with capture_internal_exceptions():
+ FlaskRequestExtractor(request).extract_into_event(event)
+
+ if _should_send_default_pii():
+ with capture_internal_exceptions():
+ _add_user_to_event(event)
+
+ return event
+
+ return inner
+
+
+def _capture_exception(sender, exception, **kwargs):
+ # type: (Flask, Union[ValueError, BaseException], **Any) -> None
+ hub = Hub.current
+ if hub.get_integration(FlaskIntegration) is None:
+ return
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ event, hint = event_from_exception(
+ exception,
+ client_options=client.options,
+ mechanism={"type": "flask", "handled": False},
+ )
+
+ hub.capture_event(event, hint=hint)
+
+
+def _add_user_to_event(event):
+ # type: (Dict[str, Any]) -> None
+ if flask_login is None:
+ return
+
+ user = flask_login.current_user
+ if user is None:
+ return
+
+ with capture_internal_exceptions():
+ # Access this object as late as possible as accessing the user
+ # is relatively costly
+
+ user_info = event.setdefault("user", {})
+
+ try:
+ user_info.setdefault("id", user.get_id())
+ # TODO: more configurable user attrs here
+ except AttributeError:
+ # might happen if:
+ # - flask_login could not be imported
+ # - flask_login is not configured
+ # - no user is logged in
+ pass
+
+ # The following attribute accesses are ineffective for the general
+ # Flask-Login case, because the User interface of Flask-Login does not
+ # care about anything but the ID. However, Flask-User (based on
+ # Flask-Login) documents a few optional extra attributes.
+ #
+ # https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names
+
+ try:
+ user_info.setdefault("email", user.email)
+ except Exception:
+ pass
+
+ try:
+ user_info.setdefault("username", user.username)
+ user_info.setdefault("username", user.email)
+ except Exception:
+ pass
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/gnu_backtrace.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/gnu_backtrace.py
new file mode 100644
index 0000000000..e0ec110547
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/gnu_backtrace.py
@@ -0,0 +1,107 @@
+import re
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration
+from sentry_sdk.scope import add_global_event_processor
+from sentry_sdk.utils import capture_internal_exceptions
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Dict
+
+
+MODULE_RE = r"[a-zA-Z0-9/._:\\-]+"
+TYPE_RE = r"[a-zA-Z0-9._:<>,-]+"
+HEXVAL_RE = r"[A-Fa-f0-9]+"
+
+
+FRAME_RE = r"""
+^(?P<index>\d+)\.\s
+(?P<package>{MODULE_RE})\(
+ (?P<retval>{TYPE_RE}\ )?
+ ((?P<function>{TYPE_RE})
+ (?P<args>\(.*\))?
+ )?
+ ((?P<constoffset>\ const)?\+0x(?P<offset>{HEXVAL_RE}))?
+\)\s
+\[0x(?P<retaddr>{HEXVAL_RE})\]$
+""".format(
+ MODULE_RE=MODULE_RE, HEXVAL_RE=HEXVAL_RE, TYPE_RE=TYPE_RE
+)
+
+FRAME_RE = re.compile(FRAME_RE, re.MULTILINE | re.VERBOSE)
+
+
+class GnuBacktraceIntegration(Integration):
+ identifier = "gnu_backtrace"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ @add_global_event_processor
+ def process_gnu_backtrace(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+ with capture_internal_exceptions():
+ return _process_gnu_backtrace(event, hint)
+
+
+def _process_gnu_backtrace(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+ if Hub.current.get_integration(GnuBacktraceIntegration) is None:
+ return event
+
+ exc_info = hint.get("exc_info", None)
+
+ if exc_info is None:
+ return event
+
+ exception = event.get("exception", None)
+
+ if exception is None:
+ return event
+
+ values = exception.get("values", None)
+
+ if values is None:
+ return event
+
+ for exception in values:
+ frames = exception.get("stacktrace", {}).get("frames", [])
+ if not frames:
+ continue
+
+ msg = exception.get("value", None)
+ if not msg:
+ continue
+
+ additional_frames = []
+ new_msg = []
+
+ for line in msg.splitlines():
+ match = FRAME_RE.match(line)
+ if match:
+ additional_frames.append(
+ (
+ int(match.group("index")),
+ {
+ "package": match.group("package") or None,
+ "function": match.group("function") or None,
+ "platform": "native",
+ },
+ )
+ )
+ else:
+ # Put garbage lines back into message, not sure what to do with them.
+ new_msg.append(line)
+
+ if additional_frames:
+ additional_frames.sort(key=lambda x: -x[0])
+ for _, frame in additional_frames:
+ frames.append(frame)
+
+ new_msg.append("<stacktrace parsed and removed by GnuBacktraceIntegration>")
+ exception["value"] = "\n".join(new_msg)
+
+ return event
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/logging.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/logging.py
new file mode 100644
index 0000000000..6edd785e91
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/logging.py
@@ -0,0 +1,237 @@
+from __future__ import absolute_import
+
+import logging
+import datetime
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.utils import (
+ to_string,
+ event_from_exception,
+ current_stacktrace,
+ capture_internal_exceptions,
+)
+from sentry_sdk.integrations import Integration
+from sentry_sdk._compat import iteritems
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from logging import LogRecord
+ from typing import Any
+ from typing import Dict
+ from typing import Optional
+
+DEFAULT_LEVEL = logging.INFO
+DEFAULT_EVENT_LEVEL = logging.ERROR
+
+_IGNORED_LOGGERS = set(["sentry_sdk.errors"])
+
+
+def ignore_logger(
+ name, # type: str
+):
+ # type: (...) -> None
+ """This disables recording (both in breadcrumbs and as events) calls to
+ a logger of a specific name. Among other uses, many of our integrations
+ use this to prevent their actions being recorded as breadcrumbs. Exposed
+ to users as a way to quiet spammy loggers.
+
+ :param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``).
+ """
+ _IGNORED_LOGGERS.add(name)
+
+
+class LoggingIntegration(Integration):
+ identifier = "logging"
+
+ def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL):
+ # type: (Optional[int], Optional[int]) -> None
+ self._handler = None
+ self._breadcrumb_handler = None
+
+ if level is not None:
+ self._breadcrumb_handler = BreadcrumbHandler(level=level)
+
+ if event_level is not None:
+ self._handler = EventHandler(level=event_level)
+
+ def _handle_record(self, record):
+ # type: (LogRecord) -> None
+ if self._handler is not None and record.levelno >= self._handler.level:
+ self._handler.handle(record)
+
+ if (
+ self._breadcrumb_handler is not None
+ and record.levelno >= self._breadcrumb_handler.level
+ ):
+ self._breadcrumb_handler.handle(record)
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ old_callhandlers = logging.Logger.callHandlers # type: ignore
+
+ def sentry_patched_callhandlers(self, record):
+ # type: (Any, LogRecord) -> Any
+ try:
+ return old_callhandlers(self, record)
+ finally:
+ # This check is done twice, once also here before we even get
+ # the integration. Otherwise we have a high chance of getting
+ # into a recursion error when the integration is resolved
+ # (this also is slower).
+ if record.name not in _IGNORED_LOGGERS:
+ integration = Hub.current.get_integration(LoggingIntegration)
+ if integration is not None:
+ integration._handle_record(record)
+
+ logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore
+
+
+def _can_record(record):
+ # type: (LogRecord) -> bool
+ return record.name not in _IGNORED_LOGGERS
+
+
+def _breadcrumb_from_record(record):
+ # type: (LogRecord) -> Dict[str, Any]
+ return {
+ "ty": "log",
+ "level": _logging_to_event_level(record.levelname),
+ "category": record.name,
+ "message": record.message,
+ "timestamp": datetime.datetime.utcfromtimestamp(record.created),
+ "data": _extra_from_record(record),
+ }
+
+
+def _logging_to_event_level(levelname):
+ # type: (str) -> str
+ return {"critical": "fatal"}.get(levelname.lower(), levelname.lower())
+
+
+COMMON_RECORD_ATTRS = frozenset(
+ (
+ "args",
+ "created",
+ "exc_info",
+ "exc_text",
+ "filename",
+ "funcName",
+ "levelname",
+ "levelno",
+ "linenno",
+ "lineno",
+ "message",
+ "module",
+ "msecs",
+ "msg",
+ "name",
+ "pathname",
+ "process",
+ "processName",
+ "relativeCreated",
+ "stack",
+ "tags",
+ "thread",
+ "threadName",
+ "stack_info",
+ )
+)
+
+
+def _extra_from_record(record):
+ # type: (LogRecord) -> Dict[str, None]
+ return {
+ k: v
+ for k, v in iteritems(vars(record))
+ if k not in COMMON_RECORD_ATTRS
+ and (not isinstance(k, str) or not k.startswith("_"))
+ }
+
+
+class EventHandler(logging.Handler, object):
+ """
+ A logging handler that emits Sentry events for each log record
+
+ Note that you do not have to use this class if the logging integration is enabled, which it is by default.
+ """
+
+ def emit(self, record):
+ # type: (LogRecord) -> Any
+ with capture_internal_exceptions():
+ self.format(record)
+ return self._emit(record)
+
+ def _emit(self, record):
+ # type: (LogRecord) -> None
+ if not _can_record(record):
+ return
+
+ hub = Hub.current
+ if hub.client is None:
+ return
+
+ client_options = hub.client.options
+
+ # exc_info might be None or (None, None, None)
+ if record.exc_info is not None and record.exc_info[0] is not None:
+ event, hint = event_from_exception(
+ record.exc_info,
+ client_options=client_options,
+ mechanism={"type": "logging", "handled": True},
+ )
+ elif record.exc_info and record.exc_info[0] is None:
+ event = {}
+ hint = {}
+ with capture_internal_exceptions():
+ event["threads"] = {
+ "values": [
+ {
+ "stacktrace": current_stacktrace(
+ client_options["with_locals"]
+ ),
+ "crashed": False,
+ "current": True,
+ }
+ ]
+ }
+ else:
+ event = {}
+ hint = {}
+
+ hint["log_record"] = record
+
+ event["level"] = _logging_to_event_level(record.levelname)
+ event["logger"] = record.name
+ event["logentry"] = {"message": to_string(record.msg), "params": record.args}
+ event["extra"] = _extra_from_record(record)
+
+ hub.capture_event(event, hint=hint)
+
+
+# Legacy name
+SentryHandler = EventHandler
+
+
+class BreadcrumbHandler(logging.Handler, object):
+ """
+ A logging handler that records breadcrumbs for each log record.
+
+ Note that you do not have to use this class if the logging integration is enabled, which it is by default.
+ """
+
+ def emit(self, record):
+ # type: (LogRecord) -> Any
+ with capture_internal_exceptions():
+ self.format(record)
+ return self._emit(record)
+
+ def _emit(self, record):
+ # type: (LogRecord) -> None
+ if not _can_record(record):
+ return
+
+ Hub.current.add_breadcrumb(
+ _breadcrumb_from_record(record), hint={"log_record": record}
+ )
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/modules.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/modules.py
new file mode 100644
index 0000000000..3d78cb89bb
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/modules.py
@@ -0,0 +1,56 @@
+from __future__ import absolute_import
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration
+from sentry_sdk.scope import add_global_event_processor
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Dict
+ from typing import Tuple
+ from typing import Iterator
+
+ from sentry_sdk._types import Event
+
+
+_installed_modules = None
+
+
+def _generate_installed_modules():
+ # type: () -> Iterator[Tuple[str, str]]
+ try:
+ import pkg_resources
+ except ImportError:
+ return
+
+ for info in pkg_resources.working_set:
+ yield info.key, info.version
+
+
+def _get_installed_modules():
+ # type: () -> Dict[str, str]
+ global _installed_modules
+ if _installed_modules is None:
+ _installed_modules = dict(_generate_installed_modules())
+ return _installed_modules
+
+
+class ModulesIntegration(Integration):
+ identifier = "modules"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ @add_global_event_processor
+ def processor(event, hint):
+ # type: (Event, Any) -> Dict[str, Any]
+ if event.get("type") == "transaction":
+ return event
+
+ if Hub.current.get_integration(ModulesIntegration) is None:
+ return event
+
+ event["modules"] = _get_installed_modules()
+ return event
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/pyramid.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/pyramid.py
new file mode 100644
index 0000000000..ee9682343a
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/pyramid.py
@@ -0,0 +1,217 @@
+from __future__ import absolute_import
+
+import os
+import sys
+import weakref
+
+from pyramid.httpexceptions import HTTPException
+from pyramid.request import Request
+
+from sentry_sdk.hub import Hub, _should_send_default_pii
+from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
+from sentry_sdk._compat import reraise, iteritems
+
+from sentry_sdk.integrations import Integration
+from sentry_sdk.integrations._wsgi_common import RequestExtractor
+from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from pyramid.response import Response
+ from typing import Any
+ from sentry_sdk.integrations.wsgi import _ScopedResponse
+ from typing import Callable
+ from typing import Dict
+ from typing import Optional
+ from webob.cookies import RequestCookies # type: ignore
+ from webob.compat import cgi_FieldStorage # type: ignore
+
+ from sentry_sdk.utils import ExcInfo
+ from sentry_sdk._types import EventProcessor
+
+
+if getattr(Request, "authenticated_userid", None):
+
+ def authenticated_userid(request):
+ # type: (Request) -> Optional[Any]
+ return request.authenticated_userid
+
+
+else:
+ # bw-compat for pyramid < 1.5
+ from pyramid.security import authenticated_userid # type: ignore
+
+
+TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern")
+
+
+class PyramidIntegration(Integration):
+ identifier = "pyramid"
+
+ transaction_style = None
+
+ def __init__(self, transaction_style="route_name"):
+ # type: (str) -> None
+ if transaction_style not in TRANSACTION_STYLE_VALUES:
+ raise ValueError(
+ "Invalid value for transaction_style: %s (must be in %s)"
+ % (transaction_style, TRANSACTION_STYLE_VALUES)
+ )
+ self.transaction_style = transaction_style
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ from pyramid.router import Router
+ from pyramid.request import Request
+
+ old_handle_request = Router.handle_request
+
+ def sentry_patched_handle_request(self, request, *args, **kwargs):
+ # type: (Any, Request, *Any, **Any) -> Response
+ hub = Hub.current
+ integration = hub.get_integration(PyramidIntegration)
+ if integration is not None:
+ with hub.configure_scope() as scope:
+ scope.add_event_processor(
+ _make_event_processor(weakref.ref(request), integration)
+ )
+
+ return old_handle_request(self, request, *args, **kwargs)
+
+ Router.handle_request = sentry_patched_handle_request
+
+ if hasattr(Request, "invoke_exception_view"):
+ old_invoke_exception_view = Request.invoke_exception_view
+
+ def sentry_patched_invoke_exception_view(self, *args, **kwargs):
+ # type: (Request, *Any, **Any) -> Any
+ rv = old_invoke_exception_view(self, *args, **kwargs)
+
+ if (
+ self.exc_info
+ and all(self.exc_info)
+ and rv.status_int == 500
+ and Hub.current.get_integration(PyramidIntegration) is not None
+ ):
+ _capture_exception(self.exc_info)
+
+ return rv
+
+ Request.invoke_exception_view = sentry_patched_invoke_exception_view
+
+ old_wsgi_call = Router.__call__
+
+ def sentry_patched_wsgi_call(self, environ, start_response):
+ # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse
+ hub = Hub.current
+ integration = hub.get_integration(PyramidIntegration)
+ if integration is None:
+ return old_wsgi_call(self, environ, start_response)
+
+ def sentry_patched_inner_wsgi_call(environ, start_response):
+ # type: (Dict[str, Any], Callable[..., Any]) -> Any
+ try:
+ return old_wsgi_call(self, environ, start_response)
+ except Exception:
+ einfo = sys.exc_info()
+ _capture_exception(einfo)
+ reraise(*einfo)
+
+ return SentryWsgiMiddleware(sentry_patched_inner_wsgi_call)(
+ environ, start_response
+ )
+
+ Router.__call__ = sentry_patched_wsgi_call
+
+
+def _capture_exception(exc_info):
+ # type: (ExcInfo) -> None
+ if exc_info[0] is None or issubclass(exc_info[0], HTTPException):
+ return
+ hub = Hub.current
+ if hub.get_integration(PyramidIntegration) is None:
+ return
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ event, hint = event_from_exception(
+ exc_info,
+ client_options=client.options,
+ mechanism={"type": "pyramid", "handled": False},
+ )
+
+ hub.capture_event(event, hint=hint)
+
+
+class PyramidRequestExtractor(RequestExtractor):
+ def url(self):
+ # type: () -> str
+ return self.request.path_url
+
+ def env(self):
+ # type: () -> Dict[str, str]
+ return self.request.environ
+
+ def cookies(self):
+ # type: () -> RequestCookies
+ return self.request.cookies
+
+ def raw_data(self):
+ # type: () -> str
+ return self.request.text
+
+ def form(self):
+ # type: () -> Dict[str, str]
+ return {
+ key: value
+ for key, value in iteritems(self.request.POST)
+ if not getattr(value, "filename", None)
+ }
+
+ def files(self):
+ # type: () -> Dict[str, cgi_FieldStorage]
+ return {
+ key: value
+ for key, value in iteritems(self.request.POST)
+ if getattr(value, "filename", None)
+ }
+
+ def size_of_file(self, postdata):
+ # type: (cgi_FieldStorage) -> int
+ file = postdata.file
+ try:
+ return os.fstat(file.fileno()).st_size
+ except Exception:
+ return 0
+
+
+def _make_event_processor(weak_request, integration):
+ # type: (Callable[[], Request], PyramidIntegration) -> EventProcessor
+ def event_processor(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+ request = weak_request()
+ if request is None:
+ return event
+
+ try:
+ if integration.transaction_style == "route_name":
+ event["transaction"] = request.matched_route.name
+ elif integration.transaction_style == "route_pattern":
+ event["transaction"] = request.matched_route.pattern
+ except Exception:
+ pass
+
+ with capture_internal_exceptions():
+ PyramidRequestExtractor(request).extract_into_event(event)
+
+ if _should_send_default_pii():
+ with capture_internal_exceptions():
+ user_info = event.setdefault("user", {})
+ user_info.setdefault("id", authenticated_userid(request))
+
+ return event
+
+ return event_processor
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/redis.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/redis.py
new file mode 100644
index 0000000000..510fdbb22c
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/redis.py
@@ -0,0 +1,70 @@
+from __future__ import absolute_import
+
+from sentry_sdk import Hub
+from sentry_sdk.utils import capture_internal_exceptions
+from sentry_sdk.integrations import Integration
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+
+
+class RedisIntegration(Integration):
+ identifier = "redis"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ import redis
+
+ patch_redis_client(redis.StrictRedis)
+
+ try:
+ import rb.clients # type: ignore
+ except ImportError:
+ pass
+ else:
+ patch_redis_client(rb.clients.FanoutClient)
+ patch_redis_client(rb.clients.MappingClient)
+ patch_redis_client(rb.clients.RoutingClient)
+
+
+def patch_redis_client(cls):
+ # type: (Any) -> None
+ """
+ This function can be used to instrument custom redis client classes or
+ subclasses.
+ """
+
+ old_execute_command = cls.execute_command
+
+ def sentry_patched_execute_command(self, name, *args, **kwargs):
+ # type: (Any, str, *Any, **Any) -> Any
+ hub = Hub.current
+
+ if hub.get_integration(RedisIntegration) is None:
+ return old_execute_command(self, name, *args, **kwargs)
+
+ description = name
+
+ with capture_internal_exceptions():
+ description_parts = [name]
+ for i, arg in enumerate(args):
+ if i > 10:
+ break
+
+ description_parts.append(repr(arg))
+
+ description = " ".join(description_parts)
+
+ with hub.start_span(op="redis", description=description) as span:
+ if name:
+ span.set_tag("redis.command", name)
+
+ if name and args and name.lower() in ("get", "set", "setex", "setnx"):
+ span.set_tag("redis.key", args[0])
+
+ return old_execute_command(self, name, *args, **kwargs)
+
+ cls.execute_command = sentry_patched_execute_command
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/rq.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/rq.py
new file mode 100644
index 0000000000..fbe8cdda3d
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/rq.py
@@ -0,0 +1,150 @@
+from __future__ import absolute_import
+
+import weakref
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.tracing import Span
+from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
+
+
+try:
+ from rq.version import VERSION as RQ_VERSION
+ from rq.timeouts import JobTimeoutException
+ from rq.worker import Worker
+ from rq.queue import Queue
+except ImportError:
+ raise DidNotEnable("RQ not installed")
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Dict
+ from typing import Callable
+
+ from rq.job import Job
+
+ from sentry_sdk.utils import ExcInfo
+ from sentry_sdk._types import EventProcessor
+
+
+class RqIntegration(Integration):
+ identifier = "rq"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+
+ try:
+ version = tuple(map(int, RQ_VERSION.split(".")[:3]))
+ except (ValueError, TypeError):
+ raise DidNotEnable("Unparseable RQ version: {}".format(RQ_VERSION))
+
+ if version < (0, 6):
+ raise DidNotEnable("RQ 0.6 or newer is required.")
+
+ old_perform_job = Worker.perform_job
+
+ def sentry_patched_perform_job(self, job, *args, **kwargs):
+ # type: (Any, Job, *Queue, **Any) -> bool
+ hub = Hub.current
+ integration = hub.get_integration(RqIntegration)
+
+ if integration is None:
+ return old_perform_job(self, job, *args, **kwargs)
+
+ client = hub.client
+ assert client is not None
+
+ with hub.push_scope() as scope:
+ scope.clear_breadcrumbs()
+ scope.add_event_processor(_make_event_processor(weakref.ref(job)))
+
+ span = Span.continue_from_headers(
+ job.meta.get("_sentry_trace_headers") or {}
+ )
+ span.op = "rq.task"
+
+ with capture_internal_exceptions():
+ span.transaction = job.func_name
+
+ with hub.start_span(span):
+ rv = old_perform_job(self, job, *args, **kwargs)
+
+ if self.is_horse:
+ # We're inside of a forked process and RQ is
+ # about to call `os._exit`. Make sure that our
+ # events get sent out.
+ client.flush()
+
+ return rv
+
+ Worker.perform_job = sentry_patched_perform_job
+
+ old_handle_exception = Worker.handle_exception
+
+ def sentry_patched_handle_exception(self, job, *exc_info, **kwargs):
+ # type: (Worker, Any, *Any, **Any) -> Any
+ _capture_exception(exc_info) # type: ignore
+ return old_handle_exception(self, job, *exc_info, **kwargs)
+
+ Worker.handle_exception = sentry_patched_handle_exception
+
+ old_enqueue_job = Queue.enqueue_job
+
+ def sentry_patched_enqueue_job(self, job, **kwargs):
+ # type: (Queue, Any, **Any) -> Any
+ hub = Hub.current
+ if hub.get_integration(RqIntegration) is not None:
+ job.meta["_sentry_trace_headers"] = dict(
+ hub.iter_trace_propagation_headers()
+ )
+
+ return old_enqueue_job(self, job, **kwargs)
+
+ Queue.enqueue_job = sentry_patched_enqueue_job
+
+
+def _make_event_processor(weak_job):
+ # type: (Callable[[], Job]) -> EventProcessor
+ def event_processor(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+ job = weak_job()
+ if job is not None:
+ with capture_internal_exceptions():
+ extra = event.setdefault("extra", {})
+ extra["rq-job"] = {
+ "job_id": job.id,
+ "func": job.func_name,
+ "args": job.args,
+ "kwargs": job.kwargs,
+ "description": job.description,
+ }
+
+ if "exc_info" in hint:
+ with capture_internal_exceptions():
+ if issubclass(hint["exc_info"][0], JobTimeoutException):
+ event["fingerprint"] = ["rq", "JobTimeoutException", job.func_name]
+
+ return event
+
+ return event_processor
+
+
+def _capture_exception(exc_info, **kwargs):
+ # type: (ExcInfo, **Any) -> None
+ hub = Hub.current
+ if hub.get_integration(RqIntegration) is None:
+ return
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ event, hint = event_from_exception(
+ exc_info,
+ client_options=client.options,
+ mechanism={"type": "rq", "handled": False},
+ )
+
+ hub.capture_event(event, hint=hint)
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/sanic.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/sanic.py
new file mode 100644
index 0000000000..e8fdca422a
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/sanic.py
@@ -0,0 +1,233 @@
+import sys
+import weakref
+from inspect import isawaitable
+
+from sentry_sdk._compat import urlparse, reraise
+from sentry_sdk.hub import Hub
+from sentry_sdk.utils import (
+ capture_internal_exceptions,
+ event_from_exception,
+ HAS_REAL_CONTEXTVARS,
+)
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers
+from sentry_sdk.integrations.logging import ignore_logger
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Callable
+ from typing import Optional
+ from typing import Union
+ from typing import Tuple
+ from typing import Dict
+
+ from sanic.request import Request, RequestParameters
+
+ from sentry_sdk._types import Event, EventProcessor, Hint
+
+try:
+ from sanic import Sanic, __version__ as SANIC_VERSION
+ from sanic.exceptions import SanicException
+ from sanic.router import Router
+ from sanic.handlers import ErrorHandler
+except ImportError:
+ raise DidNotEnable("Sanic not installed")
+
+
+class SanicIntegration(Integration):
+ identifier = "sanic"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ try:
+ version = tuple(map(int, SANIC_VERSION.split(".")))
+ except (TypeError, ValueError):
+ raise DidNotEnable("Unparseable Sanic version: {}".format(SANIC_VERSION))
+
+ if version < (0, 8):
+ raise DidNotEnable("Sanic 0.8 or newer required.")
+
+ if not HAS_REAL_CONTEXTVARS:
+ # We better have contextvars or we're going to leak state between
+ # requests.
+ raise DidNotEnable(
+ "The sanic integration for Sentry requires Python 3.7+ "
+ " or aiocontextvars package"
+ )
+
+ if SANIC_VERSION.startswith("0.8."):
+ # Sanic 0.8 and older creates a logger named "root" and puts a
+ # stringified version of every exception in there (without exc_info),
+ # which our error deduplication can't detect.
+ #
+ # We explicitly check the version here because it is a very
+ # invasive step to ignore this logger and not necessary in newer
+ # versions at all.
+ #
+ # https://github.com/huge-success/sanic/issues/1332
+ ignore_logger("root")
+
+ old_handle_request = Sanic.handle_request
+
+ async def sentry_handle_request(self, request, *args, **kwargs):
+ # type: (Any, Request, *Any, **Any) -> Any
+ hub = Hub.current
+ if hub.get_integration(SanicIntegration) is None:
+ return old_handle_request(self, request, *args, **kwargs)
+
+ weak_request = weakref.ref(request)
+
+ with Hub(hub) as hub:
+ with hub.configure_scope() as scope:
+ scope.clear_breadcrumbs()
+ scope.add_event_processor(_make_request_processor(weak_request))
+
+ response = old_handle_request(self, request, *args, **kwargs)
+ if isawaitable(response):
+ response = await response
+
+ return response
+
+ Sanic.handle_request = sentry_handle_request
+
+ old_router_get = Router.get
+
+ def sentry_router_get(self, request):
+ # type: (Any, Request) -> Any
+ rv = old_router_get(self, request)
+ hub = Hub.current
+ if hub.get_integration(SanicIntegration) is not None:
+ with capture_internal_exceptions():
+ with hub.configure_scope() as scope:
+ scope.transaction = rv[0].__name__
+ return rv
+
+ Router.get = sentry_router_get
+
+ old_error_handler_lookup = ErrorHandler.lookup
+
+ def sentry_error_handler_lookup(self, exception):
+ # type: (Any, Exception) -> Optional[object]
+ _capture_exception(exception)
+ old_error_handler = old_error_handler_lookup(self, exception)
+
+ if old_error_handler is None:
+ return None
+
+ if Hub.current.get_integration(SanicIntegration) is None:
+ return old_error_handler
+
+ async def sentry_wrapped_error_handler(request, exception):
+ # type: (Request, Exception) -> Any
+ try:
+ response = old_error_handler(request, exception)
+ if isawaitable(response):
+ response = await response
+ return response
+ except Exception:
+ # Report errors that occur in Sanic error handler. These
+ # exceptions will not even show up in Sanic's
+ # `sanic.exceptions` logger.
+ exc_info = sys.exc_info()
+ _capture_exception(exc_info)
+ reraise(*exc_info)
+
+ return sentry_wrapped_error_handler
+
+ ErrorHandler.lookup = sentry_error_handler_lookup
+
+
+def _capture_exception(exception):
+ # type: (Union[Tuple[Optional[type], Optional[BaseException], Any], BaseException]) -> None
+ hub = Hub.current
+ integration = hub.get_integration(SanicIntegration)
+ if integration is None:
+ return
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ with capture_internal_exceptions():
+ event, hint = event_from_exception(
+ exception,
+ client_options=client.options,
+ mechanism={"type": "sanic", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+
+def _make_request_processor(weak_request):
+ # type: (Callable[[], Request]) -> EventProcessor
+ def sanic_processor(event, hint):
+ # type: (Event, Optional[Hint]) -> Optional[Event]
+
+ try:
+ if hint and issubclass(hint["exc_info"][0], SanicException):
+ return None
+ except KeyError:
+ pass
+
+ request = weak_request()
+ if request is None:
+ return event
+
+ with capture_internal_exceptions():
+ extractor = SanicRequestExtractor(request)
+ extractor.extract_into_event(event)
+
+ request_info = event["request"]
+ urlparts = urlparse.urlsplit(request.url)
+
+ request_info["url"] = "%s://%s%s" % (
+ urlparts.scheme,
+ urlparts.netloc,
+ urlparts.path,
+ )
+
+ request_info["query_string"] = urlparts.query
+ request_info["method"] = request.method
+ request_info["env"] = {"REMOTE_ADDR": request.remote_addr}
+ request_info["headers"] = _filter_headers(dict(request.headers))
+
+ return event
+
+ return sanic_processor
+
+
+class SanicRequestExtractor(RequestExtractor):
+ def content_length(self):
+ # type: () -> int
+ if self.request.body is None:
+ return 0
+ return len(self.request.body)
+
+ def cookies(self):
+ # type: () -> Dict[str, str]
+ return dict(self.request.cookies)
+
+ def raw_data(self):
+ # type: () -> bytes
+ return self.request.body
+
+ def form(self):
+ # type: () -> RequestParameters
+ return self.request.form
+
+ def is_json(self):
+ # type: () -> bool
+ raise NotImplementedError()
+
+ def json(self):
+ # type: () -> Optional[Any]
+ return self.request.json
+
+ def files(self):
+ # type: () -> RequestParameters
+ return self.request.files
+
+ def size_of_file(self, file):
+ # type: (Any) -> int
+ return len(file.body or ())
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/serverless.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/serverless.py
new file mode 100644
index 0000000000..6dd90b43d0
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/serverless.py
@@ -0,0 +1,87 @@
+import functools
+import sys
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.utils import event_from_exception
+from sentry_sdk._compat import reraise
+
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Callable
+ from typing import TypeVar
+ from typing import Union
+ from typing import Optional
+
+ from typing import overload
+
+ F = TypeVar("F", bound=Callable[..., Any])
+
+else:
+
+ def overload(x):
+ # type: (F) -> F
+ return x
+
+
+@overload
+def serverless_function(f, flush=True):
+ # type: (F, bool) -> F
+ pass
+
+
+@overload # noqa
+def serverless_function(f=None, flush=True):
+ # type: (None, bool) -> Callable[[F], F]
+ pass
+
+
+def serverless_function(f=None, flush=True): # noqa
+ # type: (Optional[F], bool) -> Union[F, Callable[[F], F]]
+ def wrapper(f):
+ # type: (F) -> F
+ @functools.wraps(f)
+ def inner(*args, **kwargs):
+ # type: (*Any, **Any) -> Any
+ with Hub(Hub.current) as hub:
+ with hub.configure_scope() as scope:
+ scope.clear_breadcrumbs()
+
+ try:
+ return f(*args, **kwargs)
+ except Exception:
+ _capture_and_reraise()
+ finally:
+ if flush:
+ _flush_client()
+
+ return inner # type: ignore
+
+ if f is None:
+ return wrapper
+ else:
+ return wrapper(f)
+
+
+def _capture_and_reraise():
+ # type: () -> None
+ exc_info = sys.exc_info()
+ hub = Hub.current
+ if hub is not None and hub.client is not None:
+ event, hint = event_from_exception(
+ exc_info,
+ client_options=hub.client.options,
+ mechanism={"type": "serverless", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+ reraise(*exc_info)
+
+
+def _flush_client():
+ # type: () -> None
+ hub = Hub.current
+ if hub is not None:
+ hub.flush()
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/__init__.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/__init__.py
new file mode 100644
index 0000000000..10d94163c5
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/__init__.py
@@ -0,0 +1,4 @@
+from sentry_sdk.integrations.spark.spark_driver import SparkIntegration
+from sentry_sdk.integrations.spark.spark_worker import SparkWorkerIntegration
+
+__all__ = ["SparkIntegration", "SparkWorkerIntegration"]
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_driver.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_driver.py
new file mode 100644
index 0000000000..ea43c37821
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_driver.py
@@ -0,0 +1,263 @@
+from sentry_sdk import configure_scope
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration
+from sentry_sdk.utils import capture_internal_exceptions
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Optional
+
+ from sentry_sdk._types import Event, Hint
+
+
+class SparkIntegration(Integration):
+ identifier = "spark"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ patch_spark_context_init()
+
+
+def _set_app_properties():
+ # type: () -> None
+ """
+ Set properties in driver that propagate to worker processes, allowing for workers to have access to those properties.
+ This allows worker integration to have access to app_name and application_id.
+ """
+ from pyspark import SparkContext
+
+ spark_context = SparkContext._active_spark_context
+ if spark_context:
+ spark_context.setLocalProperty("sentry_app_name", spark_context.appName)
+ spark_context.setLocalProperty(
+ "sentry_application_id", spark_context.applicationId
+ )
+
+
+def _start_sentry_listener(sc):
+ # type: (Any) -> None
+ """
+ Start java gateway server to add custom `SparkListener`
+ """
+ from pyspark.java_gateway import ensure_callback_server_started
+
+ gw = sc._gateway
+ ensure_callback_server_started(gw)
+ listener = SentryListener()
+ sc._jsc.sc().addSparkListener(listener)
+
+
+def patch_spark_context_init():
+ # type: () -> None
+ from pyspark import SparkContext
+
+ spark_context_init = SparkContext._do_init
+
+ def _sentry_patched_spark_context_init(self, *args, **kwargs):
+ # type: (SparkContext, *Any, **Any) -> Optional[Any]
+ init = spark_context_init(self, *args, **kwargs)
+
+ if Hub.current.get_integration(SparkIntegration) is None:
+ return init
+
+ _start_sentry_listener(self)
+ _set_app_properties()
+
+ with configure_scope() as scope:
+
+ @scope.add_event_processor
+ def process_event(event, hint):
+ # type: (Event, Hint) -> Optional[Event]
+ with capture_internal_exceptions():
+ if Hub.current.get_integration(SparkIntegration) is None:
+ return event
+
+ event.setdefault("user", {}).setdefault("id", self.sparkUser())
+
+ event.setdefault("tags", {}).setdefault(
+ "executor.id", self._conf.get("spark.executor.id")
+ )
+ event["tags"].setdefault(
+ "spark-submit.deployMode",
+ self._conf.get("spark.submit.deployMode"),
+ )
+ event["tags"].setdefault(
+ "driver.host", self._conf.get("spark.driver.host")
+ )
+ event["tags"].setdefault(
+ "driver.port", self._conf.get("spark.driver.port")
+ )
+ event["tags"].setdefault("spark_version", self.version)
+ event["tags"].setdefault("app_name", self.appName)
+ event["tags"].setdefault("application_id", self.applicationId)
+ event["tags"].setdefault("master", self.master)
+ event["tags"].setdefault("spark_home", self.sparkHome)
+
+ event.setdefault("extra", {}).setdefault("web_url", self.uiWebUrl)
+
+ return event
+
+ return init
+
+ SparkContext._do_init = _sentry_patched_spark_context_init
+
+
+class SparkListener(object):
+ def onApplicationEnd(self, applicationEnd): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onApplicationStart(self, applicationStart): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onBlockManagerAdded(self, blockManagerAdded): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onBlockManagerRemoved(self, blockManagerRemoved): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onBlockUpdated(self, blockUpdated): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onEnvironmentUpdate(self, environmentUpdate): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onExecutorAdded(self, executorAdded): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onExecutorBlacklisted(self, executorBlacklisted): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onExecutorBlacklistedForStage( # noqa: N802
+ self, executorBlacklistedForStage # noqa: N803
+ ):
+ # type: (Any) -> None
+ pass
+
+ def onExecutorMetricsUpdate(self, executorMetricsUpdate): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onExecutorRemoved(self, executorRemoved): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onJobEnd(self, jobEnd): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onJobStart(self, jobStart): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onNodeBlacklisted(self, nodeBlacklisted): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onNodeBlacklistedForStage(self, nodeBlacklistedForStage): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onNodeUnblacklisted(self, nodeUnblacklisted): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onOtherEvent(self, event): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onSpeculativeTaskSubmitted(self, speculativeTask): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onStageCompleted(self, stageCompleted): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onStageSubmitted(self, stageSubmitted): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onTaskEnd(self, taskEnd): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onTaskGettingResult(self, taskGettingResult): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onTaskStart(self, taskStart): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ def onUnpersistRDD(self, unpersistRDD): # noqa: N802,N803
+ # type: (Any) -> None
+ pass
+
+ class Java:
+ implements = ["org.apache.spark.scheduler.SparkListenerInterface"]
+
+
+class SentryListener(SparkListener):
+ def __init__(self):
+ # type: () -> None
+ self.hub = Hub.current
+
+ def onJobStart(self, jobStart): # noqa: N802,N803
+ # type: (Any) -> None
+ message = "Job {} Started".format(jobStart.jobId())
+ self.hub.add_breadcrumb(level="info", message=message)
+ _set_app_properties()
+
+ def onJobEnd(self, jobEnd): # noqa: N802,N803
+ # type: (Any) -> None
+ level = ""
+ message = ""
+ data = {"result": jobEnd.jobResult().toString()}
+
+ if jobEnd.jobResult().toString() == "JobSucceeded":
+ level = "info"
+ message = "Job {} Ended".format(jobEnd.jobId())
+ else:
+ level = "warning"
+ message = "Job {} Failed".format(jobEnd.jobId())
+
+ self.hub.add_breadcrumb(level=level, message=message, data=data)
+
+ def onStageSubmitted(self, stageSubmitted): # noqa: N802,N803
+ # type: (Any) -> None
+ stage_info = stageSubmitted.stageInfo()
+ message = "Stage {} Submitted".format(stage_info.stageId())
+ data = {"attemptId": stage_info.attemptId(), "name": stage_info.name()}
+ self.hub.add_breadcrumb(level="info", message=message, data=data)
+ _set_app_properties()
+
+ def onStageCompleted(self, stageCompleted): # noqa: N802,N803
+ # type: (Any) -> None
+ from py4j.protocol import Py4JJavaError # type: ignore
+
+ stage_info = stageCompleted.stageInfo()
+ message = ""
+ level = ""
+ data = {"attemptId": stage_info.attemptId(), "name": stage_info.name()}
+
+ # Have to Try Except because stageInfo.failureReason() is typed with Scala Option
+ try:
+ data["reason"] = stage_info.failureReason().get()
+ message = "Stage {} Failed".format(stage_info.stageId())
+ level = "warning"
+ except Py4JJavaError:
+ message = "Stage {} Completed".format(stage_info.stageId())
+ level = "info"
+
+ self.hub.add_breadcrumb(level=level, message=message, data=data)
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_worker.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_worker.py
new file mode 100644
index 0000000000..bae4413d11
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/spark/spark_worker.py
@@ -0,0 +1,120 @@
+from __future__ import absolute_import
+
+import sys
+
+from sentry_sdk import configure_scope
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration
+from sentry_sdk.utils import (
+ capture_internal_exceptions,
+ exc_info_from_error,
+ single_exception_from_error_tuple,
+ walk_exception_chain,
+ event_hint_with_exc_info,
+)
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Optional
+
+ from sentry_sdk._types import ExcInfo, Event, Hint
+
+
+class SparkWorkerIntegration(Integration):
+ identifier = "spark_worker"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ import pyspark.daemon as original_daemon
+
+ original_daemon.worker_main = _sentry_worker_main
+
+
+def _capture_exception(exc_info, hub):
+ # type: (ExcInfo, Hub) -> None
+ client = hub.client
+
+ client_options = client.options # type: ignore
+
+ mechanism = {"type": "spark", "handled": False}
+
+ exc_info = exc_info_from_error(exc_info)
+
+ exc_type, exc_value, tb = exc_info
+ rv = []
+
+ # On Exception worker will call sys.exit(-1), so we can ignore SystemExit and similar errors
+ for exc_type, exc_value, tb in walk_exception_chain(exc_info):
+ if exc_type not in (SystemExit, EOFError, ConnectionResetError):
+ rv.append(
+ single_exception_from_error_tuple(
+ exc_type, exc_value, tb, client_options, mechanism
+ )
+ )
+
+ if rv:
+ rv.reverse()
+ hint = event_hint_with_exc_info(exc_info)
+ event = {"level": "error", "exception": {"values": rv}}
+
+ _tag_task_context()
+
+ hub.capture_event(event, hint=hint)
+
+
+def _tag_task_context():
+ # type: () -> None
+ from pyspark.taskcontext import TaskContext
+
+ with configure_scope() as scope:
+
+ @scope.add_event_processor
+ def process_event(event, hint):
+ # type: (Event, Hint) -> Optional[Event]
+ with capture_internal_exceptions():
+ integration = Hub.current.get_integration(SparkWorkerIntegration)
+ task_context = TaskContext.get()
+
+ if integration is None or task_context is None:
+ return event
+
+ event.setdefault("tags", {}).setdefault(
+ "stageId", task_context.stageId()
+ )
+ event["tags"].setdefault("partitionId", task_context.partitionId())
+ event["tags"].setdefault("attemptNumber", task_context.attemptNumber())
+ event["tags"].setdefault("taskAttemptId", task_context.taskAttemptId())
+
+ if task_context._localProperties:
+ if "sentry_app_name" in task_context._localProperties:
+ event["tags"].setdefault(
+ "app_name", task_context._localProperties["sentry_app_name"]
+ )
+ event["tags"].setdefault(
+ "application_id",
+ task_context._localProperties["sentry_application_id"],
+ )
+
+ if "callSite.short" in task_context._localProperties:
+ event.setdefault("extra", {}).setdefault(
+ "callSite", task_context._localProperties["callSite.short"]
+ )
+
+ return event
+
+
+def _sentry_worker_main(*args, **kwargs):
+ # type: (*Optional[Any], **Optional[Any]) -> None
+ import pyspark.worker as original_worker
+
+ try:
+ original_worker.main(*args, **kwargs)
+ except SystemExit:
+ if Hub.current.get_integration(SparkWorkerIntegration) is not None:
+ hub = Hub.current
+ exc_info = sys.exc_info()
+ with capture_internal_exceptions():
+ _capture_exception(exc_info, hub)
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/sqlalchemy.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/sqlalchemy.py
new file mode 100644
index 0000000000..f24d2f20bf
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/sqlalchemy.py
@@ -0,0 +1,86 @@
+from __future__ import absolute_import
+
+from sentry_sdk._types import MYPY
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.tracing import record_sql_queries
+
+try:
+ from sqlalchemy.engine import Engine # type: ignore
+ from sqlalchemy.event import listen # type: ignore
+ from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore
+except ImportError:
+ raise DidNotEnable("SQLAlchemy not installed.")
+
+if MYPY:
+ from typing import Any
+ from typing import ContextManager
+ from typing import Optional
+
+ from sentry_sdk.tracing import Span
+
+
+class SqlalchemyIntegration(Integration):
+ identifier = "sqlalchemy"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+
+ try:
+ version = tuple(map(int, SQLALCHEMY_VERSION.split("b")[0].split(".")))
+ except (TypeError, ValueError):
+ raise DidNotEnable(
+ "Unparseable SQLAlchemy version: {}".format(SQLALCHEMY_VERSION)
+ )
+
+ if version < (1, 2):
+ raise DidNotEnable("SQLAlchemy 1.2 or newer required.")
+
+ listen(Engine, "before_cursor_execute", _before_cursor_execute)
+ listen(Engine, "after_cursor_execute", _after_cursor_execute)
+ listen(Engine, "handle_error", _handle_error)
+
+
+def _before_cursor_execute(
+ conn, cursor, statement, parameters, context, executemany, *args
+):
+ # type: (Any, Any, Any, Any, Any, bool, *Any) -> None
+ hub = Hub.current
+ if hub.get_integration(SqlalchemyIntegration) is None:
+ return
+
+ ctx_mgr = record_sql_queries(
+ hub,
+ cursor,
+ statement,
+ parameters,
+ paramstyle=context and context.dialect and context.dialect.paramstyle or None,
+ executemany=executemany,
+ )
+ conn._sentry_sql_span_manager = ctx_mgr
+
+ span = ctx_mgr.__enter__()
+
+ if span is not None:
+ conn._sentry_sql_span = span
+
+
+def _after_cursor_execute(conn, cursor, statement, *args):
+ # type: (Any, Any, Any, *Any) -> None
+ ctx_mgr = getattr(
+ conn, "_sentry_sql_span_manager", None
+ ) # type: ContextManager[Any]
+
+ if ctx_mgr is not None:
+ conn._sentry_sql_span_manager = None
+ ctx_mgr.__exit__(None, None, None)
+
+
+def _handle_error(context, *args):
+ # type: (Any, *Any) -> None
+ conn = context.connection
+ span = getattr(conn, "_sentry_sql_span", None) # type: Optional[Span]
+
+ if span is not None:
+ span.set_status("internal_error")
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/stdlib.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/stdlib.py
new file mode 100644
index 0000000000..56cece70ac
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/stdlib.py
@@ -0,0 +1,230 @@
+import os
+import subprocess
+import sys
+import platform
+
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration
+from sentry_sdk.scope import add_global_event_processor
+from sentry_sdk.tracing import EnvironHeaders
+from sentry_sdk.utils import capture_internal_exceptions, safe_repr
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Callable
+ from typing import Dict
+ from typing import Optional
+ from typing import List
+
+ from sentry_sdk._types import Event, Hint
+
+
+try:
+ from httplib import HTTPConnection # type: ignore
+except ImportError:
+ from http.client import HTTPConnection
+
+
+_RUNTIME_CONTEXT = {
+ "name": platform.python_implementation(),
+ "version": "%s.%s.%s" % (sys.version_info[:3]),
+ "build": sys.version,
+}
+
+
+class StdlibIntegration(Integration):
+ identifier = "stdlib"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ _install_httplib()
+ _install_subprocess()
+
+ @add_global_event_processor
+ def add_python_runtime_context(event, hint):
+ # type: (Event, Hint) -> Optional[Event]
+ if Hub.current.get_integration(StdlibIntegration) is not None:
+ contexts = event.setdefault("contexts", {})
+ if isinstance(contexts, dict) and "runtime" not in contexts:
+ contexts["runtime"] = _RUNTIME_CONTEXT
+
+ return event
+
+
+def _install_httplib():
+ # type: () -> None
+ real_putrequest = HTTPConnection.putrequest
+ real_getresponse = HTTPConnection.getresponse
+
+ def putrequest(self, method, url, *args, **kwargs):
+ # type: (HTTPConnection, str, str, *Any, **Any) -> Any
+ hub = Hub.current
+ if hub.get_integration(StdlibIntegration) is None:
+ return real_putrequest(self, method, url, *args, **kwargs)
+
+ host = self.host
+ port = self.port
+ default_port = self.default_port
+
+ real_url = url
+ if not real_url.startswith(("http://", "https://")):
+ real_url = "%s://%s%s%s" % (
+ default_port == 443 and "https" or "http",
+ host,
+ port != default_port and ":%s" % port or "",
+ url,
+ )
+
+ span = hub.start_span(op="http", description="%s %s" % (method, real_url))
+
+ span.set_data("method", method)
+ span.set_data("url", real_url)
+
+ rv = real_putrequest(self, method, url, *args, **kwargs)
+
+ for key, value in hub.iter_trace_propagation_headers():
+ self.putheader(key, value)
+
+ self._sentrysdk_span = span
+
+ return rv
+
+ def getresponse(self, *args, **kwargs):
+ # type: (HTTPConnection, *Any, **Any) -> Any
+ span = getattr(self, "_sentrysdk_span", None)
+
+ if span is None:
+ return real_getresponse(self, *args, **kwargs)
+
+ rv = real_getresponse(self, *args, **kwargs)
+
+ span.set_data("status_code", rv.status)
+ span.set_http_status(int(rv.status))
+ span.set_data("reason", rv.reason)
+ span.finish()
+
+ return rv
+
+ HTTPConnection.putrequest = putrequest
+ HTTPConnection.getresponse = getresponse
+
+
+def _init_argument(args, kwargs, name, position, setdefault_callback=None):
+ # type: (List[Any], Dict[Any, Any], str, int, Optional[Callable[[Any], Any]]) -> Any
+ """
+ given (*args, **kwargs) of a function call, retrieve (and optionally set a
+ default for) an argument by either name or position.
+
+ This is useful for wrapping functions with complex type signatures and
+ extracting a few arguments without needing to redefine that function's
+ entire type signature.
+ """
+
+ if name in kwargs:
+ rv = kwargs[name]
+ if setdefault_callback is not None:
+ rv = setdefault_callback(rv)
+ if rv is not None:
+ kwargs[name] = rv
+ elif position < len(args):
+ rv = args[position]
+ if setdefault_callback is not None:
+ rv = setdefault_callback(rv)
+ if rv is not None:
+ args[position] = rv
+ else:
+ rv = setdefault_callback and setdefault_callback(None)
+ if rv is not None:
+ kwargs[name] = rv
+
+ return rv
+
+
+def _install_subprocess():
+ # type: () -> None
+ old_popen_init = subprocess.Popen.__init__
+
+ def sentry_patched_popen_init(self, *a, **kw):
+ # type: (subprocess.Popen[Any], *Any, **Any) -> None
+
+ hub = Hub.current
+ if hub.get_integration(StdlibIntegration) is None:
+ return old_popen_init(self, *a, **kw) # type: ignore
+
+ # Convert from tuple to list to be able to set values.
+ a = list(a)
+
+ args = _init_argument(a, kw, "args", 0) or []
+ cwd = _init_argument(a, kw, "cwd", 9)
+
+ # if args is not a list or tuple (and e.g. some iterator instead),
+ # let's not use it at all. There are too many things that can go wrong
+ # when trying to collect an iterator into a list and setting that list
+ # into `a` again.
+ #
+ # Also invocations where `args` is not a sequence are not actually
+ # legal. They just happen to work under CPython.
+ description = None
+
+ if isinstance(args, (list, tuple)) and len(args) < 100:
+ with capture_internal_exceptions():
+ description = " ".join(map(str, args))
+
+ if description is None:
+ description = safe_repr(args)
+
+ env = None
+
+ for k, v in hub.iter_trace_propagation_headers():
+ if env is None:
+ env = _init_argument(a, kw, "env", 10, lambda x: dict(x or os.environ))
+ env["SUBPROCESS_" + k.upper().replace("-", "_")] = v
+
+ with hub.start_span(op="subprocess", description=description) as span:
+ if cwd:
+ span.set_data("subprocess.cwd", cwd)
+
+ rv = old_popen_init(self, *a, **kw) # type: ignore
+
+ span.set_tag("subprocess.pid", self.pid)
+ return rv
+
+ subprocess.Popen.__init__ = sentry_patched_popen_init # type: ignore
+
+ old_popen_wait = subprocess.Popen.wait
+
+ def sentry_patched_popen_wait(self, *a, **kw):
+ # type: (subprocess.Popen[Any], *Any, **Any) -> Any
+ hub = Hub.current
+
+ if hub.get_integration(StdlibIntegration) is None:
+ return old_popen_wait(self, *a, **kw)
+
+ with hub.start_span(op="subprocess.wait") as span:
+ span.set_tag("subprocess.pid", self.pid)
+ return old_popen_wait(self, *a, **kw)
+
+ subprocess.Popen.wait = sentry_patched_popen_wait # type: ignore
+
+ old_popen_communicate = subprocess.Popen.communicate
+
+ def sentry_patched_popen_communicate(self, *a, **kw):
+ # type: (subprocess.Popen[Any], *Any, **Any) -> Any
+ hub = Hub.current
+
+ if hub.get_integration(StdlibIntegration) is None:
+ return old_popen_communicate(self, *a, **kw)
+
+ with hub.start_span(op="subprocess.communicate") as span:
+ span.set_tag("subprocess.pid", self.pid)
+ return old_popen_communicate(self, *a, **kw)
+
+ subprocess.Popen.communicate = sentry_patched_popen_communicate # type: ignore
+
+
+def get_subprocess_traceparent_headers():
+ # type: () -> EnvironHeaders
+ return EnvironHeaders(os.environ, prefix="SUBPROCESS_")
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/threading.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/threading.py
new file mode 100644
index 0000000000..b750257e2a
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/threading.py
@@ -0,0 +1,90 @@
+from __future__ import absolute_import
+
+import sys
+from threading import Thread, current_thread
+
+from sentry_sdk import Hub
+from sentry_sdk._compat import reraise
+from sentry_sdk._types import MYPY
+from sentry_sdk.integrations import Integration
+from sentry_sdk.utils import event_from_exception, capture_internal_exceptions
+
+if MYPY:
+ from typing import Any
+ from typing import TypeVar
+ from typing import Callable
+ from typing import Optional
+
+ from sentry_sdk._types import ExcInfo
+
+ F = TypeVar("F", bound=Callable[..., Any])
+
+
+class ThreadingIntegration(Integration):
+ identifier = "threading"
+
+ def __init__(self, propagate_hub=False):
+ # type: (bool) -> None
+ self.propagate_hub = propagate_hub
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ old_start = Thread.start
+
+ def sentry_start(self, *a, **kw):
+ # type: (Thread, *Any, **Any) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(ThreadingIntegration)
+ if integration is not None:
+ if not integration.propagate_hub:
+ hub_ = None
+ else:
+ hub_ = Hub(hub)
+ # Patching instance methods in `start()` creates a reference cycle if
+ # done in a naive way. See
+ # https://github.com/getsentry/sentry-python/pull/434
+ #
+ # In threading module, using current_thread API will access current thread instance
+ # without holding it to avoid a reference cycle in an easier way.
+ with capture_internal_exceptions():
+ new_run = _wrap_run(hub_, getattr(self.run, "__func__", self.run))
+ self.run = new_run # type: ignore
+
+ return old_start(self, *a, **kw) # type: ignore
+
+ Thread.start = sentry_start # type: ignore
+
+
+def _wrap_run(parent_hub, old_run_func):
+ # type: (Optional[Hub], F) -> F
+ def run(*a, **kw):
+ # type: (*Any, **Any) -> Any
+ hub = parent_hub or Hub.current
+ with hub:
+ try:
+ self = current_thread()
+ return old_run_func(self, *a, **kw)
+ except Exception:
+ reraise(*_capture_exception())
+
+ return run # type: ignore
+
+
+def _capture_exception():
+ # type: () -> ExcInfo
+ hub = Hub.current
+ exc_info = sys.exc_info()
+
+ if hub.get_integration(ThreadingIntegration) is not None:
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ event, hint = event_from_exception(
+ exc_info,
+ client_options=client.options,
+ mechanism={"type": "threading", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+ return exc_info
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/tornado.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/tornado.py
new file mode 100644
index 0000000000..d3ae065690
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/tornado.py
@@ -0,0 +1,203 @@
+import weakref
+from inspect import iscoroutinefunction
+
+from sentry_sdk.hub import Hub, _should_send_default_pii
+from sentry_sdk.utils import (
+ HAS_REAL_CONTEXTVARS,
+ event_from_exception,
+ capture_internal_exceptions,
+ transaction_from_function,
+)
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations._wsgi_common import (
+ RequestExtractor,
+ _filter_headers,
+ _is_json_content_type,
+)
+from sentry_sdk.integrations.logging import ignore_logger
+from sentry_sdk._compat import iteritems
+
+try:
+ from tornado import version_info as TORNADO_VERSION # type: ignore
+ from tornado.web import RequestHandler, HTTPError
+ from tornado.gen import coroutine
+except ImportError:
+ raise DidNotEnable("Tornado not installed")
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Any
+ from typing import Optional
+ from typing import Dict
+ from typing import Callable
+
+ from sentry_sdk._types import EventProcessor
+
+
+class TornadoIntegration(Integration):
+ identifier = "tornado"
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ if TORNADO_VERSION < (5, 0):
+ raise DidNotEnable("Tornado 5+ required")
+
+ if not HAS_REAL_CONTEXTVARS:
+ # Tornado is async. We better have contextvars or we're going to leak
+ # state between requests.
+ raise DidNotEnable(
+ "The tornado integration for Sentry requires Python 3.6+ or the aiocontextvars package"
+ )
+
+ ignore_logger("tornado.access")
+
+ old_execute = RequestHandler._execute # type: ignore
+
+ awaitable = iscoroutinefunction(old_execute)
+
+ if awaitable:
+ # Starting Tornado 6 RequestHandler._execute method is a standard Python coroutine (async/await)
+ # In that case our method should be a coroutine function too
+ async def sentry_execute_request_handler(self, *args, **kwargs):
+ # type: (Any, *Any, **Any) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(TornadoIntegration)
+ if integration is None:
+ return await old_execute(self, *args, **kwargs)
+
+ weak_handler = weakref.ref(self)
+
+ with Hub(hub) as hub:
+ with hub.configure_scope() as scope:
+ scope.clear_breadcrumbs()
+ processor = _make_event_processor(weak_handler) # type: ignore
+ scope.add_event_processor(processor)
+ return await old_execute(self, *args, **kwargs)
+
+ else:
+
+ @coroutine # type: ignore
+ def sentry_execute_request_handler(self, *args, **kwargs):
+ # type: (RequestHandler, *Any, **Any) -> Any
+ hub = Hub.current
+ integration = hub.get_integration(TornadoIntegration)
+ if integration is None:
+ return old_execute(self, *args, **kwargs)
+
+ weak_handler = weakref.ref(self)
+
+ with Hub(hub) as hub:
+ with hub.configure_scope() as scope:
+ scope.clear_breadcrumbs()
+ processor = _make_event_processor(weak_handler) # type: ignore
+ scope.add_event_processor(processor)
+ result = yield from old_execute(self, *args, **kwargs)
+ return result
+
+ RequestHandler._execute = sentry_execute_request_handler # type: ignore
+
+ old_log_exception = RequestHandler.log_exception
+
+ def sentry_log_exception(self, ty, value, tb, *args, **kwargs):
+ # type: (Any, type, BaseException, Any, *Any, **Any) -> Optional[Any]
+ _capture_exception(ty, value, tb)
+ return old_log_exception(self, ty, value, tb, *args, **kwargs) # type: ignore
+
+ RequestHandler.log_exception = sentry_log_exception # type: ignore
+
+
+def _capture_exception(ty, value, tb):
+ # type: (type, BaseException, Any) -> None
+ hub = Hub.current
+ if hub.get_integration(TornadoIntegration) is None:
+ return
+ if isinstance(value, HTTPError):
+ return
+
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+
+ event, hint = event_from_exception(
+ (ty, value, tb),
+ client_options=client.options,
+ mechanism={"type": "tornado", "handled": False},
+ )
+
+ hub.capture_event(event, hint=hint)
+
+
+def _make_event_processor(weak_handler):
+ # type: (Callable[[], RequestHandler]) -> EventProcessor
+ def tornado_processor(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+ handler = weak_handler()
+ if handler is None:
+ return event
+
+ request = handler.request
+
+ with capture_internal_exceptions():
+ method = getattr(handler, handler.request.method.lower())
+ event["transaction"] = transaction_from_function(method)
+
+ with capture_internal_exceptions():
+ extractor = TornadoRequestExtractor(request)
+ extractor.extract_into_event(event)
+
+ request_info = event["request"]
+
+ request_info["url"] = "%s://%s%s" % (
+ request.protocol,
+ request.host,
+ request.path,
+ )
+
+ request_info["query_string"] = request.query
+ request_info["method"] = request.method
+ request_info["env"] = {"REMOTE_ADDR": request.remote_ip}
+ request_info["headers"] = _filter_headers(dict(request.headers))
+
+ with capture_internal_exceptions():
+ if handler.current_user and _should_send_default_pii():
+ event.setdefault("user", {}).setdefault("is_authenticated", True)
+
+ return event
+
+ return tornado_processor
+
+
+class TornadoRequestExtractor(RequestExtractor):
+ def content_length(self):
+ # type: () -> int
+ if self.request.body is None:
+ return 0
+ return len(self.request.body)
+
+ def cookies(self):
+ # type: () -> Dict[str, str]
+ return {k: v.value for k, v in iteritems(self.request.cookies)}
+
+ def raw_data(self):
+ # type: () -> bytes
+ return self.request.body
+
+ def form(self):
+ # type: () -> Dict[str, Any]
+ return {
+ k: [v.decode("latin1", "replace") for v in vs]
+ for k, vs in iteritems(self.request.body_arguments)
+ }
+
+ def is_json(self):
+ # type: () -> bool
+ return _is_json_content_type(self.request.headers.get("content-type"))
+
+ def files(self):
+ # type: () -> Dict[str, Any]
+ return {k: v[0] for k, v in iteritems(self.request.files) if v}
+
+ def size_of_file(self, file):
+ # type: (Any) -> int
+ return len(file.body or ())
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/trytond.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/trytond.py
new file mode 100644
index 0000000000..062a756993
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/trytond.py
@@ -0,0 +1,55 @@
+import sentry_sdk.hub
+import sentry_sdk.utils
+import sentry_sdk.integrations
+import sentry_sdk.integrations.wsgi
+from sentry_sdk._types import MYPY
+
+from trytond.exceptions import TrytonException # type: ignore
+from trytond.wsgi import app # type: ignore
+
+if MYPY:
+ from typing import Any
+
+
+# TODO: trytond-worker, trytond-cron and trytond-admin intergations
+
+
+class TrytondWSGIIntegration(sentry_sdk.integrations.Integration):
+ identifier = "trytond_wsgi"
+
+ def __init__(self): # type: () -> None
+ pass
+
+ @staticmethod
+ def setup_once(): # type: () -> None
+
+ app.wsgi_app = sentry_sdk.integrations.wsgi.SentryWsgiMiddleware(app.wsgi_app)
+
+ def error_handler(e): # type: (Exception) -> None
+ hub = sentry_sdk.hub.Hub.current
+
+ if hub.get_integration(TrytondWSGIIntegration) is None:
+ return
+ elif isinstance(e, TrytonException):
+ return
+ else:
+ # If an integration is there, a client has to be there.
+ client = hub.client # type: Any
+ event, hint = sentry_sdk.utils.event_from_exception(
+ e,
+ client_options=client.options,
+ mechanism={"type": "trytond", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+ # Expected error handlers signature was changed
+ # when the error_handler decorator was introduced
+ # in Tryton-5.4
+ if hasattr(app, "error_handler"):
+
+ @app.error_handler
+ def _(app, request, e): # type: ignore
+ error_handler(e)
+
+ else:
+ app.error_handlers.append(error_handler)
diff --git a/third_party/python/sentry-sdk/sentry_sdk/integrations/wsgi.py b/third_party/python/sentry-sdk/sentry_sdk/integrations/wsgi.py
new file mode 100644
index 0000000000..22982d8bb1
--- /dev/null
+++ b/third_party/python/sentry-sdk/sentry_sdk/integrations/wsgi.py
@@ -0,0 +1,309 @@
+import functools
+import sys
+
+from sentry_sdk.hub import Hub, _should_send_default_pii
+from sentry_sdk.utils import (
+ ContextVar,
+ capture_internal_exceptions,
+ event_from_exception,
+)
+from sentry_sdk._compat import PY2, reraise, iteritems
+from sentry_sdk.tracing import Span
+from sentry_sdk.sessions import auto_session_tracking
+from sentry_sdk.integrations._wsgi_common import _filter_headers
+
+from sentry_sdk._types import MYPY
+
+if MYPY:
+ from typing import Callable
+ from typing import Dict
+ from typing import Iterator
+ from typing import Any
+ from typing import Tuple
+ from typing import Optional
+ from typing import TypeVar
+ from typing import Protocol
+
+ from sentry_sdk.utils import ExcInfo
+ from sentry_sdk._types import EventProcessor
+
+ WsgiResponseIter = TypeVar("WsgiResponseIter")
+ WsgiResponseHeaders = TypeVar("WsgiResponseHeaders")
+ WsgiExcInfo = TypeVar("WsgiExcInfo")
+
+ class StartResponse(Protocol):
+ def __call__(self, status, response_headers, exc_info=None):
+ # type: (str, WsgiResponseHeaders, Optional[WsgiExcInfo]) -> WsgiResponseIter
+ pass
+
+
+_wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
+
+
+if PY2:
+
+ def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
+ # type: (str, str, str) -> str
+ return s.decode(charset, errors)
+
+
+else:
+
+ def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
+ # type: (str, str, str) -> str
+ return s.encode("latin1").decode(charset, errors)
+
+
+def get_host(environ):
+ # type: (Dict[str, str]) -> str
+ """Return the host for the given WSGI environment. Yanked from Werkzeug."""
+ if environ.get("HTTP_HOST"):
+ rv = environ["HTTP_HOST"]
+ if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
+ rv = rv[:-3]
+ elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
+ rv = rv[:-4]
+ elif environ.get("SERVER_NAME"):
+ rv = environ["SERVER_NAME"]
+ if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in (
+ ("https", "443"),
+ ("http", "80"),
+ ):
+ rv += ":" + environ["SERVER_PORT"]
+ else:
+ # In spite of the WSGI spec, SERVER_NAME might not be present.
+ rv = "unknown"
+
+ return rv
+
+
+def get_request_url(environ):
+ # type: (Dict[str, str]) -> str
+ """Return the absolute URL without query string for the given WSGI
+ environment."""
+ return "%s://%s/%s" % (
+ environ.get("wsgi.url_scheme"),
+ get_host(environ),
+ wsgi_decoding_dance(environ.get("PATH_INFO") or "").lstrip("/"),
+ )
+
+
+class SentryWsgiMiddleware(object):
+ __slots__ = ("app",)
+
+ def __init__(self, app):
+ # type: (Callable[[Dict[str, str], Callable[..., Any]], Any]) -> None
+ self.app = app
+
+ def __call__(self, environ, start_response):
+ # type: (Dict[str, str], Callable[..., Any]) -> _ScopedResponse
+ if _wsgi_middleware_applied.get(False):
+ return self.app(environ, start_response)
+
+ _wsgi_middleware_applied.set(True)
+ try:
+ hub = Hub(Hub.current)
+ with auto_session_tracking(hub):
+ with hub:
+ with capture_internal_exceptions():
+ with hub.configure_scope() as scope:
+ scope.clear_breadcrumbs()
+ scope._name = "wsgi"
+ scope.add_event_processor(
+ _make_wsgi_event_processor(environ)
+ )
+
+ span = Span.continue_from_environ(environ)
+ span.op = "http.server"
+ span.transaction = "generic WSGI request"
+
+ with hub.start_span(span) as span:
+ try:
+ rv = self.app(
+ environ,
+ functools.partial(
+ _sentry_start_response, start_response, span
+ ),
+ )
+ except BaseException:
+ reraise(*_capture_exception(hub))
+ finally:
+ _wsgi_middleware_applied.set(False)
+
+ return _ScopedResponse(hub, rv)
+
+
+def _sentry_start_response(
+ old_start_response, # type: StartResponse
+ span, # type: Span
+ status, # type: str
+ response_headers, # type: WsgiResponseHeaders
+ exc_info=None, # type: Optional[WsgiExcInfo]
+):
+ # type: (...) -> WsgiResponseIter
+ with capture_internal_exceptions():
+ status_int = int(status.split(" ", 1)[0])
+ span.set_http_status(status_int)
+
+ if exc_info is None:
+ # The Django Rest Framework WSGI test client, and likely other
+ # (incorrect) implementations, cannot deal with the exc_info argument
+ # if one is present. Avoid providing a third argument if not necessary.
+ return old_start_response(status, response_headers)
+ else:
+ return old_start_response(status, response_headers, exc_info)
+
+
+def _get_environ(environ):
+ # type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
+ """
+ Returns our whitelisted environment variables.
+ """
+ keys = ["SERVER_NAME", "SERVER_PORT"]
+ if _should_send_default_pii():
+ # make debugging of proxy setup easier. Proxy headers are
+ # in headers.
+ keys += ["REMOTE_ADDR"]
+
+ for key in keys:
+ if key in environ:
+ yield key, environ[key]
+
+
+# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
+#
+# We need this function because Django does not give us a "pure" http header
+# dict. So we might as well use it for all WSGI integrations.
+def _get_headers(environ):
+ # type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
+ """
+ Returns only proper HTTP headers.
+
+ """
+ for key, value in iteritems(environ):
+ key = str(key)
+ if key.startswith("HTTP_") and key not in (
+ "HTTP_CONTENT_TYPE",
+ "HTTP_CONTENT_LENGTH",
+ ):
+ yield key[5:].replace("_", "-").title(), value
+ elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
+ yield key.replace("_", "-").title(), value
+
+
+def get_client_ip(environ):
+ # type: (Dict[str, str]) -> Optional[Any]
+ """
+ Infer the user IP address from various headers. This cannot be used in
+ security sensitive situations since the value may be forged from a client,
+ but it's good enough for the event payload.
+ """
+ try:
+ return environ["HTTP_X_FORWARDED_FOR"].split(",")[0].strip()
+ except (KeyError, IndexError):
+ pass
+
+ try:
+ return environ["HTTP_X_REAL_IP"]
+ except KeyError:
+ pass
+
+ return environ.get("REMOTE_ADDR")
+
+
+def _capture_exception(hub):
+ # type: (Hub) -> ExcInfo
+ exc_info = sys.exc_info()
+
+ # Check client here as it might have been unset while streaming response
+ if hub.client is not None:
+ e = exc_info[1]
+
+ # SystemExit(0) is the only uncaught exception that is expected behavior
+ should_skip_capture = isinstance(e, SystemExit) and e.code in (0, None)
+ if not should_skip_capture:
+ event, hint = event_from_exception(
+ exc_info,
+ client_options=hub.client.options,
+ mechanism={"type": "wsgi", "handled": False},
+ )
+ hub.capture_event(event, hint=hint)
+
+ return exc_info
+
+
+class _ScopedResponse(object):
+ __slots__ = ("_response", "_hub")
+
+ def __init__(self, hub, response):
+ # type: (Hub, Iterator[bytes]) -> None
+ self._hub = hub
+ self._response = response
+
+ def __iter__(self):
+ # type: () -> Iterator[bytes]
+ iterator = iter(self._response)
+
+ while True:
+ with self._hub:
+ try:
+ chunk = next(iterator)
+ except StopIteration:
+ break
+ except BaseException:
+ reraise(*_capture_exception(self._hub))
+
+ yield chunk
+
+ def close(self):
+ # type: () -> None
+ with self._hub:
+ try:
+ self._response.close() # type: ignore
+ except AttributeError:
+ pass
+ except BaseException:
+ reraise(*_capture_exception(self._hub))
+
+
+def _make_wsgi_event_processor(environ):
+ # type: (Dict[str, str]) -> EventProcessor
+ # It's a bit unfortunate that we have to extract and parse the request data
+ # from the environ so eagerly, but there are a few good reasons for this.
+ #
+ # We might be in a situation where the scope/hub never gets torn down
+ # properly. In that case we will have an unnecessary strong reference to
+ # all objects in the environ (some of which may take a lot of memory) when
+ # we're really just interested in a few of them.
+ #
+ # Keeping the environment around for longer than the request lifecycle is
+ # also not necessarily something uWSGI can deal with:
+ # https://github.com/unbit/uwsgi/issues/1950
+
+ client_ip = get_client_ip(environ)
+ request_url = get_request_url(environ)
+ query_string = environ.get("QUERY_STRING")
+ method = environ.get("REQUEST_METHOD")
+ env = dict(_get_environ(environ))
+ headers = _filter_headers(dict(_get_headers(environ)))
+
+ def event_processor(event, hint):
+ # type: (Dict[str, Any], Dict[str, Any]) -> Dict[str, Any]
+ with capture_internal_exceptions():
+ # if the code below fails halfway through we at least have some data
+ request_info = event.setdefault("request", {})
+
+ if _should_send_default_pii():
+ user_info = event.setdefault("user", {})
+ if client_ip:
+ user_info.setdefault("ip_address", client_ip)
+
+ request_info["url"] = request_url
+ request_info["query_string"] = query_string
+ request_info["method"] = method
+ request_info["env"] = env
+ request_info["headers"] = headers
+
+ return event
+
+ return event_processor