diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..e8686d7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,30 @@
+if: tag IS present OR type = pull_request OR (branch = master AND type = push)   # we only CI the master, tags and PRs
+
+language: python
+cache: pip
+dist: xenial
+python:
+  - "3.5"
+  - "3.6"
+  - "3.7"
+
+env:
+  - DB=mongodb DATABASE_URL=mongodb://localhost:27017/alerta5
+  - DB=postgres DATABASE_URL=postgres://localhost:5432/alerta5
+
+services:
+  - mongodb
+  - postgresql
+
+addons:
+  postgresql: "9.5"
+
+before_script:
+  - sh -c "if [ '$DB' = 'postgres' ]; then psql -c 'DROP DATABASE IF EXISTS alerta5;' -U postgres; fi"
+  - sh -c "if [ '$DB' = 'postgres' ]; then psql -c 'CREATE DATABASE alerta5;' -U postgres; fi"
+
+install:
+  - pip install -r requirements-dev.txt
+
+script:
+  - pytest -v webhooks/sentry
diff --git a/README.md b/README.md
index 87947c5..8501506 100644
--- a/README.md
+++ b/README.md
@@ -69,8 +69,17 @@ Webhooks
   * [Mailgun](webhooks/mailgun)
   * [Sentry](webhooks/sentry)
 
+Tests
+-----
+
+To run the tests using a local Postgres database run:
+
+    $ pip install -r requirements-dev.txt
+    $ createdb test5
+    $ ALERTA_SVR_CONF_FILE= DATABASE_URL=postgres:///test5 pytest -v webhooks/*/test*
+
 License
 -------
 
-Copyright (c) 2014-2018 Nick Satterly and [AUTHORS](AUTHORS). Available under the MIT License.
+Copyright (c) 2014-2019 Nick Satterly and [AUTHORS](AUTHORS). Available under the MIT License.
 
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..1337c45
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,2 @@
+pytest
+alerta-server
diff --git a/webhooks/sentry/alerta_sentry.py b/webhooks/sentry/alerta_sentry.py
index 5c6291f..96b2a23 100644
--- a/webhooks/sentry/alerta_sentry.py
+++ b/webhooks/sentry/alerta_sentry.py
@@ -27,7 +27,7 @@ class SentryWebhook(WebhookBase):
             value=payload['level'],
             text='{} {}'.format(payload['message'], payload['url']),
             tags=['{}={}'.format(k, v) for k, v in payload['event']['tags']],
-            attributes={'modules': payload['event']['modules']},
+            attributes={'modules': ['{}=={}'.format(k, v) for k, v in payload['event']['modules'].items()]},
             origin='sentry.io',
-            raw_data=payload
+            raw_data=str(payload)
         )
diff --git a/webhooks/sentry/test_sentry.py b/webhooks/sentry/test_sentry.py
new file mode 100644
index 0000000..3410026
--- /dev/null
+++ b/webhooks/sentry/test_sentry.py
@@ -0,0 +1,416 @@
+
+import json
+import unittest
+
+from alerta.app import create_app, custom_webhooks
+
+import alerta_sentry
+
+
+class SentryWebhookTestCase(unittest.TestCase):
+
+    def setUp(self):
+
+        test_config = {
+            'TESTING': True,
+            'AUTH_REQUIRED': False
+        }
+        self.app = create_app(test_config)
+        self.client = self.app.test_client()
+
+    def test_sentry_webhook(self):
+
+        custom_webhooks.webhooks['sentry'] = alerta_sentry.SentryWebhook()
+
+        payload = r"""
+            {
+              "project": "alerta5",
+              "project_name": "alerta5",
+              "culprit": "raven.scripts.runner in main",
+              "project_slug": "alerta5",
+              "url": "https://sentry.io/alertaio/alerta5/issues/541485531/",
+              "logger": null,
+              "level": "error",
+              "message": "This is an example Python exception",
+              "id": "541485531",
+              "event": {
+                "received": 1525248430,
+                "sentry.interfaces.User": {
+                  "username": "sentry",
+                  "id": "1",
+                  "ip_address": "127.0.0.1",
+                  "name": "Sentry",
+                  "email": "sentry@example.com"
+                },
+                "sentry.interfaces.Message": {
+                  "message": "This is an example Python exception"
+                },
+                "errors": [],
+                "extra": {
+                  "emptyList": [],
+                  "unauthorized": false,
+                  "emptyMap": {},
+                  "url": "http://example.org/foo/bar/",
+                  "results": [
+                    1,
+                    2,
+                    3,
+                    4,
+                    5
+                  ],
+                  "length": 10837790,
+                  "session": {
+                    "foo": "bar"
+                  }
+                },
+                "event_id": "0476467adf8f499ea795a48fcc4bf290",
+                "fingerprint": [
+                  "{{ default }}"
+                ],
+                "modules": {
+                  "my.package": "1.0.0"
+                },
+                "id": 19293981000,
+                "sentry.interfaces.Http": {
+                  "cookies": [
+                    [
+                      "foo",
+                      "bar"
+                    ],
+                    [
+                      "biz",
+                      "baz"
+                    ]
+                  ],
+                  "url": "http://example.com/foo",
+                  "headers": [
+                    [
+                      "Content-Type",
+                      "application/json"
+                    ],
+                    [
+                      "Referer",
+                      "http://example.com"
+                    ],
+                    [
+                      "User-Agent",
+                      "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36"
+                    ]
+                  ],
+                  "env": {
+                    "ENV": "prod"
+                  },
+                  "query_string": "foo=bar",
+                  "data": {
+                    "hello": "world"
+                  },
+                  "method": "GET",
+                  "inferred_content_type": "application/json"
+                },
+                "sentry.interfaces.Template": {
+                  "abs_path": "/srv/example/templates/debug_toolbar/base.html",
+                  "pre_context": [
+                    "{% endif %}\n",
+                    "<script src=\"{% static 'debug_toolbar/js/toolbar.js' %}\"></script>\n",
+                    "<div id=\"djDebug\" hidden=\"hidden\" dir=\"ltr\"\n"
+                  ],
+                  "post_context": [
+                    "     {{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }}>\n",
+                    "\t<div hidden=\"hidden\" id=\"djDebugToolbar\">\n",
+                    "\t\t<ul id=\"djDebugPanelList\">\n"
+                  ],
+                  "filename": "debug_toolbar/base.html",
+                  "lineno": 14,
+                  "context_line": "     data-store-id=\"{{ toolbar.store_id }}\" data-render-panel-url=\"{% url 'djdt:render_panel' %}\"\n"
+                },
+                "version": "5",
+                "_ref_version": 2,
+                "_ref": 153768,
+                "type": "default",
+                "sentry.interfaces.Stacktrace": {
+                  "frames": [
+                    {
+                      "function": "build_msg",
+                      "abs_path": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/base.py",
+                      "pre_context": [
+                        "                frames = stack",
+                        "",
+                        "            data.update({",
+                        "                'sentry.interfaces.Stacktrace': {",
+                        "                    'frames': get_stack_info(frames,"
+                      ],
+                      "vars": {
+                        "'frames'": "<generator object iter_stack_frames at 0x107bcc3c0>",
+                        "'culprit'": null,
+                        "'event_type'": "'raven.events.Message'",
+                        "'handler'": "<raven.events.Message object at 0x107bd0890>",
+                        "'date'": "datetime.datetime(2013, 8, 13, 3, 8, 24, 880386)",
+                        "'extra'": {
+                          "'go_deeper'": [
+                            [
+                              {
+                                "'bar'": [
+                                  "'baz'"
+                                ],
+                                "'foo'": "'bar'"
+                              }
+                            ]
+                          ],
+                          "'user'": "'dcramer'",
+                          "'loadavg'": [
+                            0.37255859375,
+                            0.5341796875,
+                            0.62939453125
+                          ]
+                        },
+                        "'v'": {
+                          "'message'": "u'This is a test message generated using ``raven test``'",
+                          "'params'": []
+                        },
+                        "'stack'": true,
+                        "'event_id'": "'54a322436e1b47b88e239b78998ae742'",
+                        "'tags'": null,
+                        "'time_spent'": null,
+                        "'self'": "<raven.base.Client object at 0x107bb8210>",
+                        "'data'": {
+                          "'sentry.interfaces.Message'": {
+                            "'message'": "u'This is a test message generated using ``raven test``'",
+                            "'params'": []
+                          },
+                          "'message'": "u'This is a test message generated using ``raven test``'"
+                        },
+                        "'result'": {
+                          "'sentry.interfaces.Message'": {
+                            "'message'": "u'This is a test message generated using ``raven test``'",
+                            "'params'": []
+                          },
+                          "'message'": "u'This is a test message generated using ``raven test``'"
+                        },
+                        "'kwargs'": {
+                          "'message'": "'This is a test message generated using ``raven test``'",
+                          "'level'": 20
+                        },
+                        "'k'": "'sentry.interfaces.Message'",
+                        "'public_key'": null
+                      },
+                      "post_context": [
+                        "                },",
+                        "            })",
+                        "",
+                        "        if 'sentry.interfaces.Stacktrace' in data:",
+                        "            if self.include_paths:"
+                      ],
+                      "filename": "raven/base.py",
+                      "module": "raven.base",
+                      "in_app": false,
+                      "context_line": "                        transformer=self.transform)",
+                      "lineno": 303
+                    },
+                    {
+                      "function": "capture",
+                      "abs_path": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/base.py",
+                      "pre_context": [
+                        "        if not self.is_enabled():",
+                        "            return",
+                        "",
+                        "        data = self.build_msg(",
+                        "            event_type, data, date, time_spent, extra, stack, tags=tags,"
+                      ],
+                      "vars": {
+                        "'event_type'": "'raven.events.Message'",
+                        "'date'": null,
+                        "'extra'": {
+                          "'go_deeper'": [
+                            [
+                              {
+                                "'bar'": [
+                                  "'baz'"
+                                ],
+                                "'foo'": "'bar'"
+                              }
+                            ]
+                          ],
+                          "'user'": "'dcramer'",
+                          "'loadavg'": [
+                            0.37255859375,
+                            0.5341796875,
+                            0.62939453125
+                          ]
+                        },
+                        "'stack'": true,
+                        "'tags'": null,
+                        "'time_spent'": null,
+                        "'self'": "<raven.base.Client object at 0x107bb8210>",
+                        "'data'": null,
+                        "'kwargs'": {
+                          "'message'": "'This is a test message generated using ``raven test``'",
+                          "'level'": 20
+                        }
+                      },
+                      "post_context": [
+                        "",
+                        "        self.send(**data)",
+                        "",
+                        "        return (data.get('event_id'),)",
+                        ""
+                      ],
+                      "filename": "raven/base.py",
+                      "module": "raven.base",
+                      "in_app": false,
+                      "context_line": "            **kwargs)",
+                      "lineno": 459
+                    },
+                    {
+                      "function": "captureMessage",
+                      "abs_path": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/base.py",
+                      "pre_context": [
+                        "        \"\"\"",
+                        "        Creates an event from ``message``.",
+                        "",
+                        "        >>> client.captureMessage('My event just happened!')",
+                        "        \"\"\""
+                      ],
+                      "vars": {
+                        "'message'": "'This is a test message generated using ``raven test``'",
+                        "'kwargs'": {
+                          "'extra'": {
+                            "'go_deeper'": [
+                              [
+                                {
+                                  "'bar'": [
+                                    "'baz'"
+                                  ],
+                                  "'foo'": "'bar'"
+                                }
+                              ]
+                            ],
+                            "'user'": "'dcramer'",
+                            "'loadavg'": [
+                              0.37255859375,
+                              0.5341796875,
+                              0.62939453125
+                            ]
+                          },
+                          "'tags'": null,
+                          "'data'": null,
+                          "'level'": 20,
+                          "'stack'": true
+                        },
+                        "'self'": "<raven.base.Client object at 0x107bb8210>"
+                      },
+                      "post_context": [
+                        "",
+                        "    def captureException(self, exc_info=None, **kwargs):",
+                        "        \"\"\"",
+                        "        Creates an event from an exception.",
+                        ""
+                      ],
+                      "filename": "raven/base.py",
+                      "module": "raven.base",
+                      "in_app": false,
+                      "context_line": "        return self.capture('raven.events.Message', message=message, **kwargs)",
+                      "lineno": 577
+                    },
+                    {
+                      "function": "send_test_message",
+                      "abs_path": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/scripts/runner.py",
+                      "pre_context": [
+                        "        level=logging.INFO,",
+                        "        stack=True,",
+                        "        tags=options.get('tags', {}),",
+                        "        extra={",
+                        "            'user': get_uid(),"
+                      ],
+                      "vars": {
+                        "'client'": "<raven.base.Client object at 0x107bb8210>",
+                        "'options'": {
+                          "'tags'": null,
+                          "'data'": null
+                        },
+                        "'data'": null,
+                        "'k'": "'secret_key'"
+                      },
+                      "post_context": [
+                        "        },",
+                        "    ))",
+                        "",
+                        "    if client.state.did_fail():",
+                        "        print('error!')"
+                      ],
+                      "filename": "raven/scripts/runner.py",
+                      "module": "raven.scripts.runner",
+                      "in_app": false,
+                      "context_line": "            'loadavg': get_loadavg(),",
+                      "lineno": 77
+                    },
+                    {
+                      "function": "main",
+                      "abs_path": "/home/ubuntu/.virtualenvs/getsentry/src/raven/raven/scripts/runner.py",
+                      "pre_context": [
+                        "    print(\"Using DSN configuration:\")",
+                        "    print(\" \", dsn)",
+                        "    print()",
+                        "",
+                        "    client = Client(dsn, include_paths=['raven'])"
+                      ],
+                      "vars": {
+                        "'root'": "<logging.Logger object at 0x107ba5b10>",
+                        "'parser'": "<optparse.OptionParser instance at 0x107ba3368>",
+                        "'dsn'": "'https://ebc35f33e151401f9deac549978bda11:f3403f81e12e4c24942d505f086b2cad@sentry.io/1'",
+                        "'opts'": "<Values at 0x107ba3b00: {'data': None, 'tags': None}>",
+                        "'client'": "<raven.base.Client object at 0x107bb8210>",
+                        "'args'": [
+                          "'test'",
+                          "'https://ebc35f33e151401f9deac549978bda11:f3403f81e12e4c24942d505f086b2cad@sentry.io/1'"
+                        ]
+                      },
+                      "module": "raven.scripts.runner",
+                      "filename": "raven/scripts/runner.py",
+                      "lineno": 112,
+                      "in_app": false,
+                      "context_line": "    send_test_message(client, opts.__dict__)"
+                    }
+                  ]
+                },
+                "tags": [
+                  [
+                    "browser",
+                    "Chrome 28.0"
+                  ],
+                  [
+                    "device",
+                    "Other"
+                  ],
+                  [
+                    "level",
+                    "error"
+                  ],
+                  [
+                    "os",
+                    "Windows 8"
+                  ],
+                  [
+                    "sentry:user",
+                    "id:1"
+                  ],
+                  [
+                    "url",
+                    "http://example.com/foo"
+                  ]
+                ],
+                "metadata": {
+                  "title": "This is an example Python exception"
+                }
+              }
+            }
+        """
+
+        response = self.client.post('/webhooks/sentry', data=payload, content_type='application/json')
+        self.assertEqual(response.status_code, 201)
+        data = json.loads(response.data.decode('utf-8'))
+        self.assertEqual(data['alert']['resource'], 'raven.scripts.runner in main')
+        self.assertEqual(data['alert']['event'], '0476467adf8f499ea795a48fcc4bf290')
+        self.assertEqual(data['alert']['value'], 'error')
+        self.assertEqual(data['alert']['text'], 'This is an example Python exception https://sentry.io/alertaio/alerta5/issues/541485531/')
+        self.assertEqual(sorted(data['alert']['tags']), ['browser=Chrome 28.0', 'device=Other', 'level=error', 'os=Windows 8', 'sentry:user=id:1', 'url=http://example.com/foo'])
+        self.assertEqual(data['alert']['attributes']['modules'], ['my.package==1.0.0'])