Skip to content

Commit 002174c

Browse files
committed
Revert "Revert "feat(code-mappings): Add support for stacktrace frames with backslashes for automatic code mappings (#68845)""
This reverts commit f186916.
1 parent 58f267f commit 002174c

File tree

3 files changed

+106
-10
lines changed

3 files changed

+106
-10
lines changed

src/sentry/integrations/utils/code_mapping.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,19 @@ class UnsupportedFrameFilename(Exception):
6161
class FrameFilename:
6262
def __init__(self, frame_file_path: str) -> None:
6363
self.raw_path = frame_file_path
64-
if frame_file_path[0] == "/":
64+
is_windows_path = False
65+
if "\\" in frame_file_path:
66+
is_windows_path = True
67+
frame_file_path = frame_file_path.replace("\\", "/")
68+
69+
if frame_file_path[0] == "/" or frame_file_path[0] == "\\":
6570
frame_file_path = frame_file_path[1:]
6671

6772
# Using regexes would be better but this is easier to understand
6873
if (
6974
not frame_file_path
7075
or frame_file_path[0] in ["[", "<"]
7176
or frame_file_path.find(" ") > -1
72-
or frame_file_path.find("\\") > -1 # Windows support
7377
or frame_file_path.find("/") == -1
7478
):
7579
raise UnsupportedFrameFilename("This path is not supported.")
@@ -79,8 +83,20 @@ def __init__(self, frame_file_path: str) -> None:
7983
if not self.extension:
8084
raise UnsupportedFrameFilename("It needs an extension.")
8185

86+
# Remove drive letter if it exists
87+
if is_windows_path and frame_file_path[1] == ":":
88+
frame_file_path = frame_file_path[2:]
89+
# windows drive letters can be like C:\ or C:
90+
# so we need to remove the slash if it exists
91+
if frame_file_path[0] == "/":
92+
frame_file_path = frame_file_path[1:]
93+
8294
start_at_index = get_straight_path_prefix_end_index(frame_file_path)
8395
self.straight_path_prefix = frame_file_path[:start_at_index]
96+
97+
# We normalize the path to be as close to what the path would
98+
# look like in the source code repository, hence why we remove
99+
# the straight path prefix and drive letter
84100
self.normalized_path = frame_file_path[start_at_index:]
85101
if start_at_index == 0:
86102
self.root = frame_file_path.split("/")[0]
@@ -145,7 +161,7 @@ def list_file_matches(self, frame_filename: FrameFilename) -> list[dict[str, str
145161
)
146162
continue
147163

148-
if stack_path.replace(stack_root, source_root, 1) != source_path:
164+
if stack_path.replace(stack_root, source_root, 1).replace("\\", "/") != source_path:
149165
logger.info(
150166
"Unexpected stack_path/source_path found. A code mapping was not generated.",
151167
extra={
@@ -259,7 +275,7 @@ def _generate_code_mapping_from_tree(
259275
)
260276
return []
261277

262-
if stack_path.replace(stack_root, source_root, 1) != source_path:
278+
if stack_path.replace(stack_root, source_root, 1).replace("\\", "/") != source_path:
263279
logger.info(
264280
"Unexpected stack_path/source_path found. A code mapping was not generated.",
265281
extra={
@@ -370,6 +386,10 @@ def convert_stacktrace_frame_path_to_source_path(
370386
If the code mapping does not apply to the frame, returns None.
371387
"""
372388

389+
stack_root = code_mapping.stack_root
390+
if "\\" in code_mapping.stack_root:
391+
stack_root = code_mapping.stack_root.replace("\\", "/")
392+
373393
# In most cases, code mappings get applied to frame.filename, but some platforms such as Java
374394
# contain folder info in other parts of the frame (e.g. frame.module="com.example.app.MainActivity"
375395
# gets transformed to "com/example/app/MainActivity.java"), so in those cases we use the
@@ -379,13 +399,13 @@ def convert_stacktrace_frame_path_to_source_path(
379399
)
380400

381401
if stacktrace_path and stacktrace_path.startswith(code_mapping.stack_root):
382-
return stacktrace_path.replace(code_mapping.stack_root, code_mapping.source_root, 1)
402+
return stacktrace_path.replace(stack_root, code_mapping.source_root, 1)
383403

384404
# Some platforms only provide the file's name without folder paths, so we
385405
# need to use the absolute path instead. If the code mapping has a non-empty
386406
# stack_root value and it matches the absolute path, we do the mapping on it.
387407
if frame.abs_path and frame.abs_path.startswith(code_mapping.stack_root):
388-
return frame.abs_path.replace(code_mapping.stack_root, code_mapping.source_root, 1)
408+
return frame.abs_path.replace(stack_root, code_mapping.source_root, 1)
389409

390410
return None
391411

@@ -505,8 +525,8 @@ def find_roots(stack_path: str, source_path: str) -> tuple[str, str]:
505525
If there is no overlap, raise an exception since this should not happen
506526
"""
507527
stack_root = ""
508-
if stack_path[0] == "/":
509-
stack_root += "/"
528+
if stack_path[0] == "/" or stack_path[0] == "\\":
529+
stack_root += stack_path[0]
510530
stack_path = stack_path[1:]
511531

512532
if stack_path == source_path:
@@ -534,7 +554,7 @@ def find_roots(stack_path: str, source_path: str) -> tuple[str, str]:
534554
source_root = source_path.rpartition(overlap)[0]
535555
stack_root += stack_path_delim.join(stack_root_items)
536556

537-
if stack_root and stack_root[-1] != SLASH: # append trailing slash
557+
if stack_root and stack_root[-1] != stack_path_delim: # append trailing slash
538558
stack_root = f"{stack_root}{stack_path_delim}"
539559
if source_root and source_root[-1] != SLASH:
540560
source_root = f"{source_root}{SLASH}"

tests/sentry/integrations/utils/test_code_mapping.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
"README", # no extension
4949
"ssl.py",
5050
# XXX: The following will need to be supported
51-
"C:\\Users\\Donia\\AppData\\Roaming\\Adobe\\UXP\\Plugins\\External\\452f92d2_0.13.0\\main.js",
5251
"initialization.dart",
5352
"backburner.js",
5453
]

tests/sentry/tasks/test_derive_code_mappings.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,83 @@ def test_raises_generic_errors(self, mock_logger):
9797
)
9898

9999

100+
class TestBackSlashDeriveCodeMappings(BaseDeriveCodeMappings):
101+
def setUp(self):
102+
super().setUp()
103+
self.platform = "python"
104+
# The lack of a \ after the drive letter in the third frame signals that
105+
# this is a relative path. This may be unlikely to occur in practice,
106+
# but worth testing nonetheless.
107+
self.event_data = self.generate_data(
108+
[
109+
{"in_app": True, "filename": "\\sentry\\mouse.py"},
110+
{"in_app": True, "filename": "\\sentry\\dog\\cat\\parrot.py"},
111+
{"in_app": True, "filename": "C:sentry\\tasks.py"},
112+
{"in_app": True, "filename": "D:\\Users\\code\\sentry\\models\\release.py"},
113+
]
114+
)
115+
116+
@responses.activate
117+
def test_backslash_filename_simple(self):
118+
repo_name = "foo/bar"
119+
with patch(
120+
"sentry.integrations.github.client.GitHubClientMixin.get_trees_for_org"
121+
) as mock_get_trees_for_org:
122+
mock_get_trees_for_org.return_value = {
123+
repo_name: RepoTree(Repo(repo_name, "master"), ["sentry/mouse.py"])
124+
}
125+
derive_code_mappings(self.project.id, self.event_data)
126+
code_mapping = RepositoryProjectPathConfig.objects.all()[0]
127+
assert code_mapping.stack_root == "\\"
128+
assert code_mapping.source_root == ""
129+
assert code_mapping.repository.name == repo_name
130+
131+
@responses.activate
132+
def test_backslash_drive_letter_filename_simple(self):
133+
repo_name = "foo/bar"
134+
with patch(
135+
"sentry.integrations.github.client.GitHubClientMixin.get_trees_for_org"
136+
) as mock_get_trees_for_org:
137+
mock_get_trees_for_org.return_value = {
138+
repo_name: RepoTree(Repo(repo_name, "master"), ["sentry/tasks.py"])
139+
}
140+
derive_code_mappings(self.project.id, self.event_data)
141+
code_mapping = RepositoryProjectPathConfig.objects.all()[0]
142+
assert code_mapping.stack_root == "C:sentry\\"
143+
assert code_mapping.source_root == "sentry/"
144+
assert code_mapping.repository.name == repo_name
145+
146+
@responses.activate
147+
def test_backslash_drive_letter_filename_monorepo(self):
148+
repo_name = "foo/bar"
149+
with patch(
150+
"sentry.integrations.github.client.GitHubClientMixin.get_trees_for_org"
151+
) as mock_get_trees_for_org:
152+
mock_get_trees_for_org.return_value = {
153+
repo_name: RepoTree(Repo(repo_name, "master"), ["src/sentry/tasks.py"])
154+
}
155+
derive_code_mappings(self.project.id, self.event_data)
156+
code_mapping = RepositoryProjectPathConfig.objects.all()[0]
157+
assert code_mapping.stack_root == "C:sentry\\"
158+
assert code_mapping.source_root == "src/sentry/"
159+
assert code_mapping.repository.name == repo_name
160+
161+
@responses.activate
162+
def test_backslash_drive_letter_filename_abs_path(self):
163+
repo_name = "foo/bar"
164+
with patch(
165+
"sentry.integrations.github.client.GitHubClientMixin.get_trees_for_org"
166+
) as mock_get_trees_for_org:
167+
mock_get_trees_for_org.return_value = {
168+
repo_name: RepoTree(Repo(repo_name, "master"), ["sentry/models/release.py"])
169+
}
170+
derive_code_mappings(self.project.id, self.event_data)
171+
code_mapping = RepositoryProjectPathConfig.objects.all()[0]
172+
assert code_mapping.stack_root == "D:\\Users\\code\\"
173+
assert code_mapping.source_root == ""
174+
assert code_mapping.repository.name == repo_name
175+
176+
100177
class TestJavascriptDeriveCodeMappings(BaseDeriveCodeMappings):
101178
def setUp(self):
102179
super().setUp()

0 commit comments

Comments
 (0)