8000 fix: Introduce transaction styles for backwards compat (#105) · etherscan-io/sentry-python@15c782e · GitHub
[go: up one dir, main page]

Skip to content

Commit 15c782e

Browse files
authored
fix: Introduce transaction styles for backwards compat (getsentry#105)
* fix: Introduce transaction styles for backwards compat * fix: skip entire django folder if no django installed * fix: Change default for Django * fix: styling
1 parent 4038bdc commit 15c782e

File tree

9 files changed

+391
-63
lines changed

9 files changed

+391
-63
lines changed

sentry_sdk/integrations/django.py renamed to sentry_sdk/integrations/django/__init__.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
from sentry_sdk.integrations import Integration
2424
from sentry_sdk.integrations.logging import ignore_logger
2525
from sentry_sdk.integrations._wsgi import RequestExtractor, run_wsgi_app
26+
from sentry_sdk.integrations.django.transactions import (
27+
LEGACY_RESOLVER,
28+
transaction_from_function,
29+
)
2630

2731

2832
if DJANGO_VERSION < (1, 10):
@@ -40,6 +44,17 @@ def is_authenticated(request_user):
4044
class DjangoIntegration(Integration):
4145
identifier = "django"
4246

47+
transaction_style = None
48+
49+
def __init__(self, transaction_style="url"):
50+
TRANSACTION_STYLE_VALUES = ("function_name", "url")
51+
if transaction_style not in TRANSACTION_STYLE_VALUES:
52+
raise ValueError(
53+
"Invalid value for transaction_style: %s (must be in %s)"
54+
% (transaction_style, TRANSACTION_STYLE_VALUES)
55+
)
56+
self.transaction_style = transaction_style
57+
4358
def install(self):
4459
install_sql_hook()
4560
# Patch in our custom middleware.
@@ -60,18 +75,21 @@ def sentry_patched_wsgi_handler(self, environ, start_response):
6075
from django.core.handlers.base import BaseHandler
6176

6277
old_get_response = BaseHandler.get_response
78+
integration = self
6379

6480
def sentry_patched_get_response(self, request):
6581
with configure_scope() as scope:
66-
scope.add_event_processor(_make_event_processor(weakref.ref(request)))
82+
scope.add_event_processor(
83+
_make_event_processor(weakref.ref(request), integration)
84+
)
6785
return old_get_response(self, request)
6886

6987
BaseHandler.get_response = sentry_patched_get_response
7088

7189
signals.got_request_exception.connect(_got_request_exception)
7290

7391

74-
def _make_event_processor(weak_request):
92+
def _make_event_processor(weak_request, integration):
7593
def event_processor(event, hint):
7694
# if the request is gone we are fine not logging the data from
7795
# it. This might happen if the processor is pushed away to
@@ -82,7 +100,12 @@ def event_processor(event, hint):
82100

83101
if "transaction" not in event:
84102
try:
85-
event["transaction"] = resolve(request.path).func.__name__
103+
if integration.transaction_style == "function_name":
104+
event["transaction"] = transaction_from_function(
105+
resolve(request.path).func
106+
)
107+
elif integration.transaction_style == "url":
108+
event["transaction"] = LEGACY_RESOLVER.resolve(request.path)
86109
except Exception:
87110
pass
88111

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""
2+
Copied from raven-python. Used for
3+
`DjangoIntegration(transaction_fron="raven_legacy")`.
4+
"""
5+
6+
from __future__ import absolute_import
7+
8+
import re
9+
10+
try:
11+
from django.urls import get_resolver
12+
except ImportError:
13+
from django.core.urlresolvers import get_resolver
14+
15+
16+
def get_regex(resolver_or_pattern):
17+
"""Utility method for django's deprecated resolver.regex"""
18+
try:
19+
regex = resolver_or_pattern.regex
20+
except AttributeError:
21+
regex = resolver_or_pattern.pattern.regex
22+
return regex
23+
24+
25+
class RavenResolver(object):
26+
_optional_group_matcher = re.compile(r"\(\?\:([^\)]+)\)")
27+
_named_group_matcher = re.compile(r"\(\?P<(\w+)>[^\)]+\)")
28+
_non_named_group_matcher = re.compile(r"\([^\)]+\)")
29+
# [foo|bar|baz]
30+
_either_option_matcher = re.compile(r"\[([^\]]+)\|([^\]]+)\]")
31+
_camel_re = re.compile(r"([A-Z]+)([a-z])")
32+
33+
_cache = {}
34+
35+
def _simplify(self, pattern):
36+
r"""
37+
Clean up urlpattern regexes into something readable by humans:
38+
39+
From:
40+
> "^(?P<sport_slug>\w+)/athletes/(?P<athlete_slug>\w+)/$"
41+
42+
To:
43+
> "{sport_slug}/athletes/{athlete_slug}/"
44+
"""
45+
# remove optional params
46+
# TODO(dcramer): it'd be nice to change these into [%s] but it currently
47+
# conflicts with the other rules because we're doing regexp matches
48+
# rather than parsing tokens
49+
result = self._optional_group_matcher.sub(lambda m: "%s" % m.group(1), pattern)
50+
51+
# handle named groups first
52+
result = self._named_group_matcher.sub(lambda m: "{%s}" % m.group(1), result)
53+
54+
# handle non-named groups
55+
result = self._non_named_group_matcher.sub("{var}", result)
56+
57+
# handle optional params
58+
result = self._either_option_matcher.sub(lambda m: m.group(1), result)
59+
60+
# clean up any outstanding regex-y characters.
61+
result = (
62+
result.replace("^", "")
63+
.replace("$", "")
64+
.replace("?", "")
65+
.replace("//", "/")
66+
.replace("\\", "")
67+
)
68+
69+
return result
70+
71+
def _resolve(self, resolver, path, parents=None):
72+
73+
match = get_regex(resolver).search(path) # Django < 2.0
74+
75+
if not match:
76+
return
77+
78+
if parents is None:
79+
parents = [resolver]
80+
elif resolver not in parents:
81+
parents = parents + [resolver]
82+
83+
new_path = path[match.end() :]
84+
for pattern in resolver.url_patterns:
85+
# this is an include()
86+
if not pattern.callback:
87+
match = self._resolve(pattern, new_path, parents)
88+
if match:
89+
return match
90+
continue
91+
elif not get_regex(pattern).search(new_path):
92+
continue
93+
94+
try:
95+
return self._cache[pattern]
96+
except KeyError:
97+
pass
98+
99+
prefix = "".join(self._simplify(get_regex(p).pattern) for p in parents)
100+
result = prefix + self._simplify(get_regex(pattern).pattern)
101+
if not result.startswith("/"):
102+
result = "/" + result
103+
self._cache[pattern] = result
104+
return result
105+
106+
def resolve(self, path, urlconf=None):
107+
resolver = get_resolver(urlconf)
108+
match = self._resolve(resolver, path)
109+
return match or path
110+
111+
112+
LEGACY_RESOLVER = RavenResolver()
113+
114+
115+
def transaction_from_function(func):
116+
# Methods in Python 2
117+
try:
118+
return "%s.%s.%s" % (
119+
func.im_class.__module__,
120+
func.im_class.__name__,
121+
func.__name__,
122+
)
123+
except Exception:
124+
pass
125+
126+
func_qualname = (
127+
getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None
128+
)
129+
130+
if not func_qualname:
131+
# No idea what it is
132+
return None
133+
134+
# Methods in Python 3
135+
# Functions
136+
# Classes
137+
try:
138+
return "%s.%s" % (func.__module__, func_qualname)
139+
except Exception:
140+
pass
141+
142+
# Possibly a lambda
143+
return func_qualname

sentry_sdk/integrations/flask.py

Lines changed: 55 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,21 @@
2525
class FlaskIntegration(Integration):
2626
identifier = "flask"
2727

28+
transaction_style = None
29+
30+
def __init__(self, transaction_style="endpoint"):
31+
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
32+
if transaction_style not in TRANSACTION_STYLE_VALUES:
33+
raise ValueError(
34+
"Invalid value for transaction_style: %s (must be in %s)"
35+
% (transaction_style, TRANSACTION_STYLE_VALUES)
36+
)
37+
self.transaction_style = transaction_style
38+
2839
def install(self):
29-
appcontext_pushed.connect(_push_appctx)
30-
appcontext_tearing_down.connect(_pop_appctx)
31-
request_started.connect(_request_started)
40+
appcontext_pushed.connect(self._push_appctx)
41+
appcontext_tearing_down.connect(self._pop_appctx)
42+
request_started.connect(self._request_started)
3243
got_request_exception.connect(_capture_exception)
3344

3445
old_app = Flask.__call__
@@ -40,23 +51,52 @@ def sentry_patched_wsgi_app(self, environ, start_response):
4051

4152
Flask.__call__ = sentry_patched_wsgi_app
4253

54+
def _push_appctx(self, *args, **kwargs):
55+
# always want to push scope regardless of whether WSGI app might already
56+
# have (not the case for CLI for example)
57+
hub = Hub.current
58+
hub.push_scope()
59+
60+
def _pop_appctx(self, *args, **kwargs):
61+
Hub.current.pop_scope_unsafe()
62+
63+
def _request_started(self, sender, **kwargs):
64+
weak_request = weakref.ref(_request_ctx_stack.top.request)
65+
app = _app_ctx_stack.top.app
66+
with configure_scope() as scope:
67+
scope.add_event_processor(
68+
self._make_request_event_processor(app, weak_request)
69+
)
4370

44-
def _push_appctx(*args, **kwargs):
45-
# always want to push scope regardless of whether WSGI app might already
46-
# have (not the case for CLI for example)
47-
hub = Hub.current
48-
hub.push_scope()
71+
def _make_request_event_processor(self, app, weak_request):
72+
def inner(event, hint):
73+
request = weak_request()
74+
75+
# if the request is gone we are fine not logging the data from
76+
# it. This might happen if the processor is pushed away to
77+
# another thread.
78+
if request is None:
79+
return event
80+
81+
if "transaction" not in event:
82+
try:
83+
if self.transaction_style == "endpoint":
84+
event["transaction"] = request.url_rule.endpoint
85+
elif self.transaction_style == "url":
86+
event["transaction"] = request.url_rule.rule
87+
except Exception:
88+
pass
4989

90+
with capture_internal_exceptions():
91+
FlaskRequestExtractor(request).extract_into_event(event)
5092

51-
def _pop_appctx(*args, **kwargs):
52-
Hub.current.pop_scope_unsafe()
93+
if _should_send_default_pii():
94+
with capture_internal_exceptions():
95+
_add_user_to_event(event)
5396

97+
return event
5498

55-
def _request_started(sender, **kwargs):
56-
weak_request = weakref.ref(_request_ctx_stack.top.request)
57-
app = _app_ctx_stack.top.app
58-
with configure_scope() as scope:
59-
scope.add_event_processor(_make_request_event_processor(app, weak_request))
99+
return inner
60100

61101

62102
class FlaskRequestExtractor(RequestExtractor):
@@ -93,34 +133,6 @@ def _capture_exception(sender, exception, **kwargs):
93133
hub.capture_event(event, hint=hint)
94134

95135

96-
def _make_request_event_processor(app, weak_request):
97-
def inner(event, hint):
98-
request = weak_request()
99-
100-
# if the request is gone we are fine not logging the data from
101-
# it. This might happen if the processor is pushed away to
102-
# another thread.
103-
if request is None:
104-
return event
105-
106-
if "transaction" not in event:
107-
try:
108-
event["transaction"] = request.url_rule.endpoint
109-
except Exception:
110-
pass
111-
112-
with capture_internal_exceptions():
113-
FlaskRequestExtractor(request).extract_into_event(event)
114-
115-
if _should_send_default_pii():
116-
with capture_internal_exceptions():
117-
_add_user_to_event(event)
118-
119-
return event
120-
121-
return inner
122-
123-
124136
def _add_user_to_event(event):
125137
if flask_login is None:
126138
return

tests/integrations/django/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pytest
2+
3+
django = pytest.importorskip("django")

tests/integrations/django/myapp/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
path("middleware-exc", views.message, name="middleware_exc"),
2828
path("message", views.message, name="message"),
2929
path("mylogin", views.mylogin, name="mylogin"),
30+
path("classbased", views.ClassBasedView.as_view(), name="classbased"),
3031
]
3132

3233
handler500 = views.handler500

tests/integrations/django/myapp/views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.contrib.auth import login
22
from django.contrib.auth.models import User
33
from django.http import HttpResponse, HttpResponseServerError
4+
from django.views.generic import ListView
45

56
import sentry_sdk
67

@@ -23,3 +24,11 @@ def mylogin(request):
2324

2425
def handler500(request):
2526
return HttpResponseServerError("Sentry error: %s" % sentry_sdk.last_event_id())
27+
28+
29+
class ClassBasedView(ListView):
30+
model = None
31+
32+
def head(self, *args, **kwargs):
33+
sentry_sdk.capture_message("hi")
34+
return HttpResponse("")

0 commit comments

Comments
 (0)
0