From e7aad45db22d569b2202cbc70dd713ffb70104b2 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven Date: Tue, 4 Nov 2025 10:47:04 +0100 Subject: [PATCH] Minidumps: PoC for minidump 'endpoint' See #82 --- ingest/urls.py | 5 ++- ingest/views.py | 54 +++++++++++++++++++++++++ issues/templates/issues/stacktrace.html | 4 +- sentry/minidump.py | 23 ++++------- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/ingest/urls.py b/ingest/urls.py index e075f75..9d42b03 100644 --- a/ingest/urls.py +++ b/ingest/urls.py @@ -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("/store/", IngestEventAPIView.as_view()), path("/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("/minidump/", MinidumpAPIView.as_view()), ] diff --git a/ingest/views.py b/ingest/views.py index fdae6c5..b9d47c9 100644 --- a/ingest/views.py +++ b/ingest/views.py @@ -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) diff --git a/issues/templates/issues/stacktrace.html b/issues/templates/issues/stacktrace.html index 870735c..9eb0a62 100644 --- a/issues/templates/issues/stacktrace.html +++ b/issues/templates/issues/stacktrace.html @@ -66,9 +66,9 @@
{# filename, function, lineno #} {% if frame.in_app %} - {{ frame.filename }}{% if frame.function %} in {{ frame.function }}{% endif %}{% if frame.lineno %} line {{ frame.lineno }}{% endif %}. + {{ frame.filename }}{% if frame.function %} in {{ frame.function }}{% endif %}{% if frame.lineno %} line {{ frame.lineno }}{% endif %}{% if frame.instruction_addr %} {{ frame.instruction_addr }}{% endif %}. {% else %} - {{ frame.filename }}{% if frame.function %} in {{ frame.function }}{% endif %}{% if frame.lineno%} line {{ frame.lineno }}{% endif %}. + {{ frame.filename }}{% if frame.function %} in {{ frame.function }}{% endif %}{% if frame.lineno%} line {{ frame.lineno }}{% endif %}{% if frame.instruction_addr %} {{ frame.instruction_addr }}{% endif %}. {% endif %}
diff --git a/sentry/minidump.py b/sentry/minidump.py index 4f15379..8a126c3 100644 --- a/sentry/minidump.py +++ b/sentry/minidump.py @@ -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!)