diff --git a/ddtrace/internal/packages.py b/ddtrace/internal/packages.py index d5f7f5e4e4a..1c7205cc20e 100644 --- a/ddtrace/internal/packages.py +++ b/ddtrace/internal/packages.py @@ -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 @@ -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 @@ -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 diff --git a/tests/debugging/origin/test_span.py b/tests/debugging/origin/test_span.py index b443f784d2a..6b2847761ef 100644 --- a/tests/debugging/origin/test_span.py +++ b/tests/debugging/origin/test_span.py @@ -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) diff --git a/tests/internal/test_packages.py b/tests/internal/test_packages.py index 3e41ecddc3b..7cb2e062141 100644 --- a/tests/internal/test_packages.py +++ b/tests/internal/test_packages.py @@ -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)