Browse Source

Add js test data sample
Move sentry open source code to sentry module
Add culprit generate code

David Burke 5 years ago
parent
commit
aa38f9223b

+ 15 - 4
event_store/serializers.py

@@ -1,6 +1,8 @@
+from django.utils.encoding import force_text
 from rest_framework import serializers
 from issues.event_store.error import ErrorEvent
 from issues.models import EventType, EventTag, Event, Issue
+from sentry.culprit import generate_culprit
 
 
 class StoreDefaultSerializer(serializers.Serializer):
@@ -30,10 +32,9 @@ class StoreErrorSerializer(StoreDefaultSerializer):
     def create(self, project, data):
         error = ErrorEvent()
         metadata = error.get_metadata(data)
+        title = error.get_title(metadata)
         issue, _ = Issue.objects.get_or_create(
-            title=error.get_title(metadata),
-            culprit=error.get_location(metadata),
-            project=project,
+            title=title, culprit=error.get_location(metadata), project=project,
         )
 
         level_tag, _ = EventTag.objects.get_or_create(key="level", value=data["level"])
@@ -48,7 +49,8 @@ class StoreErrorSerializer(StoreDefaultSerializer):
             "issue": issue,
             # https://gitlab.com/glitchtip/sentry-open-source/sentry/blob/master/src/sentry/event_manager.py#L412
             # Sentry SDK primarily uses transaction. It has a fallback of get_culprit but isn't preferred. We don't implement this fallback
-            "culprit": data.get("transaction"),
+            "culprit": self.get_culprit(data),
+            "title": title,
         }
         if data.get("contexts"):
             params["contexts"] = data["contexts"]
@@ -60,6 +62,15 @@ class StoreErrorSerializer(StoreDefaultSerializer):
         event = Event.objects.create(**params)
         event.tags.add(level_tag)
 
+    def get_culprit(self, data):
+        """Helper to calculate the default culprit"""
+        return force_text(
+            data.get("culprit")
+            or data.get("transaction")
+            or generate_culprit(data)
+            or ""
+        )
+
 
 class StoreCSPReportSerializer(serializers.Serializer):
     """ Very different format from others """

+ 845 - 0
event_store/test_data/django_template_error_event.json

@@ -0,0 +1,845 @@
+{
+  "eventID": "9cccfef1b2e64e7ba09120418acd402f",
+  "dist": null,
+  "userReport": null,
+  "projectID": "1855811",
+  "previousEventID": null,
+  "message": null,
+  "id": "9cccfef1b2e64e7ba09120418acd402f",
+  "size": 20065,
+  "errors": [],
+  "culprit": "/template-error/",
+  "title": "NoReverseMatch: Reverse for 'nope' not found. 'nope' is not a valid view function or pattern name.",
+  "sdkUpdates": [],
+  "platform": "python",
+  "location": "django/urls/resolvers.py",
+  "nextEventID": null,
+  "type": "error",
+  "metadata": {
+    "function": "_reverse_with_prefix",
+    "type": "NoReverseMatch",
+    "value": "Reverse for 'nope' not found. 'nope' is not a valid view function or pattern name.",
+    "filename": "django/urls/resolvers.py"
+  },
+  "groupingConfig": {
+    "enhancements": "eJybzDhxY3J-bm5-npWRgaGlroGxrpHxBABcTQcY",
+    "id": "newstyle:2019-10-29"
+  },
+  "crashFile": null,
+  "tags": [
+    { "value": "Firefox 72.0", "key": "browser", "_meta": null },
+    { "value": "Firefox", "key": "browser.name", "_meta": null },
+    { "value": "Ubuntu", "key": "client_os.name", "_meta": null },
+    { "value": "no", "key": "handled", "_meta": null },
+    { "value": "error", "key": "level", "_meta": null },
+    { "value": "django", "key": "mechanism", "_meta": null },
+    { "value": "CPython 3.8.1", "key": "runtime", "_meta": null },
+    { "value": "CPython", "key": "runtime.name", "_meta": null },
+    { "value": "210ecca56d59", "key": "server_name", "_meta": null },
+    {
+      "value": "054a97f6e65b4b00a4f44c7befe60632",
+      "key": "trace",
+      "_meta": null
+    },
+    {
+      "value": "054a97f6e65b4b00a4f44c7befe60632-91c32e43e3ca1f52",
+      "key": "trace.ctx",
+      "_meta": null
+    },
+    { "value": "91c32e43e3ca1f52", "key": "trace.span", "_meta": null },
+    { "value": "/template-error/", "key": "transaction", "_meta": null },
+    {
+      "value": "http://localhost:8001/template-error/",
+      "key": "url",
+      "_meta": null
+    }
+  ],
+  "dateCreated": "2020-01-11T17:04:31Z",
+  "dateReceived": "2020-01-11T17:04:31.672903Z",
+  "user": null,
+  "entries": [
+    {
+      "type": "exception",
+      "data": {
+        "values": [
+          {
+            "stacktrace": {
+              "frames": [
+                {
+                  "function": "inner",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "get_response": "<bound method BaseHandler._get_response of <django.core.handlers.wsgi.WSGIHandler object at 0x7fc4b9f92580>>",
+                    "request": "<WSGIRequest: GET '/template-error/'>",
+                    "exc": "NoReverseMatch(\"Reverse for 'nope' not found. 'nope' is not a valid view function or pattern name.\")"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/core/handlers/exception.py",
+                  "inApp": false,
+                  "lineNo": 34,
+                  "module": "django.core.handlers.exception",
+                  "filename": "django/core/handlers/exception.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [
+                      29,
+                      "    can rely on getting a response instead of an exception."
+                    ],
+                    [30, "    \"\"\""],
+                    [31, "    @wraps(get_response)"],
+                    [32, "    def inner(request):"],
+                    [33, "        try:"],
+                    [34, "            response = get_response(request)"],
+                    [35, "        except Exception as exc:"],
+                    [
+                      36,
+                      "            response = response_for_exception(request, exc)"
+                    ],
+                    [37, "        return response"],
+                    [38, "    return inner"],
+                    [39, ""]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "_get_response",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "resolver_match": "ResolverMatch(func=errors.views.TemplateErrorView, args=(), kwargs={}, url_name=template_error, app_names=[], namespaces=[], route=template-error/)",
+                    "callback_args": [],
+                    "middleware_method": "<function CsrfViewMiddleware.process_view at 0x7fc4b913b160>",
+                    "self": "<django.core.handlers.wsgi.WSGIHandler object at 0x7fc4b9f92580>",
+                    "request": "<WSGIRequest: GET '/template-error/'>",
+                    "callback": "<function TemplateErrorView at 0x7fc4b92473a0>",
+                    "wrapped_callback": "<function TemplateErrorView at 0x7fc4b92473a0>",
+                    "resolver": "<URLResolver 'django_error_factory.urls' (None:None) '^/'>",
+                    "callback_kwargs": {},
+                    "response": "<TemplateResponse status_code=200, \"text/html; charset=utf-8\">"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/core/handlers/base.py",
+                  "inApp": false,
+                  "lineNo": 145,
+                  "module": "django.core.handlers.base",
+                  "filename": "django/core/handlers/base.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [140, "                    )"],
+                    [141, ""],
+                    [142, "            try:"],
+                    [143, "                response = response.render()"],
+                    [144, "            except Exception as e:"],
+                    [
+                      145,
+                      "                response = self.process_exception_by_middleware(e, request)"
+                    ],
+                    [146, ""],
+                    [147, "        return response"],
+                    [148, ""],
+                    [
+                      149,
+                      "    def process_exception_by_middleware(self, exception, request):"
+                    ],
+                    [150, "        \"\"\""]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "_get_response",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "resolver_match": "ResolverMatch(func=errors.views.TemplateErrorView, args=(), kwargs={}, url_name=template_error, app_names=[], namespaces=[], route=template-error/)",
+                    "callback_args": [],
+                    "middleware_method": "<function CsrfViewMiddleware.process_view at 0x7fc4b913b160>",
+                    "self": "<django.core.handlers.wsgi.WSGIHandler object at 0x7fc4b9f92580>",
+                    "request": "<WSGIRequest: GET '/template-error/'>",
+                    "callback": "<function TemplateErrorView at 0x7fc4b92473a0>",
+                    "wrapped_callback": "<function TemplateErrorView at 0x7fc4b92473a0>",
+                    "resolver": "<URLResolver 'django_error_factory.urls' (None:None) '^/'>",
+                    "callback_kwargs": {},
+                    "response": "<TemplateResponse status_code=200, \"text/html; charset=utf-8\">"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/core/handlers/base.py",
+                  "inApp": false,
+                  "lineNo": 143,
+                  "module": "django.core.handlers.base",
+                  "filename": "django/core/handlers/base.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [
+                      138,
+                      "                        \"HttpResponse object. It returned None instead.\""
+                    ],
+                    [
+                      139,
+                      "                        % (middleware_method.__self__.__class__.__name__)"
+                    ],
+                    [140, "                    )"],
+                    [141, ""],
+                    [142, "            try:"],
+                    [143, "                response = response.render()"],
+                    [144, "            except Exception as e:"],
+                    [
+                      145,
+                      "                response = self.process_exception_by_middleware(e, request)"
+                    ],
+                    [146, ""],
+                    [147, "        return response"],
+                    [148, ""]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "render",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "self": "<TemplateResponse status_code=200, \"text/html; charset=utf-8\">",
+                    "retval": "<TemplateResponse status_code=200, \"text/html; charset=utf-8\">"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/template/response.py",
+                  "inApp": false,
+                  "lineNo": 105,
+                  "module": "django.template.response",
+                  "filename": "django/template/response.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [100, ""],
+                    [101, "        Return the baked response instance."],
+                    [102, "        \"\"\""],
+                    [103, "        retval = self"],
+                    [104, "        if not self._is_rendered:"],
+                    [105, "            self.content = self.rendered_content"],
+                    [
+                      106,
+                      "            for post_callback in self._post_render_callbacks:"
+                    ],
+                    [107, "                newretval = post_callback(retval)"],
+                    [108, "                if newretval is not None:"],
+                    [109, "                    retval = newretval"],
+                    [110, "        return retval"]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "rendered_content",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "self": "<TemplateResponse status_code=200, \"text/html; charset=utf-8\">",
+                    "template": "<django.template.backends.django.Template object at 0x7fc4b90a8880>",
+                    "context": {
+                      "view": "<errors.views.TemplateErrorView object at 0x7fc4b90a84c0>"
+                    }
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/template/response.py",
+                  "inApp": false,
+                  "lineNo": 83,
+                  "module": "django.template.response",
+                  "filename": "django/template/response.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [
+                      78,
+                      "        response content, you must either call render(), or set the"
+                    ],
+                    [
+                      79,
+                      "        content explicitly using the value of this property."
+                    ],
+                    [80, "        \"\"\""],
+                    [
+                      81,
+                      "        template = self.resolve_template(self.template_name)"
+                    ],
+                    [
+                      82,
+                      "        context = self.resolve_context(self.context_data)"
+                    ],
+                    [
+                      83,
+                      "        return template.render(context, self._request)"
+                    ],
+                    [84, ""],
+                    [85, "    def add_post_render_callback(self, callback):"],
+                    [86, "        \"\"\"Add a new post-rendering callback."],
+                    [87, ""],
+                    [88, "        If the response has already been rendered,"]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "render",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "self": "<django.template.backends.django.Template object at 0x7fc4b90a8880>",
+                    "request": "<WSGIRequest: GET '/template-error/'>",
+                    "context": "[{'True': True, 'False': False, 'None': None}, {}, {}, {'view': <errors.views.TemplateErrorView object at 0x7fc4b90a84c0>}]"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/template/backends/django.py",
+                  "inApp": false,
+                  "lineNo": 61,
+                  "module": "django.template.backends.django",
+                  "filename": "django/template/backends/django.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [56, "        return self.template.origin"],
+                    [57, ""],
+                    [58, "    def render(self, context=None, request=None):"],
+                    [
+                      59,
+                      "        context = make_context(context, request, autoescape=self.backend.engine.autoescape)"
+                    ],
+                    [60, "        try:"],
+                    [61, "            return self.template.render(context)"],
+                    [62, "        except TemplateDoesNotExist as exc:"],
+                    [63, "            reraise(exc, self.backend)"],
+                    [64, ""],
+                    [65, ""],
+                    [66, "def copy_exception(exc, backend=None):"]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "render",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "self": "<django.template.base.Template object at 0x7fc4b90a88e0>",
+                    "context": "[{'True': True, 'False': False, 'None': None}, {}, {}, {'view': <errors.views.TemplateErrorView object at 0x7fc4b90a84c0>}]"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/template/base.py",
+                  "inApp": false,
+                  "lineNo": 171,
+                  "module": "django.template.base",
+                  "filename": "django/template/base.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [
+                      166,
+                      "        \"Display stage -- can be called many times\""
+                    ],
+                    [
+                      167,
+                      "        with context.render_context.push_state(self):"
+                    ],
+                    [168, "            if context.template is None:"],
+                    [169, "                with context.bind_template(self):"],
+                    [
+                      170,
+                      "                    context.template_name = self.name"
+                    ],
+                    [171, "                    return self._render(context)"],
+                    [172, "            else:"],
+                    [173, "                return self._render(context)"],
+                    [174, ""],
+                    [175, "    def compile_nodelist(self):"],
+                    [176, "        \"\"\""]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "_render",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "self": "<django.template.base.Template object at 0x7fc4b90a88e0>",
+                    "context": "[{'True': True, 'False': False, 'None': None}, {}, {}, {'view': <errors.views.TemplateErrorView object at 0x7fc4b90a84c0>}]"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/template/base.py",
+                  "inApp": false,
+                  "lineNo": 163,
+                  "module": "django.template.base",
+                  "filename": "django/template/base.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [158, "    def __iter__(self):"],
+                    [159, "        for node in self.nodelist:"],
+                    [160, "            yield from node"],
+                    [161, ""],
+                    [162, "    def _render(self, context):"],
+                    [163, "        return self.nodelist.render(context)"],
+                    [164, ""],
+                    [165, "    def render(self, context):"],
+                    [
+                      166,
+                      "        \"Display stage -- can be called many times\""
+                    ],
+                    [
+                      167,
+                      "        with context.render_context.push_state(self):"
+                    ],
+                    [168, "            if context.template is None:"]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "render",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "node": "<django.template.defaulttags.URLNode object at 0x7fc4b90a87c0>",
+                    "bit": "'<a href=\"'",
+                    "bits": ["'<a href=\"'"],
+                    "self": [
+                      "<TextNode: '<a href=\"'>",
+                      "<django.template.defaulttags.URLNode object at 0x7fc4b90a87c0>",
+                      "<TextNode: '\">'>"
+                    ],
+                    "context": "[{'True': True, 'False': False, 'None': None}, {}, {}, {'view': <errors.views.TemplateErrorView object at 0x7fc4b90a84c0>}]"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/template/base.py",
+                  "inApp": false,
+                  "lineNo": 936,
+                  "module": "django.template.base",
+                  "filename": "django/template/base.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [931, ""],
+                    [932, "    def render(self, context):"],
+                    [933, "        bits = []"],
+                    [934, "        for node in self:"],
+                    [935, "            if isinstance(node, Node):"],
+                    [
+                      936,
+                      "                bit = node.render_annotated(context)"
+                    ],
+                    [937, "            else:"],
+                    [938, "                bit = node"],
+                    [939, "            bits.append(str(bit))"],
+                    [940, "        return mark_safe(''.join(bits))"],
+                    [941, ""]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": null,
+                  "errors": null,
+                  "colNo": null,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "/code/errors/templates/template_error.html",
+                  "inApp": true,
+                  "lineNo": 1,
+                  "module": null,
+                  "filename": "/code/errors/templates/template_error.html",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [1, "&lt;a href=&quot;{% url &#x27;nope&#x27; %}&quot;&gt;"]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "render_annotated",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "self": "<django.template.defaulttags.URLNode object at 0x7fc4b90a87c0>",
+                    "context": "[{'True': True, 'False': False, 'None': None}, {}, {}, {'view': <errors.views.TemplateErrorView object at 0x7fc4b90a84c0>}]"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/template/base.py",
+                  "inApp": false,
+                  "lineNo": 903,
+                  "module": "django.template.base",
+                  "filename": "django/template/base.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [
+                      898,
+                      "        rendering, the exception is annotated with contextual line information"
+                    ],
+                    [
+                      899,
+                      "        where it occurred in the template. For internal usage this method is"
+                    ],
+                    [
+                      900,
+                      "        preferred over using the render method directly."
+                    ],
+                    [901, "        \"\"\""],
+                    [902, "        try:"],
+                    [903, "            return self.render(context)"],
+                    [904, "        except Exception as e:"],
+                    [
+                      905,
+                      "            if context.template.engine.debug and not hasattr(e, 'template_debug'):"
+                    ],
+                    [
+                      906,
+                      "                e.template_debug = context.render_context.template.get_exception_info(e, self.token)"
+                    ],
+                    [907, "            raise"],
+                    [908, ""]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "render",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "reverse": "<function reverse at 0x7fc4ba161820>",
+                    "url": "''",
+                    "self": "<django.template.defaulttags.URLNode object at 0x7fc4b90a87c0>",
+                    "args": [],
+                    "current_app": "''",
+                    "view_name": "'nope'",
+                    "context": "[{'True': True, 'False': False, 'None': None}, {}, {}, {'view': <errors.views.TemplateErrorView object at 0x7fc4b90a84c0>}]",
+                    "kwargs": {},
+                    "NoReverseMatch": "<class 'django.urls.exceptions.NoReverseMatch'>"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/template/defaulttags.py",
+                  "inApp": false,
+                  "lineNo": 443,
+                  "module": "django.template.defaulttags",
+                  "filename": "django/template/defaulttags.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [438, "                current_app = None"],
+                    [
+                      439,
+                      "        # Try to look up the URL. If it fails, raise NoReverseMatch unless the"
+                    ],
+                    [
+                      440,
+                      "        # {% url ... as var %} construct is used, in which case return nothing."
+                    ],
+                    [441, "        url = ''"],
+                    [442, "        try:"],
+                    [
+                      443,
+                      "            url = reverse(view_name, args=args, kwargs=kwargs, current_app=current_app)"
+                    ],
+                    [444, "        except NoReverseMatch:"],
+                    [445, "            if self.asvar is None:"],
+                    [446, "                raise"],
+                    [447, ""],
+                    [448, "        if self.asvar:"]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "reverse",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "current_path": "None",
+                    "viewname": "'nope'",
+                    "args": [],
+                    "current_app": "''",
+                    "prefix": "'/'",
+                    "resolver": "<URLResolver 'django_error_factory.urls' (None:None) '^/'>",
+                    "kwargs": {},
+                    "path": [],
+                    "urlconf": "'django_error_factory.urls'",
+                    "view": "'nope'"
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/urls/base.py",
+                  "inApp": false,
+                  "lineNo": 87,
+                  "module": "django.urls.base",
+                  "filename": "django/urls/base.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [82, "                else:"],
+                    [
+                      83,
+                      "                    raise NoReverseMatch(\"%s is not a registered namespace\" % key)"
+                    ],
+                    [84, "        if ns_pattern:"],
+                    [
+                      85,
+                      "            resolver = get_ns_resolver(ns_pattern, resolver, tuple(ns_converters.items()))"
+                    ],
+                    [86, ""],
+                    [
+                      87,
+                      "    return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs))"
+                    ],
+                    [88, ""],
+                    [89, ""],
+                    [90, "reverse_lazy = lazy(reverse, str)"],
+                    [91, ""],
+                    [92, ""]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "_reverse_with_prefix",
+                  "errors": null,
+                  "colNo": null,
+                  "vars": {
+                    "patterns": [],
+                    "_prefix": "'/'",
+                    "self": "<URLResolver 'django_error_factory.urls' (None:None) '^/'>",
+                    "args": [],
+                    "m": "None",
+                    "lookup_view_s": "'nope'",
+                    "n": "None",
+                    "possibilities": [],
+                    "lookup_view": "'nope'",
+                    "msg": "\"Reverse for 'nope' not found. 'nope' is not a valid view function or pattern name.\""
+                  },
+                  "package": null,
+                  "absPath": "/usr/local/lib/python3.8/site-packages/django/urls/resolvers.py",
+                  "inApp": false,
+                  "lineNo": 677,
+                  "module": "django.urls.resolvers",
+                  "filename": "django/urls/resolvers.py",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [
+                    [672, "        else:"],
+                    [673, "            msg = ("],
+                    [
+                      674,
+                      "                \"Reverse for '%(view)s' not found. '%(view)s' is not \""
+                    ],
+                    [
+                      675,
+                      "                \"a valid view function or pattern name.\" % {'view': lookup_view_s}"
+                    ],
+                    [676, "            )"],
+                    [677, "        raise NoReverseMatch(msg)"]
+                  ],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                }
+              ],
+              "framesOmitted": null,
+              "registers": null,
+              "hasSystemFrames": true
+            },
+            "module": "django.urls.exceptions",
+            "rawStacktrace": null,
+            "mechanism": { "type": "django", "handled": false },
+            "threadId": null,
+            "value": "Reverse for 'nope' not found. 'nope' is not a valid view function or pattern name.",
+            "type": "NoReverseMatch"
+          }
+        ],
+        "excOmitted": null,
+        "hasSystemFrames": true
+      }
+    },
+    {
+      "type": "request",
+      "data": {
+        "fragment": null,
+        "cookies": [],
+        "inferredContentType": "text/plain",
+        "env": { "SERVER_PORT": "8001", "SERVER_NAME": "210ecca56d59" },
+        "headers": [
+          [
+            "Accept",
+            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
+          ],
+          ["Accept-Encoding", "gzip, deflate"],
+          ["Accept-Language", "en-US,en;q=0.5"],
+          ["Connection", "keep-alive"],
+          ["Content-Length", ""],
+          ["Content-Type", "text/plain"],
+          ["Dnt", "1"],
+          ["Host", "localhost:8001"],
+          ["Referer", "http://localhost:8001/"],
+          ["Upgrade-Insecure-Requests", "1"],
+          [
+            "User-Agent",
+            "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0"
+          ]
+        ],
+        "url": "http://localhost:8001/template-error/",
+        "query": [],
+        "data": null,
+        "method": "GET"
+      }
+    }
+  ],
+  "packages": {
+    "toml": "0.10.0",
+    "cffi": "1.13.2",
+    "ipython-genutils": "0.2.0",
+    "wheel": "0.33.6",
+    "pygments": "2.5.2",
+    "cleo": "0.7.6",
+    "pip": "19.3.1",
+    "prompt-toolkit": "3.0.2",
+    "parso": "0.5.2",
+    "jeepney": "0.4.2",
+    "html5lib": "1.0.1",
+    "appdirs": "1.4.3",
+    "requests-toolbelt": "0.8.0",
+    "regex": "2020.1.8",
+    "pastel": "0.1.1",
+    "msgpack": "0.6.2",
+    "pexpect": "4.7.0",
+    "sentry-sdk": "0.14.0",
+    "ipdb": "0.12.3",
+    "six": "1.13.0",
+    "poetry": "1.0.0",
+    "ptyprocess": "0.6.0",
+    "click": "7.0",
+    "jedi": "0.15.2",
+    "traitlets": "4.3.3",
+    "asgiref": "3.2.3",
+    "cachy": "0.3.0",
+    "pathspec": "0.7.0",
+    "cachecontrol": "0.12.6",
+    "certifi": "2019.11.28",
+    "jsonschema": "3.2.0",
+    "backcall": "0.1.0",
+    "cryptography": "2.8",
+    "sqlparse": "0.3.0",
+    "pycparser": "2.19",
+    "secretstorage": "3.1.1",
+    "urllib3": "1.25.7",
+    "webencodings": "0.5.1",
+    "pytz": "2019.3",
+    "clikit": "0.4.1",
+    "ipython": "7.11.1",
+    "lockfile": "0.12.2",
+    "pickleshare": "0.7.5",
+    "decorator": "4.4.1",
+    "tomlkit": "0.5.8",
+    "typed-ast": "1.4.0",
+    "keyring": "19.3.0",
+    "wcwidth": "0.1.8",
+    "django": "3.0.2",
+    "pyrsistent": "0.14.11",
+    "pyparsing": "2.4.6",
+    "pylev": "1.3.0",
+    "chardet": "3.0.4",
+    "setuptools": "44.0.0",
+    "pkginfo": "1.5.0.1",
+    "black": "19.10b0",
+    "requests": "2.22.0",
+    "shellingham": "1.3.1",
+    "idna": "2.8",
+    "attrs": "19.3.0"
+  },
+  "sdk": { "version": "0.14.0", "name": "sentry.python" },
+  "_meta": {
+    "user": null,
+    "context": null,
+    "entries": {
+      "0": {
+        "data": {
+          "values": {
+            "0": {
+              "": null,
+              "stacktrace": {
+                "": null,
+                "frames": {},
+                "registers": null,
+                "framesOmitted": null
+              },
+              "mechanism": null,
+              "module": null,
+              "value": null,
+              "threadId": null,
+              "type": null
+            }
+          }
+        }
+      }
+    },
+    "contexts": null,
+    "message": null,
+    "packages": null,
+    "tags": {},
+    "sdk": null
+  },
+  "contexts": {
+    "browser": { "version": "72.0", "type": "browser", "name": "Firefox" },
+    "runtime": {
+      "version": "3.8.1",
+      "type": "runtime",
+      "build": "3.8.1 (default, Jan  3 2020, 22:55:55) \n[GCC 8.3.0]",
+      "name": "CPython"
+    },
+    "trace": {
+      "description": "django.middleware.clickjacking.XFrameOptionsMiddleware.__call__",
+      "parent_span_id": "969696ce99d241b7",
+      "trace_id": "054a97f6e65b4b00a4f44c7befe60632",
+      "span_id": "91c32e43e3ca1f52",
+      "type": "trace",
+      "op": "django.middleware"
+    },
+    "client_os": { "type": "os", "name": "Ubuntu" }
+  },
+  "fingerprints": [
+    "6c23e1847ab5bf6e4d9b040c4054ca67",
+    "8b852c201d5235b712e30315ea1fdffa"
+  ],
+  "context": { "sys.argv": ["./manage.py", "runserver", "0.0.0.0:8001"] },
+  "release": null,
+  "groupID": "1435229148"
+}

+ 169 - 0
event_store/test_data/django_template_error_issue.json

@@ -0,0 +1,169 @@
+{
+  "seenBy": [
+    {
+      "username": "it@burkesoftware.com",
+      "lastLogin": "2020-01-10T19:15:16.601343Z",
+      "isSuperuser": false,
+      "emails": [
+        {
+          "is_verified": true,
+          "id": "414369",
+          "email": "david@burkesoftware.com"
+        },
+        { "is_verified": true, "id": "57124", "email": "it@burkesoftware.com" },
+        { "is_verified": true, "id": "257183", "email": "dburke@thelabnyc.com" }
+      ],
+      "isManaged": false,
+      "lastActive": "2020-01-11T18:46:19.908293Z",
+      "isStaff": false,
+      "identities": [],
+      "id": "20265",
+      "isActive": true,
+      "has2fa": false,
+      "name": "David",
+      "avatarUrl": "https://secure.gravatar.com/avatar/caa812c5ce135feee6fbd76ac67771c4?s=32&d=mm",
+      "dateJoined": "2014-02-28T17:27:30.585866Z",
+      "options": {
+        "timezone": "US/Eastern",
+        "stacktraceOrder": -1,
+        "language": "en",
+        "clock24Hours": false
+      },
+      "flags": { "newsletter_consent_prompt": false },
+      "avatar": { "avatarUuid": null, "avatarType": "gravatar" },
+      "hasPasswordAuth": true,
+      "email": "it@burkesoftware.com",
+      "lastSeen": "2020-01-11T17:04:54.279988Z"
+    }
+  ],
+  "platform": "python",
+  "pluginIssues": [],
+  "lastSeen": "2020-01-11T17:04:31.306588Z",
+  "userReportCount": 0,
+  "numComments": 0,
+  "userCount": 0,
+  "stats": {
+    "30d": [
+      [1576108800, 0],
+      [1576195200, 0],
+      [1576281600, 0],
+      [1576368000, 0],
+      [1576454400, 0],
+      [1576540800, 0],
+      [1576627200, 0],
+      [1576713600, 0],
+      [1576800000, 0],
+      [1576886400, 0],
+      [1576972800, 0],
+      [1577059200, 0],
+      [1577145600, 0],
+      [1577232000, 0],
+      [1577318400, 0],
+      [1577404800, 0],
+      [1577491200, 0],
+      [1577577600, 0],
+      [1577664000, 0],
+      [1577750400, 0],
+      [1577836800, 0],
+      [1577923200, 0],
+      [1578009600, 0],
+      [1578096000, 0],
+      [1578182400, 0],
+      [1578268800, 0],
+      [1578355200, 0],
+      [1578441600, 0],
+      [1578528000, 0],
+      [1578614400, 0],
+      [1578700800, 1]
+    ],
+    "24h": [
+      [1578679200, 0],
+      [1578682800, 0],
+      [1578686400, 0],
+      [1578690000, 0],
+      [1578693600, 0],
+      [1578697200, 0],
+      [1578700800, 0],
+      [1578704400, 0],
+      [1578708000, 0],
+      [1578711600, 0],
+      [1578715200, 0],
+      [1578718800, 0],
+      [1578722400, 0],
+      [1578726000, 0],
+      [1578729600, 0],
+      [1578733200, 0],
+      [1578736800, 0],
+      [1578740400, 0],
+      [1578744000, 0],
+      [1578747600, 0],
+      [1578751200, 0],
+      [1578754800, 0],
+      [1578758400, 0],
+      [1578762000, 1],
+      [1578765600, 0]
+    ]
+  },
+  "culprit": "/template-error/",
+  "title": "NoReverseMatch: Reverse for 'nope' not found. 'nope' is not a valid view function or pattern name.",
+  "id": "1435229148",
+  "assignedTo": null,
+  "participants": [],
+  "logger": null,
+  "type": "error",
+  "annotations": [],
+  "metadata": {
+    "function": "_reverse_with_prefix",
+    "type": "NoReverseMatch",
+    "value": "Reverse for 'nope' not found. 'nope' is not a valid view function or pattern name.",
+    "filename": "django/urls/resolvers.py"
+  },
+  "status": "unresolved",
+  "pluginActions": [],
+  "tags": [
+    { "totalValues": 1, "name": "Browser", "key": "browser" },
+    { "totalValues": 1, "name": "Browser.Name", "key": "browser.name" },
+    { "totalValues": 1, "name": "Client Os.Name", "key": "client_os.name" },
+    { "totalValues": 1, "name": "Handled", "key": "handled" },
+    { "totalValues": 1, "name": "Level", "key": "level" },
+    { "totalValues": 1, "name": "Mechanism", "key": "mechanism" },
+    { "totalValues": 1, "name": "Runtime", "key": "runtime" },
+    { "totalValues": 1, "name": "Runtime.Name", "key": "runtime.name" },
+    { "totalValues": 1, "name": "Server", "key": "server_name" },
+    { "totalValues": 1, "name": "Trace", "key": "trace" },
+    { "totalValues": 1, "name": "Trace.Ctx", "key": "trace.ctx" },
+    { "totalValues": 1, "name": "Trace.Span", "key": "trace.span" },
+    { "totalValues": 1, "name": "Transaction", "key": "transaction" },
+    { "totalValues": 1, "name": "URL", "key": "url" }
+  ],
+  "subscriptionDetails": null,
+  "isPublic": false,
+  "hasSeen": true,
+  "firstRelease": null,
+  "shortId": "TEST-1",
+  "shareId": null,
+  "firstSeen": "2020-01-11T17:04:31.306588Z",
+  "count": "1",
+  "permalink": "https://sentry.io/organizations/burke-software-and-consulting/issues/1435229148/",
+  "level": "error",
+  "isSubscribed": true,
+  "pluginContexts": [],
+  "isBookmarked": false,
+  "project": {
+    "platform": "",
+    "slug": "test",
+    "id": "1855811",
+    "name": "test"
+  },
+  "lastRelease": null,
+  "activity": [
+    {
+      "data": {},
+      "dateCreated": "2020-01-11T17:04:31.306588Z",
+      "type": "first_seen",
+      "id": "0",
+      "user": null
+    }
+  ],
+  "statusDetails": {}
+}

+ 353 - 0
event_store/test_data/js_error_factory.py

@@ -0,0 +1,353 @@
+throw_error = {
+    "exception": {
+        "values": [
+            {
+                "type": "Error",
+                "value": "an error string",
+                "stacktrace": {
+                    "frames": [
+                        {
+                            "colno": 27,
+                            "filename": "http://localhost:4200/polyfills.js",
+                            "function": "globalZoneAwareCallback",
+                            "in_app": True,
+                            "lineno": 4864,
+                        },
+                        {
+                            "colno": 14,
+                            "filename": "http://localhost:4200/polyfills.js",
+                            "function": "invokeTask",
+                            "in_app": True,
+                            "lineno": 4838,
+                        },
+                        {
+                            "colno": 34,
+                            "filename": "http://localhost:4200/polyfills.js",
+                            "function": "invokeTask",
+                            "in_app": True,
+                            "lineno": 3700,
+                        },
+                        {
+                            "colno": 47,
+                            "filename": "http://localhost:4200/polyfills.js",
+                            "function": "runTask",
+                            "in_app": True,
+                            "lineno": 3403,
+                        },
+                        {
+                            "colno": 60,
+                            "filename": "http://localhost:4200/polyfills.js",
+                            "function": "invokeTask",
+                            "in_app": True,
+                            "lineno": 3625,
+                        },
+                        {
+                            "colno": 33,
+                            "filename": "http://localhost:4200/vendor.js",
+                            "function": "onInvokeTask",
+                            "in_app": True,
+                            "lineno": 70625,
+                        },
+                        {
+                            "colno": 31,
+                            "filename": "http://localhost:4200/polyfills.js",
+                            "function": "invokeTask",
+                            "in_app": True,
+                            "lineno": 3626,
+                        },
+                        {
+                            "colno": 23,
+                            "filename": "http://localhost:4200/vendor.js",
+                            "function": "sentryWrapped",
+                            "in_app": True,
+                            "lineno": 81826,
+                        },
+                        {
+                            "colno": 50,
+                            "filename": "http://localhost:4200/vendor.js",
+                            "function": "decoratePreventDefault/<",
+                            "in_app": True,
+                            "lineno": 79300,
+                        },
+                        {
+                            "colno": 29,
+                            "filename": "http://localhost:4200/vendor.js",
+                            "function": "renderEventHandlerClosure/<",
+                            "in_app": True,
+                            "lineno": 73554,
+                        },
+                        {
+                            "colno": 25,
+                            "filename": "http://localhost:4200/vendor.js",
+                            "function": "dispatchEvent",
+                            "in_app": True,
+                            "lineno": 61709,
+                        },
+                        {
+                            "colno": 12,
+                            "filename": "http://localhost:4200/vendor.js",
+                            "function": "debugHandleEvent",
+                            "in_app": True,
+                            "lineno": 75876,
+                        },
+                        {
+                            "colno": 15,
+                            "filename": "http://localhost:4200/vendor.js",
+                            "function": "callWithDebugContext",
+                            "in_app": True,
+                            "lineno": 76251,
+                        },
+                        {
+                            "colno": 15,
+                            "filename": "http://localhost:4200/vendor.js",
+                            "function": "viewWrappedDebugError",
+                            "in_app": True,
+                            "lineno": 61054,
+                        },
+                    ]
+                },
+                "mechanism": {"handled": True, "type": "generic"},
+            }
+        ]
+    },
+    "level": "error",
+    "event_id": "3a4fe46760d04d0c9eeb4f2be32d2ba2",
+    "platform": "javascript",
+    "sdk": {
+        "name": "sentry.javascript.browser",
+        "packages": [{"name": "npm:@sentry/browser", "version": "5.11.0"}],
+        "version": "5.11.0",
+        "integrations": [
+            "InboundFilters",
+            "FunctionToString",
+            "TryCatch",
+            "Breadcrumbs",
+            "GlobalHandlers",
+            "LinkedErrors",
+            "UserAgent",
+        ],
+    },
+    "breadcrumbs": [
+        {
+            "timestamp": 1578861859.377,
+            "category": "console",
+            "data": {
+                "extra": {
+                    "arguments": [
+                        "Angular is running in the development mode. Call enableProdMode() to enable the production mode."
+                    ]
+                },
+                "logger": "console",
+            },
+            "level": "log",
+            "message": "Angular is running in the development mode. Call enableProdMode() to enable the production mode.",
+        },
+        {
+            "timestamp": 1578861859.415,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578861859.529,
+            "category": "xhr",
+            "data": {
+                "method": "GET",
+                "url": "http://localhost:4200/sockjs-node/info?t=1578861859416",
+                "status_code": 200,
+            },
+            "type": "http",
+        },
+        {
+            "timestamp": 1578861860.967,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578861861.053,
+            "category": "sentry",
+            "event_id": "953c99e1547f4f6bbcb119dc44cc7d34",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578861861.071,
+            "category": "sentry",
+            "event_id": "1b480b60cbf840ad9a87c6c8d32eaca4",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578861861.072,
+            "category": "sentry",
+            "event_id": "21ea06f91e1b429b8a6e021be50c7104",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578861861.075,
+            "category": "sentry",
+            "event_id": "7de9ecf53d7f480c86a857acdd328f3c",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862058.96,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578862059.054,
+            "category": "sentry",
+            "event_id": "15f869407e7049d391a74bc26b98fb0e",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862059.176,
+            "category": "sentry",
+            "event_id": "eb0ae9354bdb4ecd90614de54495549b",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862349.058,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578862349.163,
+            "category": "sentry",
+            "event_id": "ff7ff13b69ee46789121fd071e8bfe70",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862349.226,
+            "category": "sentry",
+            "event_id": "fa8e4ca917864dd7980fc1dd020783de",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862369.686,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578862369.811,
+            "category": "sentry",
+            "event_id": "2cec68bc7a484f64980987fb53471a8c",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862369.812,
+            "category": "sentry",
+            "event_id": "72ccc32c539e407fa4fab5737df31dbf",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862392.876,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578862393.05,
+            "category": "sentry",
+            "event_id": "abed54c077f24206aec40a04f4a838a6",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862393.053,
+            "category": "sentry",
+            "event_id": "abf87281aee443fca8169590ba6389bb",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862442.414,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578862442.473,
+            "category": "sentry",
+            "event_id": "589f50e20cec479fac95415dbe021cb7",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862442.522,
+            "category": "sentry",
+            "event_id": "8d48047f80c645868a90f72e0c3f6487",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862473.404,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578862473.554,
+            "category": "sentry",
+            "event_id": "ee14e02ac9754f79b827505548d36c47",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862473.556,
+            "category": "sentry",
+            "event_id": "12d83a79d84b40f1a2df863d1af657d1",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862870.517,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578862870.698,
+            "category": "sentry",
+            "event_id": "692223e6f4384e60b53bb19d0adde8b9",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862870.706,
+            "category": "sentry",
+            "event_id": "a7de5cc48c2544d89e43f2795e0e5457",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862876.059,
+            "category": "ui.click",
+            "message": "body > app-root > ol > li > a",
+        },
+        {
+            "timestamp": 1578862876.202,
+            "category": "sentry",
+            "event_id": "aa64fb0f82254c0b8982a21b948bf2d8",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+        {
+            "timestamp": 1578862876.204,
+            "category": "sentry",
+            "event_id": "7e590f61436447aebf2b7176023bab19",
+            "level": "error",
+            "message": "Error: an error string",
+        },
+    ],
+    "request": {
+        "url": "http://localhost:4200/",
+        "headers": {
+            "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0"
+        },
+    },
+}
+

+ 463 - 0
event_store/test_data/js_throw_error_event.json

@@ -0,0 +1,463 @@
+{
+  "eventID": "b029310e1fdd461f8866a2255c2aff92",
+  "dist": null,
+  "userReport": null,
+  "projectID": "1855811",
+  "previousEventID": null,
+  "message": null,
+  "id": "b029310e1fdd461f8866a2255c2aff92",
+  "size": 8633,
+  "errors": [
+    {
+      "data": { "url": "http://localhost:4200/polyfills.js" },
+      "message": "Cannot fetch resource due to restricted IP address",
+      "type": "restricted_ip"
+    },
+    {
+      "data": { "url": "http://localhost:4200/vendor.js" },
+      "message": "Cannot fetch resource due to restricted IP address",
+      "type": "restricted_ip"
+    }
+  ],
+  "culprit": "viewWrappedDebugError(vendor)",
+  "title": "Error: an error string",
+  "sdkUpdates": [],
+  "platform": "javascript",
+  "location": "/vendor.js",
+  "nextEventID": null,
+  "type": "error",
+  "metadata": {
+    "function": "viewWrappedDebugError",
+    "type": "Error",
+    "value": "an error string",
+    "filename": "/vendor.js"
+  },
+  "groupingConfig": {
+    "enhancements": "eJybzDhxY3J-bm5-npWRgaGlroGxrpHxBABcTQcY",
+    "id": "newstyle:2019-10-29"
+  },
+  "crashFile": null,
+  "tags": [
+    { "value": "Firefox 72.0", "key": "browser", "_meta": null },
+    { "value": "Firefox", "key": "browser.name", "_meta": null },
+    { "value": "yes", "key": "handled", "_meta": null },
+    { "value": "error", "key": "level", "_meta": null },
+    { "value": "instrument", "key": "mechanism", "_meta": null },
+    { "value": "Ubuntu", "key": "os.name", "_meta": null },
+    { "value": "http://localhost:4200/", "key": "url", "_meta": null },
+    {
+      "query": "user.ip:\"142.255.62.197\"",
+      "value": "ip:142.255.62.197",
+      "key": "user",
+      "_meta": null
+    }
+  ],
+  "dateCreated": "2020-01-12T20:39:50Z",
+  "dateReceived": "2020-01-12T20:39:50.513063Z",
+  "user": {
+    "username": null,
+    "name": null,
+    "ip_address": "142.255.62.197",
+    "email": null,
+    "data": null,
+    "id": null
+  },
+  "entries": [
+    {
+      "type": "exception",
+      "data": {
+        "values": [
+          {
+            "stacktrace": {
+              "frames": [
+                {
+                  "function": "globalZoneAwareCallback",
+                  "errors": null,
+                  "colNo": 27,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/polyfills.js",
+                  "inApp": false,
+                  "lineNo": 4864,
+                  "module": "polyfills",
+                  "filename": "/polyfills.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "invokeTask",
+                  "errors": null,
+                  "colNo": 14,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/polyfills.js",
+                  "inApp": false,
+                  "lineNo": 4838,
+                  "module": "polyfills",
+                  "filename": "/polyfills.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "invokeTask",
+                  "errors": null,
+                  "colNo": 34,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/polyfills.js",
+                  "inApp": false,
+                  "lineNo": 3700,
+                  "module": "polyfills",
+                  "filename": "/polyfills.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "runTask",
+                  "errors": null,
+                  "colNo": 47,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/polyfills.js",
+                  "inApp": false,
+                  "lineNo": 3403,
+                  "module": "polyfills",
+                  "filename": "/polyfills.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "invokeTask",
+                  "errors": null,
+                  "colNo": 60,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/polyfills.js",
+                  "inApp": false,
+                  "lineNo": 3625,
+                  "module": "polyfills",
+                  "filename": "/polyfills.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "onInvokeTask",
+                  "errors": null,
+                  "colNo": 33,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/vendor.js",
+                  "inApp": false,
+                  "lineNo": 70625,
+                  "module": "vendor",
+                  "filename": "/vendor.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "invokeTask",
+                  "errors": null,
+                  "colNo": 31,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/polyfills.js",
+                  "inApp": false,
+                  "lineNo": 3626,
+                  "module": "polyfills",
+                  "filename": "/polyfills.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "sentryWrapped",
+                  "errors": null,
+                  "colNo": 23,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/vendor.js",
+                  "inApp": false,
+                  "lineNo": 81826,
+                  "module": "vendor",
+                  "filename": "/vendor.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "decoratePreventDefault/<",
+                  "errors": null,
+                  "colNo": 50,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/vendor.js",
+                  "inApp": false,
+                  "lineNo": 79300,
+                  "module": "vendor",
+                  "filename": "/vendor.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "renderEventHandlerClosure/<",
+                  "errors": null,
+                  "colNo": 29,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/vendor.js",
+                  "inApp": false,
+                  "lineNo": 73554,
+                  "module": "vendor",
+                  "filename": "/vendor.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "dispatchEvent",
+                  "errors": null,
+                  "colNo": 25,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/vendor.js",
+                  "inApp": false,
+                  "lineNo": 61709,
+                  "module": "vendor",
+                  "filename": "/vendor.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "debugHandleEvent",
+                  "errors": null,
+                  "colNo": 12,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/vendor.js",
+                  "inApp": false,
+                  "lineNo": 75876,
+                  "module": "vendor",
+                  "filename": "/vendor.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "callWithDebugContext",
+                  "errors": null,
+                  "colNo": 15,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/vendor.js",
+                  "inApp": false,
+                  "lineNo": 76251,
+                  "module": "vendor",
+                  "filename": "/vendor.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                },
+                {
+                  "function": "viewWrappedDebugError",
+                  "errors": null,
+                  "colNo": 15,
+                  "vars": null,
+                  "package": null,
+                  "absPath": "http://localhost:4200/vendor.js",
+                  "inApp": false,
+                  "lineNo": 61054,
+                  "module": "vendor",
+                  "filename": "/vendor.js",
+                  "platform": null,
+                  "instructionAddr": null,
+                  "context": [],
+                  "symbolAddr": null,
+                  "trust": null,
+                  "symbol": null,
+                  "rawFunction": null
+                }
+              ],
+              "framesOmitted": null,
+              "registers": null,
+              "hasSystemFrames": false
+            },
+            "module": null,
+            "rawStacktrace": null,
+            "mechanism": {
+              "data": { "function": "<anonymous>" },
+              "type": "instrument",
+              "handled": true
+            },
+            "threadId": null,
+            "value": "an error string",
+            "type": "Error"
+          }
+        ],
+        "excOmitted": null,
+        "hasSystemFrames": false
+      }
+    },
+    {
+      "type": "breadcrumbs",
+      "data": {
+        "values": [
+          {
+            "category": "console",
+            "level": "info",
+            "event_id": null,
+            "timestamp": "2020-01-12T20:39:48.462000Z",
+            "data": {
+              "logger": "console",
+              "extra": {
+                "arguments": [
+                  "Angular is running in the development mode. Call enableProdMode() to enable the production mode."
+                ]
+              }
+            },
+            "message": "Angular is running in the development mode. Call enableProdMode() to enable the production mode.",
+            "type": "default"
+          },
+          {
+            "category": "ui.click",
+            "level": "info",
+            "event_id": null,
+            "timestamp": "2020-01-12T20:39:48.500000Z",
+            "data": null,
+            "message": "body > app-root > ol > li > a",
+            "type": "default"
+          },
+          {
+            "category": "xhr",
+            "level": "info",
+            "event_id": null,
+            "timestamp": "2020-01-12T20:39:48.599000Z",
+            "data": {
+              "url": "http://localhost:4200/sockjs-node/info?t=1578861588501",
+              "status_code": 200,
+              "method": "GET"
+            },
+            "message": null,
+            "type": "http"
+          },
+          {
+            "category": "sentry",
+            "level": "error",
+            "event_id": "8c9e7b937842455cbba902e1b4df9243",
+            "timestamp": "2020-01-12T20:39:48.622000Z",
+            "data": null,
+            "message": "Error: Something broke",
+            "type": "default"
+          },
+          {
+            "category": "ui.click",
+            "level": "info",
+            "event_id": null,
+            "timestamp": "2020-01-12T20:39:50.258000Z",
+            "data": null,
+            "message": "body > app-root > ol > li > a",
+            "type": "default"
+          }
+        ]
+      }
+    },
+    {
+      "type": "request",
+      "data": {
+        "fragment": null,
+        "cookies": [],
+        "inferredContentType": null,
+        "env": null,
+        "headers": [
+          [
+            "User-Agent",
+            "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0"
+          ]
+        ],
+        "url": "http://localhost:4200/",
+        "query": [],
+        "data": null,
+        "method": null
+      }
+    }
+  ],
+  "packages": {},
+  "sdk": { "version": "5.11.0", "name": "sentry.javascript.browser" },
+  "_meta": {
+    "user": null,
+    "context": null,
+    "entries": {},
+    "contexts": null,
+    "message": null,
+    "packages": null,
+    "tags": {},
+    "sdk": null
+  },
+  "contexts": {
+    "os": { "type": "os", "name": "Ubuntu" },
+    "browser": { "version": "72.0", "type": "browser", "name": "Firefox" }
+  },
+  "fingerprints": ["ae4aaeca74bb746cd3d0aba542c1ef5c"],
+  "context": { "arguments": [] },
+  "release": null,
+  "groupID": "1438437922"
+}

+ 163 - 0
event_store/test_data/js_throw_error_issue.json

@@ -0,0 +1,163 @@
+{
+  "seenBy": [
+    {
+      "username": "it@burkesoftware.com",
+      "lastLogin": "2020-01-10T19:15:16.601343Z",
+      "isSuperuser": false,
+      "emails": [
+        {
+          "is_verified": true,
+          "id": "414369",
+          "email": "david@burkesoftware.com"
+        },
+        { "is_verified": true, "id": "57124", "email": "it@burkesoftware.com" },
+        { "is_verified": true, "id": "257183", "email": "dburke@thelabnyc.com" }
+      ],
+      "isManaged": false,
+      "lastActive": "2020-01-12T21:34:33.486956Z",
+      "isStaff": false,
+      "identities": [],
+      "id": "20265",
+      "isActive": true,
+      "has2fa": false,
+      "name": "David",
+      "avatarUrl": "https://secure.gravatar.com/avatar/caa812c5ce135feee6fbd76ac67771c4?s=32&d=mm",
+      "dateJoined": "2014-02-28T17:27:30.585866Z",
+      "options": {
+        "timezone": "US/Eastern",
+        "stacktraceOrder": -1,
+        "language": "en",
+        "clock24Hours": false
+      },
+      "flags": { "newsletter_consent_prompt": false },
+      "avatar": { "avatarUuid": null, "avatarType": "gravatar" },
+      "hasPasswordAuth": true,
+      "email": "it@burkesoftware.com",
+      "lastSeen": "2020-01-12T21:04:53.907039Z"
+    }
+  ],
+  "platform": "javascript",
+  "pluginIssues": [],
+  "lastSeen": "2020-01-12T20:39:50.513063Z",
+  "userReportCount": 0,
+  "numComments": 0,
+  "userCount": 1,
+  "stats": {
+    "30d": [
+      [1576195200, 0],
+      [1576281600, 0],
+      [1576368000, 0],
+      [1576454400, 0],
+      [1576540800, 0],
+      [1576627200, 0],
+      [1576713600, 0],
+      [1576800000, 0],
+      [1576886400, 0],
+      [1576972800, 0],
+      [1577059200, 0],
+      [1577145600, 0],
+      [1577232000, 0],
+      [1577318400, 0],
+      [1577404800, 0],
+      [1577491200, 0],
+      [1577577600, 0],
+      [1577664000, 0],
+      [1577750400, 0],
+      [1577836800, 0],
+      [1577923200, 0],
+      [1578009600, 0],
+      [1578096000, 0],
+      [1578182400, 0],
+      [1578268800, 0],
+      [1578355200, 0],
+      [1578441600, 0],
+      [1578528000, 0],
+      [1578614400, 0],
+      [1578700800, 0],
+      [1578787200, 4]
+    ],
+    "24h": [
+      [1578776400, 0],
+      [1578780000, 0],
+      [1578783600, 0],
+      [1578787200, 0],
+      [1578790800, 0],
+      [1578794400, 0],
+      [1578798000, 0],
+      [1578801600, 0],
+      [1578805200, 0],
+      [1578808800, 0],
+      [1578812400, 0],
+      [1578816000, 0],
+      [1578819600, 0],
+      [1578823200, 0],
+      [1578826800, 0],
+      [1578830400, 0],
+      [1578834000, 0],
+      [1578837600, 0],
+      [1578841200, 0],
+      [1578844800, 0],
+      [1578848400, 0],
+      [1578852000, 0],
+      [1578855600, 0],
+      [1578859200, 4],
+      [1578862800, 0]
+    ]
+  },
+  "culprit": "viewWrappedDebugError(vendor)",
+  "title": "Error: an error string",
+  "id": "1438437922",
+  "assignedTo": null,
+  "participants": [],
+  "logger": null,
+  "type": "error",
+  "annotations": [],
+  "metadata": {
+    "function": "viewWrappedDebugError",
+    "type": "Error",
+    "value": "an error string",
+    "filename": "/vendor.js"
+  },
+  "status": "unresolved",
+  "pluginActions": [],
+  "tags": [
+    { "totalValues": 4, "name": "Browser", "key": "browser" },
+    { "totalValues": 4, "name": "Browser.Name", "key": "browser.name" },
+    { "totalValues": 4, "name": "Handled", "key": "handled" },
+    { "totalValues": 4, "name": "Level", "key": "level" },
+    { "totalValues": 4, "name": "Mechanism", "key": "mechanism" },
+    { "totalValues": 4, "name": "Os.Name", "key": "os.name" },
+    { "totalValues": 4, "name": "URL", "key": "url" },
+    { "totalValues": 4, "name": "User", "key": "user" }
+  ],
+  "subscriptionDetails": null,
+  "isPublic": false,
+  "hasSeen": true,
+  "firstRelease": null,
+  "shortId": "TEST-3",
+  "shareId": null,
+  "firstSeen": "2020-01-12T20:39:50.290132Z",
+  "count": "4",
+  "permalink": "https://sentry.io/organizations/burke-software-and-consulting/issues/1438437922/",
+  "level": "error",
+  "isSubscribed": true,
+  "pluginContexts": [],
+  "isBookmarked": false,
+  "project": {
+    "platform": "",
+    "slug": "test",
+    "id": "1855811",
+    "name": "test"
+  },
+  "lastRelease": null,
+  "activity": [
+    {
+      "data": {},
+      "dateCreated": "2020-01-12T20:39:50.290132Z",
+      "type": "first_seen",
+      "id": "0",
+      "user": null
+    }
+  ],
+  "statusDetails": {}
+}

+ 22 - 1
event_store/views.py

@@ -1,9 +1,10 @@
 from django.core.exceptions import SuspiciousOperation
 from django.conf import settings
 from rest_framework import permissions, exceptions
+from rest_framework.negotiation import BaseContentNegotiation
 from rest_framework.response import Response
 from rest_framework.views import APIView
-from utils.auth import parse_auth_header
+from sentry.utils.auth import parse_auth_header
 from projects.models import Project
 from .serializers import (
     StoreDefaultSerializer,
@@ -12,8 +13,28 @@ from .serializers import (
 )
 
 
+class IgnoreClientContentNegotiation(BaseContentNegotiation):
+    """
+    @sentry/browser sends an interesting content-type of text/plain when it's actually sending json
+    We have to ignore it and assume it's actually JSON
+    """
+
+    def select_parser(self, request, parsers):
+        """
+        Select the first parser in the `.parser_classes` list.
+        """
+        return parsers[0]
+
+    def select_renderer(self, request, renderers, format_suffix):
+        """
+        Select the first renderer in the `.renderer_classes` list.
+        """
+        return (renderers[0], renderers[0].media_type)
+
+
 class EventStoreAPIView(APIView):
     permission_classes = [permissions.AllowAny]
+    content_negotiation_class = IgnoreClientContentNegotiation
     http_method_names = ["post"]
 
     def get_serializer_class(self, data=[]):

+ 1 - 1
glitchtip/settings.py

@@ -130,7 +130,7 @@ SECURE_BROWSER_XSS_FILTER = True
 SECURE_CONTENT_TYPE_NOSNIFF = True
 
 ENVIRONMENT = env.str("ENVIRONMENT", None)
-GLITCH_VERSION = env.str("PASSIT_VERSION", "dev")
+GLITCHTIP_VERSION = env.str("GLITCHTIP_VERSION", "dev")
 
 # Database
 # https://docs.djangoproject.com/en/dev/ref/settings/#databases

+ 2 - 2
issues/event_store/error.py

@@ -1,7 +1,7 @@
-from .base import BaseEvent
-from utils.safe import truncatechars, get_path, trim
+from sentry.utils.safe import truncatechars, get_path, trim
 from stacktraces.processing import get_crash_frame_from_event_data
 from stacktraces.functions import get_function_name_for_frame
+from .base import BaseEvent
 
 
 def get_crash_location(data):

+ 69 - 6
issues/tests/test_sentry_api_compat.py

@@ -1,16 +1,79 @@
+import json
+from typing import List, Dict
 from django.urls import reverse
 from rest_framework.test import APITestCase
 from model_bakery import baker
 from event_store.test_data.django_error_factory import template_error
+from event_store.test_data.js_error_factory import throw_error
 from issues.models import Event
 
 
 class SentryAPICompatTestCase(APITestCase):
+    def setUp(self):
+        self.user = baker.make("users.user")
+        self.client.force_login(self.user)
+        self.project = baker.make("projects.Project")
+        key = self.project.projectkey_set.first().public_key
+        self.event_store_url = (
+            reverse("event_store", args=[self.project.id]) + "?sentry_key=" + key.hex
+        )
+
+    def assertCompareData(self, data1: Dict, data2: Dict, fields: List[str]):
+        """ Compare data of two dict objects. Compare only provided fields list """
+        for field in fields:
+            self.assertEqual(
+                data1[field], data2[field], f"Failed for field '{field}'",
+            )
+
+    def get_json_data(self, path: str):
+        with open(path) as json_file:
+            return json.load(json_file)
+
     def test_template_error(self):
-        project = baker.make("projects.Project")
-        key = project.projectkey_set.first().public_key
-        url = reverse("event_store", args=[project.id]) + "?sentry_key=" + key.hex
-        res = self.client.post(url, template_error, format="json")
-        event = Event.objects.get(event_id=res.data["id"])
-        self.assertEqual(event.culprit, "/template-error/")
+        res = self.client.post(self.event_store_url, template_error, format="json")
+        self.assertEqual(res.status_code, 200)
+
+        event_id = res.data["id"]
+        url = f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/events/{event_id}/"
+        res = self.client.get(url)
+        self.assertEqual(res.status_code, 200)
+        issue = Event.objects.get(event_id=event_id).issue
+
+        data = self.get_json_data(
+            "event_store/test_data/django_template_error_event.json"
+        )
+        self.assertCompareData(res.data, data, ["culprit", "title"])
+
+        url = reverse("issue-detail", kwargs={"pk": issue.pk})
+        res = self.client.get(url)
+        self.assertEqual(res.status_code, 200)
+
+        data = self.get_json_data(
+            "event_store/test_data/django_template_error_issue.json"
+        )
+        self.assertCompareData(res.data, data, ["title",])
+
+    def test_throw_error(self):
+        res = self.client.post(self.event_store_url, throw_error, format="json")
+        self.assertEqual(res.status_code, 200)
+
+        event_id = res.data["id"]
+        url = f"/api/0/projects/{self.project.organization.slug}/{self.project.slug}/events/{event_id}/"
+        res = self.client.get(url)
+        self.assertEqual(res.status_code, 200)
+        issue = Event.objects.get(event_id=event_id).issue
+
+        data = self.get_json_data("event_store/test_data/js_throw_error_event.json")
+        self.assertCompareData(res.data, data, ["title"])
+        self.assertEqual(
+            res.data["culprit"],
+            "viewWrappedDebugError(http://localhost:4200/vendor.js)",
+            "Not perfect match, should really be viewWrappedDebugError(vendor)",
+        )
+
+        url = reverse("issue-detail", kwargs={"pk": issue.pk})
+        res = self.client.get(url)
+        self.assertEqual(res.status_code, 200)
 
+        data = self.get_json_data("event_store/test_data/js_throw_error_issue.json")
+        self.assertCompareData(res.data, data, ["title"])

Some files were not shown because too many files changed in this diff