mirror of
https://github.com/jlengrand/bugsink.git
synced 2026-03-09 23:51:20 +00:00
Expose event stacktrace as markdown
Adds a `stacktrace_md` field to EventSerializer and a `/stacktrace` action returning the same markdown (but as the full response). Also switches `data` to use `get_parsed_data()` (as it should have) and in json dict format (rather than str).
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes, OpenApiResponse
|
||||
|
||||
|
||||
from bugsink.utils import assert_
|
||||
from bugsink.api_pagination import AscDescCursorPagination
|
||||
@@ -9,6 +12,8 @@ from bugsink.api_mixins import AtomicRequestMixin
|
||||
|
||||
from .models import Event
|
||||
from .serializers import EventListSerializer, EventDetailSerializer
|
||||
from .markdown_stacktrace import render_stacktrace_md
|
||||
from .renderers import MarkdownRenderer
|
||||
|
||||
|
||||
class EventPagination(AscDescCursorPagination):
|
||||
@@ -86,3 +91,18 @@ class EventViewSet(AtomicRequestMixin, viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
def get_serializer_class(self):
|
||||
return EventDetailSerializer if self.action == "retrieve" else EventListSerializer
|
||||
|
||||
@extend_schema(
|
||||
description="Render the event's stacktrace (frames, source, locals) as Markdown-like text.",
|
||||
responses={200: OpenApiResponse(response=str, description="Stacktrace as Markdown")},
|
||||
)
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
url_path="stacktrace",
|
||||
renderer_classes=[MarkdownRenderer],
|
||||
)
|
||||
def stacktrace(self, request, pk=None):
|
||||
event = self.get_object()
|
||||
text = render_stacktrace_md(event, frames="in_app", exceptions="last", include_locals=True)
|
||||
return Response(text)
|
||||
|
||||
10
events/renderers.py
Normal file
10
events/renderers.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from rest_framework.renderers import BaseRenderer
|
||||
|
||||
|
||||
class MarkdownRenderer(BaseRenderer):
|
||||
media_type = "text/markdown"
|
||||
format = "md"
|
||||
charset = "utf-8"
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
return data.encode("utf-8")
|
||||
@@ -1,5 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
|
||||
from .markdown_stacktrace import render_stacktrace_md
|
||||
from .models import Event
|
||||
|
||||
|
||||
@@ -26,9 +28,22 @@ class EventDetailSerializer(serializers.ModelSerializer):
|
||||
# NOTE as with Issue.grouping_keys: check viewset for prefetching
|
||||
# grouping_key = serializers.CharField(source="grouping.grouping_key", read_only=True)
|
||||
|
||||
data = serializers.SerializerMethodField()
|
||||
stacktrace_md = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = EventListSerializer.Meta.fields + [
|
||||
"data",
|
||||
"stacktrace_md",
|
||||
# "grouping_key" # TODO (likely) once we have the "expand" idea implemented
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField)
|
||||
def get_data(self, obj):
|
||||
# we override `data` to return the parsed version (which may come from the file store rather than the DB)
|
||||
return obj.get_parsed_data()
|
||||
|
||||
@extend_schema_field(serializers.CharField)
|
||||
def get_stacktrace_md(self, obj):
|
||||
return render_stacktrace_md(obj, frames="in_app", exceptions="last", include_locals=True)
|
||||
|
||||
@@ -35,6 +35,30 @@ class EventApiTests(TransactionTestCase):
|
||||
detail = response.json()
|
||||
self.assertEqual(detail["id"], str(self.event.id))
|
||||
self.assertIn("data", detail)
|
||||
self.assertTrue("event_id" in detail["data"])
|
||||
|
||||
def test_detail_includes_stacktrace_md_field(self):
|
||||
url = reverse("api:event-detail", args=[self.event.id])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
detail = response.json()
|
||||
|
||||
self.assertIn("stacktrace_md", detail)
|
||||
self.assertIsInstance(detail["stacktrace_md"], str)
|
||||
self.assertTrue(len(detail["stacktrace_md"]) > 0)
|
||||
|
||||
self.assertEqual("_No stacktrace available._", detail["stacktrace_md"])
|
||||
|
||||
def test_stacktrace_action_returns_markdown(self):
|
||||
url = reverse("api:event-stacktrace", args=[self.event.id])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertTrue(response["Content-Type"].startswith("text/markdown"))
|
||||
body = response.content.decode("utf-8")
|
||||
self.assertTrue(len(body) > 0)
|
||||
|
||||
self.assertEqual("_No stacktrace available._", body)
|
||||
|
||||
def test_list_by_issue_is_light_payload(self):
|
||||
response = self.client.get(reverse("api:event-list"), {"issue": str(self.issue.id)})
|
||||
|
||||
Reference in New Issue
Block a user