Repository: honeybadger-io/honeybadger-python

Issue:
Register Lambda plugin automatically?
@Kelvin4664 Would it be possible to detect the AWS Lambda environment and register the Lambda plugin automatically, so that the user can skip doing [this](https://github.com/honeybadger-io/honeybadger-python/blob/master/examples/aws_lambda/handler.py#L6)?

Discussion:

Patch:
diff --git a/examples/aws_lambda/handler.py b/examples/aws_lambda/handler.py
index 6b30b5b..79d6011 100644
--- a/examples/aws_lambda/handler.py
+++ b/examples/aws_lambda/handler.py
@@ -3,7 +3,6 @@ from honeybadger.contrib.aws_lambda import AWSLambdaPlugin
 from honeybadger.plugins import default_plugin_manager
 
 honeybadger.configure(api_key='your api key')
-default_plugin_manager.register(AWSLambdaPlugin())
 
 
 def lambda_handler(event, context):
diff --git a/honeybadger/contrib/__init__.py b/honeybadger/contrib/__init__.py
index d75f802..eba9d8b 100644
--- a/honeybadger/contrib/__init__.py
+++ b/honeybadger/contrib/__init__.py
@@ -1,4 +1,5 @@
 from honeybadger.contrib.flask import FlaskHoneybadger
 from honeybadger.contrib.django import DjangoHoneybadgerMiddleware
+from honeybadger.contrib.aws_lambda import AWSLambdaPlugin
 
-__all__ = ['FlaskHoneybadger', 'DjangoHoneybadgerMiddleware']
+__all__ = ['FlaskHoneybadger', 'DjangoHoneybadgerMiddleware', 'AWSLambdaPlugin']
diff --git a/honeybadger/contrib/aws_lambda.py b/honeybadger/contrib/aws_lambda.py
index a6fa670..c74e543 100644
--- a/honeybadger/contrib/aws_lambda.py
+++ b/honeybadger/contrib/aws_lambda.py
@@ -1,3 +1,4 @@
+import os
 import sys
 
 from honeybadger import honeybadger
@@ -82,18 +83,40 @@ class AWSLambdaPlugin(Plugin):
     def supports(self, config, context):
         return config.is_aws_lambda_environment
 
-    def generate_payload(self, config, context):
+    def generate_payload(self, default_payload, config, context):
         """
         Generate payload by checking the lambda's
         request event
         """
-        payload = {
+        request_payload = {
             "params": {
                 "event": current_event()
             },
-            "context": {}
+            "context": context
         }
-        return filter_dict(payload, config.params_filters)
+        default_payload["request"] = filter_dict(request_payload, config.params_filters)
+
+        AWS_ENV_MAP = (
+            ("_HANDLER", "handler"),
+            ("AWS_REGION", "region"),
+            ("AWS_EXECUTION_ENV", "runtime"),
+            ("AWS_LAMBDA_FUNCTION_NAME", "function"),
+            ("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "memory"),
+            ("AWS_LAMBDA_FUNCTION_VERSION", "version"),
+            ("AWS_LAMBDA_LOG_GROUP_NAME", "log_group"),
+            ("AWS_LAMBDA_LOG_STREAM_NAME", "log_name")
+        )
+
+        lambda_details = {detail[1]: os.environ.get(detail[0], None) for detail in AWS_ENV_MAP}
+        default_payload["details"] = {}
+        default_payload["details"]["Lambda Details"] = lambda_details
+        default_payload["request"]["component"] = lambda_details["function"]
+        default_payload["request"]["action"] = lambda_details["handler"]
+        trace_id = os.environ.get("_X_AMZN_TRACE_ID", None)
+        if trace_id:
+            default_payload["request"]["context"]["lambda_trace_id"] = trace_id
+
+        return default_payload
         
     def initialize_request_handler(self, lambda_bootstrap):
         """
diff --git a/honeybadger/contrib/django.py b/honeybadger/contrib/django.py
index ed3aea9..bfff58b 100644
--- a/honeybadger/contrib/django.py
+++ b/honeybadger/contrib/django.py
@@ -59,7 +59,7 @@ class DjangoPlugin(Plugin):
         request = current_request()
         return request is not None and re.match(r'^django\.', request.__module__)
 
-    def generate_payload(self, config, context):
+    def generate_payload(self, default_payload, config, context):
         """
         Generate payload by checking Django request object.
         :param context: current context.
@@ -68,7 +68,7 @@ class DjangoPlugin(Plugin):
         """
         request = current_request()
 
-        payload = {
+        request_payload = {
             'url': request.build_absolute_uri(),
             'component': request.resolver_match.app_name,
             'action': request.resolver_match.func.__name__,
@@ -79,18 +79,20 @@ class DjangoPlugin(Plugin):
         }
 
         if hasattr(request, 'session'):
-            payload['session'] = filter_dict(dict(request.session), config.params_filters)
+            request_payload['session'] = filter_dict(dict(request.session), config.params_filters)
 
         if hasattr(request, 'COOKIES'):
-            payload['cgi_data']['HTTP_COOKIE'] = filter_dict(request.COOKIES, config.params_filters)
+            request_payload['cgi_data']['HTTP_COOKIE'] = filter_dict(request.COOKIES, config.params_filters)
 
         if request.method == 'GET':
-            payload['params'] = filter_dict(dict(request.GET), config.params_filters)
+            request_payload['params'] = filter_dict(dict(request.GET), config.params_filters)
             
         else:
-            payload['params'] = filter_dict(dict(request.POST), config.params_filters)
+            request_payload['params'] = filter_dict(dict(request.POST), config.params_filters)
 
-        return payload
+        default_payload['request'] = request_payload
+
+        return default_payload
 
 
 class DjangoHoneybadgerMiddleware(object):
diff --git a/honeybadger/contrib/flask.py b/honeybadger/contrib/flask.py
index 455f1e0..1a0d4f4 100644
--- a/honeybadger/contrib/flask.py
+++ b/honeybadger/contrib/flask.py
@@ -32,7 +32,7 @@ class FlaskPlugin(Plugin):
         else:
             return bool(request)
 
-    def generate_payload(self, config, context):
+    def generate_payload(self, default_payload, config, context):
         """
         Generate payload by checking Flask request object.
         :param context: current context.
@@ -70,7 +70,9 @@ class FlaskPlugin(Plugin):
 
         payload['params'] = params
 
-        return payload
+        default_payload['request'] = payload
+
+        return default_payload
 
 
 class FlaskHoneybadger(object):
diff --git a/honeybadger/core.py b/honeybadger/core.py
index 69d4459..13236b6 100644
--- a/honeybadger/core.py
+++ b/honeybadger/core.py
@@ -4,6 +4,7 @@ import sys
 import logging
 import copy
 
+from honeybadger.plugins import default_plugin_manager
 import honeybadger.connection as connection
 import honeybadger.fake_connection as fake_connection
 from .payload import create_payload
@@ -53,6 +54,14 @@ class Honeybadger(object):
 
     def configure(self, **kwargs):
         self.config.set_config_from_dict(kwargs)
+        self.auto_discover_plugins()
+
+    def auto_discover_plugins(self):
+        #Avoiding circular import error
+        from honeybadger import contrib
+        
+        if self.config.is_aws_lambda_environment:
+            default_plugin_manager.register(contrib.AWSLambdaPlugin())
 
     def set_context(self, **kwargs):
         # This operation is an update, not a set!
diff --git a/honeybadger/payload.py b/honeybadger/payload.py
index 6b0fd22..e4d4e0a 100644
--- a/honeybadger/payload.py
+++ b/honeybadger/payload.py
@@ -95,8 +95,6 @@ def create_payload(exception, exc_traceback=None, config=None, context={}):
     if exc_traceback is None:
         exc_traceback = sys.exc_info()[2]
 
-    request_payload = default_plugin_manager.generate_payload(config, context)
-
     payload =  {
         'notifier': {
             'name': "Honeybadger for Python",
@@ -105,32 +103,8 @@ def create_payload(exception, exc_traceback=None, config=None, context={}):
         },
         'error':  error_payload(exception, exc_traceback, config),
         'server': server_payload(config),
-        'request': request_payload,
-        'details': {}
+        'request': {'context':context}
     }
-    
-    if config.is_aws_lambda_environment:
-
-        # Should this be in the lambda plugin instead?
-        # We can't manipulate the payload directly from the plugin
-        # So this makes sense for now
-        AWS_ENV_MAP = (
-            ("_HANDLER", "handler"),
-            ("AWS_REGION", "region"),
-            ("AWS_EXECUTION_ENV", "runtime"),
-            ("AWS_LAMBDA_FUNCTION_NAME", "function"),
-            ("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "memory"),
-            ("AWS_LAMBDA_FUNCTION_VERSION", "version"),
-            ("AWS_LAMBDA_LOG_GROUP_NAME", "log_group"),
-            ("AWS_LAMBDA_LOG_STREAM_NAME", "log_name")
-        )
-
-        lambda_details = {detail[1]: os.environ.get(detail[0], None) for detail in AWS_ENV_MAP}
-        payload["details"]["Lambda Details"] = lambda_details
-        payload["request"]["component"] = lambda_details["function"]
-        payload["request"]["action"] = lambda_details["handler"]
-        trace_id = os.environ.get("_X_AMZN_TRACE_ID", None)
-        if trace_id:
-            payload["request"]["context"]["lambda_trace_id"] = trace_id
 
-    return payload
+    return default_plugin_manager.generate_payload(payload, config, context)
+    
diff --git a/honeybadger/plugins.py b/honeybadger/plugins.py
index f29017b..65424d5 100644
--- a/honeybadger/plugins.py
+++ b/honeybadger/plugins.py
@@ -59,7 +59,7 @@ class PluginManager(object):
         else:
             logger.warn('Plugin %s already registered' % plugin.name)
 
-    def generate_payload(self, config=None, context=None):
+    def generate_payload(self, default_payload, config=None, context=None):
         """
         Generate payload by iterating over registered plugins. Merges .
         :param context: current context.
@@ -69,12 +69,12 @@ class PluginManager(object):
         for name, plugin in iteritems(self._registered):
             if plugin.supports(config, context):
                 logger.debug('Returning payload from plugin %s' % name)
-                return plugin.generate_payload(config, context)
+                
+                return plugin.generate_payload(default_payload, config, context)
 
         logger.debug('No active plugin to generate payload')
-        return {
-            'context': context
-        }
+        
+        return default_payload
 
 # Global plugin manager
 default_plugin_manager = PluginManager()


Test patch:
diff --git a/honeybadger/tests/contrib/test_django.py b/honeybadger/tests/contrib/test_django.py
index d034863..ea095aa 100644
--- a/honeybadger/tests/contrib/test_django.py
+++ b/honeybadger/tests/contrib/test_django.py
@@ -50,6 +50,7 @@ class DjangoPluginTestCase(unittest.TestCase):
         self.rf = RequestFactory()
         self.config = Configuration()
         self.url = url(r'test', plain_view, name='test_view')
+        self.default_payload = {}
 
     def tearDown(self):
         clear_request()
@@ -65,24 +66,24 @@ class DjangoPluginTestCase(unittest.TestCase):
         request.resolver_match = self.url.resolve('test')
         set_request(request)
 
-        payload = self.plugin.generate_payload(self.config, {'foo': 'bar'})
-        self.assertEqual(payload['url'], 'http://testserver/test?a=1')
-        self.assertEqual(payload['action'], 'plain_view')
-        self.assertDictEqual(payload['params'], {'a': ['1']})
-        self.assertDictEqual(payload['session'], {})
-        self.assertDictEqual(payload['context'], {'foo': 'bar'})
+        payload = self.plugin.generate_payload(self.default_payload, self.config, {'foo': 'bar'})
+        self.assertEqual(payload['request']['url'], 'http://testserver/test?a=1')
+        self.assertEqual(payload['request']['action'], 'plain_view')
+        self.assertDictEqual(payload['request']['params'], {'a': ['1']})
+        self.assertDictEqual(payload['request']['session'], {})
+        self.assertDictEqual(payload['request']['context'], {'foo': 'bar'})
 
     def test_generate_payload_post(self):
         request = self.rf.post('test', data={'a': 1, 'b': 2, 'password': 'notsafe'})
         request.resolver_match = self.url.resolve('test')
         set_request(request)
 
-        payload = self.plugin.generate_payload(self.config, {'foo': 'bar'})
-        self.assertEqual(payload['url'], 'http://testserver/test')
-        self.assertEqual(payload['action'], 'plain_view')
-        self.assertDictEqual(payload['params'], {'a': ['1'], 'b': ['2'], 'password': '[FILTERED]'})
-        self.assertDictEqual(payload['session'], {})
-        self.assertDictEqual(payload['context'], {'foo': 'bar'})
+        payload = self.plugin.generate_payload(self.default_payload, self.config, {'foo': 'bar'})
+        self.assertEqual(payload['request']['url'], 'http://testserver/test')
+        self.assertEqual(payload['request']['action'], 'plain_view')
+        self.assertDictEqual(payload['request']['params'], {'a': ['1'], 'b': ['2'], 'password': '[FILTERED]'})
+        self.assertDictEqual(payload['request']['session'], {})
+        self.assertDictEqual(payload['request']['context'], {'foo': 'bar'})
 
     def test_generate_payload_with_session(self):
         request = self.rf.get('test')
@@ -90,11 +91,11 @@ class DjangoPluginTestCase(unittest.TestCase):
         request.session = {'lang': 'en'}
         set_request(request)
 
-        payload = self.plugin.generate_payload(self.config, {'foo': 'bar'})
-        self.assertEqual(payload['url'], 'http://testserver/test')
-        self.assertEqual(payload['action'], 'plain_view')
-        self.assertDictEqual(payload['session'], {'lang': 'en'})
-        self.assertDictEqual(payload['context'], {'foo': 'bar'})
+        payload = self.plugin.generate_payload(self.default_payload, self.config, {'foo': 'bar'})
+        self.assertEqual(payload['request']['url'], 'http://testserver/test')
+        self.assertEqual(payload['request']['action'], 'plain_view')
+        self.assertDictEqual(payload['request']['session'], {'lang': 'en'})
+        self.assertDictEqual(payload['request']['context'], {'foo': 'bar'})
 
 # TODO: add an integration test case that tests the actual integration with Django
 
diff --git a/honeybadger/tests/contrib/test_flask.py b/honeybadger/tests/contrib/test_flask.py
index a62e3fa..fbc03d8 100644
--- a/honeybadger/tests/contrib/test_flask.py
+++ b/honeybadger/tests/contrib/test_flask.py
@@ -23,6 +23,7 @@ class FlaskPluginTestCase(unittest.TestCase):
             self.skipTest('Flask 1.1 requires Python > 3.4')
 
         self.config = Configuration()
+        self.default_payload = {}
 
         self.app = flask.Flask(__name__)
         self.app.secret_key = 'safe'
@@ -49,77 +50,77 @@ class FlaskPluginTestCase(unittest.TestCase):
                                            headers={
                                                'X-Wizard-Color': 'grey'
                                            }):
-            payload = self.plugin.generate_payload(self.config, {'k': 'value'})
+            payload = self.plugin.generate_payload(self.default_payload, self.config, {'k': 'value'})
 
-            self.assertEqual(payload['url'], 'http://server:1234/path/test')
-            self.assertEqual(payload['component'], 'honeybadger.tests.contrib.test_flask')
-            self.assertEqual(payload['action'], 'foo')
-            self.assertDictEqual(payload['params'], {'a': ['1', '2'], 'foo': ['bar']})
-            self.assertDictEqual(payload['session'], {})
+            self.assertEqual(payload['request']['url'], 'http://server:1234/path/test')
+            self.assertEqual(payload['request']['component'], 'honeybadger.tests.contrib.test_flask')
+            self.assertEqual(payload['request']['action'], 'foo')
+            self.assertDictEqual(payload['request']['params'], {'a': ['1', '2'], 'foo': ['bar']})
+            self.assertDictEqual(payload['request']['session'], {})
             self.assertDictContainsSubset({
                 'Host': 'server:1234',
                 'X-Wizard-Color': 'grey',
                 'REQUEST_METHOD': 'GET'
-            }, payload['cgi_data'])
-            self.assertDictEqual(payload['context'], {'k': 'value'})
+            }, payload['request']['cgi_data'])
+            self.assertDictEqual(payload['request']['context'], {'k': 'value'})
 
     def test_get_request_with_session(self):
         with self.app.test_request_context(path='/test', base_url='http://server:1234/path') as ctx:
             ctx.session['answer'] = 42
             ctx.session['password'] = 'this is fine'
 
-            payload = self.plugin.generate_payload(self.config, {'k': 'value'})
+            payload = self.plugin.generate_payload(self.default_payload, self.config, {'k': 'value'})
 
-            self.assertEqual(payload['url'], 'http://server:1234/path/test')
-            self.assertEqual(payload['component'], 'honeybadger.tests.contrib.test_flask')
-            self.assertEqual(payload['action'], 'foo')
-            self.assertDictEqual(payload['params'], {})
-            self.assertDictEqual(payload['session'], {'answer': 42, 'password': '[FILTERED]'})
+            self.assertEqual(payload['request']['url'], 'http://server:1234/path/test')
+            self.assertEqual(payload['request']['component'], 'honeybadger.tests.contrib.test_flask')
+            self.assertEqual(payload['request']['action'], 'foo')
+            self.assertDictEqual(payload['request']['params'], {})
+            self.assertDictEqual(payload['request']['session'], {'answer': 42, 'password': '[FILTERED]'})
             self.assertDictContainsSubset({
                 'Host': 'server:1234',
                 'REQUEST_METHOD': 'GET'
-            }, payload['cgi_data'])
-            self.assertDictEqual(payload['context'], {'k': 'value'})
+            }, payload['request']['cgi_data'])
+            self.assertDictEqual(payload['request']['context'], {'k': 'value'})
 
     def test_post_request(self):
         with self.app.test_request_context(path='/test', base_url='http://server:1234/path', method='POST',
                                            data={'foo': 'bar', 'password': 'this is file'}):
 
-            payload = self.plugin.generate_payload(self.config, {'k': 'value'})
+            payload = self.plugin.generate_payload(self.default_payload, self.config, {'k': 'value'})
 
-            self.assertEqual(payload['url'], 'http://server:1234/path/test')
-            self.assertEqual(payload['component'], 'honeybadger.tests.contrib.test_flask')
-            self.assertEqual(payload['action'], 'foo')
-            self.assertDictEqual(payload['params'], {'foo': ['bar'], 'password': '[FILTERED]'})
-            self.assertDictEqual(payload['session'], {})
-            self.assertDictEqual(payload['cgi_data'], {
+            self.assertEqual(payload['request']['url'], 'http://server:1234/path/test')
+            self.assertEqual(payload['request']['component'], 'honeybadger.tests.contrib.test_flask')
+            self.assertEqual(payload['request']['action'], 'foo')
+            self.assertDictEqual(payload['request']['params'], {'foo': ['bar'], 'password': '[FILTERED]'})
+            self.assertDictEqual(payload['request']['session'], {})
+            self.assertDictEqual(payload['request']['cgi_data'], {
                 'Host': 'server:1234',
                 'Content-Type': 'application/x-www-form-urlencoded',
                 'Content-Length': '29',
                 'REQUEST_METHOD': 'POST',
                 'HTTP_COOKIE': {}
             })
-            self.assertDictEqual(payload['context'], {'k': 'value'})
+            self.assertDictEqual(payload['request']['context'], {'k': 'value'})
 
     def test_put_request(self):
         with self.app.test_request_context(path='/test', base_url='http://server:1234/path', method='PUT',
                                            data={'foo': 'bar', 'password': 'this is file'}):
 
-            payload = self.plugin.generate_payload(self.config, {'k': 'value'})
+            payload = self.plugin.generate_payload(self.default_payload, self.config, {'k': 'value'})
 
-            self.assertEqual(payload['url'], 'http://server:1234/path/test')
-            self.assertEqual(payload['component'], 'honeybadger.tests.contrib.test_flask')
-            self.assertEqual(payload['action'], 'foo')
-            self.assertDictEqual(payload['params'], {'foo': ['bar'], 'password': '[FILTERED]'})
-            self.assertDictEqual(payload['session'], {})
-            self.assertDictEqual(payload['cgi_data'], {
+            self.assertEqual(payload['request']['url'], 'http://server:1234/path/test')
+            self.assertEqual(payload['request']['component'], 'honeybadger.tests.contrib.test_flask')
+            self.assertEqual(payload['request']['action'], 'foo')
+            self.assertDictEqual(payload['request']['params'], {'foo': ['bar'], 'password': '[FILTERED]'})
+            self.assertDictEqual(payload['request']['session'], {})
+            self.assertDictEqual(payload['request']['cgi_data'], {
                 'Host': 'server:1234',
                 'Content-Type': 'application/x-www-form-urlencoded',
                 'Content-Length': '29',
                 'REQUEST_METHOD': 'PUT',
                 'HTTP_COOKIE': {}
             })
-            self.assertDictEqual(payload['context'], {'k': 'value'})
+            self.assertDictEqual(payload['request']['context'], {'k': 'value'})
 
 
 class FlaskHoneybadgerTestCase(unittest.TestCase):
diff --git a/honeybadger/tests/test_plugins.py b/honeybadger/tests/test_plugins.py
index 8737f8d..ef86700 100644
--- a/honeybadger/tests/test_plugins.py
+++ b/honeybadger/tests/test_plugins.py
@@ -11,6 +11,7 @@ class PluginManagerTestCase(unittest.TestCase):
         self.plugin1.name = 'plugin1'
         self.plugin2 = Mock()
         self.plugin2.name = 'plugin2'
+        self.default_payload = {}
 
     def test_register(self):
         self.manager.register(self.plugin1)
@@ -31,11 +32,11 @@ class PluginManagerTestCase(unittest.TestCase):
         self.plugin2.supports.return_value = True
         self.plugin2.generate_payload.return_value = {'name': 'plugin2'}
 
-        payload = self.manager.generate_payload(context=context)
+        payload = self.manager.generate_payload(self.default_payload, context=context)
         # Expect order to be preferred and use value from first plugin
         self.assertDictEqual({'name': 'plugin1'}, payload)
         self.plugin1.supports.assert_called_once_with(None, context)
-        self.plugin1.generate_payload.assert_called_once_with(None, context)
+        self.plugin1.generate_payload.assert_called_once_with(self.default_payload, None, context)
         self.assertEqual(0, self.plugin2.supports.call_count)
         self.assertEqual(0, self.plugin2.generate_payload.call_count)
 
@@ -49,26 +50,28 @@ class PluginManagerTestCase(unittest.TestCase):
         self.plugin2.supports.return_value = True
         self.plugin2.generate_payload.return_value = {'name': 'plugin2'}
 
-        payload = self.manager.generate_payload(context=context)
+        payload = self.manager.generate_payload(default_payload=self.default_payload, context=context)
         # Expect order to be preferred and use value from second plugin
         self.assertDictEqual({'name': 'plugin2'}, payload)
         self.plugin1.supports.assert_called_once_with(None, context)
         self.assertEqual(0, self.plugin1.generate_payload.call_count)
         self.plugin2.supports.assert_called_once_with(None, context)
-        self.plugin2.generate_payload.assert_called_once_with(None, context)
+        self.plugin2.generate_payload.assert_called_once_with(self.default_payload, None, context)
 
     def test_generate_payload_none(self):
         self.manager.register(self.plugin1)
         self.manager.register(self.plugin2)
         context = {'test': 'context'}
+        default_payload = {'request':{'context':context}}
 
         # Given no registered plugin supports providing payload
         self.plugin1.supports.return_value = False
         self.plugin2.supports.return_value = False
 
-        payload = self.manager.generate_payload(context=context)
+        payload = self.manager.generate_payload(default_payload=default_payload, context=context)
+        print(payload)
         # Expect order to be preferred and use input context value
-        self.assertDictEqual({'context': context}, payload)
+        self.assertDictEqual({'request':{'context': context}}, payload)
         self.plugin1.supports.assert_called_once_with(None, context)
         self.assertEqual(0, self.plugin1.generate_payload.call_count)
         self.plugin2.supports.assert_called_once_with(None, context)