Skip to content

Commit 0557479

Browse files
authored
ref: add CodeLocationResolver (#2085)
1 parent 71a6e25 commit 0557479

5 files changed

Lines changed: 216 additions & 17 deletions

File tree

analysis-baseline.toml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -438,18 +438,6 @@ code = "unreachable-else-clause"
438438
message = "Unreachable else clause"
439439
count = 1
440440

441-
[[issues]]
442-
file = "src/Integration/ModulesIntegration.php"
443-
code = "no-value"
444-
message = "Argument #1 passed to function `array_keys` has type `never`, meaning it cannot produce a value."
445-
count = 1
446-
447-
[[issues]]
448-
file = "src/Integration/ModulesIntegration.php"
449-
code = "non-existent-class-like"
450-
message = 'Class, interface, enum, or trait `PackageVersions\Versions` not found.'
451-
count = 1
452-
453441
[[issues]]
454442
file = "src/Integration/RequestIntegration.php"
455443
code = "invalid-property-assignment-value"

mago.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ excludes = [
55
"tests/resources/**",
66
"tests/Fixtures/**",
77
"src/Util/ClockMock.php",
8+
"vendor/open-telemetry/gen-otlp-protobuf/GPBMetadata/**",
89
]
910

1011
[analyzer]

src/Integration/ModulesIntegration.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use Composer\InstalledVersions;
88
use Jean85\PrettyVersions;
9-
use PackageVersions\Versions;
109
use Sentry\Event;
1110
use Sentry\SentrySdk;
1211
use Sentry\State\Scope;
@@ -67,12 +66,19 @@ private static function getInstalledPackages(): array
6766
return InstalledVersions::getInstalledPackages();
6867
}
6968

70-
if (class_exists(Versions::class)) {
69+
$versionsClass = 'PackageVersions\\Versions';
70+
71+
if (class_exists($versionsClass)) {
7172
// BC layer for Composer 1, using a transient dependency
72-
/** @var string[] $packages */
73-
$packages = array_keys(Versions::VERSIONS);
73+
/** @var mixed $versions */
74+
$versions = \constant($versionsClass . '::VERSIONS');
75+
76+
if (\is_array($versions)) {
77+
/** @var string[] $packages */
78+
$packages = array_keys($versions);
7479

75-
return $packages;
80+
return $packages;
81+
}
7682
}
7783

7884
// this should not happen

src/Util/CodeLocationResolver.php

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Util;
6+
7+
use Sentry\Frame;
8+
use Sentry\FrameBuilder;
9+
use Sentry\Options;
10+
use Sentry\Serializer\RepresentationSerializerInterface;
11+
12+
/**
13+
* Resolves code location metadata from backtraces.
14+
*
15+
* @internal
16+
*
17+
* @phpstan-import-type StacktraceFrame from FrameBuilder
18+
*/
19+
final class CodeLocationResolver
20+
{
21+
/**
22+
* @var FrameBuilder An instance of the builder of {@see Frame} objects
23+
*/
24+
private $frameBuilder;
25+
26+
/**
27+
* Constructor.
28+
*
29+
* @param Options $options The SDK client options
30+
* @param RepresentationSerializerInterface $representationSerializer The representation serializer
31+
*/
32+
public function __construct(Options $options, RepresentationSerializerInterface $representationSerializer)
33+
{
34+
$this->frameBuilder = new FrameBuilder($options, $representationSerializer);
35+
}
36+
37+
/**
38+
* Resolves the first in-app frame from the current backtrace into code
39+
* location metadata.
40+
*
41+
* @return array{'code.filepath': string, 'code.function': string|null, 'code.lineno': int}|null
42+
*/
43+
public function resolve(int $limit = 20): ?array
44+
{
45+
/** @var list<StacktraceFrame> $backtrace */
46+
$backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, $limit);
47+
48+
return $this->resolveFromBacktrace($backtrace);
49+
}
50+
51+
/**
52+
* Resolves the first in-app frame from a backtrace into code location metadata.
53+
*
54+
* @param array<int, array<string, mixed>> $backtrace The backtrace
55+
*
56+
* @phpstan-param list<StacktraceFrame> $backtrace
57+
*
58+
* @return array{'code.filepath': string, 'code.function': string|null, 'code.lineno': int}|null
59+
*/
60+
public function resolveFromBacktrace(array $backtrace): ?array
61+
{
62+
$frame = $this->findFirstInAppFrameForBacktrace($backtrace);
63+
64+
if ($frame === null) {
65+
return null;
66+
}
67+
68+
return $this->getCodeLocationForFrame($frame);
69+
}
70+
71+
/**
72+
* Find the first in-app frame for a given backtrace.
73+
*
74+
* @param array<int, array<string, mixed>> $backtrace The backtrace
75+
*
76+
* @phpstan-param list<StacktraceFrame> $backtrace
77+
*/
78+
public function findFirstInAppFrameForBacktrace(array $backtrace): ?Frame
79+
{
80+
$file = Frame::INTERNAL_FRAME_FILENAME;
81+
$line = 0;
82+
83+
foreach ($backtrace as $backtraceFrame) {
84+
$frame = $this->frameBuilder->buildFromBacktraceFrame($file, $line, $backtraceFrame);
85+
86+
if ($frame->isInApp()) {
87+
return $frame;
88+
}
89+
90+
$file = $backtraceFrame['file'] ?? Frame::INTERNAL_FRAME_FILENAME;
91+
$line = $backtraceFrame['line'] ?? 0;
92+
}
93+
94+
return null;
95+
}
96+
97+
/**
98+
* Converts a frame into code location metadata.
99+
*
100+
* @return array{'code.filepath': string, 'code.function': string|null, 'code.lineno': int}
101+
*/
102+
public function getCodeLocationForFrame(Frame $frame): array
103+
{
104+
return [
105+
'code.filepath' => $frame->getFile(),
106+
'code.function' => $frame->getFunctionName(),
107+
'code.lineno' => $frame->getLine(),
108+
];
109+
}
110+
}

tests/CodeLocationResolverTest.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Tests;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Sentry\Frame;
9+
use Sentry\Options;
10+
use Sentry\Serializer\RepresentationSerializer;
11+
use Sentry\Util\CodeLocationResolver;
12+
13+
final class CodeLocationResolverTest extends TestCase
14+
{
15+
public function testFindFirstInAppFrameForBacktrace(): void
16+
{
17+
$expectedLine = 123;
18+
$resolver = $this->createResolver([
19+
'prefixes' => [],
20+
]);
21+
22+
$frame = $resolver->findFirstInAppFrameForBacktrace($this->createQueryBacktrace($expectedLine));
23+
24+
$this->assertNotNull($frame);
25+
$this->assertSame(__FILE__, $frame->getFile());
26+
$this->assertSame($expectedLine, $frame->getLine());
27+
$this->assertSame('App\\Repository\\UserRepository::findActiveUsers', $frame->getFunctionName());
28+
}
29+
30+
public function testResolveFromBacktraceReturnsCodeLocationMetadata(): void
31+
{
32+
$expectedLine = 321;
33+
$resolver = $this->createResolver([
34+
'prefixes' => [\dirname(__DIR__)],
35+
]);
36+
37+
$location = $resolver->resolveFromBacktrace($this->createQueryBacktrace($expectedLine));
38+
39+
$this->assertNotNull($location);
40+
$this->assertSame(\DIRECTORY_SEPARATOR . 'tests' . \DIRECTORY_SEPARATOR . 'CodeLocationResolverTest.php', $location['code.filepath']);
41+
$this->assertSame('App\\Repository\\UserRepository::findActiveUsers', $location['code.function']);
42+
$this->assertSame($expectedLine, $location['code.lineno']);
43+
}
44+
45+
public function testResolveFromBacktraceReturnsNullWithoutInAppFrame(): void
46+
{
47+
$resolver = $this->createResolver();
48+
49+
$location = $resolver->resolveFromBacktrace([
50+
[
51+
'file' => Frame::INTERNAL_FRAME_FILENAME,
52+
'line' => 0,
53+
'function' => 'internal',
54+
],
55+
[
56+
'class' => 'Doctrine\\DBAL\\Connection',
57+
'function' => 'executeQuery',
58+
],
59+
]);
60+
61+
$this->assertNull($location);
62+
}
63+
64+
private function createResolver(array $options = []): CodeLocationResolver
65+
{
66+
$options = new Options($options);
67+
68+
return new CodeLocationResolver($options, new RepresentationSerializer($options));
69+
}
70+
71+
/**
72+
* @return array<int, array<string, mixed>>
73+
*/
74+
private function createQueryBacktrace(int $line): array
75+
{
76+
return [
77+
[
78+
'file' => Frame::INTERNAL_FRAME_FILENAME,
79+
'line' => 0,
80+
'function' => 'internal',
81+
],
82+
[
83+
'file' => __FILE__,
84+
'line' => $line,
85+
'class' => 'Doctrine\\DBAL\\Connection',
86+
'function' => 'executeQuery',
87+
],
88+
[
89+
'class' => 'App\\Repository\\UserRepository',
90+
'function' => 'findActiveUsers',
91+
],
92+
];
93+
}
94+
}

0 commit comments

Comments
 (0)