diff --git a/src/sentry/lang/javascript/processor.py b/src/sentry/lang/javascript/processor.py index ce4f330dfe1b26..5d521a6081a0a0 100644 --- a/src/sentry/lang/javascript/processor.py +++ b/src/sentry/lang/javascript/processor.py @@ -7,6 +7,7 @@ import zlib from datetime import datetime from io import BytesIO +from itertools import groupby from os.path import splitext from typing import IO, Optional, Tuple from urllib.parse import urlsplit @@ -843,6 +844,36 @@ def get_function_for_token(frame, token, previous_frame=None): return frame_function_name +def fold_function_name(function_name): + """ + Fold multiple consecutive occurences of the same property name into a single group, excluding the last component. + + foo | foo + foo.foo | foo.foo + foo.foo.foo | {foo#2}.foo + bar.foo.foo | bar.foo.foo + bar.foo.foo.foo | bar.{foo#2}.foo + bar.foo.foo.onError | bar.{foo#2}.onError + bar.bar.bar.foo.foo.onError | {bar#3}.{foo#2}.onError + bar.foo.foo.bar.bar.onError | bar.{foo#2}.{bar#2}.onError + """ + + parts = function_name.split(".") + + if len(parts) == 1: + return function_name + + tail = parts.pop() + grouped = [list(g) for _, g in groupby(parts)] + + def format_groups(p): + if len(p) == 1: + return p[0] + return f"\u007b{p[0]}#{len(p)}\u007d" + + return f'{".".join(map(format_groups, grouped))}.{tail}' + + class JavaScriptStacktraceProcessor(StacktraceProcessor): """ Modern SourceMap processor using symbolic-sourcemapcache. @@ -1030,8 +1061,8 @@ def process_frame(self, processable_frame, processing_task): # The tokens are 1-indexed. new_frame["lineno"] = token.line new_frame["colno"] = token.col - new_frame["function"] = get_function_for_token( - new_frame, token, processable_frame.previous_frame + new_frame["function"] = fold_function_name( + get_function_for_token(new_frame, token, processable_frame.previous_frame) ) filename = token.src diff --git a/tests/sentry/lang/javascript/test_processor.py b/tests/sentry/lang/javascript/test_processor.py index 03224c2bc25f65..08611de646d90d 100644 --- a/tests/sentry/lang/javascript/test_processor.py +++ b/tests/sentry/lang/javascript/test_processor.py @@ -24,6 +24,7 @@ fetch_release_archive_for_url, fetch_release_file, fetch_sourcemap, + fold_function_name, generate_module, get_function_for_token, get_max_age, @@ -1143,6 +1144,18 @@ def test_fallback_to_original_name(self): assert get_function_for_token(frame, token, previous_frame) == "original" +class FoldFunctionNameTest(unittest.TestCase): + def test_dedupe_properties(self): + assert fold_function_name("foo") == "foo" + assert fold_function_name("foo.foo") == "foo.foo" + assert fold_function_name("foo.foo.foo") == "{foo#2}.foo" + assert fold_function_name("bar.foo.foo") == "bar.foo.foo" + assert fold_function_name("bar.foo.foo.foo") == "bar.{foo#2}.foo" + assert fold_function_name("bar.foo.foo.onError") == "bar.{foo#2}.onError" + assert fold_function_name("bar.bar.bar.foo.foo.onError") == "{bar#3}.{foo#2}.onError" + assert fold_function_name("bar.foo.foo.bar.bar.onError") == "bar.{foo#2}.{bar#2}.onError" + + class FetchSourcemapTest(TestCase): def test_simple_base64(self): smap_view = fetch_sourcemap(base64_sourcemap)