diff --git a/events/utils.py b/events/utils.py index f4b2888..622bf6a 100644 --- a/events/utils.py +++ b/events/utils.py @@ -1,3 +1,15 @@ +import json +import sourcemap +from issues.utils import get_values + +from files.models import FileMetadata + + +# Dijkstra, Sourcemaps and Python lists start at 0, but editors and our UI show lines starting at 1. +FROM_DISPLAY = -1 +TO_DISPLAY = 1 + + class IncompleteList(list): def __init__(self, lst, cnt): super().__init__(lst) @@ -68,3 +80,59 @@ def annotate_var_with_meta(var, meta_var): var[at(meta_k)] = annotate_var_with_meta(var[at(meta_k)], meta_v) return var + + +def apply_sourcemaps(event_data): + images = event_data.get("debug_meta", {}).get("images", []) + if not images: + return + + debug_id_for_filename = { + image["code_file"]: image["debug_id"] + for image in images + if "debug_id" in image and "code_file" in image and image["type"] == "sourcemap" + } + + metadata_obj_lookup = { + str(metadata_obj.debug_id): metadata_obj + for metadata_obj in FileMetadata.objects.filter( + debug_id__in=debug_id_for_filename.values(), file_type="source_map").select_related("file") + } + + filenames_with_metas = [ + (filename, metadata_obj_lookup[debug_id]) + for (filename, debug_id) in debug_id_for_filename.items() + if debug_id in metadata_obj_lookup # if not: sourcemap not uploaded + ] + + sourcemap_for_filename = { + filename: sourcemap.loads(meta.file.data) + for (filename, meta) in filenames_with_metas + } + + source_for_filename = {} + for filename, meta in filenames_with_metas: + sm_data = json.loads(meta.file.data) + if "sourcesContent" not in sm_data or len(sm_data["sourcesContent"]) != 1: + # our assumption is: 1 sourcemap, 1 source. The fact that both "sources" (a list of filenames) and + # "sourcesContent" are lists seems to indicate that this assumption does not generally hold. But it not + # holding does not play well with the id of debug_id, I think? + continue + + source_for_filename[filename] = sm_data["sourcesContent"][0].splitlines() + + for exception in get_values(event_data.get("exception", {})): + for frame in exception.get("stacktrace", {}).get("frames", []): + # NOTE: try/except in the loop would allow us to selectively skip frames that we fail to process + + if frame.get("filename") in sourcemap_for_filename and frame["filename"] in source_for_filename: + sm = sourcemap_for_filename[frame["filename"]] + lines = source_for_filename[frame["filename"]] + + token = sm.lookup(frame["lineno"] + FROM_DISPLAY, frame["colno"]) + + frame["pre_context"] = lines[max(0, token.src_line - 5):token.src_line] + frame["context_line"] = lines[token.src_line] + frame["post_context"] = lines[token.src_line + 1:token.src_line + 5] + frame["lineno"] = token.src_line + TO_DISPLAY + # frame["colno"] = token.src_col + TO_DISPLAY not actually used diff --git a/issues/views.py b/issues/views.py index 8c34aa0..de26f8f 100644 --- a/issues/views.py +++ b/issues/views.py @@ -31,7 +31,7 @@ from tags.search import search_issues, search_events, search_events_optimized from .models import Issue, IssueQuerysetStateManager, IssueStateManager, TurningPoint, TurningPointKind from .forms import CommentForm from .utils import get_values, get_main_exception -from events.utils import annotate_with_meta +from events.utils import annotate_with_meta, apply_sourcemaps MuteOption = namedtuple("MuteOption", ["for_or_until", "period_name", "nr_of_periods", "gte_threshold"]) @@ -401,6 +401,12 @@ def issue_event_stacktrace(request, issue, event_pk=None, digest_order=None, nav # swallow the error in that case. sentry_sdk.capture_exception(e) + try: + apply_sourcemaps(parsed_data) + except Exception as e: + # sourcemaps are still experimental; we don't want to fail on them, so we just log the error and move on. + sentry_sdk.capture_exception(e) + # NOTE: I considered making this a clickable button of some sort, but decided against it in the end. Getting the UI # right is quite hard (https://ux.stackexchange.com/questions/1318) but more generally I would assume that having # your whole screen turned upside down is not something you do willy-nilly. Better to just have good defaults and diff --git a/requirements.txt b/requirements.txt index 4381f86..35e3b4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ monofy==1.1.* user-agents==2.2.* fastjsonschema==2.21.* verbose_csrf_middleware==1.0.* +sourcemap==0.2.*