David Burke 1 year ago
parent
commit
480a404021

+ 3 - 11
.vscode/settings.json

@@ -1,13 +1,5 @@
 {
-  "python.formatting.provider": "black",
-  "python.linting.pylintArgs": [
-    "--enable=W0614",
-    "--django-settings-module=glitchtip.settings",
-    "--load-plugins",
-    "pylint_django"
-  ],
-  "python.linting.enabled": true,
-  "python.linting.pylintEnabled": true,
-  "editor.formatOnSave": true,
-  "files.trimTrailingWhitespace": true
+  "files.trimTrailingWhitespace": true,
+  "ruff.lint.run": "onSave",
+  "editor.formatOnSave": true
 }

+ 8 - 0
apps/event_ingest/process_event.py

@@ -24,6 +24,7 @@ class ProcessingEvent:
     event: InterchangeIssueEvent
     issue_hash: str
     title: str
+    metadata: dict[str, Any]
     event_data: dict[str, Any]
     issue_id: Optional[int] = None
     issue_created = False
@@ -83,6 +84,10 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]):
         issue_hash = generate_hash(title, culprit, event.type, event.fingerprint)
         event_data["culprit"] = culprit
         event_data["metadata"] = metadata
+        # if breadcrumbs := event.breadcrumbs:
+        #     event_data["breadcrumbs"] = [
+        #         breadcrumb.dict() for breadcrumb in breadcrumbs
+        #     ]
         if exception := event.exception:
             event_data["exception"] = exception.dict()
         processing_events.append(
@@ -90,6 +95,7 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]):
                 event=ingest_event,
                 issue_hash=issue_hash,
                 title=title,
+                metadata=metadata,
                 event_data=event_data,
             )
         )
@@ -106,6 +112,7 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]):
         issue_defaults = {
             "type": event_type,
             "title": processing_event.title,
+            "metadata": processing_event.metadata,
         }
         for hash_obj in hash_queryset:
             if (
@@ -136,6 +143,7 @@ def process_issue_events(ingest_events: list[InterchangeIssueEvent]):
         issue_events.append(
             IssueEvent(
                 id=processing_event.event.event_id,
+                date_created=processing_event.event.payload.timestamp,
                 date_received=processing_event.event.received_at,
                 issue_id=processing_event.issue_id,
                 type=event_type,

+ 1 - 1
apps/event_ingest/schema.py

@@ -137,7 +137,7 @@ class EventBreadcrumb(Schema):
     category: Optional[str] = None
     message: Optional[str] = None
     data: Optional[dict[str, Any]] = None
-    level: Optional[Level] = None
+    level: Annotated[Optional[Level], WrapValidator(invalid_to_none)] = None
     timestamp: Optional[datetime] = None
 
 

+ 59 - 14
apps/event_ingest/tests/test_process_issue_event.py

@@ -6,7 +6,11 @@ from apps.issue_events.constants import EventStatus
 from apps.issue_events.models import Issue, IssueEvent, IssueHash
 
 from ..process_event import process_issue_events
-from ..schema import ErrorIssueEventSchema, InterchangeIssueEvent, IssueEventSchema
+from ..schema import (
+    ErrorIssueEventSchema,
+    InterchangeIssueEvent,
+    IssueEventSchema,
+)
 from .utils import EventIngestTestCase
 
 COMPAT_TEST_DATA_DIR = "events/test_data"
@@ -74,6 +78,18 @@ class SentryCompatTestCase(IssueEventIngestTestCase):
         )
         return event, sentry_json, api_sentry_event
 
+    def get_event_json(self, event: IssueEvent):
+        return self.client.get(
+            reverse(
+                "api:get_event_json",
+                kwargs={
+                    "organization_slug": self.organization.slug,
+                    "issue_id": event.issue_id,
+                    "event_id": event.id,
+                },
+            )
+        ).json()
+
     # Upgrade functions handle intentional differences between GlitchTip and Sentry OSS
     def upgrade_title(self, value: str):
         """Sentry OSS uses ... while GlitchTip uses unicode …"""
@@ -90,6 +106,10 @@ class SentryCompatTestCase(IssueEventIngestTestCase):
         for field in fields:
             field_value1 = data1.get(field)
             field_value2 = data2.get(field)
+            if field == "datetime":
+                # Check that it's close enough
+                field_value1 = field_value1[:23]
+                field_value2 = field_value2[:23]
             if field == "title" and isinstance(field_value1, str):
                 field_value1 = self.upgrade_title(field_value1)
                 if field_value2:
@@ -110,7 +130,7 @@ class SentryCompatTestCase(IssueEventIngestTestCase):
 
     def get_project_events_detail(self, event_id: str):
         return reverse(
-            "api:project_issue_event_retrieve",
+            "api:get_project_issue_event",
             kwargs={
                 "organization_slug": self.project.organization.slug,
                 "project_slug": self.project.slug,
@@ -118,18 +138,20 @@ class SentryCompatTestCase(IssueEventIngestTestCase):
             },
         )
 
-    def test_template_error(self):
-        sdk_error, sentry_json, sentry_data = self.get_json_test_data(
-            "django_template_error"
-        )
+    def submit_event(self, event_data: dict) -> IssueEvent:
         event = InterchangeIssueEvent(
-            event_id=sdk_error["event_id"],
+            event_id=event_data["event_id"],
             project_id=self.project.id,
-            payload=ErrorIssueEventSchema(**sdk_error),
+            payload=ErrorIssueEventSchema(**event_data),
         )
         process_issue_events([event])
+        return IssueEvent.objects.get(pk=event.event_id)
 
-        event = IssueEvent.objects.get(pk=event.event_id)
+    def test_template_error(self):
+        sdk_error, sentry_json, sentry_data = self.get_json_test_data(
+            "django_template_error"
+        )
+        event = self.submit_event(sdk_error)
 
         url = self.get_project_events_detail(event.id.hex)
         res = self.client.get(url)
@@ -165,9 +187,32 @@ class SentryCompatTestCase(IssueEventIngestTestCase):
             ["env", "headers", "url", "method", "inferredContentType"],
         )
 
-        # url = reverse("issue-detail", kwargs={"pk": event.issue.pk})
-        # res = self.client.get(url)
-        # self.assertEqual(res.status_code, 200)
+        url = reverse("api:get_issue", kwargs={"issue_id": event.issue.pk})
+        res = self.client.get(url)
+        self.assertEqual(res.status_code, 200)
+        res_data = res.json()
+
+        data = self.get_json_data("events/test_data/django_template_error_issue.json")
+        self.assertCompareData(res_data, data, ["title", "metadata"])
 
-        # data = self.get_json_data("events/test_data/django_template_error_issue.json")
-        # self.assertCompareData(res.data, data, ["title", "metadata"])
+    def test_js_sdk_with_unix_timestamp(self):
+        sdk_error, sentry_json, sentry_data = self.get_json_test_data(
+            "js_event_with_unix_timestamp"
+        )
+        event = self.submit_event(sdk_error)
+        self.assertNotEqual(event.date_created, sdk_error["timestamp"])
+        self.assertEqual(event.date_created.year, 2020)
+
+        event_json = self.get_event_json(event)
+        self.assertCompareData(event_json, sentry_json, ["datetime"])
+        # self.assertCompareData(event_json, sentry_json, ["datetime", "breadcrumbs"])
+
+        # url = self.get_project_events_detail(event.pk)
+        # res = self.client.get(url)
+        # res_data = res.json()
+        # self.assertCompareData(res_data, sentry_data, ["datetime"])
+        # self.assertEqual(res_data["entries"][1].get("type"), "breadcrumbs")
+        # self.assertEqual(
+        #     res_data["entries"][1],
+        #     self.upgrade_data(sentry_data["entries"][1]),
+        # )

+ 0 - 1
apps/event_ingest/tests/test_store_api.py

@@ -1,4 +1,3 @@
-
 from django.test import override_settings
 from django.urls import reverse
 

+ 2 - 0
apps/issue_events/api/__init__.py

@@ -0,0 +1,2 @@
+from .api import router  # noqa
+from . import events, issues  # noqa

+ 3 - 0
apps/issue_events/api/api.py

@@ -0,0 +1,3 @@
+from ninja import Router
+
+router = Router()

+ 23 - 12
apps/issue_events/api.py → apps/issue_events/api/events.py

@@ -4,14 +4,12 @@ from typing import Optional
 from django.db.models import OuterRef, Subquery, Window
 from django.db.models.functions import Lag
 from django.http import Http404
-from ninja import Router
 
 from glitchtip.api.authentication import AuthHttpRequest
 
-from .models import IssueEvent
-from .schema import IssueEventDetailSchema, IssueEventSchema
-
-router = Router()
+from ..models import IssueEvent
+from ..schema import IssueEventDetailSchema, IssueEventSchema, IssueEventJsonSchema
+from . import router
 
 
 def get_queryset(
@@ -34,7 +32,7 @@ def get_queryset(
 @router.get(
     "/issues/{int:issue_id}/events/", response=list[IssueEventSchema], by_alias=True
 )
-async def issue_event_list(request: AuthHttpRequest, issue_id: int):
+async def list_issue_event(request: AuthHttpRequest, issue_id: int):
     return [obj async for obj in get_queryset(request, issue_id=issue_id)]
 
 
@@ -43,7 +41,7 @@ async def issue_event_list(request: AuthHttpRequest, issue_id: int):
     response=IssueEventDetailSchema,
     by_alias=True,
 )
-async def issue_event_latest(request: AuthHttpRequest, issue_id: int):
+async def get_latest_issue_event(request: AuthHttpRequest, issue_id: int):
     qs = get_queryset(request, issue_id)
     qs = qs.annotate(
         previous=Window(expression=Lag("id"), order_by="date_received"),
@@ -60,9 +58,7 @@ async def issue_event_latest(request: AuthHttpRequest, issue_id: int):
     response=IssueEventDetailSchema,
     by_alias=True,
 )
-async def issue_event_retrieve(
-    request: AuthHttpRequest, issue_id: int, event_id: uuid.UUID
-):
+async def get_issue_event(request: AuthHttpRequest, issue_id: int, event_id: uuid.UUID):
     qs = get_queryset(request, issue_id)
     qs = qs.annotate(
         previous=Subquery(
@@ -83,7 +79,7 @@ async def issue_event_retrieve(
     response=list[IssueEventSchema],
     by_alias=True,
 )
-async def project_issue_event_list(
+async def list_project_issue_event(
     request: AuthHttpRequest,
     organization_slug: str,
     project_slug: str,
@@ -101,7 +97,7 @@ async def project_issue_event_list(
     response=IssueEventDetailSchema,
     by_alias=True,
 )
-async def project_issue_event_retrieve(
+async def get_project_issue_event(
     request: AuthHttpRequest,
     organization_slug: str,
     project_slug: str,
@@ -122,3 +118,18 @@ async def project_issue_event_retrieve(
         return await qs.aget(id=event_id)
     except IssueEvent.DoesNotExist:
         raise Http404()
+
+
+@router.get(
+    "/organizations/{slug:organization_slug}/issues/{int:issue_id}/events/{event_id}/json/",
+    response=IssueEventJsonSchema,
+    by_alias=True,
+)
+async def get_event_json(
+    request: AuthHttpRequest, organization_slug: str, issue_id: int, event_id: uuid.UUID
+):
+    qs = get_queryset(request, organization_slug=organization_slug, issue_id=issue_id)
+    try:
+        return await qs.aget(id=event_id)
+    except IssueEvent.DoesNotExist:
+        raise Http404()

+ 25 - 0
apps/issue_events/api/issues.py

@@ -0,0 +1,25 @@
+from django.http import Http404
+
+from glitchtip.api.authentication import AuthHttpRequest
+
+from ..models import Issue
+from ..schema import IssueSchema
+from . import router
+
+
+def get_queryset(request: AuthHttpRequest):
+    user_id = request.auth
+    return Issue.objects.filter(project__organization__users=user_id)
+
+
+@router.get(
+    "/issues/{int:issue_id}/",
+    response=IssueSchema,
+    by_alias=True,
+)
+async def get_issue(request: AuthHttpRequest, issue_id: int):
+    qs = get_queryset(request)
+    try:
+        return await qs.filter(id=issue_id).aget()
+    except Issue.DoesNotExist:
+        raise Http404()

+ 176 - 46
apps/issue_events/migrations/0001_initial.py

@@ -10,89 +10,219 @@ import uuid
 
 
 class Migration(migrations.Migration):
-
     initial = True
 
     dependencies = [
-        ('projects', '0013_merge_20231017_1350'),
+        ("projects", "0013_merge_20231017_1350"),
         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
     ]
 
     operations = [
         migrations.CreateModel(
-            name='Issue',
+            name="Issue",
             fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('culprit', models.CharField(blank=True, max_length=1024, null=True)),
-                ('is_public', models.BooleanField(default=False)),
-                ('level', models.PositiveSmallIntegerField(choices=[(0, 'sample'), (1, 'debug'), (2, 'info'), (3, 'warning'), (4, 'error'), (5, 'fatal')], default=4)),
-                ('title', models.CharField(max_length=255)),
-                ('type', models.PositiveSmallIntegerField(choices=[(0, 'default'), (1, 'error'), (2, 'csp')], default=0)),
-                ('status', models.PositiveSmallIntegerField(choices=[(0, 'unresolved'), (1, 'resolved'), (2, 'ignored')], default=0)),
-                ('short_id', models.PositiveIntegerField(null=True)),
-                ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='projects.project')),
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created", models.DateTimeField(auto_now_add=True)),
+                ("culprit", models.CharField(blank=True, max_length=1024, null=True)),
+                ("is_public", models.BooleanField(default=False)),
+                (
+                    "level",
+                    models.PositiveSmallIntegerField(
+                        choices=[
+                            (0, "sample"),
+                            (1, "debug"),
+                            (2, "info"),
+                            (3, "warning"),
+                            (4, "error"),
+                            (5, "fatal"),
+                        ],
+                        default=4,
+                    ),
+                ),
+                ("metadata", models.JSONField()),
+                ("title", models.CharField(max_length=255)),
+                (
+                    "type",
+                    models.PositiveSmallIntegerField(
+                        choices=[(0, "default"), (1, "error"), (2, "csp")], default=0
+                    ),
+                ),
+                (
+                    "status",
+                    models.PositiveSmallIntegerField(
+                        choices=[(0, "unresolved"), (1, "resolved"), (2, "ignored")],
+                        default=0,
+                    ),
+                ),
+                ("short_id", models.PositiveIntegerField(null=True)),
+                (
+                    "project",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="issues",
+                        to="projects.project",
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='IssueHash',
+            name="IssueHash",
             fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('value', models.UUIDField(db_index=True)),
-                ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hashes', to='issue_events.issue')),
-                ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='projects.project')),
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("value", models.UUIDField(db_index=True)),
+                (
+                    "issue",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="hashes",
+                        to="issue_events.issue",
+                    ),
+                ),
+                (
+                    "project",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="projects.project",
+                    ),
+                ),
             ],
         ),
         migrations.CreateModel(
-            name='IssueEvent',
+            name="IssueEvent",
             fields=[
-                ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
-                ('type', models.PositiveSmallIntegerField(choices=[(0, 'default'), (1, 'error'), (2, 'csp')], default=0)),
-                ('date_created', models.DateTimeField(auto_now_add=True, help_text='Time at which event happened')),
-                ('date_received', models.DateTimeField(auto_now_add=True, help_text='Time at which GlitchTip accepted event')),
-                ('data', models.JSONField()),
-                ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='issue_events.issue')),
+                (
+                    "id",
+                    models.UUIDField(
+                        default=uuid.uuid4,
+                        editable=False,
+                        primary_key=True,
+                        serialize=False,
+                    ),
+                ),
+                (
+                    "type",
+                    models.PositiveSmallIntegerField(
+                        choices=[(0, "default"), (1, "error"), (2, "csp")], default=0
+                    ),
+                ),
+                (
+                    "date_created",
+                    models.DateTimeField(help_text="Time at which event happened"),
+                ),
+                (
+                    "date_received",
+                    models.DateTimeField(
+                        help_text="Time at which GlitchTip accepted event",
+                    ),
+                ),
+                ("data", models.JSONField()),
+                (
+                    "issue",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="issue_events.issue",
+                    ),
+                ),
             ],
             options={
-                'abstract': False,
-                'base_manager_name': 'objects',
+                "abstract": False,
+                "base_manager_name": "objects",
             },
             managers=[
-                ('objects', psqlextra.manager.manager.PostgresManager()),
+                ("objects", psqlextra.manager.manager.PostgresManager()),
             ],
         ),
         migrations.CreateModel(
-            name='Comment',
+            name="Comment",
             fields=[
-                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
-                ('created', models.DateTimeField(auto_now_add=True)),
-                ('text', models.TextField(blank=True, null=True)),
-                ('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='issue_events.issue')),
-                ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("created", models.DateTimeField(auto_now_add=True)),
+                ("text", models.TextField(blank=True, null=True)),
+                (
+                    "issue",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="comments",
+                        to="issue_events.issue",
+                    ),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        related_name="+",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
             ],
             options={
-                'ordering': ('-created',),
+                "ordering": ("-created",),
             },
         ),
         migrations.CreateModel(
-            name='IssueStats',
+            name="IssueStats",
             fields=[
-                ('issue', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='issue_events.issue')),
-                ('search_vector', django.contrib.postgres.search.SearchVectorField(editable=False, null=True)),
-                ('search_vector_created', models.DateTimeField(auto_now_add=True)),
-                ('count', models.PositiveIntegerField(default=1, editable=False)),
-                ('last_seen', models.DateTimeField(auto_now_add=True, db_index=True)),
+                (
+                    "issue",
+                    models.OneToOneField(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        primary_key=True,
+                        serialize=False,
+                        to="issue_events.issue",
+                    ),
+                ),
+                (
+                    "search_vector",
+                    django.contrib.postgres.search.SearchVectorField(
+                        editable=False, null=True
+                    ),
+                ),
+                ("search_vector_created", models.DateTimeField(auto_now_add=True)),
+                ("count", models.PositiveIntegerField(default=1, editable=False)),
+                ("last_seen", models.DateTimeField(auto_now_add=True, db_index=True)),
             ],
             options={
-                'indexes': [django.contrib.postgres.indexes.GinIndex(fields=['search_vector'], name='issue_event_search__0754e8_gin')],
+                "indexes": [
+                    django.contrib.postgres.indexes.GinIndex(
+                        fields=["search_vector"], name="issue_event_search__0754e8_gin"
+                    )
+                ],
             },
         ),
         migrations.AddConstraint(
-            model_name='issuehash',
-            constraint=models.UniqueConstraint(fields=('project', 'value'), name='issue hash project'),
+            model_name="issuehash",
+            constraint=models.UniqueConstraint(
+                fields=("project", "value"), name="issue hash project"
+            ),
         ),
         migrations.AlterUniqueTogether(
-            name='issue',
-            unique_together={('project', 'short_id')},
+            name="issue",
+            unique_together={("project", "short_id")},
         ),
     ]

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