Skip to content

Commit 1b783e4

Browse files
armenzgchromy
authored andcommitted
feat(auto_source): Derive a code mapping per module (#101288)
The work to derive code mappings for Java fell short of supporting multi module directory structure. This adds support for multi module directory structure behind a feature flag.
1 parent 9e748d5 commit 1b783e4

File tree

8 files changed

+107
-11
lines changed

8 files changed

+107
-11
lines changed

src/sentry/issues/auto_source_code_config/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
DERIVED_ENHANCEMENTS_OPTION_KEY = "sentry:derived_grouping_enhancements"
1010
SUPPORTED_INTEGRATIONS = [IntegrationProviderSlug.GITHUB.value]
1111
STACK_ROOT_MAX_LEVEL = 4
12-
# Stacktrace roots that match one of these will have three levels of granularity
12+
# Stacktrace roots that match one of these will have an extra level of granularity
1313
# com.au, co.uk, org.uk, gov.uk, net.uk, edu.uk, ct.uk
1414
# This list does not have to be exhaustive as the fallback is two levels of granularity
1515
SECOND_LEVEL_TLDS = ("com", "co", "org", "gov", "net", "edu")

src/sentry/issues/auto_source_code_config/frame_info.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
import re
44
from abc import ABC, abstractmethod
5-
from collections.abc import Mapping
5+
from collections.abc import Mapping, Sequence
66
from typing import Any
77

8+
from sentry import options
89
from sentry.integrations.source_code_management.repo_trees import get_extension
910

1011
from .constants import SECOND_LEVEL_TLDS, STACK_ROOT_MAX_LEVEL
@@ -42,7 +43,7 @@ def __init__(self, frame: Mapping[str, Any]) -> None:
4243
self.process_frame(frame)
4344

4445
def __repr__(self) -> str:
45-
return f"FrameInfo: {self.raw_path}"
46+
return f"FrameInfo: {self.raw_path} stack_root: {self.stack_root}"
4647

4748
def __eq__(self, other: object) -> bool:
4849
if not isinstance(other, FrameInfo):
@@ -148,6 +149,14 @@ def get_path_from_module(module: str, abs_path: str) -> tuple[str, str]:
148149
# Gets rid of the class name
149150
parts = module.rsplit(".", 1)[0].split(".")
150151
dirpath = "/".join(parts)
152+
granularity = get_granularity(parts)
153+
154+
stack_root = "/".join(parts[:granularity]) + "/"
155+
file_path = f"{dirpath}/{abs_path}"
156+
return stack_root, file_path
157+
158+
159+
def get_granularity(parts: Sequence[str]) -> int:
151160
# a.Bar, Bar.kt -> stack_root: a/, file_path: a/Bar.kt
152161
granularity = 1
153162

@@ -163,6 +172,10 @@ def get_path_from_module(module: str, abs_path: str) -> tuple[str, str]:
163172
# file_path: uk/co/example/foo/bar/Baz.kt
164173
granularity = STACK_ROOT_MAX_LEVEL
165174

166-
stack_root = "/".join(parts[:granularity]) + "/"
167-
file_path = f"{dirpath}/{abs_path}"
168-
return stack_root, file_path
175+
elif options.get("auto_source_code_config.multi_module_java"):
176+
# com.example.multi.foo.bar.Baz$InnerClass, Baz.kt ->
177+
# stack_root: com/example/multi/foo/
178+
# file_path: com/example/multi/foo/bar/Baz.kt
179+
granularity = STACK_ROOT_MAX_LEVEL
180+
181+
return granularity

src/sentry/options/defaults.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,13 @@
903903
flags=FLAG_AUTOMATOR_MODIFIABLE,
904904
)
905905

906+
register(
907+
"auto_source_code_config.multi_module_java",
908+
type=Bool,
909+
default=False,
910+
flags=FLAG_AUTOMATOR_MODIFIABLE,
911+
)
912+
906913
register(
907914
"issues.severity.first-event-severity-calculation-projects-allowlist",
908915
type=Sequence,

src/sentry/utils/event_frames.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class SdkFrameMunger:
4545
def java_frame_munger(frame: EventFrame) -> str | None:
4646
stacktrace_path = None
4747
if not frame.module or not frame.abs_path:
48+
logger.warning("Module or absPath is missing", extra={"frame": frame})
4849
return None
4950

5051
from sentry.issues.auto_source_code_config.errors import (

tests/sentry/issues/auto_source_code_config/test_frame_info.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
class TestFrameInfo:
5050
def test_frame_filename_repr(self) -> None:
5151
path = "getsentry/billing/tax/manager.py"
52-
assert create_frame_info({"filename": path}).__repr__() == f"FrameInfo: {path}"
52+
frame_info = create_frame_info({"filename": path})
53+
expected = f"FrameInfo: {path} stack_root: {frame_info.stack_root}"
54+
assert frame_info.__repr__() == expected
5355

5456
@pytest.mark.parametrize("filepath", UNSUPPORTED_FRAME_FILENAMES)
5557
def test_raises_unsupported(self, filepath: str) -> None:
@@ -88,6 +90,8 @@ def test_java_raises_exception(
8890
with pytest.raises(expected_exception):
8991
create_frame_info(frame, "java")
9092

93+
# Only necessary while auto_source_code_config.multi_module_java is used
94+
@pytest.mark.django_db
9195
@pytest.mark.parametrize(
9296
"frame, expected_stack_root, expected_normalized_path",
9397
[

tests/sentry/issues/auto_source_code_config/test_process_event.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ def _process_and_assert_configuration_changes(
176176
expected_new_code_mappings
177177
)
178178
for expected_cm in expected_new_code_mappings:
179-
code_mapping = current_code_mappings.filter(
180-
stack_root=expected_cm["stack_root"],
181-
source_root=expected_cm["source_root"],
182-
).first()
179+
code_mapping = current_code_mappings.get(
180+
project_id=self.project.id, stack_root=expected_cm["stack_root"]
181+
)
183182
assert code_mapping is not None
183+
assert code_mapping.source_root == expected_cm["source_root"]
184184
assert code_mapping.repository.name == expected_cm["repo_name"]
185185
else:
186186
assert current_code_mappings.count() == starting_code_mappings_count
@@ -1003,3 +1003,64 @@ def test_categorized_frames_are_not_processed(self) -> None:
10031003
expected_new_code_mappings=[self.code_mapping("android/app/", "src/android/app/")],
10041004
expected_new_in_app_stack_trace_rules=["stack.module:android.app.** +app"],
10051005
)
1006+
1007+
def test_multi_module_with_old_granularity(self) -> None:
1008+
java_module_prefix = "com.example.multi"
1009+
module_prefix = java_module_prefix.replace(".", "/") + "/"
1010+
repo_trees = {
1011+
REPO1: [
1012+
f"modules/modX/{module_prefix}foo/Bar.kt",
1013+
f"modules/modY/{module_prefix}bar/Baz.kt",
1014+
]
1015+
}
1016+
frames = [
1017+
self.frame_from_module(f"{java_module_prefix}.foo.Bar", "Bar.kt"),
1018+
self.frame_from_module(f"{java_module_prefix}.bar.Baz", "Baz.kt"),
1019+
]
1020+
self._process_and_assert_configuration_changes(
1021+
repo_trees=repo_trees,
1022+
frames=frames,
1023+
platform=self.platform,
1024+
expected_new_code_mappings=[
1025+
# It's missing the extra granularity
1026+
# It's going to pick modY since it is the first frame processed, thus,
1027+
# the other frame will not have a working code mapping
1028+
self.code_mapping("com/example/multi/", "modules/modY/com/example/multi/")
1029+
],
1030+
expected_new_in_app_stack_trace_rules=["stack.module:com.example.** +app"],
1031+
)
1032+
1033+
def test_multi_module(self) -> None:
1034+
# Some Java projects have all modules under the same com/foo/bar directory
1035+
# however, some projects have different modules under different directories
1036+
# Case 1:
1037+
# com.example.multi.foo -> modules/com/example/multi/foo/Bar.kt
1038+
# com.example.multi.bar -> modules/com/example/multi/bar/Baz.kt
1039+
# Case 2:
1040+
# com.example.multi.foo -> modules/modX/com/example/multi/foo/Bar.kt (Notice modX infix)
1041+
# com.example.multi.bar -> modules/modY/com/example/multi/bar/Baz.kt (Notice modY infix)
1042+
java_module_prefix = "com.example.multi"
1043+
module_prefix = java_module_prefix.replace(".", "/") + "/"
1044+
repo_trees = {
1045+
REPO1: [
1046+
f"modules/modX/{module_prefix}foo/Bar.kt",
1047+
f"modules/modY/{module_prefix}bar/Baz.kt",
1048+
]
1049+
}
1050+
frames = [
1051+
self.frame_from_module(f"{java_module_prefix}.foo.Bar", "Bar.kt"),
1052+
self.frame_from_module(f"{java_module_prefix}.bar.Baz", "Baz.kt"),
1053+
]
1054+
with self.options({"auto_source_code_config.multi_module_java": True}):
1055+
self._process_and_assert_configuration_changes(
1056+
repo_trees=repo_trees,
1057+
frames=frames,
1058+
platform=self.platform,
1059+
expected_new_code_mappings=[
1060+
self.code_mapping(f"{module_prefix}foo/", f"modules/modX/{module_prefix}foo/"),
1061+
self.code_mapping(f"{module_prefix}bar/", f"modules/modY/{module_prefix}bar/"),
1062+
],
1063+
expected_new_in_app_stack_trace_rules=[
1064+
f"stack.module:{java_module_prefix}.** +app"
1065+
],
1066+
)

tests/sentry/issues/ownership/test_grammar.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ def test_matcher_test_threads() -> None:
230230
assert not Matcher("url", "*.py").test(data, munged_data)
231231

232232

233+
# Only necessary while auto_source_code_config.multi_module_java is used
234+
@pytest.mark.django_db
233235
def test_matcher_test_platform_java_threads() -> None:
234236
data = {
235237
"platform": "java",

tests/sentry/utils/test_event_frames.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import unittest
22

3+
import pytest
4+
35
from sentry.testutils.cases import TestCase
46
from sentry.utils.event_frames import (
57
EventFrame,
@@ -76,6 +78,8 @@ def test_platform_other(self) -> None:
7678
def test_platform_sdk_name_not_supported(self) -> None:
7779
assert not munged_filename_and_frames("javascript", [], "munged", "sdk.other")
7880

81+
# Only necessary while auto_source_code_config.multi_module_java is used
82+
@pytest.mark.django_db
7983
def test_supported_platform_sdk_name_not_required(self) -> None:
8084
frames = [
8185
{
@@ -88,6 +92,8 @@ def test_supported_platform_sdk_name_not_required(self) -> None:
8892

8993

9094
class JavaFilenameMungingTestCase(unittest.TestCase):
95+
# Only necessary while auto_source_code_config.multi_module_java is used
96+
@pytest.mark.django_db
9197
def test_platform_java(self) -> None:
9298
frames = [
9399
{
@@ -140,6 +146,8 @@ def test_platform_java_do_not_follow_java_package_naming_convention_does_not_rai
140146
munged = munged_filename_and_frames("java", [frame])
141147
assert munged is None
142148

149+
# Only necessary while auto_source_code_config.multi_module_java is used
150+
@pytest.mark.django_db
143151
def test_platform_android_kotlin(self) -> None:
144152
exception_frames = [
145153
{

0 commit comments

Comments
 (0)