Skip to content

Commit 877224d

Browse files
authored
feat: flask integration (getsentry#5)
* feat: basic flask integration * feat: move integrations tests, add more scope data in flask integration * meta: refactor travis setup
1 parent b3a8996 commit 877224d

22 files changed

Lines changed: 384 additions & 103 deletions

.travis.yml

Lines changed: 8 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,15 @@
11
language: python
22
sudo: false
33

4-
matrix:
5-
include:
6-
- python: "2.7"
7-
env: DJANGO_VERSION=1.4.*
8-
- python: "2.7"
9-
env: DJANGO_VERSION=1.5.*
10-
- python: "2.7"
11-
env: DJANGO_VERSION=1.6.*
12-
- python: "2.7"
13-
env: DJANGO_VERSION=1.7.*
14-
- python: "2.7"
15-
env: DJANGO_VERSION=1.8.*
16-
- python: "2.7"
17-
env: DJANGO_VERSION=1.9.*
18-
19-
- python: "3.4"
20-
env: DJANGO_VERSION=1.5.*
21-
- python: "3.4"
22-
env: DJANGO_VERSION=1.6.*
23-
- python: "3.4"
24-
env: DJANGO_VERSION=1.7.*
25-
- python: "3.4"
26-
env: DJANGO_VERSION=1.8.*
27-
- python: "3.4"
28-
env: DJANGO_VERSION=1.9.*
29-
- python: "3.4"
30-
env: DJANGO_VERSION=2.0.*
31-
32-
- python: "3.5"
33-
env: DJANGO_VERSION=1.8.*
34-
- python: "3.5"
35-
env: DJANGO_VERSION=1.9.*
36-
- python: "3.5"
37-
env: DJANGO_VERSION=2.0.*
38-
39-
- python: "3.6"
40-
env: DJANGO_VERSION=1.8.*
41-
- python: "3.6"
42-
env: DJANGO_VERSION=1.9.*
43-
- python: "3.6"
44-
env: DJANGO_VERSION=2.0.*
45-
46-
- python: "3.6"
47-
env: DJANGO_VERSION=1.8.*
48-
- python: "3.6"
49-
env: DJANGO_VERSION=1.9.*
50-
- python: "3.6"
51-
env: DJANGO_VERSION=2.0.*
52-
53-
- python: "3.7-dev"
54-
env: DJANGO_VERSION=1.8.*
55-
- python: "3.7-dev"
56-
env: DJANGO_VERSION=1.9.*
57-
- python: "3.7-dev"
58-
env: DJANGO_VERSION=2.0.*
4+
python:
5+
- "2.7"
6+
- "3.4"
7+
- "3.5"
8+
- "3.6"
9+
- "3.7-dev"
5910

6011
install:
61-
- pip install -e .
62-
- pip install -Ur dev-requirements.txt
63-
- pip install -U "django==$DJANGO_VERSION"
64-
- |
65-
if [ "$DJANGO_VERSION" = 1.4.* ] || [ "$DJANGO_VERSION" = 1.5.* ] || [ "$DJANGO_VERSION" = 1.6.* ] || [ "$DJANGO_VERSION" = 1.7.* ] || [ "$DJANGO_VERSION" = 1.8.* ]; then
66-
pip install 'pytest-django<3.0';
67-
fi
12+
- pip install tox
6813

6914
script:
70-
- pytest # or py.test for Python versions 3.5 and below
15+
- tox -e $(tox -l | grep $(echo $TRAVIS_PYTHON_VERSION | sed -e 's/[^0-9]//g') | tr '\n' ',')

dev-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
pytest
22
pytest-django
33
django
4+
flask
5+
flask-login

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[pytest]
2-
DJANGO_SETTINGS_MODULE = tests.django.myapp.settings
2+
DJANGO_SETTINGS_MODULE = tests.integrations.django.myapp.settings

sentry_minimal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class Hub(object):
6161
current = main = None
6262

6363
class Scope(object):
64-
fingerprint = transaction = user = None
64+
fingerprint = transaction = user = request = None
6565

6666
def set_tag(self, key, value):
6767
pass

sentry_sdk/integrations/_wsgi.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def get_environ(environ):
2+
"""
3+
Returns our whitelisted environment variables.
4+
"""
5+
for key in ('REMOTE_ADDR', 'SERVER_NAME', 'SERVER_PORT'):
6+
if key in environ:
7+
yield key, environ[key]

sentry_sdk/integrations/django/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
MiddlewareMixin = object
2222

2323
def _get_transaction_from_request(request):
24-
return resolve(request.path).func
24+
return resolve(request.path).func.__name__
2525

2626
_request_scope = local()
2727

sentry_sdk/integrations/flask.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from __future__ import absolute_import
2+
3+
from sentry_sdk import capture_exception, configure_scope, get_current_hub
4+
from ._wsgi import get_environ
5+
6+
try:
7+
from flask_login import current_user
8+
except ImportError:
9+
current_user = None
10+
11+
from flask import current_app, request, _app_ctx_stack
12+
from flask.signals import appcontext_pushed, appcontext_popped, got_request_exception
13+
14+
class FlaskSentry(object):
15+
def __init__(self, app=None):
16+
self.app = app
17+
if app is not None:
18+
self.init_app(app)
19+
20+
def init_app(self, app):
21+
if not hasattr(app, 'extensions'):
22+
app.extensions = {}
23+
elif app.extensions.get(__name__, None):
24+
raise RuntimeError('Sentry registration is already registered')
25+
app.extensions[__name__] = True
26+
27+
appcontext_pushed.connect(_push_appctx, sender=app)
28+
app.teardown_appcontext(_pop_appctx)
29+
got_request_exception.connect(_capture_exception, sender=app)
30+
app.before_request(_before_request)
31+
32+
33+
def _push_appctx(*args, **kwargs):
34+
assert getattr(
35+
_app_ctx_stack.top,
36+
'_sentry_app_scope_manager',
37+
None
38+
) is None, 'race condition'
39+
_app_ctx_stack.top._sentry_app_scope_manager = \
40+
get_current_hub().push_scope().__enter__()
41+
42+
43+
def _pop_appctx(exception):
44+
assert getattr(
45+
_app_ctx_stack.top,
46+
'_sentry_app_scope_manager',
47+
None
48+
) is not None, 'race condition'
49+
_app_ctx_stack.top._sentry_app_scope_manager.__exit__(None, None, None)
50+
_app_ctx_stack.top._sentry_app_scope_manager = None
51+
52+
53+
def _capture_exception(sender, exception, **kwargs):
54+
capture_exception(exception)
55+
56+
57+
def _before_request(*args, **kwargs):
58+
assert getattr(
59+
_app_ctx_stack.top,
60+
'_sentry_app_scope_manager',
61+
None
62+
) is not None, 'scope push failed'
63+
64+
with configure_scope() as scope:
65+
if request.url_rule:
66+
scope.transaction = request.url_rule.endpoint
67+
68+
try:
69+
scope.request = _get_request_info()
70+
except Exception:
71+
get_current_hub().capture_internal_exception()
72+
73+
try:
74+
scope.user = _get_user_info()
75+
except Exception:
76+
raise
77+
get_current_hub().capture_internal_exception()
78+
79+
def _get_request_info():
80+
{
81+
'url': '%s://%s%s' % (request.scheme, urlparts.host, request.path),
82+
'query_string': urlparts.query,
83+
'method': request.method,
84+
'data': request.get_data(cache=True, as_text=True, parse_form_data=True),
85+
'headers': dict(request.headers),
86+
'env': get_environ(request.environ)
87+
}
88+
89+
90+
def _get_user_info():
91+
try:
92+
ip_address = request.access_route[0]
93+
except IndexError:
94+
ip_address = request.remote_addr
95+
96+
user_info = {
97+
'id': None,
98+
'ip_address': ip_address
99+
}
100+
101+
try:
102+
user_info['id'] = current_user.get_id()
103+
# TODO: more configurable user attrs here
104+
except AttributeError:
105+
# might happen if:
106+
# - flask_login could not be imported
107+
# - flask_login is not configured
108+
# - no user is logged in
109+
pass
110+
111+
return user_info

sentry_sdk/scope.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ def _set_user(self, value):
2121
user = property(fset=_set_user)
2222
del _set_user
2323

24+
def _set_request(self, request):
25+
self._data['request'] = request
26+
request = property(fset=_set_request)
27+
del _set_request
28+
2429
def set_tag(self, key, value):
2530
self._data.setdefault('tags', {})[key] = value
2631

setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@
1313
packages=find_packages(exclude=("tests", "tests.*",)),
1414
zip_safe=False,
1515
license='BSD',
16-
install_requires=['urllib3', 'certifi']
16+
install_requires=['urllib3', 'certifi'],
17+
extras_require={
18+
'flask': ['flask>=0.8', 'blinker>=1.1']
19+
}
1720
)

tests/conftest.py

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)