Skip to content

ref(derived_code_mappings): Pass frame filename to find_roots #85931

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 26, 2025
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
4 changes: 2 additions & 2 deletions src/sentry/api/endpoints/project_repo_path_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sentry.integrations.base import IntegrationFeatures
from sentry.integrations.manager import default_manager as integrations
from sentry.integrations.services.integration import RpcIntegration, integration_service
from sentry.issues.auto_source_code_config.code_mapping import find_roots
from sentry.issues.auto_source_code_config.code_mapping import FrameFilename, find_roots
from sentry.models.repository import Repository


Expand Down Expand Up @@ -119,7 +119,7 @@ def post(self, request: Request, project) -> Response:

branch = installation.extract_branch_from_source_url(repo, source_url)
source_path = installation.extract_source_path_from_source_url(repo, source_url)
stack_root, source_root = find_roots(stack_path, source_path)
stack_root, source_root = find_roots(FrameFilename({"filename": stack_path}), source_path)
Copy link
Member Author

Choose a reason for hiding this comment

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

A FrameFilename has more information than stack_path, thus, I can use it within find_roots.


return self.respond(
{
Expand Down
47 changes: 20 additions & 27 deletions src/sentry/issues/auto_source_code_config/code_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class UnsupportedFrameFilename(Exception):
pass


class NeedsExtension(Exception):
pass


def derive_code_mappings(
organization: Organization,
frame: Mapping[str, Any],
Expand Down Expand Up @@ -86,7 +90,7 @@ def __init__(self, frame: Mapping[str, Any]) -> None:
self.full_path = frame_file_path
self.extension = get_extension(frame_file_path)
if not self.extension:
raise UnsupportedFrameFilename("It needs an extension.")
raise NeedsExtension("It needs an extension.")
Copy link
Member Author

Choose a reason for hiding this comment

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

This is an orthogonal change and it helps to differentiate from this code above:
image


# Remove drive letter if it exists
if is_windows_path and frame_file_path[1] == ":":
Expand Down Expand Up @@ -158,25 +162,19 @@ def list_file_matches(self, frame_filename: FrameFilename) -> list[dict[str, str
for file in matches:
stack_path = frame_filename.raw_path
source_path = file
extra = {"stack_path": stack_path, "source_path": source_path}

try:
stack_root, source_root = find_roots(stack_path, source_path)
stack_root, source_root = find_roots(frame_filename, source_path)
except UnexpectedPathException:
logger.info(
"Unexpected format for stack_path or source_path",
extra={"stack_path": stack_path, "source_path": source_path},
)
logger.warning("Unexpected format for stack_path or source_path", extra=extra)
Copy link
Member Author

Choose a reason for hiding this comment

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

Changing it to warning so I can inspect the errors in Sentry.

continue

extra.update({"stack_root": stack_root, "source_root": source_root})
if stack_path.replace(stack_root, source_root, 1).replace("\\", "/") != source_path:
logger.info(
logger.warning(
"Unexpected stack_path/source_path found. A code mapping was not generated.",
extra={
"stack_path": stack_path,
"source_path": source_path,
"stack_root": stack_root,
"source_root": source_root,
},
extra=extra,
)
else:
file_matches.append(
Expand All @@ -201,7 +199,7 @@ def _stacktrace_buckets(
# Any files without a top directory will be grouped together
buckets[frame_filename.root].append(frame_filename)
except UnsupportedFrameFilename:
logger.info("Frame's filepath not supported: %s", frame.get("filename"))
logger.warning("Frame's filepath not supported: %s", frame.get("filename"))
except Exception:
logger.exception("Unable to split stacktrace path into buckets")

Expand Down Expand Up @@ -270,24 +268,18 @@ def _generate_code_mapping_from_tree(
stack_path = frame_filename.raw_path
source_path = matched_files[0]

extra = {"stack_path": stack_path, "source_path": source_path}
try:
stack_root, source_root = find_roots(stack_path, source_path)
stack_root, source_root = find_roots(frame_filename, source_path)
except UnexpectedPathException:
logger.info(
"Unexpected format for stack_path or source_path",
extra={"stack_path": stack_path, "source_path": source_path},
)
logger.warning("Unexpected format for stack_path or source_path", extra=extra)
return []

extra.update({"stack_root": stack_root, "source_root": source_root})
if stack_path.replace(stack_root, source_root, 1).replace("\\", "/") != source_path:
logger.info(
logger.warning(
"Unexpected stack_path/source_path found. A code mapping was not generated.",
extra={
"stack_path": stack_path,
"source_path": source_path,
"stack_root": stack_root,
"source_root": source_root,
},
extra=extra,
)
return []

Expand Down Expand Up @@ -484,11 +476,12 @@ def get_straight_path_prefix_end_index(file_path: str) -> int:
return index


def find_roots(stack_path: str, source_path: str) -> tuple[str, str]:
def find_roots(frame_filename: FrameFilename, source_path: str) -> tuple[str, str]:
"""
Returns a tuple containing the stack_root, and the source_root.
If there is no overlap, raise an exception since this should not happen
"""
stack_path = frame_filename.raw_path
stack_root = ""
if stack_path[0] == "/" or stack_path[0] == "\\":
stack_root += stack_path[0]
Expand Down
77 changes: 47 additions & 30 deletions tests/sentry/issues/auto_source_code_config/test_code_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
CodeMapping,
CodeMappingTreesHelper,
FrameFilename,
NeedsExtension,
UnexpectedPathException,
UnsupportedFrameFilename,
convert_stacktrace_frame_path_to_source_path,
Expand All @@ -26,7 +27,7 @@
from sentry.testutils.silo import assume_test_silo_mode
from sentry.utils.event_frames import EventFrame

sentry_files = [
SENTRY_FILES = [
"bin/__init__.py",
"bin/example1.py",
"bin/example2.py",
Expand All @@ -37,8 +38,6 @@
"src/sentry/wsgi.py",
"src/sentry_plugins/slack/client.py",
]


UNSUPPORTED_FRAME_FILENAMES = [
"async https://s1.sentry-cdn.com/_static/dist/sentry/entrypoints/app.js",
"/gtm.js", # Top source; starts with backslash
Expand All @@ -47,20 +46,22 @@
"[native code]",
"O$t",
"async https://s1.sentry-cdn.com/_static/dist/sentry/entrypoints/app.js",
"/foo/bar/baz", # no extension
"README", # no extension
"README", # top level file
"ssl.py",
# XXX: The following will need to be supported
"initialization.dart",
"backburner.js",
]
NO_EXTENSION_FRAME_FILENAMES = [
"/foo/bar/baz", # no extension
]


class TestRepoFiles(TestCase):
"""These evaluate which files should be included as part of a repo."""

def test_filter_source_code_files(self) -> None:
source_code_files = filter_source_code_files(sentry_files)
source_code_files = filter_source_code_files(SENTRY_FILES)

assert source_code_files.index("bin/__init__.py") == 0
assert source_code_files.index("docs-ui/.eslintrc.js") == 3
Expand Down Expand Up @@ -109,19 +110,6 @@ def test_buckets_logic() -> None:


class TestFrameFilename:
def test_frame_filename_package_and_more_than_one_level(self) -> None:
pytest.skip("This test is outdated because of refactors have been made to code mappings")
# ff = FrameFilename("getsentry/billing/tax/manager.py")
# assert f"{ff.root}/{ff.dir_path}/{ff.file_name}" == "getsentry/billing/tax/manager.py"
# assert f"{ff.dir_path}/{ff.file_name}" == ff.file_and_dir_path

def test_frame_filename_package_and_no_levels(self) -> None:
pytest.skip("This test is outdated because of refactors have been made to code mappings")
# ff = FrameFilename("root/bar.py")
# assert f"{ff.root}/{ff.file_name}" == "root/bar.py"
# assert f"{ff.root}/{ff.file_and_dir_path}" == "root/bar.py"
# assert ff.dir_path == ""

def test_frame_filename_repr(self) -> None:
path = "getsentry/billing/tax/manager.py"
assert FrameFilename({"filename": path}).__repr__() == f"FrameFilename: {path}"
Expand All @@ -131,6 +119,11 @@ def test_raises_unsupported(self) -> None:
with pytest.raises(UnsupportedFrameFilename):
FrameFilename({"filename": filepath})

def test_raises_no_extension(self) -> None:
for filepath in NO_EXTENSION_FRAME_FILENAMES:
with pytest.raises(NeedsExtension):
FrameFilename({"filename": filepath})

@pytest.mark.parametrize(
"frame_filename, prefix",
[
Expand Down Expand Up @@ -167,7 +160,7 @@ def setUp(self) -> None:
self.bar_repo = RepoAndBranch("Test-Organization/bar", "main")
self.code_mapping_helper = CodeMappingTreesHelper(
{
self.foo_repo.name: RepoTree(self.foo_repo, files=sentry_files),
self.foo_repo.name: RepoTree(self.foo_repo, files=SENTRY_FILES),
self.bar_repo.name: RepoTree(self.bar_repo, files=["sentry/web/urls.py"]),
}
)
Expand Down Expand Up @@ -303,52 +296,76 @@ def test_list_file_matches_multiple(self) -> None:
assert matches == expected_matches

def test_find_roots_starts_with_period_slash(self) -> None:
stacktrace_root, source_path = find_roots("./app/", "static/app/")
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't know why I originally wrote the tests without a filename at the end of the path. It does not make sense to me.

From looking at the calls, it is obvious that two full paths are used rather than base dirs:

stack_path = frame_filename.raw_path
source_path = file
try:
stack_root, source_root = find_roots(stack_path, source_path)

stacktrace_root, source_path = find_roots(
FrameFilename({"filename": "./app/foo.tsx"}), "static/app/foo.tsx"
)
assert stacktrace_root == "./"
assert source_path == "static/"

def test_find_roots_starts_with_period_slash_no_containing_directory(self) -> None:
stacktrace_root, source_path = find_roots("./app/", "app/")
stacktrace_root, source_path = find_roots(
FrameFilename({"filename": "./app/foo.tsx"}), "app/foo.tsx"
)
assert stacktrace_root == "./"
assert source_path == ""

def test_find_roots_not_matching(self) -> None:
stacktrace_root, source_path = find_roots("sentry/", "src/sentry/")
stacktrace_root, source_path = find_roots(
FrameFilename({"filename": "sentry/foo.py"}), "src/sentry/foo.py"
)
assert stacktrace_root == "sentry/"
assert source_path == "src/sentry/"

def test_find_roots_equal(self) -> None:
stacktrace_root, source_path = find_roots("source/", "source/")
stacktrace_root, source_path = find_roots(
FrameFilename({"filename": "source/foo.py"}), "source/foo.py"
)
assert stacktrace_root == ""
assert source_path == ""

def test_find_roots_starts_with_period_slash_two_levels(self) -> None:
stacktrace_root, source_path = find_roots("./app/", "app/foo/app/")
stacktrace_root, source_path = find_roots(
FrameFilename({"filename": "./app/foo.tsx"}), "app/foo/app/foo.tsx"
)
assert stacktrace_root == "./"
assert source_path == "app/foo/"

def test_find_roots_starts_with_app(self) -> None:
stacktrace_root, source_path = find_roots("app:///utils/", "utils/")
stacktrace_root, source_path = find_roots(
FrameFilename({"filename": "app:///utils/foo.tsx"}), "utils/foo.tsx"
)
assert stacktrace_root == "app:///"
assert source_path == ""

def test_find_roots_starts_with_multiple_dot_dot_slash(self) -> None:
stacktrace_root, source_path = find_roots("../../../../../../packages/", "packages/")
stacktrace_root, source_path = find_roots(
FrameFilename({"filename": "../../../../../../packages/foo.tsx"}),
"packages/foo.tsx",
)
assert stacktrace_root == "../../../../../../"
assert source_path == ""

def test_find_roots_starts_with_app_dot_dot_slash(self) -> None:
stacktrace_root, source_path = find_roots("app:///../services/", "services/")
stacktrace_root, source_path = find_roots(
FrameFilename({"filename": "app:///../services/foo.tsx"}),
"services/foo.tsx",
)
assert stacktrace_root == "app:///../"
assert source_path == ""

def test_find_roots_bad_stack_path(self) -> None:
with pytest.raises(UnexpectedPathException):
find_roots("https://yrurlsinyourstackpath.com/", "sentry/something.py")
find_roots(
FrameFilename({"filename": "https://yrurlsinyourstackpath.com/"}),
"sentry/something.py",
)

def test_find_roots_bad_source_path(self) -> None:
with pytest.raises(UnexpectedPathException):
find_roots("sentry/random.py", "nothing/something.js")
find_roots(
FrameFilename({"filename": "sentry/random.py"}),
"nothing/something.js",
)


class TestConvertStacktraceFramePathToSourcePath(TestCase):
Expand Down
Loading