diff --git a/src/sentry/integrations/utils/code_mapping.py b/src/sentry/integrations/utils/code_mapping.py index 4eabab1ab05d89..3740c8d5451722 100644 --- a/src/sentry/integrations/utils/code_mapping.py +++ b/src/sentry/integrations/utils/code_mapping.py @@ -61,7 +61,11 @@ class UnsupportedFrameFilename(Exception): class FrameFilename: def __init__(self, frame_file_path: str) -> None: self.raw_path = frame_file_path - if frame_file_path[0] == "/": + + if "\\" in frame_file_path: + frame_file_path = frame_file_path.replace("\\", "/") + + if frame_file_path[0] == "/" or frame_file_path[0] == "\\": frame_file_path = frame_file_path[1:] # Using regexes would be better but this is easier to understand @@ -69,7 +73,6 @@ def __init__(self, frame_file_path: str) -> None: not frame_file_path or frame_file_path[0] in ["[", "<"] or frame_file_path.find(" ") > -1 - or frame_file_path.find("\\") > -1 # Windows support or frame_file_path.find("/") == -1 ): raise UnsupportedFrameFilename("This path is not supported.") @@ -79,8 +82,16 @@ def __init__(self, frame_file_path: str) -> None: if not self.extension: raise UnsupportedFrameFilename("It needs an extension.") + # Remove drive letter if it exists + if self.is_windows_path and frame_file_path[1] == ":": + frame_file_path = frame_file_path[2:] + start_at_index = get_straight_path_prefix_end_index(frame_file_path) self.straight_path_prefix = frame_file_path[:start_at_index] + + # We normalize the path to be as close to what the path would + # look like in the source code repository, hence why we remove + # the straight path prefix and drive letter self.normalized_path = frame_file_path[start_at_index:] if start_at_index == 0: self.root = frame_file_path.split("/")[0] @@ -370,6 +381,10 @@ def convert_stacktrace_frame_path_to_source_path( If the code mapping does not apply to the frame, returns None. """ + stack_root = code_mapping.stack_root + if "\\" in code_mapping.stack_root: + stack_root = code_mapping.stack_root.replace("\\", "/") + # In most cases, code mappings get applied to frame.filename, but some platforms such as Java # contain folder info in other parts of the frame (e.g. frame.module="com.example.app.MainActivity" # gets transformed to "com/example/app/MainActivity.java"), so in those cases we use the @@ -379,13 +394,13 @@ def convert_stacktrace_frame_path_to_source_path( ) if stacktrace_path and stacktrace_path.startswith(code_mapping.stack_root): - return stacktrace_path.replace(code_mapping.stack_root, code_mapping.source_root, 1) + return stacktrace_path.replace(stack_root, code_mapping.source_root, 1) # Some platforms only provide the file's name without folder paths, so we # need to use the absolute path instead. If the code mapping has a non-empty # stack_root value and it matches the absolute path, we do the mapping on it. if frame.abs_path and frame.abs_path.startswith(code_mapping.stack_root): - return frame.abs_path.replace(code_mapping.stack_root, code_mapping.source_root, 1) + return frame.abs_path.replace(stack_root, code_mapping.source_root, 1) return None diff --git a/tests/sentry/tasks/test_derive_code_mappings.py b/tests/sentry/tasks/test_derive_code_mappings.py index 11f0067f6f2a55..f056e8a458fdfc 100644 --- a/tests/sentry/tasks/test_derive_code_mappings.py +++ b/tests/sentry/tasks/test_derive_code_mappings.py @@ -97,6 +97,80 @@ def test_raises_generic_errors(self, mock_logger): ) +class TestBackSlashDeriveCodeMappings(BaseDeriveCodeMappings): + def setUp(self): + super().setUp() + self.platform = "python" + self.event_data = self.generate_data( + [ + {"in_app": True, "filename": "\\sentry\\mouse.py"}, + {"in_app": True, "filename": "\\sentry\\dog\\cat\\parrot.py"}, + {"in_app": True, "filename": "C:\\sentry\\tasks.py"}, + {"in_app": True, "filename": "D:\\Users\\code\\sentry\\models\\release.py"}, + ] + ) + + @responses.activate + def test_backslash_filename_simple(self): + repo_name = "foo/bar" + with patch( + "sentry.integrations.github.client.GitHubClientMixin.get_trees_for_org" + ) as mock_get_trees_for_org: + mock_get_trees_for_org.return_value = { + repo_name: RepoTree(Repo(repo_name, "master"), ["sentry/mouse.py"]) + } + derive_code_mappings(self.project.id, self.event_data) + code_mapping = RepositoryProjectPathConfig.objects.all()[0] + assert code_mapping.stack_root == "\\" + assert code_mapping.source_root == "" + assert code_mapping.repository.name == repo_name + + @responses.activate + def test_backslash_drive_letter_filename_simple(self): + repo_name = "foo/bar" + with patch( + "sentry.integrations.github.client.GitHubClientMixin.get_trees_for_org" + ) as mock_get_trees_for_org: + mock_get_trees_for_org.return_value = { + repo_name: RepoTree(Repo(repo_name, "master"), ["sentry/tasks.py"]) + } + derive_code_mappings(self.project.id, self.event_data) + code_mapping = RepositoryProjectPathConfig.objects.all()[0] + assert code_mapping.stack_root == "C:\\" + assert code_mapping.source_root == "" + assert code_mapping.repository.name == repo_name + + @responses.activate + def test_backslash_drive_letter_filename_monorepo(self): + repo_name = "foo/bar" + with patch( + "sentry.integrations.github.client.GitHubClientMixin.get_trees_for_org" + ) as mock_get_trees_for_org: + mock_get_trees_for_org.return_value = { + repo_name: RepoTree(Repo(repo_name, "master"), ["src/sentry/tasks.py"]) + } + derive_code_mappings(self.project.id, self.event_data) + code_mapping = RepositoryProjectPathConfig.objects.all()[0] + assert code_mapping.stack_root == "C:\\sentry\\" + assert code_mapping.source_root == "src/" + assert code_mapping.repository.name == repo_name + + @responses.activate + def test_backslash_drive_letter_filename_abs_path(self): + repo_name = "foo/bar" + with patch( + "sentry.integrations.github.client.GitHubClientMixin.get_trees_for_org" + ) as mock_get_trees_for_org: + mock_get_trees_for_org.return_value = { + repo_name: RepoTree(Repo(repo_name, "master"), ["sentry/models/release.py"]) + } + derive_code_mappings(self.project.id, self.event_data) + code_mapping = RepositoryProjectPathConfig.objects.all()[0] + assert code_mapping.stack_root == "D:\\Users\\code\\" + assert code_mapping.source_root == "" + assert code_mapping.repository.name == repo_name + + class TestJavascriptDeriveCodeMappings(BaseDeriveCodeMappings): def setUp(self): super().setUp()