Lines 100.00% 19 / 19
Methods 100.00% 5 / 5
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 recordFailure 100.00% 4 / 4 100.00% 1 / 1 2
 isSampled 100.00% 1 / 1 100.00% 1 / 1 1
 locateFailingLine 100.00% 12 / 12 100.00% 1 / 1 6
 isAppCode 100.00% 1 / 1 100.00% 1 / 1 3
12final class HandlerFailureSampler
13{
14    private const int ONE_OFF_FAILURE_OCCURRENCE = 1;
15
16    private const string STEWART_NAMESPACE = 'Stewart\\';
17
18    private const string VENDOR_DIRECTORY = '/vendor/';
19
20    /** @var array<string, int> */
21    private array $occurrencesByFailingLine = [];
22
23    private readonly EveryNthOccurrence $samples;
24
25    public function __construct()
26    {
27        $this->samples = EveryNthOccurrence::forRepeatedWarnings();
28    }
29
30    public function recordFailure(ResourceScope $scope, AppFailurePhase $phase, Throwable $error): int
31    {
32        if ($phase !== AppFailurePhase::Handler) {
33            return self::ONE_OFF_FAILURE_OCCURRENCE;
34        }
35
36        // Origins carry task ids and moments, so only the failing line keeps the map bounded.
37        $key = \sprintf("%s\0%s\0%s", $scope->wireValue(), $error::class, $this->locateFailingLine($error));
38
39        return $this->occurrencesByFailingLine[$key] = ($this->occurrencesByFailingLine[$key] ?? 0) + 1;
40    }
41
42    public function isSampled(int $occurrence): bool
43    {
44        return $this->samples->includesOccurrence($occurrence);
45    }
46
47    // The first app line, so two app lines failing inside the same runtime call stay apart.
48    private function locateFailingLine(Throwable $error): string
49    {
50        $file = $error->getFile();
51        $line = $error->getLine();
52        $firstLineOutsideFactories = null;
53
54        foreach ($error->getTrace() as $caller) {
55            $class = $caller['class'] ?? null;
56
57            if ($this->isAppCode($file, $class)) {
58                return $file . ':' . $line;
59            }
60
61            if ($firstLineOutsideFactories === null && $class !== null && !is_a($class, Throwable::class, true)) {
62                $firstLineOutsideFactories = $file . ':' . $line;
63            }
64
65            $file = $caller['file'] ?? '';
66            $line = $caller['line'] ?? 0;
67        }
68
69        return $firstLineOutsideFactories ?? $file . ':' . $line;
70    }
71
72    private function isAppCode(string $file, ?string $class): bool
73    {
74        return $class !== null && !str_starts_with($class, self::STEWART_NAMESPACE) && !str_contains($file, self::VENDOR_DIRECTORY);
75    }
76}