Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions sentry_sdk/integrations/_wsgi_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,7 @@ def parsed_body(self):
return self.json()

def is_json(self):
mt = (self.env().get("CONTENT_TYPE") or "").split(";", 1)[0]
return (
mt == "application/json"
or (mt.startswith("application/"))
and mt.endswith("+json")
)
return _is_json_content_type(self.env().get("CONTENT_TYPE"))

def json(self):
try:
Expand All @@ -98,6 +93,18 @@ def files(self):
def size_of_file(self, file):
raise NotImplementedError()

def env(self):
raise NotImplementedError()


def _is_json_content_type(ct):
mt = (ct or "").split(";", 1)[0]
return (
mt == "application/json"
or (mt.startswith("application/"))
and mt.endswith("+json")
)


def _filter_headers(headers):
if _should_send_default_pii():
Expand Down
6 changes: 2 additions & 4 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,13 @@ def sql_to_string(sql):
event_from_exception,
safe_repr,
format_and_strip,
transaction_from_function,
)
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.integrations._wsgi_common import RequestExtractor
from sentry_sdk.integrations.django.transactions import (
LEGACY_RESOLVER,
transaction_from_function,
)
from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER


if DJANGO_VERSION < (1, 10):
Expand Down
31 changes: 0 additions & 31 deletions sentry_sdk/integrations/django/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,34 +110,3 @@ def resolve(self, path, urlconf=None):


LEGACY_RESOLVER = RavenResolver()


def transaction_from_function(func):
# Methods in Python 2
try:
return "%s.%s.%s" % (
func.im_class.__module__,
func.im_class.__name__,
func.__name__,
)
except Exception:
pass

func_qualname = (
getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None
)

if not func_qualname:
# No idea what it is
return None

# Methods in Python 3
# Functions
# Classes
try:
return "%s.%s" % (func.__module__, func_qualname)
except Exception:
pass

# Possibly a lambda
return func_qualname
157 changes: 157 additions & 0 deletions sentry_sdk/integrations/tornado.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import sys
import weakref

from sentry_sdk.hub import Hub, _should_send_default_pii
from sentry_sdk.utils import (
event_from_exception,
capture_internal_exceptions,
transaction_from_function,
)
from sentry_sdk.integrations import Integration
from sentry_sdk.integrations._wsgi_common import (
RequestExtractor,
_filter_headers,
_is_json_content_type,
)
from sentry_sdk.integrations.logging import ignore_logger

from tornado.web import RequestHandler, HTTPError
from tornado.gen import coroutine


class TornadoIntegration(Integration):
identifier = "tornado"

@staticmethod
def setup_once():
import tornado

tornado_version = getattr(tornado, "version_info", None)
if tornado_version is None or tornado_version < (5, 0):
raise RuntimeError("Tornado 5+ required")

if sys.version_info < (3, 7):
# Tornado is async. We better have contextvars or we're going to leak
# state between requests.
raise RuntimeError(
"The tornado integration for Sentry requires Python 3.7+"
)

ignore_logger("tornado.application")
ignore_logger("tornado.access")

old_execute = RequestHandler._execute

@coroutine
def sentry_execute_request_handler(self, *args, **kwargs):
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.add_event_processor(_make_event_processor(weak_handler))
result = yield from old_execute(self, *args, **kwargs)
return result

RequestHandler._execute = sentry_execute_request_handler

old_log_exception = RequestHandler.log_exception

def sentry_log_exception(self, ty, value, tb, *args, **kwargs):
_capture_exception(ty, value, tb)
return old_log_exception(self, ty, value, tb, *args, **kwargs)

RequestHandler.log_exception = sentry_log_exception


def _capture_exception(ty, value, tb):
hub = Hub.current
if hub.get_integration(TornadoIntegration) is None:
return
if isinstance(value, HTTPError):
return

event, hint = event_from_exception(
(ty, value, tb),
client_options=hub.client.options,
mechanism={"type": "tornado", "handled": False},
)

hub.capture_event(event, hint=hint)


def _make_event_processor(weak_handler):
def tornado_processor(event, hint):
handler = weak_handler()
if handler is None:
return event

request = handler.request

if "transaction" not in event:
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"]

if "url" not in request_info:
request_info["url"] = "%s://%s%s" % (
request.protocol,
request.host,
request.path,
)

if "query_string" not in request_info:
request_info["query_string"] = request.query

if "method" not in request_info:
request_info["method"] = request.method

if "env" not in request_info:
request_info["env"] = {"REMOTE_ADDR": request.remote_ip}

if "headers" not in request_info:
request_info["headers"] = _filter_headers(dict(request.headers))

with capture_internal_exceptions():
if handler.current_user and _should_send_default_pii():
event.setdefault("user", {})["is_authenticated"] = True

return event

return tornado_processor


class TornadoRequestExtractor(RequestExtractor):
def content_length(self):
if self.request.body is None:
return 0
return len(self.request.body)

def cookies(self):
return dict(self.request.cookies)

def raw_data(self):
return self.request.body

def form(self):
# TODO: Where to get formdata and nothing else?
return None

def is_json(self):
return _is_json_content_type(self.request.headers.get("content-type"))

def files(self):
return {k: v[0] for k, v in self.request.files.items() if v}

def size_of_file(self, file):
return len(file.body or ())
31 changes: 31 additions & 0 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,3 +712,34 @@ def get(self, default):

def set(self, value):
setattr(self._local, "value", value)


def transaction_from_function(func):
# Methods in Python 2
try:
return "%s.%s.%s" % (
func.im_class.__module__,
func.im_class.__name__,
func.__name__,
)
except Exception:
pass

func_qualname = (
getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None
)

if not func_qualname:
# No idea what it is
return None

# Methods in Python 3
# Functions
# Classes
try:
return "%s.%s" % (func.__module__, func_qualname)
except Exception:
pass

# Possibly a lambda
return func_qualname
27 changes: 1 addition & 26 deletions tests/integrations/django/test_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@
# for Django version less than 1.4
from django.conf.urls.defaults import url, include # NOQA

from sentry_sdk.integrations.django.transactions import (
RavenResolver,
transaction_from_function,
)
from sentry_sdk.integrations.django.transactions import RavenResolver


if django.VERSION < (1, 9):
Expand Down Expand Up @@ -53,25 +50,3 @@ def test_legacy_resolver_newstyle_django20_urlconf():
resolver = RavenResolver()
result = resolver.resolve("/api/v2/1234/store/", url_conf)
assert result == "/api/v2/{project_id}/store/"


class MyClass:
def myfunc():
pass


def myfunc():
pass


def test_transaction_from_function():
x = transaction_from_function
assert x(MyClass) == "tests.integrations.django.test_transactions.MyClass"
assert (
x(MyClass.myfunc)
== "tests.integrations.django.test_transactions.MyClass.myfunc"
)
assert x(myfunc) == "tests.integrations.django.test_transactions.myfunc"
assert x(None) is None
assert x(42) is None
assert x(lambda: None).endswith("<lambda>")
3 changes: 3 additions & 0 deletions tests/integrations/tornado/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import pytest

tornado = pytest.importorskip("tornado")
Loading