From 93601bf1d92ffaa3ceb1a6e734ba2dfce4c4ccd5 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 7 May 2026 12:14:38 +0200 Subject: [PATCH 1/5] ref: add CodeLocationResolver --- src/CodeLocationResolver.php | 107 +++++++++++++++++++++++++++++ tests/CodeLocationResolverTest.php | 94 +++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 src/CodeLocationResolver.php create mode 100644 tests/CodeLocationResolverTest.php diff --git a/src/CodeLocationResolver.php b/src/CodeLocationResolver.php new file mode 100644 index 000000000..73ab47ead --- /dev/null +++ b/src/CodeLocationResolver.php @@ -0,0 +1,107 @@ +frameBuilder = new FrameBuilder($options, $representationSerializer); + } + + /** + * Resolves the first in-app frame from the current backtrace into code + * location metadata. + * + * @return array{'code.filepath': string, 'code.function': string|null, 'code.lineno': int}|null + */ + public function resolve(int $limit = 20): ?array + { + /** @var list $backtrace */ + $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, $limit); + + return $this->resolveFromBacktrace($backtrace); + } + + /** + * Resolves the first in-app frame from a backtrace into code location metadata. + * + * @param array> $backtrace The backtrace + * + * @phpstan-param list $backtrace + * + * @return array{'code.filepath': string, 'code.function': string|null, 'code.lineno': int}|null + */ + public function resolveFromBacktrace(array $backtrace): ?array + { + $frame = $this->findFirstInAppFrameForBacktrace($backtrace); + + if ($frame === null) { + return null; + } + + return $this->getCodeLocationForFrame($frame); + } + + /** + * Find the first in-app frame for a given backtrace. + * + * @param array> $backtrace The backtrace + * + * @phpstan-param list $backtrace + */ + public function findFirstInAppFrameForBacktrace(array $backtrace): ?Frame + { + $file = Frame::INTERNAL_FRAME_FILENAME; + $line = 0; + + foreach ($backtrace as $backtraceFrame) { + $frame = $this->frameBuilder->buildFromBacktraceFrame($file, $line, $backtraceFrame); + + if ($frame->isInApp()) { + return $frame; + } + + $file = $backtraceFrame['file'] ?? Frame::INTERNAL_FRAME_FILENAME; + $line = $backtraceFrame['line'] ?? 0; + } + + return null; + } + + /** + * Converts a frame into code location metadata. + * + * @return array{'code.filepath': string, 'code.function': string|null, 'code.lineno': int} + */ + public function getCodeLocationForFrame(Frame $frame): array + { + return [ + 'code.filepath' => $frame->getFile(), + 'code.function' => $frame->getFunctionName(), + 'code.lineno' => $frame->getLine(), + ]; + } +} diff --git a/tests/CodeLocationResolverTest.php b/tests/CodeLocationResolverTest.php new file mode 100644 index 000000000..41d316e4a --- /dev/null +++ b/tests/CodeLocationResolverTest.php @@ -0,0 +1,94 @@ +createResolver([ + 'prefixes' => [], + ]); + + $frame = $resolver->findFirstInAppFrameForBacktrace($this->createQueryBacktrace($expectedLine)); + + $this->assertNotNull($frame); + $this->assertSame(__FILE__, $frame->getFile()); + $this->assertSame($expectedLine, $frame->getLine()); + $this->assertSame('App\\Repository\\UserRepository::findActiveUsers', $frame->getFunctionName()); + } + + public function testResolveFromBacktraceReturnsCodeLocationMetadata(): void + { + $expectedLine = 321; + $resolver = $this->createResolver([ + 'prefixes' => [\dirname(__DIR__)], + ]); + + $location = $resolver->resolveFromBacktrace($this->createQueryBacktrace($expectedLine)); + + $this->assertNotNull($location); + $this->assertSame('/tests/CodeLocationResolverTest.php', $location['code.filepath']); + $this->assertSame('App\\Repository\\UserRepository::findActiveUsers', $location['code.function']); + $this->assertSame($expectedLine, $location['code.lineno']); + } + + public function testResolveFromBacktraceReturnsNullWithoutInAppFrame(): void + { + $resolver = $this->createResolver(); + + $location = $resolver->resolveFromBacktrace([ + [ + 'file' => Frame::INTERNAL_FRAME_FILENAME, + 'line' => 0, + 'function' => 'internal', + ], + [ + 'class' => 'Doctrine\\DBAL\\Connection', + 'function' => 'executeQuery', + ], + ]); + + $this->assertNull($location); + } + + private function createResolver(array $options = []): CodeLocationResolver + { + $options = new Options($options); + + return new CodeLocationResolver($options, new RepresentationSerializer($options)); + } + + /** + * @return array> + */ + private function createQueryBacktrace(int $line): array + { + return [ + [ + 'file' => Frame::INTERNAL_FRAME_FILENAME, + 'line' => 0, + 'function' => 'internal', + ], + [ + 'file' => __FILE__, + 'line' => $line, + 'class' => 'Doctrine\\DBAL\\Connection', + 'function' => 'executeQuery', + ], + [ + 'class' => 'App\\Repository\\UserRepository', + 'function' => 'findActiveUsers', + ], + ]; + } +} From b3f88ef884ac8141cd3cf748765f0715575cd3b4 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 7 May 2026 12:25:33 +0200 Subject: [PATCH 2/5] fix --- tests/CodeLocationResolverTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CodeLocationResolverTest.php b/tests/CodeLocationResolverTest.php index 41d316e4a..0669775a6 100644 --- a/tests/CodeLocationResolverTest.php +++ b/tests/CodeLocationResolverTest.php @@ -37,7 +37,7 @@ public function testResolveFromBacktraceReturnsCodeLocationMetadata(): void $location = $resolver->resolveFromBacktrace($this->createQueryBacktrace($expectedLine)); $this->assertNotNull($location); - $this->assertSame('/tests/CodeLocationResolverTest.php', $location['code.filepath']); + $this->assertSame(\DIRECTORY_SEPARATOR . 'tests' . \DIRECTORY_SEPARATOR . 'CodeLocationResolverTest.php', $location['code.filepath']); $this->assertSame('App\\Repository\\UserRepository::findActiveUsers', $location['code.function']); $this->assertSame($expectedLine, $location['code.lineno']); } From cbd92837eda6c3b30a8c774c1e78a91f192502ae Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 7 May 2026 12:31:42 +0200 Subject: [PATCH 3/5] mago --- mago.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/mago.toml b/mago.toml index d1b63f09d..f8b021c00 100644 --- a/mago.toml +++ b/mago.toml @@ -5,6 +5,7 @@ excludes = [ "tests/resources/**", "tests/Fixtures/**", "src/Util/ClockMock.php", + "vendor/open-telemetry/gen-otlp-protobuf/GPBMetadata/**", ] [analyzer] From c536a44868d890f51d66df943657c30cfaad4c6e Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 7 May 2026 13:15:38 +0200 Subject: [PATCH 4/5] mago --- analysis-baseline.toml | 12 ------------ src/Integration/ModulesIntegration.php | 16 +++++++++++----- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/analysis-baseline.toml b/analysis-baseline.toml index 6259f5930..f214ee80e 100644 --- a/analysis-baseline.toml +++ b/analysis-baseline.toml @@ -438,18 +438,6 @@ code = "unreachable-else-clause" message = "Unreachable else clause" count = 1 -[[issues]] -file = "src/Integration/ModulesIntegration.php" -code = "no-value" -message = "Argument #1 passed to function `array_keys` has type `never`, meaning it cannot produce a value." -count = 1 - -[[issues]] -file = "src/Integration/ModulesIntegration.php" -code = "non-existent-class-like" -message = 'Class, interface, enum, or trait `PackageVersions\Versions` not found.' -count = 1 - [[issues]] file = "src/Integration/RequestIntegration.php" code = "invalid-property-assignment-value" diff --git a/src/Integration/ModulesIntegration.php b/src/Integration/ModulesIntegration.php index 8148b3778..6dd620d24 100644 --- a/src/Integration/ModulesIntegration.php +++ b/src/Integration/ModulesIntegration.php @@ -6,7 +6,6 @@ use Composer\InstalledVersions; use Jean85\PrettyVersions; -use PackageVersions\Versions; use Sentry\Event; use Sentry\SentrySdk; use Sentry\State\Scope; @@ -67,12 +66,19 @@ private static function getInstalledPackages(): array return InstalledVersions::getInstalledPackages(); } - if (class_exists(Versions::class)) { + $versionsClass = 'PackageVersions\\Versions'; + + if (class_exists($versionsClass)) { // BC layer for Composer 1, using a transient dependency - /** @var string[] $packages */ - $packages = array_keys(Versions::VERSIONS); + /** @var mixed $versions */ + $versions = \constant($versionsClass . '::VERSIONS'); + + if (\is_array($versions)) { + /** @var string[] $packages */ + $packages = array_keys($versions); - return $packages; + return $packages; + } } // this should not happen From cd456e3faaf8755afcf5a5418513b76105fd6526 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Fri, 8 May 2026 11:55:42 +0200 Subject: [PATCH 5/5] move to Util namespace --- src/{ => Util}/CodeLocationResolver.php | 5 ++++- tests/CodeLocationResolverTest.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) rename src/{ => Util}/CodeLocationResolver.php (97%) diff --git a/src/CodeLocationResolver.php b/src/Util/CodeLocationResolver.php similarity index 97% rename from src/CodeLocationResolver.php rename to src/Util/CodeLocationResolver.php index 73ab47ead..cc968e70c 100644 --- a/src/CodeLocationResolver.php +++ b/src/Util/CodeLocationResolver.php @@ -2,8 +2,11 @@ declare(strict_types=1); -namespace Sentry; +namespace Sentry\Util; +use Sentry\Frame; +use Sentry\FrameBuilder; +use Sentry\Options; use Sentry\Serializer\RepresentationSerializerInterface; /** diff --git a/tests/CodeLocationResolverTest.php b/tests/CodeLocationResolverTest.php index 0669775a6..4a45f8a23 100644 --- a/tests/CodeLocationResolverTest.php +++ b/tests/CodeLocationResolverTest.php @@ -5,10 +5,10 @@ namespace Sentry\Tests; use PHPUnit\Framework\TestCase; -use Sentry\CodeLocationResolver; use Sentry\Frame; use Sentry\Options; use Sentry\Serializer\RepresentationSerializer; +use Sentry\Util\CodeLocationResolver; final class CodeLocationResolverTest extends TestCase {