Skip to content
Closed
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Changelog
=========

## TBD

### Enhancements

* Add Python version string to report and session payloads (device.runtimeVersions)
[#179](https://github.com/bugsnag/bugsnag-python/pull/179)

## 3.5.2 (2019-03-15)

### Fixes
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Bugsnag exception reporter for Python
[![Build status](https://travis-ci.org/bugsnag/bugsnag-python.svg?branch=master)](https://travis-ci.org/bugsnag/bugsnag-python)
[![Build status](https://img.shields.io/travis/bugsnag/bugsnag-python/master.svg?style=flat-square)](https://travis-ci.com/bugsnag/bugsnag-python)
[![Documentation](https://img.shields.io/badge/documentation-latest-blue.svg)](https://docs.bugsnag.com/platforms/python/)

The Bugsnag exception reporter for Python automatically detects and reports
Expand Down
3 changes: 3 additions & 0 deletions bugsnag/configuration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import absolute_import, division, print_function

import os
import platform
import socket
try:
import sysconfig
Expand Down Expand Up @@ -92,6 +93,8 @@ def __init__(self):
else:
self.hostname = None

self.runtime_versions = {"python": str(platform.python_version())}

def should_notify(self): # type: () -> bool
return self.notify_release_stages is None or \
(isinstance(self.notify_release_stages, (tuple, list)) and
Expand Down
4 changes: 3 additions & 1 deletion bugsnag/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def get_config(key):
self.release_stage = get_config("release_stage")
self.app_version = get_config("app_version")
self.hostname = get_config("hostname")
self.runtime_versions = get_config("runtime_versions")
self.send_code = get_config("send_code")

self.context = options.pop("context", None)
Expand Down Expand Up @@ -242,7 +243,8 @@ def _payload(self):
"metaData": FilterDict(self.meta_data),
"user": FilterDict(self.user),
"device": FilterDict({
"hostname": self.hostname
"hostname": self.hostname,
"runtimeVersions": self.runtime_versions
}),
"projectRoot": self.config.get("project_root"),
"libRoot": self.config.get("lib_root"),
Expand Down
1 change: 1 addition & 0 deletions bugsnag/sessiontracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ def __deliver(self, sessions):
},
'device': FilterDict({
'hostname': self.config.get('hostname'),
'runtimeVersions': self.config.get('runtime_versions')
}),
'app': {
'releaseStage': self.config.get('release_stage'),
Expand Down
15 changes: 13 additions & 2 deletions bugsnag/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(self, keyword_filters=None, **kwargs):
super(SanitizingJSONEncoder, self).__init__(**kwargs)

def encode(self, obj):
print('=> encode')
safe_obj = self._sanitize(obj, False)
payload = super(SanitizingJSONEncoder, self).encode(safe_obj)
if len(payload) > MAX_PAYLOAD_LENGTH:
Expand All @@ -42,20 +43,23 @@ def filter_string_values(self, obj, ignored=None):
"""
Remove any value from the dictionary which match the key filters
"""
print('=> filter_string_values: ' + str(obj))
if not ignored:
ignored = set()

if type(ignored) is list:
ignored = set(ignored)

if id(obj) in ignored:
print('!!!!! Recursive: ' + str(id(obj)) + ' -> ' + str(obj))
return self.recursive_value

if isinstance(obj, dict):
ignored.add(id(obj))
print('***** Added: ' + str(id(obj)) + ' -> ' + str(obj))

clean_dict = {}
for key, value in six.iteritems(obj):
for key, value in sorted(six.iteritems(obj)):
is_string = isinstance(key, six.string_types)
if is_string and any(f in key.lower() for f in self.filters):
clean_dict[key] = self.filtered_value
Expand Down Expand Up @@ -86,21 +90,25 @@ def _sanitize(self, obj, trim_strings, ignored=None):
Replace recursive values and trim strings longer than
MAX_STRING_LENGTH
"""
print('=> _sanitize: ' + str(obj))
if not ignored:
ignored = set()

if type(ignored) is list:
ignored = set(ignored)

if id(obj) in ignored:
print('!!!!! Recursive: ' + str(id(obj)) + ' -> ' + str(obj))
return self.recursive_value
elif isinstance(obj, dict):
ignored.add(id(obj))
print('***** Added: ' + str(id(obj)) + ' -> ' + str(obj))
return self._sanitize_dict(obj, trim_strings, ignored)
elif isinstance(obj, (set, tuple, list)):
ignored.add(id(obj))
print('***** Added: ' + str(id(obj)) + ' -> ' + str(obj))
items = []
for value in obj:
for value in sorted(obj):
items.append(self._sanitize(value, trim_strings, ignored))
return items
elif trim_strings and isinstance(obj, six.string_types):
Expand All @@ -113,6 +121,7 @@ def _sanitize_dict_key_value(self, clean_dict, key, clean_value):
Safely sets the provided key on the dictionary by coercing the key
to a string
"""
print('=> _sanitize_dict_key_value: ' + str(clean_dict) + ', key: ' + str(key))
if isinstance(key, six.string_types):
clean_dict[key] = clean_value
else:
Expand All @@ -128,10 +137,12 @@ def _sanitize_dict(self, obj, trim_strings, ignored):
Trim individual values in an object, applying filtering if the object
is a FilterDict
"""
print('=> _sanitize_dict: ' + str(obj))
if isinstance(obj, FilterDict):
obj = self.filter_string_values(obj)

clean_dict = {}
#for key, value in sorted(six.iteritems(obj)):
for key, value in six.iteritems(obj):

clean_value = self._sanitize(value, trim_strings, ignored)
Expand Down
19 changes: 19 additions & 0 deletions test_to_death.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# tweak these
TRIES=1000
#COMMAND="pyenv exec tox -e py35-flask"
COMMAND="nosetests --tests tests.test_utils"

# tweaked from http://unix.stackexchange.com/a/82602
n=0
until [ $n -ge $TRIES ]
do
echo "****************************************************************************"
echo "****************************************************************************"
echo "ATTEMPT $n"
echo "****************************************************************************"
echo "****************************************************************************"
$COMMAND || break
n=$[$n+1]
done

echo Ended at attempt $n
18 changes: 18 additions & 0 deletions test_to_death_tx.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# tweak these
TRIES=1000
COMMAND="pyenv exec tox -e $1"

# tweaked from http://unix.stackexchange.com/a/82602
n=0
until [ $n -ge $TRIES ]
do
echo "****************************************************************************"
echo "****************************************************************************"
echo "ATTEMPT $n"
echo "****************************************************************************"
echo "****************************************************************************"
$COMMAND || break
n=$[$n+1]
done

echo Ended at attempt $n
182 changes: 5 additions & 177 deletions tests/integrations/test_flask.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from flask import Flask
import json

from bugsnag.flask import handle_exceptions
import bugsnag.notification
from tests.utils import IntegrationTest
Expand All @@ -19,37 +21,6 @@ def setUp(self):
release_stage='dev',
asynchronous=False)

def test_bugsnag_middleware_working(self):
app = Flask("bugsnag")

@app.route("/hello")
def hello():
return "OK"

handle_exceptions(app)

resp = app.test_client().get('/hello')
self.assertEqual(resp.data, b'OK')

self.assertEqual(0, len(self.server.received))

def test_bugsnag_crash(self):
app = Flask("bugsnag")

@app.route("/hello")
def hello():
raise SentinelError("oops")

handle_exceptions(app)
app.test_client().get('/hello')

self.assertEqual(1, len(self.server.received))
payload = self.server.received[0]['json_body']
self.assertEqual(payload['events'][0]['exceptions'][0]['errorClass'],
'test_flask.SentinelError')
self.assertEqual(payload['events'][0]['metaData']['request']['url'],
'http://localhost/hello')

def test_bugsnag_notify(self):
app = Flask("bugsnag")

Expand All @@ -63,151 +34,8 @@ def hello():

self.assertEqual(1, len(self.server.received))
payload = self.server.received[0]['json_body']
self.assertEqual(payload['events'][0]['metaData']['request']['url'],
'http://localhost/hello')

def test_bugsnag_custom_data(self):
meta_data = [{"hello": {"world": "once"}},
{"again": {"hello": "world"}}]

app = Flask("bugsnag")

@app.route("/hello")
def hello():
bugsnag.configure_request(meta_data=meta_data.pop())
raise SentinelError("oops")

handle_exceptions(app)
with app.test_client() as client:
client.get('/hello')
client.get('/hello')

payload = self.server.received[0]['json_body']
event = payload['events'][0]
self.assertEqual(event['metaData'].get('hello'), None)
self.assertEqual(event['metaData']['again']['hello'], 'world')

payload = self.server.received[1]['json_body']
event = payload['events'][0]
self.assertEqual(event['metaData']['hello']['world'], 'once')
self.assertEqual(event['metaData'].get('again'), None)
self.assertEqual(2, len(self.server.received))

def test_bugsnag_includes_posted_json_data(self):
app = Flask("bugsnag")

@app.route("/ajax", methods=["POST"])
def hello():
raise SentinelError("oops")

handle_exceptions(app)
app.test_client().post(
'/ajax', data='{"key": "value"}', content_type='application/json')

self.assertEqual(1, len(self.server.received))
payload = self.server.received[0]['json_body']
event = payload['events'][0]
self.assertEqual(event['exceptions'][0]['errorClass'],
'test_flask.SentinelError')
self.assertEqual(event['metaData']['request']['url'],
'http://localhost/ajax')
self.assertEqual(event['metaData']['request']['data'],
dict(key='value'))

def test_bugsnag_includes_request_when_json_malformed(self):
app = Flask("bugsnag")

@app.route("/ajax", methods=["POST"])
def hello():
raise SentinelError("oops")

handle_exceptions(app)
app.test_client().post(
'/ajax', data='{"key": "value"', content_type='application/json')
self.assertEqual(1, len(self.server.received))
payload = self.server.received[0]['json_body']
event = payload['events'][0]
self.assertEqual(event['exceptions'][0]['errorClass'],
'test_flask.SentinelError')
self.assertEqual(event['metaData']['request']['url'],
'http://localhost/ajax')
self.assertEqual(event['metaData']['request']['data']['body'],
'{"key": "value"')

def test_bugsnag_add_metadata_tab(self):
app = Flask("bugsnag")

@app.route("/form", methods=["PUT"])
def hello():
bugsnag.add_metadata_tab("account", {"id": 1, "premium": True})
bugsnag.add_metadata_tab("account", {"premium": False})
raise SentinelError("oops")

handle_exceptions(app)
app.test_client().put(
'/form', data='_data', content_type='application/octet-stream')

self.assertEqual(1, len(self.server.received))
payload = self.server.received[0]['json_body']
event = payload['events'][0]
self.assertEqual(event['metaData']['account']['premium'], False)
self.assertEqual(event['metaData']['account']['id'], 1)

def test_bugsnag_includes_unknown_content_type_posted_data(self):
app = Flask("bugsnag")

@app.route("/form", methods=["PUT"])
def hello():
raise SentinelError("oops")

handle_exceptions(app)
app.test_client().put(
'/form', data='_data', content_type='application/octet-stream')

self.assertEqual(1, len(self.server.received))
payload = self.server.received[0]['json_body']
event = payload['events'][0]
self.assertEqual(event['exceptions'][0]['errorClass'],
'test_flask.SentinelError')
self.assertEqual(event['metaData']['request']['url'],
'http://localhost/form')
body = event['metaData']['request']['data']['body']
self.assertTrue('_data' in body)

def test_bugsnag_notify_with_custom_context(self):
app = Flask("bugsnag")

@app.route("/hello")
def hello():
bugsnag.notify(SentinelError("oops"),
context="custom_context_notification_testing")
return "OK"

handle_exceptions(app)
app.test_client().get('/hello')

self.assertEqual(1, len(self.server.received))
payload = self.server.received[0]['json_body']
self.assertEqual(payload['events'][0]['context'],
'custom_context_notification_testing')

def test_flask_intergration_includes_middleware_severity(self):
app = Flask("bugsnag")

@app.route("/test")
def test():
raise SentinelError("oops")
print(payload)

handle_exceptions(app)
app.test_client().get("/test")

self.assertEqual(1, len(self.server.received))
payload = self.server.received[0]['json_body']
event = payload['events'][0]
self.assertTrue(event['unhandled'])
self.assertEqual(event['severityReason'], {
"type": "unhandledExceptionMiddleware",
"attributes": {
"framework": "Flask"
}
})
self.assertEqual(payload['events'][0]['metaData']['request']['url'],
'http://localhost/hello')
Loading