Skip to content

Commit d3afa10

Browse files
feat(auto_source_config): Derive in-app stack trace rules (#87842)
This is the logic to mark frames as in-app when the files are found in the developers' GitHub repositories. This is very important for languages where the SDK or our internal rules cannot determine which frames are in-app and which are not. The initial language to get support for this is Java. This will run in dry run mode to discover errors or scaling problems. --------- Co-authored-by: Katie Byers <[email protected]>
1 parent 012aa85 commit d3afa10

File tree

8 files changed

+314
-24
lines changed

8 files changed

+314
-24
lines changed

src/sentry/grouping/api.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
HashedChecksumVariant,
3838
SaltedComponentVariant,
3939
)
40+
from sentry.issues.auto_source_code_config.constants import DERIVED_ENHANCEMENTS_OPTION_KEY
4041
from sentry.models.grouphash import GroupHash
4142

4243
if TYPE_CHECKING:
@@ -87,6 +88,7 @@ def get_config_dict(self, project: Project) -> GroupingConfig:
8788
}
8889

8990
def _get_enhancements(self, project: Project) -> str:
91+
derived_enhancements = project.get_option(DERIVED_ENHANCEMENTS_OPTION_KEY)
9092
project_enhancements = project.get_option("sentry:grouping_enhancements")
9193

9294
config_id = self._get_config_id(project)
@@ -100,15 +102,27 @@ def _get_enhancements(self, project: Project) -> str:
100102
cache_prefix = self.cache_prefix
101103
cache_prefix += f"{LATEST_VERSION}:"
102104
cache_key = (
103-
cache_prefix + md5_text(f"{enhancements_base}|{project_enhancements}").hexdigest()
105+
cache_prefix
106+
+ md5_text(
107+
f"{enhancements_base}|{derived_enhancements}|{project_enhancements}"
108+
).hexdigest()
104109
)
105110
enhancements = cache.get(cache_key)
106111
if enhancements is not None:
107112
return enhancements
108113

109114
try:
115+
# Automatic enhancements are always applied first, so they can be overridden by
116+
# project-specific enhancements.
117+
enhancements_string = project_enhancements or ""
118+
if derived_enhancements:
119+
enhancements_string = (
120+
f"{derived_enhancements}\n{enhancements_string}"
121+
if enhancements_string
122+
else derived_enhancements
123+
)
110124
enhancements = Enhancements.from_config_string(
111-
project_enhancements, bases=[enhancements_base] if enhancements_base else []
125+
enhancements_string, bases=[enhancements_base] if enhancements_base else []
112126
).base64_string
113127
except InvalidEnhancerConfig:
114128
enhancements = get_default_enhancements()
@@ -441,7 +455,7 @@ def get_grouping_variants_for_event(
441455

442456

443457
def get_contributing_variant_and_component(
444-
variants: dict[str, BaseVariant]
458+
variants: dict[str, BaseVariant],
445459
) -> tuple[BaseVariant, ContributingComponent | None]:
446460
if len(variants) == 1:
447461
contributing_variant = list(variants.values())[0]

src/sentry/issues/auto_source_code_config/code_mapping.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -540,8 +540,8 @@ def find_roots(frame_filename: FrameInfo, source_path: str) -> tuple[str, str]:
540540
elif source_path.endswith(stack_path): # "Packaged" logic
541541
source_prefix = source_path.rpartition(stack_path)[0]
542542
return (
543-
f"{stack_root}{frame_filename.stack_root}/",
544-
f"{source_prefix}{frame_filename.stack_root}/",
543+
f"{stack_root}{frame_filename.stack_root}/".replace("//", "/"),
544+
f"{source_prefix}{frame_filename.stack_root}/".replace("//", "/"),
545545
)
546546
elif stack_path.endswith(source_path):
547547
stack_prefix = stack_path.rpartition(source_path)[0]
@@ -593,9 +593,17 @@ def get_path_from_module(module: str, abs_path: str) -> tuple[str, str]:
593593
if "." not in module:
594594
raise DoesNotFollowJavaPackageNamingConvention
595595

596-
# If module has a dot, take everything before the last dot
597-
# com.example.foo.Bar$InnerClass -> com/example/foo
598-
stack_root = module.rsplit(".", 1)[0].replace(".", "/")
599-
file_path = f"{stack_root}/{abs_path}"
596+
parts = module.split(".")
597+
598+
if len(parts) > 2:
599+
# com.example.foo.bar.Baz$InnerClass, Baz.kt ->
600+
# stack_root: com/example/
601+
# file_path: com/example/foo/bar/Baz.kt
602+
stack_root = "/".join(parts[:2])
603+
file_path = "/".join(parts[:-1]) + "/" + abs_path
604+
else:
605+
# a.Bar, Bar.kt -> stack_root: a/, file_path: a/Bar.kt
606+
stack_root = parts[0] + "/"
607+
file_path = f"{stack_root}{abs_path}"
600608

601609
return stack_root, file_path

src/sentry/issues/auto_source_code_config/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any
55

66
METRIC_PREFIX = "auto_source_code_config"
7+
DERIVED_ENHANCEMENTS_OPTION_KEY = "sentry:derived_grouping_enhancements"
78
SUPPORTED_INTEGRATIONS = ["github"]
89

910
# Any new languages should also require updating the stacktraceLink.tsx
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import logging
2+
from collections.abc import Sequence
3+
4+
from sentry.issues.auto_source_code_config.code_mapping import CodeMapping
5+
from sentry.issues.auto_source_code_config.utils import PlatformConfig
6+
from sentry.models.project import Project
7+
from sentry.utils import metrics
8+
9+
from .constants import DERIVED_ENHANCEMENTS_OPTION_KEY, METRIC_PREFIX
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def save_in_app_stack_trace_rules(
15+
project: Project, code_mappings: Sequence[CodeMapping], platform_config: PlatformConfig
16+
) -> list[str]:
17+
"""Save in app stack trace rules for a given project"""
18+
rules_from_code_mappings = set()
19+
for code_mapping in code_mappings:
20+
try:
21+
rules_from_code_mappings.add(generate_rule_for_code_mapping(code_mapping))
22+
except ValueError:
23+
pass
24+
25+
current_enhancements = project.get_option(DERIVED_ENHANCEMENTS_OPTION_KEY)
26+
current_rules = set(current_enhancements.split("\n")) if current_enhancements else set()
27+
28+
united_rules = rules_from_code_mappings.union(current_rules)
29+
if not platform_config.is_dry_run_platform() and united_rules != current_rules:
30+
project.update_option(DERIVED_ENHANCEMENTS_OPTION_KEY, "\n".join(sorted(united_rules)))
31+
32+
new_rules_added = united_rules - current_rules
33+
metrics.incr(
34+
key=f"{METRIC_PREFIX}.in_app_stack_trace_rules.created",
35+
amount=len(new_rules_added),
36+
tags={
37+
"platform": platform_config.platform,
38+
"dry_run": platform_config.is_dry_run_platform(),
39+
},
40+
sample_rate=1.0,
41+
)
42+
return list(new_rules_added)
43+
44+
45+
# XXX: This is very Java specific. If we want to support other languages, we need to
46+
# come up with a better way to generate the rule.
47+
def generate_rule_for_code_mapping(code_mapping: CodeMapping) -> str:
48+
"""Generate an in-app rule for a given code mapping"""
49+
stacktrace_root = code_mapping.stacktrace_root
50+
if stacktrace_root == "":
51+
raise ValueError("Stacktrace root is empty")
52+
53+
parts = stacktrace_root.rstrip("/").split("/", 2)
54+
# We only want the first two parts
55+
module = ".".join(parts[:2])
56+
57+
if module == "":
58+
raise ValueError("Module is empty")
59+
60+
# a/ -> a.**
61+
# x/y/ -> x.y.**
62+
# com/example/foo/bar/ -> com.example.**
63+
# uk/co/example/foo/bar/ -> uk.co.**
64+
return f"stack.module:{module}.** +app"

src/sentry/issues/auto_source_code_config/task.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from sentry.utils.locking import UnableToAcquireLock
2626

2727
from .constants import METRIC_PREFIX
28+
from .in_app_stack_trace_rules import save_in_app_stack_trace_rules
2829
from .integration_utils import (
2930
InstallationCannotGetTreesError,
3031
InstallationNotFoundError,
@@ -192,8 +193,9 @@ def create_configurations(
192193

193194
in_app_stack_trace_rules: list[str] = []
194195
if platform_config.creates_in_app_stack_trace_rules():
195-
# XXX: This will be changed on the next PR
196-
pass
196+
in_app_stack_trace_rules = save_in_app_stack_trace_rules(
197+
project, code_mappings, platform_config
198+
)
197199

198200
# We return this to allow tests running in dry-run mode to assert
199201
# what would have been created.

src/sentry/models/options/project_option.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"sentry:grouping_enhancements_base",
5454
"sentry:secondary_grouping_config",
5555
"sentry:secondary_grouping_expiry",
56+
"sentry:derived_grouping_enhancements",
5657
"sentry:similarity_backfill_completed",
5758
"sentry:fingerprinting_rules",
5859
"sentry:relay_pii_config",

src/sentry/projectoptions/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
)
3131

3232
register(key="sentry:grouping_enhancements", default="")
33+
register(key="sentry:derived_grouping_enhancements", default="")
3334

3435
# server side fingerprinting defaults.
3536
register(key="sentry:fingerprinting_rules", default="")

0 commit comments

Comments
 (0)