Skip to content
Closed
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
18 changes: 15 additions & 3 deletions ddtrace/internal/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ def _root_module(path: Path) -> str:
# Try the most likely prefixes first
for parent_path in (purelib_path, platlib_path):
try:
return _effective_root(path.relative_to(parent_path), parent_path)
# Resolve the path to use the shortest relative path.
return _effective_root(path.resolve().relative_to(parent_path), parent_path)
except ValueError:
# Not relative to this path
pass
Expand Down Expand Up @@ -223,7 +224,18 @@ def filename_to_package(filename: t.Union[str, Path]) -> t.Optional[Distribution

try:
path = Path(filename) if isinstance(filename, str) else filename
return mapping.get(_root_module(path.resolve()))
# Avoid calling .resolve() on the path here to prevent breaking symlink matching in `_root_module`.
root_module_path = _root_module(path)
if root_module_path in mapping:
return mapping[root_module_path]

# Loop through mapping and check the distribution name, since the key isn't always the same, for example:
# '__editable__.ddtrace-3.9.0.dev...pth': Distribution(name='ddtrace', version='...')
for distribution in mapping.values():
if distribution.name == root_module_path:
return distribution

return None
except (ValueError, OSError):
return None

Expand Down Expand Up @@ -252,7 +264,7 @@ def is_stdlib(path: Path) -> bool:

@cached(maxsize=256)
def is_third_party(path: Path) -> bool:
package = filename_to_package(str(path))
package = filename_to_package(path)
if package is None:
return False

Expand Down
4 changes: 2 additions & 2 deletions tests/debugging/origin/test_span.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,5 @@ def entry_call():

# Check for the expected tags on the exit span
assert _exit.get_tag("_dd.code_origin.type") == "exit"
assert _exit.get_tag("_dd.code_origin.frames.2.file") == str(Path(__file__).resolve())
assert _exit.get_tag("_dd.code_origin.frames.2.line") == str(self.test_span_origin.__code__.co_firstlineno)
assert _exit.get_tag("_dd.code_origin.frames.0.file") == str(Path(__file__).resolve())
assert _exit.get_tag("_dd.code_origin.frames.0.line") == str(self.test_span_origin.__code__.co_firstlineno)
39 changes: 39 additions & 0 deletions tests/internal/test_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,42 @@ def test_third_party_packages_excludes_includes():

assert {"myfancypackage", "myotherfancypackage"} < _third_party_packages()
assert "requests" not in _third_party_packages()


def test_third_party_packages_symlinks(tmp_path):
"""
Test that a symlink doesn't break our logic of detecting user code.
"""
import os

from ddtrace.internal.packages import is_user_code

# Use pathlib for more pythonic directory creation
actual_path = tmp_path / "site-packages" / "ddtrace"
runfiles_path = tmp_path / "test.runfiles" / "site-packages" / "ddtrace"

# Create directories using pathlib (more pythonic)
actual_path.mkdir(parents=True)
runfiles_path.mkdir(parents=True)

# Assert that the runfiles path is considered user code when symlinked.
code_file = actual_path / "test.py"
code_file.write_bytes(b"#")

symlink_file = runfiles_path / "test.py"
os.symlink(code_file, symlink_file)

assert is_user_code(code_file)
# Symlinks with `.runfiles` in the path should not be considered user code.
from ddtrace.internal.compat import Path

p = Path(symlink_file)
p2 = Path(symlink_file).resolve()
print(symlink_file, p, p2)

assert not is_user_code(symlink_file)

code_file_2 = runfiles_path / "test2.py"
code_file_2.write_bytes(b"#")

assert not is_user_code(code_file_2)
Loading