Minidumps: PoC for minidump 'endpoint'

See #82
This commit is contained in:
Klaas van Schelven
2025-11-04 10:47:04 +01:00
parent b09e6d02a1
commit e7aad45db2
4 changed files with 67 additions and 19 deletions

View File

@@ -1,9 +1,12 @@
from django.urls import path
from .views import IngestEventAPIView, IngestEnvelopeAPIView
from .views import IngestEventAPIView, IngestEnvelopeAPIView, MinidumpAPIView
urlpatterns = [
# project_pk has to be an int per Sentry Client expectations.
path("<int:project_pk>/store/", IngestEventAPIView.as_view()),
path("<int:project_pk>/envelope/", IngestEnvelopeAPIView.as_view()),
# is this "ingest"? it is at least in the sense that it matches the API schema and downstream auth etc.
path("<int:project_pk>/minidump/", MinidumpAPIView.as_view()),
]

View File

@@ -39,6 +39,7 @@ from alerts.tasks import send_new_issue_alert, send_regression_alert
from compat.timestamp import format_timestamp, parse_timestamp
from tags.models import digest_tags
from bsmain.utils import b108_makedirs
from sentry.minidump import merge_minidump_event
from .parsers import StreamingEnvelopeParser, ParseError
from .filestore import get_filename_for_event_id
@@ -680,6 +681,59 @@ class IngestEnvelopeAPIView(BaseIngestAPIView):
#
class MinidumpAPIView(BaseIngestAPIView):
# A Base "Ingest" APIView in the sense that it reuses some key building blocks (auth).
# I'm not 100% sure whether "philosophically" the minidump endpoint is also "ingesting"; we'll see.
@classmethod
def _ingest(cls, ingested_at, event_data, project, request):
# TSTTCPW: just ingest the invent as normally after we've done the minidump-parsing "immediately". We make
# ready for the expectations of process_event (DIGEST_IMMEDIATELY/event_output_stream) with an if-statement
event_output_stream = MaxDataWriter("MAX_EVENT_SIZE", io.BytesIO())
if get_settings().DIGEST_IMMEDIATELY:
# in this case the stream will be an BytesIO object, so we can actually call .get_value() on it.
event_output_stream.write(json.dumps(event_data).encode("utf-8"))
else:
# no need to actually touch event_output_stream for this case, we just need to write a file
filename = get_filename_for_event_id(event_data["event_id"])
b108_makedirs(os.path.dirname(filename))
with open(filename, 'w') as f:
json.dump(event_data, f)
cls.process_event(ingested_at, event_data["event_id"], event_output_stream, project, request)
def post(self, request, project_pk=None):
# not reusing the CORS stuff here; minidump-from-browser doesn't make sense.
ingested_at = datetime.now(timezone.utc)
project = self.get_project_for_request(project_pk, request)
try:
# in this flow, we don't get an event_id from the client, so we just generate one here.
event_id = uuid.uuid4().hex
minidump_bytes = request.FILES["upload_file_minidump"].read()
data = {
"event_id": event_id,
"platform": "native",
"extra": {},
"errors": [],
}
merge_minidump_event(data, minidump_bytes)
self._ingest(ingested_at, data, project, request)
return JsonResponse({"id": event_id})
except Exception as e:
raise
return JsonResponse({"detail": str(e)}, status=HTTP_400_BAD_REQUEST)
@user_passes_test(lambda u: u.is_superuser)
def download_envelope(request, envelope_id=None):
envelope = get_object_or_404(Envelope, pk=envelope_id)

View File

@@ -66,9 +66,9 @@
<div class="text-ellipsis overflow-hidden"> {# filename, function, lineno #}
{% if frame.in_app %}
<span class="font-bold">{{ frame.filename }}</span>{% if frame.function %} in <span class="font-bold">{{ frame.function }}</span>{% endif %}{% if frame.lineno %} line <span class="font-bold">{{ frame.lineno }}</span>{% endif %}.
<span class="font-bold">{{ frame.filename }}</span>{% if frame.function %} in <span class="font-bold">{{ frame.function }}</span>{% endif %}{% if frame.lineno %} line <span class="font-bold">{{ frame.lineno }}</span>{% endif %}{% if frame.instruction_addr %} {{ frame.instruction_addr }}{% endif %}.
{% else %}
<span class="italic">{{ frame.filename }}{% if frame.function %} in {{ frame.function }}{% endif %}{% if frame.lineno%} line {{ frame.lineno }}{% endif %}.</span>
<span class="italic">{{ frame.filename }}{% if frame.function %} in {{ frame.function }}{% endif %}{% if frame.lineno%} line {{ frame.lineno }}{% endif %}{% if frame.instruction_addr %} {{ frame.instruction_addr }}{% endif %}.</span>
{% endif %}
</div>

View File

@@ -6,24 +6,15 @@ import logging
from symbolic import ProcessState
LOG_LEVELS = {
logging.NOTSET: "sample",
logging.DEBUG: "debug",
logging.INFO: "info",
logging.WARNING: "warning",
logging.ERROR: "error",
logging.FATAL: "fatal",
}
def merge_minidump_event(data, minidump_bytes):
state = ProcessState.from_minidump_buffer(minidump_bytes)
LOG_LEVELS_MAP = {v: k for k, v in LOG_LEVELS.items()}
data['level'] = 'fatal' if state.crashed else 'info'
def merge_minidump_event(data, minidump_path):
state = ProcessState.from_minidump(minidump_path)
data['level'] = LOG_LEVELS_MAP['fatal'] if state.crashed else LOG_LEVELS_MAP['info']
data['message'] = 'Assertion Error: %s' % state.assertion if state.assertion \
exception_value = 'Assertion Error: %s' % state.assertion if state.assertion \
else 'Fatal Error: %s' % state.crash_reason
# NO_BANANA: data['message'] is not the right target
# data['message'] = exception_value
if state.timestamp:
data['timestamp'] = float(state.timestamp)
@@ -62,7 +53,7 @@ def merge_minidump_event(data, minidump_path):
# Extract the crash reason and infos
exception = {
'value': data['message'],
'value': exception_value,
'thread_id': crashed_thread['id'],
'type': state.crash_reason,
# Move stacktrace here from crashed_thread (mutating!)