Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/sentry/grouping/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
HashedChecksumVariant,
SaltedComponentVariant,
)
from sentry.issues.auto_source_code_config.constants import DERIVED_ENHANCEMENTS_OPTION_KEY
from sentry.models.grouphash import GroupHash

if TYPE_CHECKING:
Expand Down Expand Up @@ -87,6 +88,7 @@ def get_config_dict(self, project: Project) -> GroupingConfig:
}

def _get_enhancements(self, project: Project) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious - where/how is this enhancement string used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primarily, it is used as part of save_event to determine which frames should be considered for app or system hash calculations.

derived_enhancements = project.get_option(DERIVED_ENHANCEMENTS_OPTION_KEY)
project_enhancements = project.get_option("sentry:grouping_enhancements")

config_id = self._get_config_id(project)
Expand All @@ -100,15 +102,27 @@ def _get_enhancements(self, project: Project) -> str:
cache_prefix = self.cache_prefix
cache_prefix += f"{LATEST_VERSION}:"
cache_key = (
cache_prefix + md5_text(f"{enhancements_base}|{project_enhancements}").hexdigest()
cache_prefix
+ md5_text(
f"{enhancements_base}|{derived_enhancements}|{project_enhancements}"
).hexdigest()
)
enhancements = cache.get(cache_key)
if enhancements is not None:
return enhancements

try:
# Automatic enhancements are always applied first, so they can be overridden by
# project-specific enhancements.
enhancements_string = project_enhancements or ""
if derived_enhancements:
enhancements_string = (
f"{derived_enhancements}\n{enhancements_string}"
if enhancements_string
else derived_enhancements
)
enhancements = Enhancements.from_config_string(
project_enhancements, bases=[enhancements_base] if enhancements_base else []
enhancements_string, bases=[enhancements_base] if enhancements_base else []
).base64_string
except InvalidEnhancerConfig:
enhancements = get_default_enhancements()
Expand Down Expand Up @@ -441,7 +455,7 @@ def get_grouping_variants_for_event(


def get_contributing_variant_and_component(
variants: dict[str, BaseVariant]
variants: dict[str, BaseVariant],
) -> tuple[BaseVariant, ContributingComponent | None]:
if len(variants) == 1:
contributing_variant = list(variants.values())[0]
Expand Down
20 changes: 14 additions & 6 deletions src/sentry/issues/auto_source_code_config/code_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,8 @@ def find_roots(frame_filename: FrameInfo, source_path: str) -> tuple[str, str]:
elif source_path.endswith(stack_path): # "Packaged" logic
source_prefix = source_path.rpartition(stack_path)[0]
return (
f"{stack_root}{frame_filename.stack_root}/",
f"{source_prefix}{frame_filename.stack_root}/",
f"{stack_root}{frame_filename.stack_root}/".replace("//", "/"),
f"{source_prefix}{frame_filename.stack_root}/".replace("//", "/"),
)
elif stack_path.endswith(source_path):
stack_prefix = stack_path.rpartition(source_path)[0]
Expand Down Expand Up @@ -593,9 +593,17 @@ def get_path_from_module(module: str, abs_path: str) -> tuple[str, str]:
if "." not in module:
raise DoesNotFollowJavaPackageNamingConvention

# If module has a dot, take everything before the last dot
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes simplify this code block.

It also fixes a bug where we were not restricting the length of the stack root, thus, we would end up with com/example/ and many more levels deep (not intended behaviour).

There's a test down below that ensures we get the right number of levels.

Inputs:

  • repo_file="src/com/example/foo/bar/Baz.kt"
  • self.frame(module="com.example.foo.bar.Baz", abs_path="Baz.kt", in_app=False)

Expected:

  • stack_root="com/example/", source_root="src/com/example/"
  • in_app_rule="stack.module:com.example.** +app"

# com.example.foo.Bar$InnerClass -> com/example/foo
stack_root = module.rsplit(".", 1)[0].replace(".", "/")
file_path = f"{stack_root}/{abs_path}"
parts = module.split(".")

if len(parts) > 2:
# com.example.foo.bar.Baz$InnerClass, Baz.kt ->
# stack_root: com/example/
# file_path: com/example/foo/bar/Baz.kt
stack_root = "/".join(parts[:2])
file_path = "/".join(parts[:-1]) + "/" + abs_path
else:
# a.Bar, Bar.kt -> stack_root: a/, file_path: a/Bar.kt
stack_root = parts[0] + "/"
file_path = f"{stack_root}{abs_path}"

return stack_root, file_path
1 change: 1 addition & 0 deletions src/sentry/issues/auto_source_code_config/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any

METRIC_PREFIX = "auto_source_code_config"
DERIVED_ENHANCEMENTS_OPTION_KEY = "sentry:derived_grouping_enhancements"
SUPPORTED_INTEGRATIONS = ["github"]

# Any new languages should also require updating the stacktraceLink.tsx
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import logging
from collections.abc import Sequence

from sentry.issues.auto_source_code_config.code_mapping import CodeMapping
from sentry.issues.auto_source_code_config.utils import PlatformConfig
from sentry.models.project import Project
from sentry.utils import metrics

from .constants import DERIVED_ENHANCEMENTS_OPTION_KEY, METRIC_PREFIX

logger = logging.getLogger(__name__)


def save_in_app_stack_trace_rules(
project: Project, code_mappings: Sequence[CodeMapping], platform_config: PlatformConfig
) -> list[str]:
"""Save in app stack trace rules for a given project"""
rules_from_code_mappings = set()
for code_mapping in code_mappings:
try:
rules_from_code_mappings.add(generate_rule_for_code_mapping(code_mapping))
except ValueError:
pass

current_enhancements = project.get_option(DERIVED_ENHANCEMENTS_OPTION_KEY)
current_rules = set(current_enhancements.split("\n")) if current_enhancements else set()

united_rules = rules_from_code_mappings.union(current_rules)
if not platform_config.is_dry_run_platform() and united_rules != current_rules:
project.update_option(DERIVED_ENHANCEMENTS_OPTION_KEY, "\n".join(sorted(united_rules)))

new_rules_added = united_rules - current_rules
metrics.incr(
key=f"{METRIC_PREFIX}.in_app_stack_trace_rules.created",
amount=len(new_rules_added),
tags={
"platform": platform_config.platform,
"dry_run": platform_config.is_dry_run_platform(),
},
sample_rate=1.0,
)
return list(new_rules_added)


# XXX: This is very Java specific. If we want to support other languages, we need to
# come up with a better way to generate the rule.
def generate_rule_for_code_mapping(code_mapping: CodeMapping) -> str:
"""Generate an in-app rule for a given code mapping"""
stacktrace_root = code_mapping.stacktrace_root
if stacktrace_root == "":
raise ValueError("Stacktrace root is empty")

parts = stacktrace_root.rstrip("/").split("/", 2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to account for backslash paths too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for Java.
When we generate the code mappings, we replace periods (e.g. com.foo) with forward slashes (e.g. com/foo).

# We only want the first two parts
module = ".".join(parts[:2])

if module == "":
raise ValueError("Module is empty")

# a/ -> a.**
# x/y/ -> x.y.**
# com/example/foo/bar/ -> com.example.**
# uk/co/example/foo/bar/ -> uk.co.**
return f"stack.module:{module}.** +app"
6 changes: 4 additions & 2 deletions src/sentry/issues/auto_source_code_config/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from sentry.utils.locking import UnableToAcquireLock

from .constants import METRIC_PREFIX
from .in_app_stack_trace_rules import save_in_app_stack_trace_rules
from .integration_utils import (
InstallationCannotGetTreesError,
InstallationNotFoundError,
Expand Down Expand Up @@ -192,8 +193,9 @@ def create_configurations(

in_app_stack_trace_rules: list[str] = []
if platform_config.creates_in_app_stack_trace_rules():
# XXX: This will be changed on the next PR
pass
in_app_stack_trace_rules = save_in_app_stack_trace_rules(
project, code_mappings, platform_config
)

# We return this to allow tests running in dry-run mode to assert
# what would have been created.
Expand Down
1 change: 1 addition & 0 deletions src/sentry/models/options/project_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"sentry:grouping_enhancements_base",
"sentry:secondary_grouping_config",
"sentry:secondary_grouping_expiry",
"sentry:derived_grouping_enhancements",
"sentry:similarity_backfill_completed",
"sentry:fingerprinting_rules",
"sentry:relay_pii_config",
Expand Down
1 change: 1 addition & 0 deletions src/sentry/projectoptions/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
)

register(key="sentry:grouping_enhancements", default="")
register(key="sentry:derived_grouping_enhancements", default="")

# server side fingerprinting defaults.
register(key="sentry:fingerprinting_rules", default="")
Expand Down
Loading
Loading